## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Company skills supply instructions and files to those agents. > - GitHub imports already exist, but users cannot manage repositories as skill sources. > - Repository refresh also needs caller-authorized access and complete local packages. > - This pull request adds Sources inside Skills and reuses GitHub connections from Apps. > - Installed snapshots let agents use skills without fetching GitHub during a run. > - Manual refresh preserves skill identity and leaves failed imports on their last good version. ## Linked Issues or Issue Description **Subsystem affected** Cross-cutting: skills UI, server, database, shared contracts, and runtime materialization. **Problem or motivation** Users keep skills in GitHub repositories. They need a clear way to select, import, and refresh those skills. Existing imports do not expose repository management or consistently preserve supporting files. **Proposed solution** Add company-scoped skill sources. Browse repositories from all accessible GitHub connections, or paste a public repository or branch URL. Select whole skill packages, inspect included files and reference warnings, and install complete, immutable snapshots. Refresh each source manually. **Alternatives considered** Project repository settings hide the workflow from Skills. A second GitHub connector would duplicate credentials and grants. Upstream editing and PR creation are separate work. **Roadmap alignment** This implements the Skills Manager direction in ROADMAP.md. The maintainer requested this scope and reviewed the component and full-app journey stories before implementation. Related reports: Refs #10285, Refs #10949, Refs #13464. Related work: #14356, #13656, #9268. ## What Changed - Add source and entry records, an idempotent migration, company-scoped APIs, and legacy GitHub import adoption. - Reuse current caller grants and credential refresh. Combine and deduplicate repository inventories across accessible connections. Pasted public URLs also prefer the active user’s authorized connections. Tokens stay in the Git child environment, never argv or disk. - Fetch a shallow Git snapshot at one immutable commit. Scan the full local tree, including hidden and nested folders. Read Git objects without checkout or archive transformations and enforce nested package boundaries. - Bound Git downloads to 128 MiB and three minutes. Cancel active process groups and remove incomplete downloads. Preserve cancellation and deadlines while progress drains; close stalled HTTP progress streams after 30 seconds. Reuse caller-scoped temporary snapshots for preview/import after reauthorization. - Index repository package boundaries once and cap expanded work at 1,000 packages, 10,000 files, and 100 MiB, including repeated copies of shared blobs. Bound path depth and the shared path index. Discovery keeps audited manifests without retaining all package bodies. - Resolve moving branches before fetching so unchanged discovery reuses caller-scoped snapshots. Limit active scans, scan frequency, and new downloads per caller and company; quotas apply before metadata reads and across connections, and cached scans do not consume the download quota. - Store complete versions with script content, binary bytes, and executable modes. Preserve these through copies, runtime caches, and runner packaging. - Stage downloads before publication. Use source leases, revision checks, and transactional activity records. Keep installed versions after failures, upstream deletion, deselection, and disconnect. - Add the approved import flow, Sources page, selection tree, provenance, read-only Studio behavior, and saved return from GitHub setup. - Add package manifests, commit-pinned file previews, and separate runtime requirements and reference warnings. Supporting files are included together; nested skills remain independently selectable. Preview requests reauthorize the caller and re-audit package content. - Show installed skills as compact links beneath each source. Repository titles open GitHub. Keep Refresh, Select skills, and Disconnect source in a three-dot menu. Source rows omit the branch, imported count, and refresh timestamp; action alignment and repository titles work at narrow widths. - Stream discovery metadata over an opt-in NDJSON response. Show measured Git download progress and real package/file counts, animate newly checked skills, support cancellation, and require a complete scan before selection. Keep the existing JSON API. - Retain component stories and add a separate full-app journey story group. Include fixed progress states and interactive scan, large-repository, interruption, and saving stories. - Update Skills documentation and product contracts. Suppress private GitHub skill references in telemetry. Privacy review requested for the telemetry changes. ## Verification - Local repository typecheck, full build, token gates, and Storybook build passed during this work. Focused transport, authorization, scanner, persistence, route, and UI tests pass. The final UI refinement passes all eight focused UI tests, UI typecheck/build, and token gates. The scanner resource and repeated-discovery fixes pass 132 focused scanner, transport, authorization, source-service, route, and rate-limit tests, plus server typecheck/build. Full-suite verification comes from CI; the older full local Vitest run was stopped after unrelated chat failures and a font-test failure, all of which passed in fresh focused runs. At commit `1098d5996`, all 54 active checks pass; two optional Storybook jobs are skipped. CI covers repository typecheck, build, the full test suites, browser shards, and the canary dry run. Greptile is 5/5 with no open findings; the security scan also passes. - Adversarial scanner tests verify repeated-blob byte accounting with and without declared sizes, package/file/path caps, one-time repository indexing, metadata-only discovery audits, and nested package boundaries. Additional tests cover branch movement, snapshot reuse, caller/company quotas, isolation across connections, active-lease cleanup, quota recovery, and rejection before any metadata API call. - Real Git tests verify hidden paths, exact binary bytes, executable modes, export-ignore preservation, symlink/submodule reporting, pinned commits, caller-scoped cache reuse, cancellation, cleanup, and credential isolation. Regression tests hold both download slots with permanently blocked progress callbacks, verify timeout/cancellation cleanup and retry, and exercise HTTP backpressure cancellation. Access tests cover automatic public-URL connection selection and revoked grants. Database tests verify company and grant audiences. - Live isolated browser test: the public `anthropics/skills` scan now completes and discovers all 20 skills without connecting an account. Imported canvas-design with all 83 files, opened it from Sources, and verified the installed binary-font preview/download control. Package previews also expose the complete file inventory before import. Cancelled an active Git download and retried successfully to all 20 discovered skills; the browser displayed measured download progress. The current audits reject four other packages; eligible selections remain importable. - Browser checks verify the simplified source rows at desktop and narrow widths, keyboard navigation into the actions menu, Refresh from the menu, selection, and fixture disconnect with installed skills retained. Storybook includes a menu-open checkpoint and a 320px layout. - Storybook includes receiving/preparing download checkpoints and a timed full-app import journey, plus cancellation, retry, large-repository, and saving states. Streaming tests cover split UTF-8 frames, incomplete streams, late responses, cross-company requests, HTTP errors, and JSON compatibility. - Earlier live acceptance on this PR imported `stitch-skill` with `DESIGN.md`, assigned it to an agent, disconnected its source, and ran a successful Studio test that read both installed files. An editable copy changed independently. Both Skills variants, mobile selection, and return from GitHub setup were exercised. - Private access, revoked credentials, OAuth success return, binary/script preservation, concurrent refresh, transaction rollback, version pins, and legacy adoption have automated coverage. A real private-repository OAuth grant was not created during this test. ## Risks - The migration groups recognizable legacy imports without provider calls. Their first successful refresh completes the local package snapshot. - Reference checks are advisory. They cover Markdown links and explicit relative resource paths, not arbitrary runtime dependency graphs. Preview text is capped at 64 KiB; imported bytes remain complete. - Git must be installed on the server. Shallow fetches still download the branch snapshot, including files outside selected packages. Downloads have size/time/concurrency limits. Temporary caches are bounded and caller-scoped. GitHub API quota still applies to repository metadata and the connection picker; content no longer uses per-file API requests. Failed scans retain installed content. - Sources depend on the current caller's GitHub access. A saved connection does not grant access to another person's token. - GitHub script support and immediate manual refresh are explicit maintainer-approved requirements. The operator trusts the selected repository and accepts upstream script and executable-mode changes on refresh. Static audits are not a sandbox or a guarantee of safe code; agents may later invoke installed helpers under their runtime permissions. Import and refresh do not execute scripts, hooks, package installation, or builds. Raw URL and skills.sh imports keep their prior script restrictions. - Source originals remain read-only. Refresh affects subsequent unpinned runs; explicit pins and active runs retain their versions. - The telemetry change removes source-managed GitHub identifiers from skill-reference events. It introduces no event or field. Please review the privacy boundary. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, code execution, and browser tools. The exact serving model ID and context-window size are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
12 KiB
Telemetry Data Contract
This document explains how contributors should use Paperclip's public telemetry contract. It does not duplicate the full list of individual events or dimensions. It documents extra semantic and privacy rules where the generated shape is not sufficient.
The canonical source for first-party event names, dimensions, optionality,
allowed primitive value types, and enum descriptions is
packages/shared/src/telemetry/generated/paperclip-telemetry.ts.
Shared enum constants live in packages/shared/src/constants.ts. Use those
constants when code needs a reusable domain, but treat the generated telemetry
types as the final authority for emitted first-party telemetry shapes.
Public Sources
Use these files when reviewing or changing telemetry code:
| Contract item | Public source |
|---|---|
| First-party event names | PaperclipEventName in generated/paperclip-telemetry.ts |
| Per-event dimensions and optionality | EventDimensionsMap in generated/paperclip-telemetry.ts |
| Enum descriptions for telemetry dimensions | PAPERCLIP_ENUM_DESCRIPTIONS in generated/paperclip-telemetry.ts |
| Schema version and event envelope helpers | SCHEMA_VERSION, makeEvent(), and makeBatch() in generated/paperclip-telemetry.ts |
| Runtime-safe event names and dimensions | TelemetryEventName and TelemetryEventDimensions in types.ts |
| Allowed primitive dimension values | TelemetryDimensionValue in types.ts |
| Shared reusable enum domains | Named exports in constants.ts |
| First-party typed emit helpers | events.ts |
| Generic client behavior | client.ts |
| Retention windows and event class assignments | RETENTION_DAYS and EVENT_RETENTION_CLASS in retention.ts |
Do not copy generated event lists or dimension tables into this README. They will drift as the generated contract changes.
Emission Boundary
Paperclip telemetry uses named events with explicit dimension fields. Treat open-ended string dimensions as public contract values, not as a place for user content or private operational data. Do not send PII, secrets, credentials, private paths, prompts, model output, or other sensitive values through telemetry dimensions.
Telemetry emitters send raw dimension values. They must not pre-normalize enum-like values into a reporting form just to match today's known domain.
The receiving layer owns canonicalization. Keeping canonicalization in one place means emitters can stay simple and accurate: emit what the product observed, use the generated contract for required and optional fields, and let the receiving layer decide how legacy spellings, aliases, unknown names, and future values map to a stable reporting shape.
Do not add client-side lowercasing, alias mapping, or fallback mapping unless the generated telemetry contract specifically requires that emitted value.
If a dimension is privacy-protected before emission, emit only the protected value and its matching public marker as defined by the typed helper or generated contract. Do not emit private source material in telemetry dimensions.
Credential-bearing chat setup failures replace provider-controlled error names,
messages, and stacks with a fresh generic error before the HTTP error handler
reports the crash. The existing error.handler_crash event still uses its
generated error_code: string contract; this path emits only Error, never a
provider-supplied error name that may contain a credential. This is a privacy
boundary, not enum canonicalization. Test the value reaching the telemetry
helper as well as the separate crash-reporting and local logging sinks.
Interaction Resolver Events
interaction.created records the interaction kind and whether the create
request used a deprecated resolver-policy alias. It does not record the prompt,
title, options, questions, target identifier, creator identifier, or resolver
identifier.
interaction.resolved records the low-cardinality interaction outcome defined
in the generated contract. Its legacy_inherited_restriction dimension is
true only when stored migration provenance preserves a legacy resolver-policy
restriction. It is false for canonical new writes. This dimension describes
policy provenance. It does not contain user content or an identifier.
Emit interaction.resolved only after the complete decision transaction commits.
Conversational answers include the card outcome, source-message reference, and
activity audit in that transaction. A rollback or matching retry must not emit
a resolution event. This changes emission timing only: message text, comment IDs,
and user IDs remain in the instance database and are not added to telemetry.
Use trackInteractionCreated() and trackInteractionResolved() from
events.ts to emit these events. The generated contract remains the authority
for their exact dimensions and optionality.
Agent Task Run Events
agent.task_run records one terminal state for one agent run: succeeded,
interrupted, failed, cancelled, or timed_out. Emit it once per run, at
the run's terminal transition. Do not emit it for a run that is still active.
The event's task_id dimension is optional and privacy-protected. Never emit
the raw task id. Pass the raw id to trackAgentTaskRun() in events.ts. The
helper calls hashPrivateRef() on client.ts, which derives the emitted
value with a salted hash of the per-installation secret.
Use trackAgentTaskRun() to emit this event. The generated contract remains
the authority for its exact dimensions and optionality.
Other Data Paths
This document covers Paperclip Telemetry only. The generated Telemetry contract covers neither the Observability path nor the run-log path. Two other data paths document their own contract in their own file:
- Observability — the OpenTelemetry trace path, the sandbox startup trace spans, and the sandbox duplex transport instrumentation.
- Run-Log Events — events written to the
local
heartbeat_run_eventstable.
Dimension Values
Telemetry dimension values must be primitives. Use only the value types allowed
by TelemetryDimensionValue:
stringnumberboolean
Do not emit null, undefined, arrays, or objects as dimension values. Optional
dimensions should be omitted when absent.
When a dimension is enum-like, use the shared constant from constants.ts when
one exists. If no shared constant exists, use the generated telemetry type as the
domain. In all cases, the generated telemetry type remains the source of truth
for the emitted value.
Required, Optional, And Sentinel Values
Required and optional dimensions are defined by EventDimensionsMap.
Required dimensions must be present for every event of that name. Optional dimensions should be emitted only when the value is known and useful.
Sentinel values are only for required fields that have no observed raw value at the emitting layer. Do not use a sentinel to hide a concrete value that is new, custom, or not yet represented by a shared constant. Emit the concrete raw value and let the receiving layer canonicalize it.
Adding Or Changing Telemetry
Client code is responsible for emitting approved telemetry events at the right place in the product. Stable event names, dimensions, and enum domains must come from the generated telemetry contract before normal emitters use them.
For product work that needs to propose a new first-party event before schema
registration, use the proposal marker workflow in doc/TELEMETRY_WORKFLOW.md.
Those proposed calls stay on client.track(), carry an @ts-expect-error
marker on the event-name argument, and are swallowed at runtime until the
generated schema registers the event name.
For stable event work:
- Start from
generated/paperclip-telemetry.ts. The generated types are what reviewers use to verify event names, dimensions, optionality, value types, enum descriptions, and schema version. - Choose stable event and dimension names. Do not include user content, local machine details, secrets, credentials, private paths, or values that are not part of the public event contract.
- Use only
string,number, orbooleandimension values. - Reuse a shared constant from
constants.tsfor enum-like dimensions when one exists. If the generated telemetry domain has values beyond a shared constant, keep the emitter aligned with the generated telemetry type. - Keep emitters raw. Do not normalize, alias-map, or lowercase enum-like values in the client unless the generated contract explicitly calls for that emitted value.
- Add or update a typed helper in
events.tswhen the event is first-party and should have a stable helper API. - Update tests for helper behavior, including raw pass-through for enum-like values when that is the intended boundary.
- Update this README only when the contributor workflow, source-of-truth pointers, or durable invariants change. Do not add an event catalog here.
Before opening a pull request, verify that the emitted code, typed helpers, and generated telemetry contract agree. If they disagree, fix the contract or code rather than documenting around the mismatch in this README.
For new first-party events that are not in the generated contract yet, follow
the public proposal and promotion workflow in
doc/TELEMETRY_WORKFLOW.md.
Retention
Retention windows are documented in retention.ts. Each event is assigned a
retention class; the class determines the window in days. This is a
housekeeping and query-cost concern managed by data-infra, not a schema
concern — updating a retention window does not require a schema version bump.
Current classes:
| Class | Window | Description |
|---|---|---|
operational_enum_count |
90 days | Enum/boolean/count/bucket events. No token material, no PII. |
When a new event carries only enums, booleans, counts, or coarse buckets and
no token material or PII, assign it to operational_enum_count in
EVENT_RETENTION_CLASS. If no existing class fits, define a new class in
RETENTION_DAYS and document it here.
GitHub-synced skills
The legacy import endpoint retains skill.imported with its existing source_type.
For source-managed GitHub skills, skill_ref is omitted, including public sources:
the saved repository may be private and its key contains user-authored names.
Source discovery, selection, and refresh add no first-party telemetry events.
Repository URLs, paths, commits, connection IDs, names, and file contents stay out
of these telemetry dimensions. Review this suppression in privacy review alongside
changes to the legacy import caller; the event schema and envelope are unchanged.