diff --git a/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/SKILL.md b/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/SKILL.md index ccf24fd2b4..5f0432c215 100644 --- a/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/SKILL.md +++ b/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/SKILL.md @@ -105,11 +105,13 @@ Collect these before step 1. Ask only for what you cannot determine safely. without a live provider proof belongs in the third state, not the second. See the visibility chain in `references/catalog-contract.md`. 5. **Location of the ingestion corpus** (`paperclip-content`), or the value for - `PAPERCLIP_CONTENT_TEMPLATES`. This corpus is in a non-public repository; the - generator refuses to run without it. If you do not have it, this skill's - path is closed to you — say so plainly rather than editing the guard, and - note that connecting the server directly (`connect-agent-tools` path 2) is a - different thing, not a lesser substitute. + `PAPERCLIP_CONTENT_TEMPLATES`, *or* a decision to run the generator with + `--definitions-only`. The corpus is in a non-public repository, but it is + not a gate on authoring: `--definitions-only` skips it and still emits every + definition and the positional registry, verified byte-for-byte. What it does + not write is `app-definitions.ingestion-report.json`, which a provider with + no capture of its own does not change anyway. See **Generator preconditions** + in `references/catalog-contract.md`. Never edit the guard. 6. **The deployment you will validate on.** An isolated self-hosted instance is the expected answer and is sufficient. @@ -136,7 +138,8 @@ reviewable claim and must come from the provider's own metadata or documentation script, a definition, connection config, application metadata, a committed fixture, a comment, a screenshot, a trace, a HAR file, or a report. If you are handed a credential, propose it as a Paperclip secret immediately and never - echo it. Assert absence in a test rather than merely not printing it. + echo it. Assert absence in a test rather than merely not printing it — + see **Asserting absence** below for the canary that does not false-positive. - **No external writes during research.** Unauthenticated metadata reads are allowed. Dynamic client registration, consent, account mutation, and any write tool call are not research — they need explicit authorization. @@ -147,6 +150,47 @@ reviewable claim and must come from the provider's own metadata or documentation skill ends at a verified local change. - **No roadmap claims.** Do not write "coming soon" or promise a future method. +### Asserting absence + +"Assert absence in a test" is easy to get wrong in one specific way, so here is +the shape that works. + +The obvious canary — grep the definition for `/bearer|token|secret|key/i` — +fails on the checked-in catalog before you have added anything. `keyPlacement` +legitimately carries `"prefix": "Bearer "`, and at `18dac1e1` the shipped +definitions carry three such values (`"Bearer "`, `"Basic "`, `"Token token="`). +A canary that fires on a clean tree gets deleted within the week, which leaves +you with no canary at all. + +Assert on **values and shapes**, not on the words that describe them: + +```ts +it("ships no credential value for acme", () => { + const definition = APP_DEFINITIONS.find((app) => app.slug === "acme")!; + const fields = definition.methods.flatMap((m) => m.credentialFields ?? []); + + // A credential field declares how to collect a secret. It never carries one. + for (const field of fields) { + expect(field).not.toHaveProperty("value"); + expect(field).not.toHaveProperty("default"); + } + + // Value-shaped canary over the whole serialized definition. `"Bearer "` is a + // scheme name and does not match; a real token does. + expect(JSON.stringify(definition)).not.toMatch( + /\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}|\bgh[pousr]_[A-Za-z0-9]{20,}|\bxox[baprs]-[A-Za-z0-9-]{10,}/, + ); +}); +``` + +Executed over the 72 checked-in definitions: the naive pattern fires on +**72 of 72**, the value-shaped one on **0 of 72**, and it still matches +`sk-…`, `ghp_…` and `xoxb-…` test strings. + +Run the same value-shaped canary over every artifact you produce, not only the +definition: your report, any fixture, and any captured output. A credential that +never reached the definition but did reach the report is the same incident. + ## Step 1 — Discover The Current Contract Read from the recorded commit, not from memory and not from a stale worktree: @@ -349,7 +393,9 @@ Stop and report rather than working around any of these: - An applicable research gate has not been taken, or does not cover this provider. -- The ingestion corpus is unavailable, so the generator cannot run. +- The change needs `app-definitions.ingestion-report.json` refreshed and the + ingestion corpus is unavailable. Authoring a definition does not — use + `--definitions-only`. - The provider has no compatible MCP server, or needs a transport with no execution path. - The only viable method is Cloud-gated (`platform_shared` Paperclip-managed diff --git a/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/catalog-contract.md b/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/catalog-contract.md index 75aeb22319..d41da2f1d6 100644 --- a/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/catalog-contract.md +++ b/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/catalog-contract.md @@ -53,14 +53,34 @@ change the next run reverts. ## Generator preconditions -- **Corpus.** `scripts/ingest-app-definitions.mjs` resolves - `PAPERCLIP_CONTENT_TEMPLATES`, defaulting to +- **Corpus, and how to author without it.** `scripts/ingest-app-definitions.mjs` + resolves `PAPERCLIP_CONTENT_TEMPLATES`, defaulting to `../../paperclip-content/research/connections/vercel/templates`. It throws `Expected 99 captures, found N` unless exactly 99 `.md` files (excluding - `INDEX.md`) are present. A new provider needs no capture of its own; the - corpus still has to be complete. The corpus lives in the non-public - `paperclip-content` repository: if you do not have it, this whole path is - closed to you and you report that rather than stubbing the guard out. + `INDEX.md`) are present. A new provider needs no capture of its own. That + corpus is in the non-public `paperclip-content` repository. + + Two corrections to what that implies, both executed at `066a4e8019`: + + - **When the default path does not exist you get a raw `ENOENT` out of + `fs.readdirSync`, not the `Expected 99 captures` guard.** The guard only + runs once the directory has been read. Do not read that `ENOENT` as a + broken checkout; it is the missing corpus. + - **`--definitions-only` skips the corpus entirely, and it is enough to + author a definition.** `node scripts/ingest-app-definitions.mjs + --definitions-only`, with no corpus present and `PAPERCLIP_CONTENT_TEMPLATES` + unset, reproduced all 72 checked-in `app-definitions/.json` files and + `app-definitions.generated.ts` byte-for-byte — `git status` was clean + afterwards. Adding one throwaway provider tuple and re-running emitted 73 + definitions and changed exactly the new `.json` plus the positional + registry. + + What the flag costs you is `app-definitions.ingestion-report.json`, which it + does not write. The report is built from corpus captures, so a provider with + no capture of its own contributes nothing to it and it is correctly left + unchanged — in the run above it stayed clean. If your change *does* need the + report refreshed, you need the corpus, and you say so rather than stubbing + the guard out. - **Branding.** `brandingFor` throws `: missing local branding provenance` unless the slug has a manifest row (only `oauth-generic` and `api-key-generic` are exempt). Branding precedes @@ -82,6 +102,15 @@ change the next run reverts. - `method(key, transport, auth, defaults, riskTier, guidanceMd, extra)` fills `ownershipModes` as `["customer", "dcr"]` for `oauth` and `["customer"]` otherwise, plus a default `whenToUse`. + + **That default is a capability claim, and it is silent.** Using the helper + asserts both that the provider accepts an operator-registered client + (`customer`) and that Paperclip may auto-register one (`dcr`). Neither is + free: omit `dcr` for a provider Paperclip must not auto-register, and omit + `customer` for a provider with no way for an operator to register a client of + their own. Pass `ownershipModes` explicitly in `extra` whenever your probe did + not establish both, and say in your report which of the two you observed + advertised rather than inherited from the helper. - `featured` is a hard-coded six-slug list in the mapper. Do not add to it as part of authoring a new provider. @@ -129,6 +158,33 @@ connection intents (`server/src/services/connection-intents.ts`), and rendered as a disabled card with the reason in `ConnectionSetupFlow.tsx` and `Browse.tsx`. +### Executing the unlisted path + +The second state is the *default* outcome of offline authoring, not an +exception, so here are its mechanics in full. Three edits, all required, and +one thing not to do: + +1. **`packages/shared/src/app-definitions.ts`** — add the slug to + `CONNECTABLE_APP_SLUGS` *and* to `APP_STORE_HIDDEN_SLUGS`. Missing the first + makes the definition unreachable; missing the second makes it listed. +2. **`packages/shared/src/app-definitions.test.ts`** — add the slug to the exact + sorted list in the `APP_STORE_HIDDEN_SLUGS` assertion. It is `toEqual` + against a literal array and it is sorted, so insert it in position rather + than appending. +3. **`ui/public/brands/apps/manifest.json`** — the branding row still has to + exist, because `brandingFor` throws without it, but set + `"catalogVisible": false`. The manifest-parity assertion compares the + `catalogVisible` set against the *store-visible* set, so an unlisted slug + left `catalogVisible: true` fails it. +4. **Do not touch the store count.** `APP_STORE_DEFINITIONS` excludes hidden + slugs, so its length does not change. Bumping it — the instruction the + store-visible path gives — fails the test by one in the other direction. + +Checked against `18dac1e1`: all 20 slugs in `APP_STORE_HIDDEN_SLUGS` have a +manifest row, every one carries `catalogVisible: false`, and the 56 rows with +`catalogVisible: true` are exactly `APP_STORE_DEFINITIONS`. The four steps above +are what the existing unlisted providers already do. + ## Assertions with exact counts or sets These fail on any addition. Update them deliberately. diff --git a/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/offline-verification.md b/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/offline-verification.md index 39fa45367d..67beb460b7 100644 --- a/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/offline-verification.md +++ b/packages/skills-catalog/catalog/optional/software-development/author-connector-definition/references/offline-verification.md @@ -6,6 +6,13 @@ you have permission to write to the repository, when another task owns the working tree, or when you want to prove a failure is attributable to your change and not to the environment. +**Check whether you need it first.** If you are already working in a disposable +worktree you created and own — the normal shape — the harness buys you little: +what it protects is a checkout other people depend on, and you do not have one. +Run the ladder in `references/catalog-contract.md` directly and `git status` at +the end. Reach for the harness when the checkout is shared, is someone else's, +or has to be provably untouched. + The recipe below builds it. It exports the needed files with `git archive` and `git show` at a pinned commit and symlinks `node_modules` read-only, so the checkout is untouched. diff --git a/packages/skills-catalog/generated/catalog.json b/packages/skills-catalog/generated/catalog.json index 57813b44f6..e7217cea72 100644 --- a/packages/skills-catalog/generated/catalog.json +++ b/packages/skills-catalog/generated/catalog.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "packageName": "@paperclipai/skills-catalog", "packageVersion": "0.3.1", - "generatedAt": "2026-09-24T21:08:13.357Z", + "generatedAt": "2026-09-24T21:13:47.041Z", "skills": [ { "id": "paperclipai:bundled:docs:doc-maintenance", @@ -1147,14 +1147,14 @@ { "path": "SKILL.md", "kind": "skill", - "sizeBytes": 23654, - "sha256": "2b8ed818bc969425f6ebb7de0b70893b69ba6cbeae55d4a75660b4ae82046302" + "sizeBytes": 25787, + "sha256": "47ddc79f4dc46b60aac3b399c2e486e994b2587dffdb6d160a58f8a0bc8cc5aa" }, { "path": "references/catalog-contract.md", "kind": "reference", - "sizeBytes": 15776, - "sha256": "e4a7f973ac2278bc23f86bf74e0199574d3341e91fe092a0fa06494a736ce930" + "sizeBytes": 19071, + "sha256": "f649eb72a31f843d566e1dbafa21b4930a6ce1bc4fee167403d51f2cfb310882" }, { "path": "references/live-acceptance.md", @@ -1165,8 +1165,8 @@ { "path": "references/offline-verification.md", "kind": "reference", - "sizeBytes": 18480, - "sha256": "f887188302dab5c3ea947a29fb21ebfe4281e57cf83c02bcfbfb42c4f2711500" + "sizeBytes": 18916, + "sha256": "a0843a9df5d1869f6e059c06c38fc63aded9e6d54f43aad83b5114b498581be6" }, { "path": "references/safe-discovery.md", @@ -1175,7 +1175,7 @@ "sha256": "d65e3dbfee183e46923af1ac3b088dc589277551749d45cd8e4867db50c0d687" } ], - "contentHash": "sha256:853e1f442eb14f95e72d312f305c657cf235c4cd501455ca0846fb58a043a889" + "contentHash": "sha256:eda0526e6cf158cd5a66d9bf678b7318cc4cee42bab4d62dc5758d27c120ba97" }, { "id": "paperclipai:optional:software-development:connect-agent-tools",