mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 16:51:28 +02:00
feat(skills-catalog): ship connector authoring and connection skills
Adds two optional catalog skills under software-development, continuing
PAP-18408 and PAP-18464:
- author-connector-definition — mechanical authoring of one Paperclip
catalog connector definition, from provider MCP probe through catalog
tests.
- connect-agent-tools — getting an agent working against an outside tool,
self-hosted first, with a per-deployment support matrix.
Both derive trustLevel markdown_only: no scripts/ or assets/ entries, so
no audit allowlist change is required. Frontmatter descriptions are 291
and 293 characters, under the 300-character shipped-catalog cap.
Self-hosted is the primary documented path. Cloud rows in the support
matrix are labelled untested or unsupported and are not inferred from
self-hosted results.
Verified in this worktree at origin/master b70641f23f:
build:manifest Wrote generated/catalog.json with 19 catalog skills.
validate Catalog manifest is valid with 19 catalog skills.
test 5 files / 20 tests passed
typecheck clean
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b70641f23f
commit
0fe055cc8f
9 files changed
+1936
-1
No files matched your search
+380
@@ -0,0 +1,380 @@
|
||||
---
|
||||
name: author-connector-definition
|
||||
description: >
|
||||
Author one Paperclip catalog connector definition: probe the provider MCP
|
||||
metadata, fix the five connection axes, add official branding, edit the
|
||||
generator source, regenerate, and get the catalog tests green with evidence.
|
||||
Mechanical authoring only; not research approval, not PR submission.
|
||||
key: paperclipai/optional/software-development/author-connector-definition
|
||||
recommendedForRoles:
|
||||
- engineer
|
||||
tags:
|
||||
- mcp
|
||||
- connectors
|
||||
- app-definition
|
||||
- catalog
|
||||
- codegen
|
||||
requires:
|
||||
- git
|
||||
- curl
|
||||
- node
|
||||
- pnpm
|
||||
---
|
||||
|
||||
# Author Connector Definition
|
||||
|
||||
Turn a provider name into a reviewable catalog-connector change in the Paperclip
|
||||
App, with executed evidence for every claim.
|
||||
|
||||
This skill owns the **mechanics** of one definition: the generator contract, the
|
||||
slug allowlist, branding validation, the exact-count test assertions, and the
|
||||
offline verification loop. It does not own the process around them.
|
||||
|
||||
**A catalog entry is a convenience layer, not a prerequisite.** The runbook is
|
||||
explicit: an operator can connect any standards-compliant remote HTTP MCP server
|
||||
from **Connect your own MCP server** or **Paste a config** with no Paperclip
|
||||
code change at all, including servers that need browser sign-in
|
||||
(`CONNECTOR-PLAYBOOK.md:23-26`). Check that the requester actually needs a
|
||||
catalog entry before spending a single edit here. `connect-agent-tools` owns
|
||||
that decision.
|
||||
|
||||
**Self-hosted first.** Everything this skill produces is authored and verified
|
||||
against an App checkout and an instance you run yourself. Phases 8 and 9 of the
|
||||
runbook are satisfied by an isolated self-hosted instance; no Paperclip Cloud
|
||||
account is required to author, validate, or submit a connector. Where a method
|
||||
is genuinely Cloud-gated — the curated `platform_shared` Paperclip-managed OAuth
|
||||
profile is the one current case — record it as `unsupported` for self-hosted
|
||||
rather than treating Cloud as the baseline.
|
||||
|
||||
## Use This When
|
||||
|
||||
Trigger on a request that names a provider and asks for a catalog connector,
|
||||
gallery card, or `AppDefinition` — for example "add a Neon connector", "put
|
||||
Fireflies in the Connectors catalog", "author the app definition for X".
|
||||
|
||||
Also trigger when an existing definition needs an axis change: a new method, a
|
||||
different `ownershipModes` set, a widened `scopesHint`, a corrected endpoint, or
|
||||
a visibility change.
|
||||
|
||||
## Do Not Use This When
|
||||
|
||||
Hand the request to the right owner instead:
|
||||
|
||||
| Request | Owner |
|
||||
| --- | --- |
|
||||
| "Connect this MCP server for our company" | No code change. The operator uses **Connect your own MCP server** or **Advanced → Paste a config**. Say so and stop. |
|
||||
| "My agent needs to use X" / "will this work self-hosted?" | `connect-agent-tools`. It owns the deployment decision, the smallest-path ladder, and the live proof. Come back here only if a catalog entry is genuinely the requirement. |
|
||||
| "Research whether we should integrate X" | The requester's research process. Inside Paperclip Content, `prepare-mcp-integration` owns the research package and its human gate — but the runbook is now self-contained, so an external contributor does **not** need it. |
|
||||
| "Open the connector PR" | Runbook Phase 12 plus the standard PR-preparation skill. This skill stops before any PR. |
|
||||
| "The provider has no MCP server" | Out of scope. A manifest cannot wrap an arbitrary REST API, run an arbitrary local command, or register an OAuth client in a console Paperclip does not control. Report the boundary. |
|
||||
| "Ship it" / "install it" / "release it" | Not authorized here. See **Authorization boundaries**. |
|
||||
|
||||
PR #13675 made the runbook self-contained so that "contributors can implement a
|
||||
connector without access to an internal issue tracker", and removed the separate
|
||||
private validation issue ("No separate private validation issue is required",
|
||||
`CONNECTOR-PLAYBOOK.md:1443`). Treat `prepare-mcp-integration` as an internal
|
||||
convenience for people working inside Paperclip Content, not as a gate a public
|
||||
contributor must pass. When it does apply, that skill drives and calls this one
|
||||
for the definition itself.
|
||||
|
||||
## Required Inputs
|
||||
|
||||
Collect these before step 1. Ask only for what you cannot determine safely.
|
||||
|
||||
1. **Provider name**, and the slug you intend to use (stable, lowercase,
|
||||
kebab-case).
|
||||
2. **App repository checkout and target branch.** Record the exact commit you
|
||||
read the contract from; reread it before you edit.
|
||||
3. **Who decided this provider should be in the catalog**, and on what basis.
|
||||
A public contributor's own reasoning, recorded in the PR, is sufficient —
|
||||
the runbook no longer requires a private research approval. Inside Paperclip
|
||||
Content, that decision is `prepare-mcp-integration`'s gate; if it applies and
|
||||
has not been taken, stop and route there.
|
||||
4. **Intended visibility**: store-visible, or connectable-but-hidden.
|
||||
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.
|
||||
6. **The deployment you will validate on.** An isolated self-hosted instance is
|
||||
the expected answer and is sufficient.
|
||||
|
||||
Refuse to guess an endpoint, a scope set, or a risk tier. Each one is a
|
||||
reviewable claim and must come from the provider's own metadata or documentation.
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
- **One provider per run.** Do not bundle two providers into one change.
|
||||
- **Do not edit the generated JSON as a source of truth.** The durable source is
|
||||
the generator script — with nine documented exceptions. See
|
||||
`references/catalog-contract.md`.
|
||||
- **Do not change global defaults.** Risk classification, the recommended access
|
||||
policy, and shared validators are not provider-local knobs. If the provider
|
||||
needs one changed, stop and say so.
|
||||
- **Do not touch unrelated providers.** Generated output renumbers on insertion;
|
||||
that is expected. Any *semantic* change to another provider is a mistake.
|
||||
- **Do not add server or UI code** until you have shown that the manifest cannot
|
||||
express the provider.
|
||||
|
||||
## Authorization Boundaries
|
||||
|
||||
- **Secrets stay in approved flows.** Never place a credential in the generator
|
||||
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.
|
||||
- **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.
|
||||
- **No credential grants.** Do not create, share, or widen a connection grant.
|
||||
- **No installs or releases.** Pushing, opening a PR, merging, deploying,
|
||||
installing a skill, or making a provider store-visible in a shipped build are
|
||||
all release-stage actions that require explicit human authorization. This
|
||||
skill ends at a verified local change.
|
||||
- **No roadmap claims.** Do not write "coming soon" or promise a future method.
|
||||
|
||||
## Step 1 — Discover The Current Contract
|
||||
|
||||
Read from the recorded commit, not from memory and not from a stale worktree:
|
||||
|
||||
- `doc/connections/CONNECTOR-PLAYBOOK.md` — the authoring runbook. Read its
|
||||
**five axes**, **reuse-path classification** (MCP-direct / OpenAPI-shim /
|
||||
vendor-deep-wrapper), **risk tiers S1–S4**, and the **production validation**
|
||||
evidence matrix in Step 9. Since PR #13675 that matrix lives in the runbook
|
||||
itself, and it is the list your evidence must answer.
|
||||
- `doc/connections/GENERIC-REMOTE-MCP.md` — the no-code baseline, so you can
|
||||
tell the requester when they do not need you. It also documents the OAuth
|
||||
client-resolution tiers and which of them need a public HTTPS origin.
|
||||
- `doc/connections/CHAT-CONNECTOR-UX.md` — **only if the provider is a chat or
|
||||
email provider.** It owns step navigation, the step-owned footer, credential
|
||||
instructions, provider handoffs, personal identity linking, the optional
|
||||
message test, and management states. Do not apply Slack's steps or credential
|
||||
types to a provider that does not work that way, and do not skip it when they
|
||||
apply. `connect-agent-tools/references/chat-and-email.md` is the short routing
|
||||
layer over it.
|
||||
- `doc/connections/README.md` — the identity/connections boundary. Every
|
||||
connector is a plane P2 resource credential, never a sign-in authenticator.
|
||||
Sign-in tokens are never reused as resource tokens; `id.paperclip.ing` never
|
||||
stores resource tokens.
|
||||
- `scripts/ingest-app-definitions.mjs` — the generator. Read its corpus guard,
|
||||
its `brandingFor`, its tuple mapper, and its `validateApp`.
|
||||
- `packages/shared/src/types/app-definition.ts` — the field contract.
|
||||
- `packages/shared/src/app-definitions.ts` — the slug allowlists and the
|
||||
recommended access defaults.
|
||||
- `packages/shared/src/app-definitions.test.ts` — the assertions your change
|
||||
must satisfy.
|
||||
|
||||
Then read `references/catalog-contract.md` in this skill for the file-by-file
|
||||
map and the assertions that carry exact counts. Treat that reference as a
|
||||
starting index, not as a substitute: if it disagrees with the commit you read,
|
||||
the commit wins, and you record the drift in your report.
|
||||
|
||||
## Step 2 — Probe The Provider, Read-Only
|
||||
|
||||
First establish where the MCP server runs and which process connects to it.
|
||||
For a desktop server, loopback means the connector runtime's machine, not the
|
||||
user's browser or a cloud worker. Record desktop/open-document prerequisites;
|
||||
do not invent hosted OAuth for a same-machine, unauthenticated server. If the
|
||||
current transport or egress policy cannot support it, report the runtime gap.
|
||||
|
||||
For OAuth providers, these unauthenticated requests inspect the transport and
|
||||
auth axes. Skip OAuth discovery for a provider that does not use OAuth:
|
||||
|
||||
```sh
|
||||
curl -s -i -X POST "$SERVER_URL" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
|
||||
# Read resource_metadata out of the WWW-Authenticate challenge, then:
|
||||
curl -s "$RESOURCE_METADATA_URL" # RFC 9728 -> authorization_servers
|
||||
curl -s "$ISSUER/.well-known/oauth-authorization-server" # RFC 8414
|
||||
```
|
||||
|
||||
Record, with the date you read it: the endpoint and its trailing-slash
|
||||
behaviour; the unauthenticated status and challenge; the protected-resource and
|
||||
authorization-server metadata URLs; the exact issuer; authorize, token,
|
||||
registration and revocation endpoints; whether `token_endpoint_auth_method:
|
||||
none` and PKCE `S256` are advertised; the provider's full `scopes_supported` and
|
||||
the narrower set you will request; documented prerequisites (plan, admin
|
||||
consent, preview enrolment, region, tenant identifier); and the revocation
|
||||
procedure.
|
||||
|
||||
Discard an authorization-server document whose `issuer` disagrees with the
|
||||
issuer used to build the URL.
|
||||
|
||||
Mark as **unprobed** anything you could not establish without registering or
|
||||
authenticating — the redirect-URI constraint usually falls here, because probing
|
||||
it properly means attempting registration. An unprobed constraint is omitted
|
||||
from the definition and reported as a gap, never guessed.
|
||||
|
||||
## Step 3 — Fix The Five Axes In Writing
|
||||
|
||||
Record all five before editing anything: transport, authentication, OAuth client
|
||||
ownership, credential source, grant identity. Then record the **reuse path** the
|
||||
runbook asks for — MCP-direct, OpenAPI-shim, or vendor-deep-wrapper — with the
|
||||
reason a lighter path is or is not enough. A provider name alone is not a
|
||||
classification: a read-only method and a method that sends messages or changes
|
||||
infrastructure can carry different risks and belong in different rows.
|
||||
|
||||
Then record the two judgements that reviewers will challenge:
|
||||
|
||||
- **Risk tier** (`S1`–`S4`), and why. Classify `S4` when the normal catalog
|
||||
includes payments, external sends, refunds, production deployment, deletion,
|
||||
or tenant-wide administration.
|
||||
- **Scope allowlist**, and what it costs. A narrow allowlist is the right
|
||||
default; state plainly which tools will fail at call time because of it.
|
||||
|
||||
If discovery worked, ship **only** `serverUrl` in `defaults`. A complete
|
||||
`authorizationEndpoint` + `tokenEndpoint` pair is authoritative and suppresses
|
||||
discovery permanently, including endpoints a previous discovery persisted. Ship
|
||||
that pair only for a provider that publishes no usable metadata, and say in your
|
||||
report that you now own keeping it current.
|
||||
|
||||
## Step 4 — Branding Before Generation
|
||||
|
||||
Generation fails closed without a branding row, so do this first. Obtain the
|
||||
official mark from the provider's brand kit, product site, or official
|
||||
repository; prefer SVG; never use a favicon proxy, a scraped icon, or an
|
||||
imitation. Add the asset and its manifest row, then run the two brand checks in
|
||||
`references/catalog-contract.md`.
|
||||
|
||||
If you cannot obtain an official mark, stop and report it. A placeholder is
|
||||
acceptable only in an explicitly labelled local fixture and must never be
|
||||
proposed for release.
|
||||
|
||||
## Step 5 — Author, Register, Generate
|
||||
|
||||
1. Add the provider to the generator source. Prefer the existing tuple shape and
|
||||
the existing `method()` helper over a hand-built object.
|
||||
2. Register the slug so the definition is reachable. A definition that generates
|
||||
cleanly but is absent from the slug allowlist is invisible in the product —
|
||||
this is the most common silent failure.
|
||||
3. Regenerate. Read the whole diff. Expect positional renumbering in the
|
||||
generated registry; reject any semantic change to another provider.
|
||||
|
||||
## Step 6 — Verify Offline, Then Report Honestly
|
||||
|
||||
Run the deterministic ladder in `references/catalog-contract.md`. When you must
|
||||
verify without mutating the checkout, build the isolated harness described in
|
||||
`references/offline-verification.md` — it runs the real generator, the real
|
||||
brand validators, and the real catalog tests against a copy, and includes a
|
||||
fidelity check that proves the harness reproduces the checked-in output
|
||||
byte-for-byte before you trust any result from it.
|
||||
|
||||
Then split your evidence into three explicit buckets and never blur them:
|
||||
|
||||
- **Executed** — the exact command and its real output.
|
||||
- **Source-inspected** — read and reasoned about, not run. Server, UI and
|
||||
browser suites usually land here when you have no instance.
|
||||
- **Not done** — including every account-bound step. A green catalog test is not
|
||||
provider validation, and a mocked OAuth test is not a live connect.
|
||||
|
||||
Then answer the runbook's nine production-validation scenarios — setup and
|
||||
consent, authentication, catalog and configuration, allowed execution, denied
|
||||
execution, runtime delivery, refresh and recovery, revoke and reconnect,
|
||||
activity and secret handling (`CONNECTOR-PLAYBOOK.md:1445-1455`) — with
|
||||
**pass / fail / not run / not applicable** and a reason for the last two, plus
|
||||
environment, method key, commit, and accessible redacted evidence. Offline
|
||||
authoring legitimately produces a column of "not run"; what it must never
|
||||
produce is a blank or an optimistic one.
|
||||
|
||||
Report those results per deployment, self-hosted first, using the four labels
|
||||
in `connect-agent-tools/references/deployment-support-matrix.md`. A pass on a
|
||||
self-hosted instance makes that row `verified` for self-hosted and leaves Cloud
|
||||
`untested`; the reverse is equally true.
|
||||
|
||||
## Optional Live Acceptance Handoff
|
||||
|
||||
Offline authoring does not certify the connector for production. When the user
|
||||
separately authorizes live testing, read
|
||||
[Live acceptance](references/live-acceptance.md). Otherwise hand that checklist
|
||||
to the tester and mark it not executed. The reference does not authorize grants,
|
||||
provider writes, runtime changes, deployment, or installation.
|
||||
|
||||
Keep four results separate: static validation, board Test calls, a real agent
|
||||
run through the managed connector, and verified provider-side output. Only the
|
||||
last two establish that an agent can complete the tested workflow.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
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 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
|
||||
OAuth). Record it as `unsupported` for self-hosted and hand it to product
|
||||
engineering; do not imply a self-hosted equivalent.
|
||||
- The provider requires a registration or approval that a customer cannot
|
||||
complete. Retain the research with an unavailable reason and expose no connect
|
||||
action.
|
||||
- No official artwork is obtainable.
|
||||
- The manifest cannot express the provider without new shared runtime code.
|
||||
- A test assertion would have to be weakened rather than updated.
|
||||
- A raw credential appears anywhere. Treat it as an incident: revoke and rotate,
|
||||
remove the evidence, trace every response, log and audit path, and add a
|
||||
canary regression test.
|
||||
- The change would need a global default, a risk classification, or a shared
|
||||
validator to move.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Deliver exactly this, and nothing that implies more:
|
||||
|
||||
1. **The change** — generator source edit, branding asset and manifest row, slug
|
||||
registration, regenerated output, and the one test-count update, as a diff or
|
||||
a working tree. No commits pushed, no PR.
|
||||
2. **A source map** — every file you changed and every claim you relied on, with
|
||||
the commit and the file:line evidence for each.
|
||||
3. **A provider evidence record** — endpoint, metadata URLs, issuer, advertised
|
||||
registration and PKCE support, scopes considered versus requested, risk tier
|
||||
with reasoning, and the date you read each one. No tool inventory you did not
|
||||
observe. No credentials.
|
||||
4. **Verification results** — executed, source-inspected and not-done, each with
|
||||
real command output for the executed set, plus the runbook's nine
|
||||
production-validation scenarios marked pass / fail / not run / not
|
||||
applicable.
|
||||
5. **A deployment support matrix** — self-hosted same-machine, self-hosted
|
||||
server/VPS, and Cloud, each capability labelled `verified`, `untested`,
|
||||
`unsupported` or `deferred` with its evidence or named follow-up owner. Use
|
||||
`connect-agent-tools/references/deployment-support-matrix.md` as the shape.
|
||||
6. **Remaining gaps** — unprobed constraints, the account-bound lifecycle, and
|
||||
anything a reviewer must authorize before release.
|
||||
7. **An explicit statement** of what did not happen: no push, no PR, no deploy,
|
||||
no install, no credential grant, no external write.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Tick each item only with evidence, and mark the ones you could not run:
|
||||
|
||||
- [ ] Endpoint and auth mode come from the provider's own metadata, dated.
|
||||
- [ ] Issuer in the authorization-server document matches the issuer used to
|
||||
fetch it.
|
||||
- [ ] `defaults` ships only `serverUrl` for a discovery-capable provider, or the
|
||||
hard-coded pair is justified and owned.
|
||||
- [ ] `scopesHint` is the reviewed minimum, not the discovered set.
|
||||
- [ ] Risk tier is justified against the provider's actual mutation surface.
|
||||
- [ ] `ownershipModes` reflects what the provider advertises; `dcr` omitted for
|
||||
a provider Paperclip must not auto-register.
|
||||
- [ ] Official artwork present, manifest row added, both brand checks pass.
|
||||
- [ ] Slug registered so the definition is connectable at the intended
|
||||
visibility.
|
||||
- [ ] Generator source and generated output are consistent in one change.
|
||||
- [ ] Generated diff contains no semantic change to another provider.
|
||||
- [ ] Catalog tests pass, with count assertions updated rather than weakened.
|
||||
- [ ] Required, non-advanced fields are the minimum, and any addition to the
|
||||
default-path allowlist is justified.
|
||||
- [ ] No credential value appears in any file, output, or report.
|
||||
- [ ] Reuse path (MCP-direct / OpenAPI-shim / vendor-deep-wrapper) is recorded
|
||||
with the reason a lighter path is or is not enough.
|
||||
- [ ] For a chat or email provider, `CHAT-CONNECTOR-UX.md` was read and applied;
|
||||
for every other provider it was correctly not applied.
|
||||
- [ ] All nine production-validation scenarios are marked pass / fail / not run
|
||||
/ not applicable, with reasons for the last two.
|
||||
- [ ] The deployment support matrix is filled in, self-hosted first, with the
|
||||
four labels and no Cloud row claimed as `verified` without Cloud evidence.
|
||||
- [ ] Account-bound lifecycle recorded as outstanding if it was not run.
|
||||
- [ ] Report states executed versus source-inspected versus not done.
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
# Catalog authoring contract
|
||||
|
||||
Index of the files and assertions a catalog-connector change has to satisfy.
|
||||
|
||||
**Verified against Paperclip App commit `e558f25e` (15 September 2026), by
|
||||
re-reading each cited file and by running the command ladder below through the
|
||||
isolated harness.** Previously verified at `728f7185` (14 September 2026); every
|
||||
claim below survived that move, but several line numbers did not, so this
|
||||
revision cites symbols and landmarks you can grep for and keeps line numbers
|
||||
only where they are load-bearing. If the commit you read disagrees, the commit
|
||||
wins — and record the drift in your report so this file gets corrected.
|
||||
|
||||
## Files a minimal store-visible connector touches
|
||||
|
||||
| File | Role | Editable? |
|
||||
| --- | --- | --- |
|
||||
| `scripts/ingest-app-definitions.mjs` | Human-authored provider source. The `apps` array starts at line 180; the tuple mapper follows it (grep for `schemaVersion: 1,` inside the `.map((`, around line 795). | Yes — this is the source. |
|
||||
| `ui/public/brands/apps/<slug>.svg` | Official mark. | Yes. |
|
||||
| `ui/public/brands/apps/manifest.json` | Branding provenance: slug, provider name, `catalogVisible`, `localAsset`, optional `darkAsset`, optional `aliases`. | Yes. |
|
||||
| `packages/shared/src/app-definitions.ts` | `CONNECTABLE_APP_SLUGS` (line 6) and `APP_STORE_HIDDEN_SLUGS` (line 44). | Yes. |
|
||||
| `packages/shared/src/types/app-definition.ts` | The field contract the generator output has to satisfy. | Read-only for authoring. |
|
||||
| `packages/shared/src/app-definitions/<slug>.json` | Generated definition. | **Generated** — see the exception below. |
|
||||
| `packages/shared/src/app-definitions.generated.ts` | Generated positional registry. | **Generated.** |
|
||||
| `packages/shared/src/app-definitions.ingestion-report.json` | Generated review report. | **Generated.** |
|
||||
| `packages/shared/src/app-definitions.test.ts` | Catalog assertions, including exact counts. | Yes — update, never weaken. |
|
||||
| `packages/shared/src/self-serve-mcp-research.json` | Dated research ledger. Membership implies connectability. | Only for that programme. |
|
||||
|
||||
The runbook's "shortest valid implementation" list omits
|
||||
`packages/shared/src/app-definitions.ts` and the ingestion report. Both are
|
||||
required for a store-visible provider.
|
||||
|
||||
## The generated-file exception
|
||||
|
||||
`scripts/ingest-app-definitions.mjs:1379-1396` (`reviewedGoogleSlugs`) reads nine
|
||||
definitions back from the output directory and re-emits them verbatim:
|
||||
|
||||
```js
|
||||
for (const slug of reviewedGoogleSlugs) {
|
||||
const existingIndex = apps.findIndex((app) => app.slug === slug);
|
||||
if (existingIndex >= 0) apps.splice(existingIndex, 1);
|
||||
apps.push(JSON.parse(fs.readFileSync(path.join(out, `${slug}.json`), "utf8")));
|
||||
}
|
||||
```
|
||||
|
||||
For `gmail` and the eight `google-*` slugs the JSON file **is** the maintained
|
||||
source. For every other provider it is generated output and editing it is a
|
||||
change the next run reverts.
|
||||
|
||||
## Generator preconditions
|
||||
|
||||
- **Corpus.** `scripts/ingest-app-definitions.mjs:4-9` resolves
|
||||
`PAPERCLIP_CONTENT_TEMPLATES`, defaulting to
|
||||
`../../paperclip-content/research/connections/vercel/templates`. Line 1541-1542
|
||||
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.
|
||||
- **Branding.** `brandingFor` (declared line 20, throws line 29) throws
|
||||
`<slug>: missing local branding provenance` unless the slug has a manifest row
|
||||
(only `oauth-generic` and `api-key-generic` are exempt). Branding precedes
|
||||
generation.
|
||||
- **Method invariants** (`validateApp`): `schemaVersion === 1`; non-empty
|
||||
`slug`, `name`, `methods`; an `api_key` tool method requires `keyPlacement`;
|
||||
an `oauth` method requires a non-empty `ownershipModes`; a required
|
||||
non-checkbox tenant/extension/credential field requires a `placeholder`.
|
||||
|
||||
## Tuple shape
|
||||
|
||||
`[slug, name, description, category, domain, urlPatterns, method|methods, extra?]`
|
||||
|
||||
- `category` is one of `ai`, `analytics`, `commerce`, `communication`,
|
||||
`content`, `data`, `developer`, `productivity`, `other`.
|
||||
- The 5th element (`domain`) is discarded by the mapper. `docsUrl` must go in
|
||||
`extra`, or it is only backfilled for providers that also appear in the
|
||||
research ledger (`ingest-app-definitions.mjs:1315`).
|
||||
- `method(key, transport, auth, defaults, riskTier, guidanceMd, extra)` fills
|
||||
`ownershipModes` as `["customer", "dcr"]` for `oauth` and `["customer"]`
|
||||
otherwise, plus a default `whenToUse`.
|
||||
- `featured` is a hard-coded six-slug list in the mapper. Do not add to it as
|
||||
part of authoring a new provider.
|
||||
|
||||
## Visibility chain
|
||||
|
||||
```txt
|
||||
APP_DEFINITIONS (generated, all providers)
|
||||
└─ filtered by CONNECTABLE_APP_SLUGS ─▶ CONNECTABLE_APP_DEFINITIONS
|
||||
└─ minus APP_STORE_HIDDEN_SLUGS ─▶ APP_STORE_DEFINITIONS ─▶ gallery API
|
||||
```
|
||||
|
||||
`GET /api/companies/:companyId/tools/apps` returns `APP_STORE_DEFINITIONS`
|
||||
directly (`server/src/routes/tool-access.ts:831`). There is no company-scoped
|
||||
definition store, and `connectToolAppSchema` accepts a `galleryKey` or a `link`
|
||||
and nothing else (`packages/shared/src/validators/tool-access.ts:472-474`). A
|
||||
catalog connector is therefore always a shared-source change.
|
||||
|
||||
## Assertions with exact counts or sets
|
||||
|
||||
These fail on any addition. Update them deliberately.
|
||||
|
||||
| Assertion | Location | What breaks |
|
||||
| --- | --- | --- |
|
||||
| `expect(APP_STORE_DEFINITIONS).toHaveLength(46)` | `app-definitions.test.ts:689`, in `it("withholds unverified and reserved providers …")` | Any new store-visible provider. Bump the count. |
|
||||
| `catalogVisible` manifest set must equal the store-visible definition set, and asset paths must equal `branding` values | same test, from line 716 (`manifest.providers.filter(… catalogVisible)`) | A manifest row without a definition, or the reverse. Also enforces PNG ≥ 128×128 and rejects script/`foreignObject`/event handlers in SVG. |
|
||||
| Required non-advanced tenant/extension fields enumerated in a short allowlist | `app-definitions.test.ts` around line 905 (`field.required && field.advanced !== true && !field.hidden`) | Any visible required field on the default path. Prefer making the field optional or advanced with a default. |
|
||||
| `expect(SELF_SERVE_MCP_CANDIDATES).toHaveLength(43)` | `app-definitions.test.ts:280` | Adding a provider to the research ledger. |
|
||||
| Ledger entry count with a fixed `verifiedAt` (`"2026-08-26"` at this commit) | `app-definitions.test.ts:434-435` | Same. Also requires HTTPS `docsUrl`/`serverUrl`, a non-empty `authMode`, a prerequisite longer than 10 characters, and a valid tier. |
|
||||
| `APP_STORE_HIDDEN_SLUGS` exact sorted list | `app-definitions.test.ts:666-688` | Hiding a provider. Hidden slugs must still be connectable. |
|
||||
| Method and field invariants across the whole catalog | `it("enforces method and field invariants")`, `app-definitions.test.ts:961` | A missing `keyPlacement`, empty `ownershipModes`, or a required credential field with no placeholder. |
|
||||
|
||||
The provider-slug membership check above those uses `arrayContaining`, so an
|
||||
addition does not break it. Confirmed at `e558f25e`: adding one store-visible
|
||||
provider failed exactly one assertion, the count at line 689.
|
||||
|
||||
## Network and deployment guard
|
||||
|
||||
A private, loopback or reserved MCP address is accepted only when the deployment
|
||||
is *not* both authenticated and publicly exposed; link-local egress is denied in
|
||||
every mode.
|
||||
|
||||
```ts
|
||||
// server/src/services/tool-access.ts:3175-3180 (and tool-gateway.ts:3132-3137)
|
||||
function allowPrivateRemoteEndpoints() {
|
||||
return (
|
||||
options.deploymentMode !== "authenticated" ||
|
||||
options.deploymentExposure !== "public"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`DEPLOYMENT_MODES` is `["local_trusted", "authenticated"]` and
|
||||
`DEPLOYMENT_EXPOSURES` is `["private", "public"]`
|
||||
(`packages/shared/src/constants.ts:4-8`). The check runs inside
|
||||
`guardedRemoteHttpFetch` at dial time rather than as a standalone pre-flight,
|
||||
which is what closes the DNS-rebinding window — so a same-machine desktop MCP
|
||||
endpoint is a local-deployment capability, not a configuration flag to widen.
|
||||
Report it as an unsupported deployment shape instead.
|
||||
|
||||
## Access defaults
|
||||
|
||||
```ts
|
||||
// packages/shared/src/app-definitions.ts:245-257
|
||||
export function recommendedDefaultsForApp(app, methodKey) {
|
||||
void app;
|
||||
void methodKey;
|
||||
return { access: "all_agents", askFirstRiskLevels: [] };
|
||||
}
|
||||
```
|
||||
|
||||
Uniform and open for every provider, method and tier: every discovered action is
|
||||
enabled and every active action defaults to **Allowed**, including `write` and
|
||||
`destructive`. This is an opt-in restriction model — finishing a connection is a
|
||||
configure-authorized, audited operation and **Ask first** stays available
|
||||
afterwards. Do not compensate by misclassifying a tool, and do not change this
|
||||
function in a provider change.
|
||||
|
||||
Changed-action quarantine activates when a *connection* sets
|
||||
`quarantineNewEntries`. It is not an `AppDefinition` field; declaring it in a
|
||||
manifest does nothing.
|
||||
|
||||
## Transport boundaries
|
||||
|
||||
| Transport | Manifest-only? | Boundary |
|
||||
| --- | --- | --- |
|
||||
| `mcp_remote` | Yes | First-class: discovery, health, catalog, gateway, OAuth, credential projection. |
|
||||
| `local_stdio` | Only via a registered template | Reported unsupported outside `local_trusted` mode or a configured trusted runtime host (`server/src/services/tool-access.ts:4788-4789`). Never put a bare command in a definition. |
|
||||
| `rest_api` | No | Not exposed through the connected MCP gateway. Needs an execution adapter first. |
|
||||
|
||||
`api_key` is an authentication mode, not a transport. Most API-key catalog
|
||||
entries authenticate a remote MCP server.
|
||||
|
||||
Header credentials and secret-bearing generated URLs have the complete generic
|
||||
runtime path. The schema also accepts `query`, `body_json` and `env` placements,
|
||||
but schema acceptance is not proof the gateway projects them — trace the
|
||||
invocation path and add an end-to-end fixture before shipping one.
|
||||
|
||||
## OAuth endpoint precedence
|
||||
|
||||
1. A **complete** `authorizationEndpoint` + `tokenEndpoint` pair in the method's
|
||||
`defaults` is used unconditionally. Discovery never runs, and endpoints
|
||||
stored on the connection and `401` challenge hints are not consulted at all.
|
||||
2. Otherwise, for `mcp_remote`, endpoints already stored on the connection (then
|
||||
the challenge hints, field by field) are used if they form a complete pair.
|
||||
3. Only then does the RFC 9728 → RFC 8414 discovery chain run.
|
||||
|
||||
So a complete manifest pair is authoritative and outlives its own accuracy.
|
||||
Ship `serverUrl` alone for a discovery-capable provider.
|
||||
|
||||
Client resolution order, independent of the above: deployment-preconfigured
|
||||
client, Client ID Metadata Document (needs a public HTTPS base URL), RFC 7591
|
||||
dynamic registration, then an operator-supplied client. For a curated method,
|
||||
`ownershipModes` gates only the curated path — omit `dcr` for a provider
|
||||
Paperclip must not auto-register.
|
||||
|
||||
## Command ladder
|
||||
|
||||
Deterministic, no vendor account:
|
||||
|
||||
```sh
|
||||
node scripts/check-app-brand-assets.mjs
|
||||
node --test scripts/app-brand-validation.test.mjs
|
||||
pnpm connections:ingest-app-definitions # honours PAPERCLIP_CONTENT_TEMPLATES
|
||||
pnpm exec vitest run \
|
||||
packages/shared/src/app-definitions.test.ts \
|
||||
packages/shared/src/app-definitions-url.test.ts
|
||||
pnpm --filter @paperclipai/shared typecheck
|
||||
```
|
||||
|
||||
Wider, when the change reaches server or UI code:
|
||||
|
||||
```sh
|
||||
pnpm exec vitest run \
|
||||
server/src/__tests__/tool-access-service.test.ts \
|
||||
server/src/__tests__/generic-mcp-connection.test.ts \
|
||||
server/src/__tests__/tool-connection-removal.test.ts \
|
||||
ui/src/pages/apps/AppsConnect.test.tsx \
|
||||
ui/src/pages/apps/Browse.test.tsx
|
||||
pnpm check:token-gates
|
||||
pnpm -r typecheck
|
||||
```
|
||||
|
||||
`server/src/__tests__/generic-mcp-connection.test.ts` stands up an in-process
|
||||
MCP server and authorization server, so the OAuth path is exercisable with no
|
||||
network and no credentials. Extend it with a fixture rather than reaching for a
|
||||
real account.
|
||||
|
||||
Diff review before handing off:
|
||||
|
||||
```sh
|
||||
git diff --check
|
||||
git status --short
|
||||
git diff -- scripts/ingest-app-definitions.mjs \
|
||||
packages/shared/src/app-definitions \
|
||||
packages/shared/src/app-definitions.generated.ts \
|
||||
ui/public/brands/apps
|
||||
```
|
||||
|
||||
`app-definitions.generated.ts` uses positional imports (`a0`, `a1`, …), so
|
||||
inserting one provider renumbers every later import — roughly 50 diff lines for
|
||||
a one-provider change. That is correct output, not churn to fix. Moving the
|
||||
tuple to the end of the tuple list does not avoid it, because several providers
|
||||
are appended to `apps` after that list.
|
||||
|
||||
## Account-bound lifecycle (not deterministic)
|
||||
|
||||
Required once per exposed method before a provider is store-ready, and outside
|
||||
this skill's authority without explicit approval: preflight on public metadata
|
||||
only; connect; catalog listing compared against reviewed expectations; one
|
||||
narrow read; write-classification check; one call through an actual run-scoped
|
||||
gateway; refresh and reconnect; revoke and confirm calls fail closed; reconnect
|
||||
after removal; and a credential scan across responses, logs, activity, audit and
|
||||
every evidence artifact.
|
||||
|
||||
Record only provider and method key, date, environment, endpoint origin and
|
||||
path, non-sensitive connection ID, tool names and counts, policy result,
|
||||
redacted outcome codes, and revoke/reconnect outcome. Never record token values,
|
||||
secret-bearing URLs, authorization codes, provider session detail, personal
|
||||
email, tenant content, HAR files, or pre-callback screenshots.
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
# Live connector acceptance
|
||||
|
||||
Use after offline authoring, only for a separately authorized test. Test one
|
||||
bounded workflow in an isolated instance and a disposable provider resource.
|
||||
Do not interpret a request to author a definition as permission to run writes.
|
||||
|
||||
## Preflight
|
||||
|
||||
- Record source commit, dirty changes, instance URL, process workspace, isolated
|
||||
database, provider endpoint, connection, agent, and required tools.
|
||||
- Establish the network path: hosted HTTP, same-machine desktop HTTP, or a
|
||||
supported local process. A laptop's loopback endpoint is not reachable as
|
||||
loopback from a cloud runtime. Never weaken shared egress rules to pass a test.
|
||||
- Check provider prerequisites such as a running desktop app and open document.
|
||||
Capture the effective tool inventory and permissions for the actual test agent;
|
||||
a catalog card or connected badge does not prove either.
|
||||
- Review instances may suppress execution. Inspect the current worktree run
|
||||
guard (historically `enableWorktreeRunExecution`) before diagnosing a queued
|
||||
task as a provider failure. Enabling it is a separate, isolated-instance
|
||||
action; keep unrelated agent schedules off and record any change for restoration.
|
||||
- Use existing narrowly authorized permissions. Ask the operator for missing
|
||||
grants; do not broaden global defaults or downgrade provider risk annotations.
|
||||
|
||||
## Prove the actual path
|
||||
|
||||
| Evidence | What it proves | What it does not prove |
|
||||
| --- | --- | --- |
|
||||
| Generator, schema and unit tests | Definition fits tested contracts | Provider connectivity |
|
||||
| Board Test call | That test endpoint can execute the action | Agent discovery or execution |
|
||||
| Fresh agent run with matching gateway audit | Agent used the managed connector | Correct final output |
|
||||
| Readback and provider-side inspection | Expected result exists and is usable | Untested lifecycle or providers |
|
||||
|
||||
Start a real, bounded agent task. Do not manufacture heartbeat rows or count a
|
||||
synthetic session as agent execution. Record terminal run state and correlate
|
||||
connection/tool invocation events with that exact run ID.
|
||||
|
||||
A direct provider plugin, raw API call, browser action, or unrelated MCP server
|
||||
can help diagnosis but cannot count as managed-connector acceptance. Inspect
|
||||
actual tool namespaces and gateway events, not just the assistant's claim.
|
||||
If an alternate plugin contaminates the test, exclude that run. With appropriate
|
||||
authorization, isolate it in the test profile, retry a fresh session, and restore
|
||||
the profile afterward. Do not disable the user's global plugins.
|
||||
|
||||
## Verify the result, not just success responses
|
||||
|
||||
For an authorized write, use an explicitly identified disposable resource. Save
|
||||
the returned resource ID and pass it to subsequent calls rather than relying on
|
||||
the active document. Read back expected content and counts. For visual output,
|
||||
inspect a rendered screenshot for actual content, missing assets and layout.
|
||||
|
||||
A successful screenshot call can precede rendering. Use a bounded read-only
|
||||
wait/recheck; a blank image is not acceptance. Stop after the test's declared
|
||||
timeout and report the gap. Before retrying any timed-out write, reconcile IDs
|
||||
and provider state to avoid duplicates. A local checkpoint is not an atomic
|
||||
transaction with the provider. Never delete existing user content as cleanup.
|
||||
|
||||
## Diagnose and test failures proportionally
|
||||
|
||||
Distinguish missing tool discovery, denied permission, provider error, transport
|
||||
timeout, and artifact-upload failure. A different profile ID alone does not
|
||||
establish stale configuration. Preserve the error and trace for the failed run;
|
||||
a later success does not prove its historical root cause.
|
||||
|
||||
Exercise applicable negative/lifecycle cases in the authorized sandbox:
|
||||
unknown tool, denied action, missing desktop/document, disconnect/reconnect,
|
||||
and OAuth refresh/revocation where supported. Mark inapplicable cases with a
|
||||
reason and untested cases as pending. Security-policy or shared-runtime changes
|
||||
need their own review; a local happy path does not clear those gates.
|
||||
|
||||
## Paper Desktop example: lessons from a local test
|
||||
|
||||
The September 2026 test used a same-machine HTTP MCP endpoint and an explicit
|
||||
new file. The board harness imported three images; a separate real agent run
|
||||
performed a bounded canvas write, readback and screenshot through the managed
|
||||
gateway. Keep those two proofs separate. An earlier run through the direct
|
||||
Paper plugin was excluded. An initially blank screenshot needed a later read.
|
||||
|
||||
For a new Paper test, recheck the official MCP docs and current tool schemas.
|
||||
Confirm desktop state, explicit file targeting, and that the provider process
|
||||
can read imported assets. Do not copy personal paths or file IDs. These findings
|
||||
do not certify cloud execution, other providers, or OAuth lifecycle behavior.
|
||||
|
||||
## Acceptance receipt
|
||||
|
||||
Report these fields with secret-free evidence links:
|
||||
|
||||
- Source commit and local changes; runtime/provider versions where available.
|
||||
- Instance, runtime placement and isolation boundary.
|
||||
- Approved workflow, resource scope and effective permissions.
|
||||
- Static checks, board calls, real run ID and correlated gateway invocations.
|
||||
- Expected versus observed readback; rendered evidence when relevant.
|
||||
- Negative/lifecycle checks: passed, failed, pending or not applicable.
|
||||
- Temporary configuration changes and restoration result.
|
||||
- Remaining release/security gates and what was not tested.
|
||||
|
||||
Provide a reproducible test with configurable IDs and documented prerequisites,
|
||||
not personal credentials or dependence on an undisclosed private corpus. Claim
|
||||
only the workflow and environment actually tested. Keep local acceptance,
|
||||
PR-ready, merged and deployed as separate states.
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
# Offline verification harness
|
||||
|
||||
Run the real catalog generator, the real brand validators, and the real catalog
|
||||
tests without mutating the App checkout. Use this when you must verify before
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
This used to ship as `scripts/make-harness.sh` inside the skill. It is inlined
|
||||
here on purpose: a skill package containing anything under `scripts/` derives
|
||||
the `scripts_executables` trust level (`deriveTrustLevel` in
|
||||
`packages/skills-catalog/src/catalog-builder.ts:763-767`), and the shipped
|
||||
catalog pins that set to exactly one key
|
||||
(`packages/skills-catalog/src/shipped-catalog.test.ts:132-136`). Carrying the
|
||||
recipe as documentation keeps this package `markdown_only` and installable
|
||||
without an audit-allowlist change. Save it to a file yourself and `chmod +x` it
|
||||
if you prefer to run it as a script.
|
||||
|
||||
## Build it
|
||||
|
||||
Save as `make-harness.sh` outside the App checkout, then:
|
||||
|
||||
```sh
|
||||
./make-harness.sh <app-repo> <commit-ish> <out-dir> <corpus-dir>
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><code>make-harness.sh</code></summary>
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# Build an isolated harness that runs the real catalog generator, the real brand
|
||||
# validators, and the real catalog tests against a copy of a pinned commit.
|
||||
#
|
||||
# Nothing in the App checkout is modified: files are exported with `git archive`
|
||||
# and `git show`, and node_modules is symlinked read-only.
|
||||
#
|
||||
# Usage:
|
||||
# make-harness.sh <app-repo> <commit-ish> <out-dir> <corpus-dir>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP_REPO=${1:?app repo path}
|
||||
COMMIT=${2:?commit-ish}
|
||||
OUT=${3:?output dir}
|
||||
CORPUS=${4:?ingestion corpus dir}
|
||||
|
||||
[ -d "$CORPUS" ] || { echo "corpus not found: $CORPUS" >&2; exit 1; }
|
||||
[ -d "$APP_REPO/node_modules" ] || { echo "install dependencies in $APP_REPO first" >&2; exit 1; }
|
||||
|
||||
SHA=$(git -C "$APP_REPO" rev-parse "$COMMIT")
|
||||
rm -rf "$OUT"; mkdir -p "$OUT/gen/scripts" "$OUT/vt/packages/shared" "$OUT/vt/ui/public/brands"
|
||||
echo "$SHA" > "$OUT/PINNED_COMMIT"
|
||||
|
||||
# --- generator harness -------------------------------------------------------
|
||||
for f in scripts/ingest-app-definitions.mjs \
|
||||
scripts/app-brand-validation.mjs \
|
||||
scripts/check-app-brand-assets.mjs \
|
||||
scripts/app-brand-validation.test.mjs; do
|
||||
git -C "$APP_REPO" show "$SHA:$f" > "$OUT/gen/$f"
|
||||
done
|
||||
git -C "$APP_REPO" archive "$SHA" ui/public/brands/apps | tar -x -C "$OUT/gen"
|
||||
git -C "$APP_REPO" archive "$SHA" \
|
||||
packages/shared/src/app-definitions \
|
||||
packages/shared/src/app-definitions.generated.ts \
|
||||
packages/shared/src/app-definitions.ingestion-report.json \
|
||||
packages/shared/src/app-definitions.ts \
|
||||
packages/shared/src/self-serve-mcp-research.json | tar -x -C "$OUT/gen"
|
||||
|
||||
# Pristine copies, so fidelity and change can be told apart.
|
||||
cp -r "$OUT/gen/packages/shared/src/app-definitions" "$OUT/gen/.baseline-definitions"
|
||||
cp "$OUT/gen/packages/shared/src/app-definitions.generated.ts" "$OUT/gen/.baseline-generated.ts"
|
||||
cp "$OUT/gen/packages/shared/src/app-definitions.ingestion-report.json" "$OUT/gen/.baseline-report.json"
|
||||
|
||||
cat > "$OUT/gen/verify-fidelity.sh" <<'FIDELITY'
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate from the pristine source and prove the harness reproduces the
|
||||
# checked-in output exactly. Run this BEFORE applying your own change.
|
||||
set -euo pipefail
|
||||
: "${PAPERCLIP_CONTENT_TEMPLATES:?point this at the ingestion corpus}"
|
||||
node scripts/ingest-app-definitions.mjs
|
||||
diff -rq .baseline-definitions packages/shared/src/app-definitions
|
||||
diff -q .baseline-generated.ts packages/shared/src/app-definitions.generated.ts
|
||||
diff -q .baseline-report.json packages/shared/src/app-definitions.ingestion-report.json
|
||||
echo "fidelity OK: harness reproduces the checked-in output byte-for-byte"
|
||||
FIDELITY
|
||||
chmod +x "$OUT/gen/verify-fidelity.sh"
|
||||
|
||||
# --- vitest harness ----------------------------------------------------------
|
||||
git -C "$APP_REPO" archive "$SHA" \
|
||||
packages/shared/src packages/shared/package.json packages/shared/tsconfig.json | tar -x -C "$OUT/vt"
|
||||
git -C "$APP_REPO" show "$SHA:tsconfig.base.json" > "$OUT/vt/tsconfig.base.json"
|
||||
ln -s "$(cd "$APP_REPO" && pwd)/node_modules" "$OUT/vt/node_modules"
|
||||
ln -s "$(cd "$APP_REPO" && pwd)/packages/shared/node_modules" "$OUT/vt/packages/shared/node_modules"
|
||||
|
||||
cat > "$OUT/vt/sync-from-gen.sh" <<'SYNC'
|
||||
#!/usr/bin/env bash
|
||||
# Copy the generator harness's current output into the vitest harness, so the
|
||||
# tests run against exactly what you generated.
|
||||
set -euo pipefail
|
||||
GEN=${1:?path to the gen harness}
|
||||
rm -rf ui/public/brands/apps && mkdir -p ui/public/brands
|
||||
cp -r "$GEN/ui/public/brands/apps" ui/public/brands/apps
|
||||
cp "$GEN/packages/shared/src/app-definitions.ts" packages/shared/src/app-definitions.ts
|
||||
cp "$GEN/packages/shared/src/app-definitions.generated.ts" packages/shared/src/app-definitions.generated.ts
|
||||
cp "$GEN/packages/shared/src/app-definitions/"*.json packages/shared/src/app-definitions/
|
||||
echo "synced from $GEN"
|
||||
SYNC
|
||||
chmod +x "$OUT/vt/sync-from-gen.sh"
|
||||
|
||||
echo "harness ready at $OUT (pinned $SHA)"
|
||||
echo " generator: $OUT/gen"
|
||||
echo " vitest: $OUT/vt/packages/shared"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
```text
|
||||
harness ready at /tmp/harness (pinned e558f25e99020b8ab762b91a8eabb0fe383f993f)
|
||||
generator: /tmp/harness/gen
|
||||
vitest: /tmp/harness/vt/packages/shared
|
||||
```
|
||||
|
||||
The output above is from a run at Paperclip App commit `e558f25e` on
|
||||
15 September 2026, Node v24.20.0. The harness was previously exercised at
|
||||
`728f7185`; the recipe did not need changing between the two.
|
||||
|
||||
Two roots, because they have different working directories:
|
||||
|
||||
- `gen/` — the generator runs from a repo-shaped root (`process.cwd()` is the
|
||||
root it reads `ui/public/brands/apps/manifest.json` from and writes
|
||||
`packages/shared/src/app-definitions/` into).
|
||||
- `vt/` — the vitest root. `app-definitions.test.ts` resolves `../../../ui/public`
|
||||
relative to its own file, so `vt/ui/public/brands/apps` has to exist.
|
||||
|
||||
## Prove the harness first
|
||||
|
||||
Never trust a harness you have not checked. Regenerate from the pristine source
|
||||
and confirm the output matches the checked-in files byte-for-byte:
|
||||
|
||||
```sh
|
||||
cd <out-dir>/gen
|
||||
PAPERCLIP_CONTENT_TEMPLATES=<corpus-dir> ./verify-fidelity.sh
|
||||
```
|
||||
|
||||
```text
|
||||
Parsed 99 captures and 179 states; emitted 70 Wave 1 definitions and flagged 63 states for review.
|
||||
fidelity OK: harness reproduces the checked-in output byte-for-byte
|
||||
```
|
||||
|
||||
The corpus is in the non-public `paperclip-content` repository. Without it the
|
||||
generator refuses to run and this harness cannot be built — report that as a
|
||||
blocked prerequisite rather than editing the guard.
|
||||
|
||||
If that diff is not empty, stop. Either the corpus is wrong or the harness is
|
||||
missing an input, and any later result is meaningless.
|
||||
|
||||
## Run your change through it
|
||||
|
||||
```sh
|
||||
cd <out-dir>/gen
|
||||
# edit scripts/ingest-app-definitions.mjs, ui/public/brands/apps/manifest.json,
|
||||
# packages/shared/src/app-definitions.ts, and add the brand asset
|
||||
node scripts/check-app-brand-assets.mjs
|
||||
node --test scripts/app-brand-validation.test.mjs
|
||||
PAPERCLIP_CONTENT_TEMPLATES=<corpus-dir> node scripts/ingest-app-definitions.mjs
|
||||
|
||||
cd <out-dir>/vt && ./sync-from-gen.sh <out-dir>/gen
|
||||
cd <out-dir>/vt/packages/shared
|
||||
node ../../node_modules/vitest/vitest.mjs run \
|
||||
src/app-definitions.test.ts src/app-definitions-url.test.ts
|
||||
```
|
||||
|
||||
Expect the count assertion to fail first, then update it and rerun. Real output
|
||||
from the Neon worked example at `e558f25e`:
|
||||
|
||||
```text
|
||||
Validated 70 brand identities: local paths, aliases and artwork safety.
|
||||
Parsed 99 captures and 179 states; emitted 71 Wave 1 definitions and flagged 63 states for review.
|
||||
|
||||
AssertionError: expected [ { schemaVersion: 1, …(8) }, …(46) ] to have a length of 46 but got 47
|
||||
❯ src/app-definitions.test.ts:689:35
|
||||
```
|
||||
|
||||
```text
|
||||
Test Files 2 passed (2)
|
||||
Tests 27 passed (27)
|
||||
```
|
||||
|
||||
Also diff the generated registry and confirm the only changes are positional.
|
||||
At this commit one inserted provider moved 125 lines of `a<N>` imports in
|
||||
`app-definitions.generated.ts` and changed nothing semantic in another
|
||||
provider:
|
||||
|
||||
```sh
|
||||
diff .baseline-generated.ts packages/shared/src/app-definitions.generated.ts \
|
||||
| grep '^[<>]' | grep -v 'a[0-9]' # must print nothing
|
||||
```
|
||||
|
||||
## What this harness does not cover
|
||||
|
||||
Be explicit about this in your report. The harness reaches the shared package
|
||||
only. It does not run:
|
||||
|
||||
- the server suites (`tool-access-service`, `generic-mcp-connection`,
|
||||
`tool-connection-removal`) — they need the server package and its fixtures;
|
||||
- the UI suites (`AppsConnect`, `Browse`) — they need the UI package;
|
||||
- `pnpm check:token-gates`, `pnpm -r typecheck`, `pnpm build`, `pnpm test:e2e`;
|
||||
- anything account-bound.
|
||||
|
||||
Source-inspecting those is a legitimate intermediate result. Reporting them as
|
||||
passed is not.
|
||||
|
||||
Above all: a green run here proves the definition fits the tested contracts. It
|
||||
is not provider validation, not an agent run, and not acceptance. See
|
||||
`references/live-acceptance.md` for the gate that is.
|
||||
|
||||
## Notes
|
||||
|
||||
- `node_modules/.bin/vitest` is a shell wrapper that Node cannot execute
|
||||
directly. Call `node_modules/vitest/vitest.mjs`.
|
||||
- `--reporter=basic` is not a valid reporter on vitest 4. Omit it.
|
||||
- The harness needs `tsconfig.base.json` at the harness root; the script exports
|
||||
it, because the shared package's tsconfig extends it and the transform fails
|
||||
without it.
|
||||
+491
@@ -0,0 +1,491 @@
|
||||
---
|
||||
name: connect-agent-tools
|
||||
description: >
|
||||
Get a Paperclip agent working with an outside tool, self-hosted or Cloud:
|
||||
start from the workflow and its proof, establish the deployment and its real
|
||||
reachability, take the smallest supported connection path, and verify a real
|
||||
agent run with provider-side readback. Not for catalog authoring.
|
||||
key: paperclipai/optional/software-development/connect-agent-tools
|
||||
recommendedForRoles:
|
||||
- engineer
|
||||
- operations
|
||||
tags:
|
||||
- mcp
|
||||
- connectors
|
||||
- integrations
|
||||
- connections
|
||||
- verification
|
||||
requires:
|
||||
- curl
|
||||
---
|
||||
|
||||
# Connect Agent Tools
|
||||
|
||||
Take "my agent needs to use X" to "an agent did the job through the managed
|
||||
connection, and here is the artifact on X's side".
|
||||
|
||||
This skill owns the **decision and the proof**. It does not own catalog
|
||||
mechanics; `author-connector-definition` does, and most runs never need it.
|
||||
|
||||
**Self-hosted Paperclip is the primary target.** Every step below works on an
|
||||
instance you run yourself, on your own machine or on your own server, with no
|
||||
Paperclip Cloud account, no managed Cloud OAuth client, and no Paperclip-operated
|
||||
infrastructure. Cloud is covered as a third deployment shape, not as the
|
||||
reference implementation. Where the two diverge, the divergence is written down
|
||||
in [`references/deployment-support-matrix.md`](references/deployment-support-matrix.md)
|
||||
with the guard or document behind it.
|
||||
|
||||
## Use This When
|
||||
|
||||
- An agent cannot reach a tool it needs, and nobody has established why.
|
||||
- Someone asks for "a connector for X" without having checked whether X is
|
||||
already connectable.
|
||||
- A connection exists, shows **Connected**, and the agent still cannot do the
|
||||
job.
|
||||
- Someone needs to know whether an integration will work on a server or in
|
||||
Paperclip Cloud, not just on their own computer.
|
||||
|
||||
## Do Not Use This When
|
||||
|
||||
| Request | Owner |
|
||||
| --- | --- |
|
||||
| "Add X to the Connectors catalog" and the catalog change is already approved | `author-connector-definition`. Come back here for the live proof. |
|
||||
| "Research whether we should integrate X" as a company decision | The requester's research process. This skill reads provider metadata; it does not own an integration decision. |
|
||||
| "Give agent Y access to the existing connection" | Operator action on the connection's grant and agent access. No new connection needed. |
|
||||
| "Build the MCP server" | Product engineering. Report the gap (step 5) and stop. |
|
||||
| "Ship it", "install it", "release it", "grant the credential" | Not authorized here. See **Authorization boundaries**. |
|
||||
|
||||
## Self-Hosted Quickstart
|
||||
|
||||
The smallest complete run, on an instance you host yourself. Use it as the
|
||||
worked shape for step 3 path 2; it needs no Paperclip Cloud account and no
|
||||
Paperclip code change.
|
||||
|
||||
**1. Establish the deployment.** Read `PAPERCLIP_DEPLOYMENT_MODE` and
|
||||
`PAPERCLIP_DEPLOYMENT_EXPOSURE` from the instance's configuration. Unset means
|
||||
`local_trusted`, and `local_trusted` forces `private`.
|
||||
|
||||
**2. Probe the provider read-only.** This is Neon's hosted MCP server, executed
|
||||
19 September 2026:
|
||||
|
||||
```sh
|
||||
curl -s -i -X POST "https://mcp.neon.tech/mcp" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
|
||||
```
|
||||
|
||||
```text
|
||||
HTTP/2 401
|
||||
www-authenticate: Bearer error="invalid_token", error_description="No authorization provided", resource_metadata="https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp"
|
||||
```
|
||||
|
||||
Follow the pointer, then the issuer's authorization-server document:
|
||||
|
||||
```sh
|
||||
curl -s "https://mcp.neon.tech/.well-known/oauth-protected-resource/mcp"
|
||||
curl -s "https://mcp.neon.tech/.well-known/oauth-authorization-server"
|
||||
```
|
||||
|
||||
```text
|
||||
resource = https://mcp.neon.tech/mcp
|
||||
authorization_servers = ["https://mcp.neon.tech"]
|
||||
|
||||
issuer = https://mcp.neon.tech
|
||||
registration_endpoint = https://mcp.neon.tech/api/register
|
||||
authorization_endpoint = https://mcp.neon.tech/api/authorize
|
||||
token_endpoint = https://mcp.neon.tech/api/token
|
||||
revocation_endpoint = https://mcp.neon.tech/api/revoke
|
||||
scopes_supported = ["read", "write"]
|
||||
code_challenge_methods_supported = ["S256"]
|
||||
token_endpoint_auth_methods_supported = ["client_secret_post", "client_secret_basic", "none"]
|
||||
client_id_metadata_document_supported = <absent>
|
||||
```
|
||||
|
||||
**3. Read the result.** The issuer matches the host used to build the URL, so
|
||||
the document is usable. A `registration_endpoint` plus `none` and `S256` means
|
||||
the DCR tier applies — and DCR is instance-local, so this works identically on a
|
||||
self-hosted instance and on Cloud. CIMD is not advertised, so the public-HTTPS
|
||||
requirement that tier carries is irrelevant here. **No Cloud account, no managed
|
||||
OAuth client, and no public origin are needed for this provider.**
|
||||
|
||||
**4. Connect it.** The operator uses **Connect your own MCP server** and pastes
|
||||
`https://mcp.neon.tech/mcp`. No catalog entry, no code change. Start writes at
|
||||
**Off**.
|
||||
|
||||
**5. Prove it.** Step 5 of this skill. A **Connected** badge is not the proof;
|
||||
a real agent run with provider-side readback is.
|
||||
|
||||
What this quickstart claims, precisely. The probe output above is executed
|
||||
against Neon. The **connect and consent for Neon specifically** are `untested`
|
||||
here, because OAuth consent needs an authorized Neon account.
|
||||
|
||||
The rest of the shape is not hypothetical. The same no-code path — connect by
|
||||
pasted URL, catalog discovery, effective policy, a real agent run correlated to
|
||||
its run ID, provider-side readback, revoke and reconnect, credential redaction —
|
||||
is **`verified` on self-hosted**, on two isolated instances, against a loopback
|
||||
MCP server and against DeepWiki, a real third-party public server. Run IDs and
|
||||
provider-side artifacts are in
|
||||
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md).
|
||||
What that buys you is confidence in the path, not in your provider: your run
|
||||
still has to produce its own proof, and the whole OAuth column remains
|
||||
`untested` on every deployment shape.
|
||||
|
||||
## Required Inputs
|
||||
|
||||
Collect these before step 1. Ask for what you cannot establish safely.
|
||||
|
||||
1. **The workflow.** What the agent should accomplish, in one sentence.
|
||||
2. **The proof.** The artifact you will open on the provider's side afterwards.
|
||||
If the requester cannot name one, the workflow is still being designed — say
|
||||
so and stop there.
|
||||
3. **Deployment shape.** Where this Paperclip runs: the same computer as the
|
||||
provider, a server or VPS, or Paperclip Cloud. Ask; do not infer it from the
|
||||
URL someone pasted in a browser.
|
||||
4. **Provider identity** and its official documentation URL. Not a search
|
||||
result, not a third-party list.
|
||||
5. **Who can authorize what.** Which operator can finish a connection, and
|
||||
whether any live write has been authorized. Absent that, this run ends at a
|
||||
read-only proof.
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
- **One tool, one workflow per run.** Breadth after the first real success.
|
||||
- **Do not weaken a guard to make a test pass.** The private-endpoint check, the
|
||||
link-local denial, the transport rules and the risk annotations are the
|
||||
product's boundaries, not obstacles.
|
||||
- **Do not widen permissions or grants.** Use the narrow authorization you were
|
||||
given and name what is missing.
|
||||
- **Do not change global defaults**, shared validators or risk classification.
|
||||
- **Do not add server or UI code** before showing that no supported path exists.
|
||||
|
||||
## Authorization Boundaries
|
||||
|
||||
- **Secrets stay in approved flows.** Never place a credential in a config file,
|
||||
a comment, a screenshot, a trace, a HAR file, a fixture or a report. If you
|
||||
are handed one, propose it as a Paperclip secret and never echo it.
|
||||
- **Unauthenticated metadata reads are research.** Dynamic client registration,
|
||||
consent, and any write tool call are not. They need explicit authorization.
|
||||
- **No credential grants.** Do not create, share or widen a grant.
|
||||
- **No installs, deploys, pushes, PRs or releases.**
|
||||
- **No new provider writes or runtime changes** without their own authorization,
|
||||
even when a read-only proof succeeded.
|
||||
- **No roadmap claims.** Do not promise a future transport or a Cloud
|
||||
capability that has not shipped.
|
||||
|
||||
## Step 1 — Write Down The Workflow And The Proof
|
||||
|
||||
Two sentences, recorded in the task before anything else:
|
||||
|
||||
- The job: "the on-call agent opens a database branch before a migration test".
|
||||
- The proof: "the branch is visible in the provider's console, and the agent's
|
||||
run shows the tool call that created it".
|
||||
|
||||
Everything below is in service of the second sentence. A **Connected** badge, a
|
||||
catalog card and a green test suite are not it.
|
||||
|
||||
## Step 2 — Establish Where This Runs
|
||||
|
||||
Two locations, routinely conflated: where Paperclip's runtime executes the call,
|
||||
and where the provider's server is. Only the runtime's view of the network
|
||||
counts.
|
||||
|
||||
Name one of three deployment shapes. Do not infer it from a product tier, from
|
||||
a URL someone pasted in a browser, or from the absence of an error — read the
|
||||
instance's actual configuration.
|
||||
|
||||
| Shape | Typical config | Call originates from | Check |
|
||||
| --- | --- | --- | --- |
|
||||
| **A. Self-hosted, same machine** | `PAPERCLIP_DEPLOYMENT_MODE=local_trusted` | That computer | Can the runtime reach the service *and* the files it needs? Desktop app running, document open, correct user session. |
|
||||
| **B. Self-hosted, server or VPS** | `authenticated`, plus `private` or `public` exposure | That server | Is the endpoint reachable from the server, not merely from the requester's browser? Does the instance have a public HTTPS origin, and does anything here actually need one? |
|
||||
| **C. Paperclip Cloud** | hosted, `authenticated` + `public` | A hosted environment | Is the integration supported and tested from the Cloud runtime? Nothing on anyone's laptop is reachable. |
|
||||
|
||||
Shapes A and B are the primary path. Work them first and completely; treat C as
|
||||
a separate deployment that needs its own evidence.
|
||||
|
||||
Self-hosted does not imply desktop-compatible. A locally launched MCP server
|
||||
that only calls a vendor's hosted API is portable; one that drives an open
|
||||
desktop app, local files or a private network needs those where the runtime is.
|
||||
|
||||
Two configuration facts decide most of what follows, both at App commit
|
||||
`9335b7db10`:
|
||||
|
||||
- `local_trusted` **forces** exposure to `private`
|
||||
(`server/src/config.ts:187-190`). A shape-A instance can never reach the
|
||||
`authenticated` + `public` combination that the private-endpoint guard
|
||||
refuses.
|
||||
- Paperclip refuses private, loopback and reserved MCP addresses only on a
|
||||
deployment that is both authenticated and publicly exposed
|
||||
(`allowPrivateRemoteEndpoints` at `server/src/services/tool-access.ts:3195-3200`,
|
||||
mirrored in `tool-gateway.ts:3135-3140`), and refuses link-local addresses in
|
||||
every mode (`server/src/services/remote-http-endpoint-guard.ts`).
|
||||
|
||||
Read those at the commit in front of you and record it. Never propose a tunnel,
|
||||
a proxy or an exception to get around the guard.
|
||||
|
||||
**An agent cannot read its own deployment's support matrix, but an operator
|
||||
always can.** `GET /api/companies/:companyId/tools/runtime-health` returns the
|
||||
live `supportMatrix` for the instance and requires board access: a run-scoped
|
||||
agent credential gets `403 {"error":"Board access required"}`, while a board
|
||||
actor on an instance they own reads it fine (both executed 19 September 2026;
|
||||
route at `server/src/routes/tool-access.ts:2500-2504`). So if you are the agent,
|
||||
ask the operator to run it and paste the `supportMatrix` block, or read the
|
||||
configuration directly. Do not guess it, and do not report the `403` as though
|
||||
the surface were unavailable — it is available to the person you are asking.
|
||||
|
||||
Label every capability you establish **verified**, **untested**, **unsupported**
|
||||
or **deferred**, each with its reason. Fill in a copy of
|
||||
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md)
|
||||
rather than inventing your own labels. A local success is never Cloud evidence,
|
||||
and Cloud success is never self-hosted evidence.
|
||||
|
||||
If the provider is a chat or email provider, read
|
||||
[`references/chat-and-email.md`](references/chat-and-email.md) now — the inbound
|
||||
delivery mechanism, not the deployment tier, decides whether a public HTTPS
|
||||
origin is a real prerequisite.
|
||||
|
||||
## Step 3 — Take The Smallest Supported Path
|
||||
|
||||
Work down. Stop at the first path that does the job. The runbook states the
|
||||
governing principle: "A catalog entry is a convenience layer, not a
|
||||
prerequisite. An operator can connect any standards-compliant remote HTTP MCP
|
||||
server from **Connect your own MCP server** or **Paste a config** with no
|
||||
Paperclip code change at all — including servers that need browser sign-in."
|
||||
(`CONNECTOR-PLAYBOOK.md:23-26`.)
|
||||
|
||||
1. **An existing connector.** Check the catalog and the provider pages first. If
|
||||
a connector exists and the workflow still fails, this is a configuration,
|
||||
grant or permission problem — diagnose that, do not build.
|
||||
2. **A compatible MCP endpoint, connected by URL.** No code change. All of these
|
||||
must hold: an `http`/`https` MCP endpoint; reachable from the runtime;
|
||||
authentication in a shape Paperclip drives (OAuth advertised through RFC 9728
|
||||
/ RFC 8414 metadata, a key presented as a header, a provider-generated
|
||||
secret-bearing URL, or none); and an OAuth client Paperclip can obtain
|
||||
(preconfigured, CIMD, dynamic registration, or one the operator registered).
|
||||
Name which condition failed rather than improvising.
|
||||
|
||||
**None of those four conditions needs Paperclip Cloud.** DCR is
|
||||
instance-local: each instance registers its own public client against its own
|
||||
`/api/tools/oauth/callback`, and "Cloud-hosted and self-hosted instances use
|
||||
the SAME path — the only per-instance difference is the hostname inside the
|
||||
redirect URI" (`CONNECTOR-PLAYBOOK.md:1569-1578`). CIMD is the one tier that
|
||||
needs a public HTTPS `PAPERCLIP_PUBLIC_URL`, and a deployment without one
|
||||
falls through to DCR rather than failing. A self-hoster whose provider cannot
|
||||
do DCR registers their own client and sets
|
||||
`PAPERCLIP_TOOL_OAUTH_<PROVIDER>_CLIENT_ID` / `_SECRET`. The only genuinely
|
||||
Cloud-gated path is the curated `platform_shared` Paperclip-managed OAuth
|
||||
profile; if that is the only option, say so and stop rather than implying a
|
||||
self-hosted equivalent exists.
|
||||
3. **A catalog definition.** Only when a *reusable, branded, reviewed* entry is
|
||||
the actual requirement, and only with the App repository and its ingestion
|
||||
corpus available. This is a shared-source change that ends in review and a
|
||||
release, not a connection anyone can use today. Hand it to
|
||||
`author-connector-definition` and come back here for the proof. If the
|
||||
repository or corpus is unavailable, say so plainly — path 2 is not a lesser
|
||||
substitute for it, and the reverse is also true.
|
||||
4. **No supported transport or network path.** Go to step 5.
|
||||
|
||||
Before any provider round-trip, probe read-only and record the date:
|
||||
|
||||
```sh
|
||||
curl -s -i -X POST "$SERVER_URL" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Accept: application/json, text/event-stream' \
|
||||
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}'
|
||||
# For an OAuth challenge, follow resource_metadata, then the issuer's
|
||||
# authorization-server document. Discard a document whose issuer disagrees with
|
||||
# the issuer used to build the URL.
|
||||
```
|
||||
|
||||
Record the endpoint and its trailing-slash behaviour, the unauthenticated
|
||||
status and challenge, the metadata URLs, the exact issuer, whether registration
|
||||
and PKCE `S256` are advertised, the provider's full `scopes_supported` and the
|
||||
narrower set you will request, documented prerequisites (plan, admin consent,
|
||||
desktop app, open document, region, tenant), and the revocation procedure.
|
||||
Mark as **unprobed** anything that would require registering or authenticating,
|
||||
and never guess it.
|
||||
|
||||
## Step 4 — Configure Within Authorization
|
||||
|
||||
Have the authorized operator finish the connection. Then, before claiming
|
||||
anything:
|
||||
|
||||
- Start writes at **Off** on a server nobody has reviewed and promote
|
||||
deliberately. Re-run **Refresh actions** after the server changes.
|
||||
|
||||
**Then re-check the profile, because promotion is not durable.** At App commit
|
||||
`9335b7db10`, a connection created from a pasted URL is flagged
|
||||
`unverifiedServer: true` and `quarantineNewEntries: false` in the same
|
||||
expression (`server/src/services/tool-access.ts:12721`), so every catalog
|
||||
refresh auto-allows whatever new tools the provider has started advertising —
|
||||
including ones classified `destructive`. Verified by execution; see **F1** in
|
||||
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md).
|
||||
On a server you do not control, the tool set is provider-controlled, not
|
||||
operator-controlled. Say that out loud in your report.
|
||||
|
||||
- **Check that the connection is installed on the agent, not merely permitted.**
|
||||
Finishing the wizard with `access: {agentIds: [...]}` writes the tool-profile
|
||||
binding but no install row, and readiness needs both
|
||||
(`server/src/services/connection-intents.ts:241-248`). The symptom is a green
|
||||
**Connected** badge and an allowed tool the agent cannot see. Verified on two
|
||||
instances — **F10** in the same reference.
|
||||
|
||||
```sh
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE/api/tool-connections/$CONNECTION_ID/installs"
|
||||
# {"installs": []} means the agent still cannot use it, whatever the badge says.
|
||||
```
|
||||
|
||||
- Check the *effective* tool inventory and permissions of the agent that will
|
||||
actually run the workflow. A connected badge proves neither.
|
||||
- Check that the instance executes tasks at all. A review or worktree instance
|
||||
can suppress execution, which looks exactly like a provider failure.
|
||||
- If a self-hosted agent run fails on *everything* with `401` or
|
||||
`getaddrinfo ENOTFOUND`, suspect the runtime's environment before the
|
||||
connector. A Paperclip server started from inside another agent's run inherits
|
||||
that run's `PAPERCLIP_API_URL` / `PAPERCLIP_API_KEY` and hands them to the
|
||||
agents it spawns; and on shape B, `auth.publicBaseUrl` is what spawned agents
|
||||
get as their control-plane URL, so it has to resolve from the runtime's own
|
||||
machine.
|
||||
|
||||
## Step 5 — Verify A Real Result
|
||||
|
||||
Four kinds of evidence, in increasing strength. Never blur them.
|
||||
|
||||
| Evidence | Proves | Does not prove |
|
||||
| --- | --- | --- |
|
||||
| Generator, schema and unit tests | The definition fits tested contracts | Anything about the provider |
|
||||
| A board **Test** call | That the endpoint can execute that action *for the board* | Anything at all about the agent — see below |
|
||||
| The policy simulator with `actorType: "agent"` | What the agent's policy decides | That the call would actually succeed |
|
||||
| A real agent run, with gateway audit events correlated to that run ID | An agent used the managed connector | That the output is correct |
|
||||
| Provider-side readback of the artifact | The expected result exists and is usable | Any other workflow, provider or deployment |
|
||||
|
||||
Only the last two answer the original question.
|
||||
|
||||
**The board Test call is labelled with an agent and does not run as one.**
|
||||
`executeTestCall` builds the session with `actorType: "user"` and the board
|
||||
user's ID, and agent tool profiles bind to agents. Executed 19 September 2026:
|
||||
testing as an *ungranted* agent returned `HTTP 200 {"decision":"allowed"}` and
|
||||
the read succeeded; testing a write the agent's policy denied also returned
|
||||
`allowed` — **and created the file on the provider.** So Test produces false
|
||||
passes in one direction and unintended writes in the other. Use
|
||||
`POST /api/companies/{c}/tools/policy/test` with `actorType: "agent"` for
|
||||
permission questions, and a real run for execution questions. Detail: **F2** in
|
||||
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md).
|
||||
|
||||
1. Start a real, bounded agent task — the workflow from step 1. Do not
|
||||
manufacture heartbeat rows and do not count a synthetic session.
|
||||
2. Correlate connection and tool-invocation events with that exact run ID, and
|
||||
inspect the tool namespaces actually used. An assistant's claim that it used
|
||||
a tool is not evidence.
|
||||
3. Read the artifact back on the provider. For visual output, inspect the
|
||||
rendered image for real content — a screenshot call can return success before
|
||||
rendering finishes, and a blank image is not acceptance. Use a bounded
|
||||
read-only recheck, then report the gap rather than waiting indefinitely.
|
||||
4. For an authorized write, target an explicitly disposable resource, save the
|
||||
returned resource ID and pass it to later calls instead of relying on "the
|
||||
active document". Reconcile provider state before retrying a timed-out write;
|
||||
a local checkpoint is not an atomic transaction with the provider. Never
|
||||
delete existing user content as cleanup.
|
||||
|
||||
A direct provider plugin, a raw API call, a browser action or an unrelated MCP
|
||||
server reaching the same service can help you diagnose and can never count as
|
||||
acceptance. If one contaminates a run, exclude the run.
|
||||
|
||||
Then exercise the applicable negative and lifecycle cases in the authorized
|
||||
sandbox. Do not invent your own list: the runbook's **production validation**
|
||||
section is the canonical one, and PR #13675 moved it into the runbook precisely
|
||||
so contributors no longer need a private validation issue for it. Its nine
|
||||
scenarios are setup and consent, authentication, catalog and configuration,
|
||||
allowed execution, denied execution, runtime delivery, refresh and recovery,
|
||||
revoke and reconnect, and activity and secret handling
|
||||
(`CONNECTOR-PLAYBOOK.md:1445-1455`).
|
||||
|
||||
Record **pass**, **fail**, **not run** or **not applicable** per scenario with a
|
||||
reason for the last two, plus environment, method key, commit, reproduction
|
||||
steps, expected and actual result, and accessible redacted evidence. Then map
|
||||
each result into the four deployment labels in
|
||||
[`references/deployment-support-matrix.md`](references/deployment-support-matrix.md):
|
||||
a scenario that passed on your self-hosted instance makes that row `verified`
|
||||
**for that deployment only** and leaves the other columns `untested`.
|
||||
|
||||
**Run the self-hosted lifecycle first and completely.** Cloud parity is
|
||||
follow-on work, and an untested Cloud column is `untested` — never `verified`
|
||||
by analogy with a self-hosted pass. One happy path does not clear a security or
|
||||
shared-runtime review.
|
||||
|
||||
When the path is path 3 and live testing has been separately authorized, the
|
||||
`author-connector-definition` skill's `references/live-acceptance.md` is the
|
||||
longer checklist for the same gate; use it rather than restating it.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
Stop and report rather than working around any of these:
|
||||
|
||||
- The requester cannot name the artifact that would prove success.
|
||||
- The provider has no MCP endpoint, or needs a transport with no execution path.
|
||||
- The endpoint is only reachable from somewhere the runtime is not.
|
||||
- A guard would have to be widened, a tunnel opened, a global default changed,
|
||||
or a risk annotation downgraded.
|
||||
- The provider requires a registration or approval the customer cannot
|
||||
complete.
|
||||
- Path 3 is the real requirement and the App repository or its ingestion corpus
|
||||
is unavailable to you.
|
||||
- A live write is needed and has not been authorized.
|
||||
- A raw credential appears anywhere. Treat it as an incident: revoke and rotate,
|
||||
remove the evidence, trace every response, log and audit path, and add a
|
||||
canary regression test.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Deliver exactly this, and nothing that implies more:
|
||||
|
||||
1. **The workflow and the proof**, as written in step 1.
|
||||
2. **A filled deployment support matrix**, in the shape of
|
||||
`references/deployment-support-matrix.md`: a row per capability, a column per
|
||||
deployment shape you care about, and every cell labelled `verified`,
|
||||
`untested`, `unsupported` or `deferred` with the guard, provider fact,
|
||||
executed command or named follow-up owner behind it, plus the commit or
|
||||
document date you read it from. Self-hosted columns come first and are
|
||||
expected to be the most complete.
|
||||
3. **The path taken**, and why the smaller ones were ruled out.
|
||||
4. **A provider evidence record** — endpoint, metadata URLs, issuer, advertised
|
||||
registration and PKCE support, scopes considered versus requested,
|
||||
prerequisites, and the date of each read. No credentials. No tool inventory
|
||||
you did not observe.
|
||||
5. **Evidence, in the four separate buckets** of step 5, with the real run ID
|
||||
and correlated invocations for anything you call an agent run.
|
||||
6. **Negative and lifecycle results**: passed, failed, pending, not applicable.
|
||||
7. **Remaining gaps** — unprobed constraints, untested deployments, and anything
|
||||
a reviewer must authorize before this is relied on.
|
||||
8. **An explicit statement** of what did not happen: no grant, no install, no
|
||||
deploy, no push, no PR, no unauthorized write, no configuration left changed.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Tick each item only with evidence, and mark the ones you could not run:
|
||||
|
||||
- [ ] The workflow and its provider-side proof are written down.
|
||||
- [ ] Runtime location and provider location are both established, not assumed.
|
||||
- [ ] The deployment shape (A, B or C) is named from actual configuration, not
|
||||
from a product tier.
|
||||
- [ ] The reachability verdict cites the current guard or provider document,
|
||||
with a date or commit.
|
||||
- [ ] The support matrix is filled in, self-hosted columns first, with every
|
||||
cell labelled `verified` / `untested` / `unsupported` / `deferred`.
|
||||
- [ ] No step of the delivered workflow requires a Cloud account, a managed
|
||||
Cloud OAuth client, or private infrastructure — or, if one does, that
|
||||
step is labelled `unsupported` for self-hosted rather than assumed away.
|
||||
- [ ] The smallest supported path was taken, and the rejected ones are named.
|
||||
- [ ] Provider metadata was read unauthenticated and dated; nothing was
|
||||
registered during research.
|
||||
- [ ] Requested scopes are the reviewed minimum, with the cost stated.
|
||||
- [ ] The acting agent's effective tools and permissions were inspected.
|
||||
- [ ] Instance execution was confirmed before any provider was blamed.
|
||||
- [ ] A real agent run exists, with audit events correlated to its run ID.
|
||||
- [ ] The artifact was read back on the provider and inspected, not inferred
|
||||
from a success response.
|
||||
- [ ] Negative and lifecycle cases are recorded with per-case status.
|
||||
- [ ] No credential value appears in any file, output or report.
|
||||
- [ ] Temporary configuration changes are listed and restored.
|
||||
- [ ] The report separates executed, source-inspected and not done.
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
# Chat and email connections
|
||||
|
||||
Read this **only when the provider is a chat or email provider** — Slack,
|
||||
Discord, Telegram, AgentMail, and others with comparable capabilities. Most
|
||||
connector work never needs it, and imposing Slack's steps on a provider that
|
||||
does not work that way is one of the failure modes the guide exists to prevent.
|
||||
|
||||
The authority is
|
||||
[`doc/connections/CHAT-CONNECTOR-UX.md`](https://github.com/paperclipai/paperclip/blob/master/doc/connections/CHAT-CONNECTOR-UX.md),
|
||||
added by PR #13675 and merged at `c9e867797939fe069278c4ae660f86f7692ed866`.
|
||||
Read it at the commit you are working against and record that commit. This page
|
||||
is the routing layer and the self-hosted reachability rules; it is not a
|
||||
substitute for the guide.
|
||||
|
||||
## Three things that are routinely conflated
|
||||
|
||||
Keep them separate in your plan, your UI, and your evidence. The guide is
|
||||
explicit that they establish different things.
|
||||
|
||||
| Concern | Question it answers | Failure if merged |
|
||||
| --- | --- | --- |
|
||||
| **Resource setup** | Is the provider app/bot/mailbox created, credentialed, and delivering to this instance? | A working bot that no individual can actually address. |
|
||||
| **Personal identity linking** | Which Paperclip person is this external account? | Someone inherits another person's authority by sending a message. |
|
||||
| **Company membership** | Should this external person have any authority here at all? | A stranger in a shared channel acts as a member. |
|
||||
|
||||
New connections deny unlinked people by default; linking requires ownership
|
||||
confirmation; nonmembers request access before receiving authority
|
||||
(`CHAT-CONNECTOR-UX.md#apply-and-verify`). An optional message test must never
|
||||
become an unexplained completion gate.
|
||||
|
||||
## Establish the delivery mechanism before you promise a prerequisite
|
||||
|
||||
This is the step that decides whether a self-hoster can use the provider at all.
|
||||
Ask the provider's own documentation, not another provider's wizard:
|
||||
|
||||
> Whether inbound delivery needs a public HTTPS callback, an outbound socket,
|
||||
> polling, or another mechanism. — `CHAT-CONNECTOR-UX.md:18-22`
|
||||
|
||||
Then apply the deployment matrix in
|
||||
[`deployment-support-matrix.md`](./deployment-support-matrix.md):
|
||||
|
||||
| Mechanism | Self-hosted, same machine | Self-hosted, server/VPS | Paperclip Cloud |
|
||||
| --- | --- | --- | --- |
|
||||
| Public HTTPS callback | `unsupported` without a public origin | Works only with a genuinely public HTTPS origin | Cloud supplies HTTPS |
|
||||
| Outbound socket | No public origin needed | No public origin needed | Supported |
|
||||
| Polling | No public origin needed | No public origin needed | Supported |
|
||||
|
||||
Two rules the guide states and this skill enforces:
|
||||
|
||||
- **Having HTTPS is not being reachable.** "A private-network HTTPS URL alone is
|
||||
not proof of public reachability" (`CHAT-CONNECTOR-UX.md:77-79`). A tailnet
|
||||
certificate does not satisfy a provider that has to POST to you from its own
|
||||
infrastructure. Verify with observed inbound traffic, not with a green
|
||||
padlock.
|
||||
- **Do not require a public URL for a connector that does not use public
|
||||
callbacks** (`CHAT-CONNECTOR-UX.md:81-82`). Showing a public-origin
|
||||
prerequisite on a socket-based provider tells a self-hoster their deployment
|
||||
is unsupported when it is not.
|
||||
|
||||
When the provider genuinely needs a public callback and the operator is
|
||||
self-hosting, say so plainly and link maintained setup documentation. Cloud
|
||||
"supplies HTTPS" is a fact worth stating; it is not the only answer, and it must
|
||||
not be the only answer offered (`CHAT-CONNECTOR-UX.md:82-84`).
|
||||
|
||||
Never propose a tunnel, a relay, or a guard exception to work around
|
||||
reachability. Report the gap.
|
||||
|
||||
## What this changes in the skill's steps
|
||||
|
||||
- **Step 2 (where this runs)** additionally establishes the inbound mechanism
|
||||
and, if it is a callback, whether the origin is publicly reachable from the
|
||||
provider — tested, not assumed.
|
||||
- **Step 4 (configure)** keeps identity linking and membership approval as
|
||||
separate actions from resource setup, and does not treat the optional
|
||||
conversation test as a completion gate.
|
||||
- **Step 5 (verify)** adds one requirement to the real-agent-run evidence: an
|
||||
incoming message *and its reply* must correlate to the same task. The runbook
|
||||
names this in its runtime-delivery row —
|
||||
"Chat/email also routes an incoming message and its reply to the same task."
|
||||
Distinguish live events from simulated ones, and distinguish an optional
|
||||
unobserved callback from a real failure.
|
||||
|
||||
## If you are designing the setup UI
|
||||
|
||||
You are past this skill's boundary. The wizard conventions — stable numbered
|
||||
steps, the single step-owned footer with **Save & exit** left and the primary
|
||||
action right, prerequisites first on a tinted background, help icons on
|
||||
credential fields, manifests behind a modal rather than inline, resume across
|
||||
back navigation and external provider visits — all live in the guide, with
|
||||
`DESIGN.md` for components and tokens. Follow it there; do not re-derive it.
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
# Deployment support matrix — self-hosted first
|
||||
|
||||
Paperclip runs as open-source software you host yourself, and as Paperclip
|
||||
Cloud. **The self-hosted path is the primary one in this skill.** Nothing in the
|
||||
documented workflow below requires a Cloud account, a managed Cloud OAuth
|
||||
client, or any Paperclip-operated infrastructure.
|
||||
|
||||
Fill in a copy of this matrix for your own deployment before you claim that a
|
||||
connector works. Do not copy the labels from this page into a report: they
|
||||
record what has been established *here*, at the commits named below, and a
|
||||
deployment you have not exercised is `untested` no matter what this file says.
|
||||
|
||||
## The four labels
|
||||
|
||||
Use exactly these, and never substitute one for another:
|
||||
|
||||
| Label | Means | Requires |
|
||||
| --- | --- | --- |
|
||||
| `verified` | Exercised on this deployment and the evidence is recorded. | A command, a run ID, or a provider-side artifact — with date, deployment mode, and commit. |
|
||||
| `untested` | The path exists in source and is expected to work, but nobody ran it here. | The file:line or document you read, and the reason it was not exercised. |
|
||||
| `unsupported` | The product refuses it on this deployment. | The guard that refuses it, quoted or cited by file:line. |
|
||||
| `deferred` | Deliberately out of scope for now. | A reason and a named owner for the follow-up. |
|
||||
|
||||
`untested` is not a soft `verified`. A green test suite, a **Connected** badge,
|
||||
a catalog card, and a successful board **Test** call are all compatible with a
|
||||
connector no agent can actually use. Source-confirmed behaviour is `untested`
|
||||
until someone runs it. The board **Test** call in particular is not evidence of
|
||||
an agent's permissions — see **F2** below, where it returns `allowed` for an
|
||||
agent whose own policy denies the call.
|
||||
|
||||
## Establish your deployment first
|
||||
|
||||
Two independent settings decide almost everything below. Read them from your own
|
||||
instance's configuration, not from a product tier name.
|
||||
|
||||
| Setting | Env var | Values | Default |
|
||||
| --- | --- | --- | --- |
|
||||
| Deployment mode | `PAPERCLIP_DEPLOYMENT_MODE` | `local_trusted`, `authenticated` | `local_trusted` |
|
||||
| Exposure | `PAPERCLIP_DEPLOYMENT_EXPOSURE` | `private`, `public` | `private` |
|
||||
|
||||
Source: `packages/shared/src/constants.ts:4-8` and `server/src/config.ts:170-190`
|
||||
at Paperclip App commit `9335b7db10`. Both are also settable as
|
||||
`config.server.deploymentMode` / `config.server.exposure` in the instance's
|
||||
`config.json`, which is what `config.ts` reads after the environment.
|
||||
|
||||
One consequence is easy to miss and worth writing down, because it is what makes
|
||||
the same-machine self-hosted path so capable:
|
||||
|
||||
> `local_trusted` **forces** exposure to `private`. `server/src/config.ts:187-190`
|
||||
> ignores `PAPERCLIP_DEPLOYMENT_EXPOSURE` when the mode is `local_trusted`.
|
||||
|
||||
So a same-machine self-hosted instance can never land in the
|
||||
`authenticated` + `public` combination that the private-endpoint guard refuses.
|
||||
|
||||
Three deployment shapes follow from this. Name yours before step 3 of the skill:
|
||||
|
||||
| Shape | Typical config | What it is |
|
||||
| --- | --- | --- |
|
||||
| **A. Self-hosted, same machine** | `local_trusted` (⇒ `private`) | Paperclip on your laptop or workstation, alongside the tools it drives. |
|
||||
| **B. Self-hosted, server or VPS** | `authenticated` + `private` or `public` | Paperclip on a box you control, reached over your network or the public internet. |
|
||||
| **C. Paperclip Cloud** | hosted, `authenticated` + `public` | Paperclip runs in Paperclip's hosted environment. |
|
||||
|
||||
Shape B splits further on one axis that matters more than exposure: whether the
|
||||
instance has a **public HTTPS origin**. A VPS on a tailnet with private HTTPS is
|
||||
a different case from one on a public domain, and the difference decides CIMD
|
||||
and inbound chat callbacks — not OAuth in general.
|
||||
|
||||
### Shape B has two hard prerequisites that are easy to discover the hard way
|
||||
|
||||
Both were hit while filling this matrix, on `authenticated` + `public`:
|
||||
|
||||
1. **No embedded database.** Startup refuses:
|
||||
`authenticated public deployments require DATABASE_URL or
|
||||
config.database.connectionString; refusing embedded PostgreSQL fallback`.
|
||||
A public self-hosted instance needs an external PostgreSQL.
|
||||
2. **An explicit, resolvable public base URL.** `auth.baseUrlMode` must be
|
||||
explicit and `auth.publicBaseUrl` is required
|
||||
(`packages/shared/src/config-schema.ts:167-187`). This value is not only
|
||||
browser-facing: the runtime hands it to spawned agents as their control-plane
|
||||
URL, so it must resolve **from the machine the runtime runs on**, not just
|
||||
from your browser. A placeholder hostname produces `getaddrinfo ENOTFOUND` on
|
||||
every MCP server and every API call in the agent's run, which reads exactly
|
||||
like a broken connector.
|
||||
|
||||
Also on shape B: `POST /api/bootstrap/claim` returns
|
||||
`404 Browser first-admin claim is not available` unless exposure is `private`,
|
||||
and a merely signed-up user is not an instance admin. Claim the admin while the
|
||||
instance is `private`, then restart it as `public`. Changing `auth.publicBaseUrl`
|
||||
invalidates existing board sessions.
|
||||
|
||||
## How this matrix was filled
|
||||
|
||||
Executed 19 September 2026 against Paperclip App commit
|
||||
`9335b7db10425277bcb84ba8137d08c498324dba` on **two isolated instances started
|
||||
for the purpose** — shape A (`local_trusted`, embedded PostgreSQL, loopback
|
||||
`127.0.0.1:3810`) and shape B (`authenticated`, external PostgreSQL, run as both
|
||||
`private` and `public`). Providers: a disposable loopback MCP notes server
|
||||
written for the test, and DeepWiki (`https://mcp.deepwiki.com/mcp`), a real
|
||||
third-party public server with `auth: "none"`.
|
||||
|
||||
**No Cloud instance was reachable.** Every Cloud cell below is `untested` or
|
||||
`unsupported`, and none of it is inferred from a self-hosted result. The same
|
||||
honesty runs in reverse: a shape-A pass is not shape-B evidence.
|
||||
|
||||
Full reproduction, run IDs, and provider-side artifacts:
|
||||
[`PAP-18465-SELF-HOSTED-VERIFICATION.md`](../../PAP-18465-SELF-HOSTED-VERIFICATION.md).
|
||||
|
||||
## Matrix
|
||||
|
||||
### Transport and reachability
|
||||
|
||||
| Capability | A. Self-hosted, same machine | B. Self-hosted, server/VPS | C. Paperclip Cloud | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `mcp_remote` to a public provider endpoint | `untested` | **`verified`** on `authenticated` + `public` | `untested` | B: DeepWiki connected, catalog refreshed to 3 tools, `read_wiki_structure` executed through the gateway, `decision: allowed`. |
|
||||
| `mcp_remote` to a loopback endpoint (`127.0.0.1`) | **`verified`** | **`verified`** on `private`; **`unsupported`** on `public` | `unsupported` | A: connected, `healthStatus ok`, 7 tools, real agent runs against it. B `public`: `HTTP 400 {"code":"remote_http_private_endpoint"}` on connect, `HTTP 502` on refresh. Guard: `allowPrivateRemoteEndpoints`, `server/src/services/tool-access.ts:3195-3200`. |
|
||||
| `mcp_remote` to a link-local address | `untested` | `untested` | `untested` | `isAlwaysDeniedLinkLocalIp` in `server/src/services/remote-http-endpoint-guard.ts` denies in every mode. Source-cited only; no live call was made. |
|
||||
| `local_stdio` with an approved template | `untested` — instance declares it **supported** | `untested` — instance declares it **unsupported** in both exposures | `untested` | The live `supportMatrix` differs by shape (blocks quoted in the verification report). No stdio connection was created, so the capability itself is untested everywhere. |
|
||||
| `rest_api` through the connected MCP gateway | `untested` | `untested` | `untested` | Not exposed through the gateway; needs an execution adapter. Runbook "Transport support and boundaries". |
|
||||
|
||||
**The self-hosted advantage is real and worth stating plainly:** shape A reaches
|
||||
desktop apps, local files, and private-network services that Cloud cannot reach
|
||||
from a hosted runtime, and the loopback row above is the executed proof. That is
|
||||
not a Cloud defect, and it is not something to engineer around with a tunnel.
|
||||
Pick the shape that matches the provider.
|
||||
|
||||
### Authentication
|
||||
|
||||
| Capability | A. Same machine | B. Server/VPS | C. Cloud | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `auth: "none"` remote MCP | **`verified`** | **`verified`** (both exposures) | `untested` | Connect → catalog → gateway execution on both instances. |
|
||||
| API key / PAT in a header | **`verified`** | `untested` | `untested` | A: connection with `credentialRefs: [{placement: "header", key: "x-api-key", secretId}]`; the provider's request log shows the header arrived, and the value appears nowhere in Paperclip (see redaction row). |
|
||||
| OAuth via dynamic client registration (RFC 7591) | `untested` | `untested` | `untested` | **DCR is instance-local.** Each instance registers its own public client against its own `/api/tools/oauth/callback`. "Cloud-hosted and self-hosted instances use the SAME path — the only per-instance difference is the hostname inside the redirect URI." `CONNECTOR-PLAYBOOK.md:1569-1578`. Consent needs an authorized provider account; not exercised. |
|
||||
| OAuth via CIMD | `untested`; falls through to DCR on loopback/plain HTTP | `untested` with a public HTTPS origin; falls through without one | `untested` | CIMD "requires a public HTTPS base URL … the authorization server has to fetch that document server-to-server, so loopback and plain-HTTP deployments fall through to the next tier." `GENERIC-REMOTE-MCP.md:94-104`. Neither test instance had a public HTTPS origin. |
|
||||
| OAuth with a client you registered yourself | `untested` | `untested` | `untested` | Deployment-preconfigured `PAPERCLIP_TOOL_OAUTH_<PROVIDER>_CLIENT_ID` / `_SECRET` outranks every other tier — `server/src/services/tool-access.ts:8787-8804`. Not exercised. |
|
||||
| OAuth redirect from a plain-HTTP non-loopback origin | n/a — loopback is HTTP-allowed | **`unsupported`** for `https-or-loopback-http` providers | n/a — Cloud is HTTPS | Fails fast with `oauth_redirect_origin_unsupported`; configure TLS. `CONNECTOR-PLAYBOOK.md:1580-1594`. |
|
||||
| Provider-generated secret-bearing URL | `untested` | `untested` | `untested` | Generic runtime path is complete. Treat the URL as a credential. |
|
||||
| Paperclip-managed OAuth (`platform_shared`) | **`unsupported`** | **`unsupported`** | `untested` | Requires a reviewed Cloud connector profile and Cloud's fixed provider callback. `CONNECTOR-PLAYBOOK.md:184`. |
|
||||
|
||||
**No Cloud account is required for self-hosted OAuth.** The playbook is explicit
|
||||
that DCR needs neither Paperclip ID nor Paperclip Connect, and that
|
||||
`id.paperclip.ing` authenticates operators only and never holds resource tokens.
|
||||
Only the curated `platform_shared` profile is genuinely Cloud-gated, and it is
|
||||
one row of this table, not the default path. Note that the whole OAuth block is
|
||||
`untested` on every shape: it is source-confirmed, not exercised.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
| Capability | A. Same machine | B. Server/VPS | C. Cloud | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Connect by URL, no code change | **`verified`** | **`verified`** | `untested` | `POST …/tools/apps/connect` with a pasted link, then `/finish`. No catalog entry authored; `CONNECTOR-PLAYBOOK.md:23-26` says none is needed, and none was. |
|
||||
| Catalog discovery and risk classification | **`verified`** | **`verified`** | `untested` | A: `read` / `write` / `destructive` assigned correctly across 7 tools with no manual input. |
|
||||
| Effective policy on the acting agent | **`verified`** | **`verified`** | `untested` | A: `allowedToolNames` moved `[] → [list_notes, read_note] → +create_note` as entries were added; an ungranted agent stayed `[]`. |
|
||||
| Narrow read through the gateway | **`verified`** | **`verified`** | `untested` | A: two agent runs. B: DeepWiki `read_wiki_structure`. |
|
||||
| Authorized write through the gateway, disposable resource | **`verified`** | `untested` | `untested` | A only. B's provider is read-only and no write was authorized there. |
|
||||
| A real agent run correlated to its run ID | **`verified`** | **`verified`** | `untested` | A: runs `af9afa19…`, `135b2654…`. B: run `f027aa16…`, `allow / succeeded`. |
|
||||
| Provider-side readback of the artifact | **`verified`** | n/a — read-only provider | `untested` | A: the note file on the provider's own store names the run that created it, and that run ID matches the gateway invocation. |
|
||||
| Denied execution for an ungranted actor | **`verified`** on the agent path | **`verified`** for an unselected tool | `untested` | A: `deny / deny_default`; the write tool was absent from the granted agent's session and the attempted call never reached the provider. **Does not hold on the board Test path — see F2.** |
|
||||
| Recoverable provider failure and recovery | **`verified`** | `untested` | `untested` | A: provider stopped → `healthStatus error`, `HTTP 502`; restarted → `ok`, no duplicate connection. |
|
||||
| Catalog refresh after the provider changes | **`verified`, adverse** | `untested` | `untested` | A: three refreshes, each discovering a new tool — and auto-allowing it. **See F1.** |
|
||||
| Revoke, then reconnect | **`verified`** | `untested` | `untested` | A: grant revoked → `organization_authorization_required`; connection disabled → `deny / deny_disabled_connection`; grant re-added → succeeds on the same connection ID. |
|
||||
| Redaction of stored credentials | **`verified`** | `untested` | `untested` | A: 0 occurrences of the test credential across 1358 text/JSON columns, the whole instance tree, and five API surfaces — while the provider confirmed the header arrived. |
|
||||
|
||||
### Operator verification surfaces
|
||||
|
||||
| Capability | Self-hosted, your own instance | Evidence |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/companies/:companyId/tools/runtime-health` returns the live `supportMatrix` | **`verified`** for a board actor | Read on both instances; both `supportMatrix` blocks are quoted in the verification report. |
|
||||
| `GET …/tools/gallery`, `…/tools/connections` | **`verified`** for a board actor | Both returned data to the board actor on the owning instance. |
|
||||
|
||||
**Who you are decides whether you can read this.** A run-scoped *agent* JWT gets
|
||||
`403 {"error":"Board access required"}` on all three routes — executed
|
||||
19 September 2026 against an instance the agent does not administer. A **board
|
||||
actor on an instance you own reads them fine.** So: an agent cannot read its own
|
||||
deployment's support matrix and must ask the operator to paste the
|
||||
`supportMatrix` block, but an operator is never blocked from it. Do not infer
|
||||
deployment capability from a product name, a URL someone pasted in a browser, or
|
||||
the absence of an error.
|
||||
|
||||
### Chat and email
|
||||
|
||||
Every cell is `untested`: no chat or email provider was connected in the
|
||||
verification pass. Inbound chat is the one place where a public HTTPS origin is a
|
||||
hard prerequisite rather than a preference, and it depends on the provider's
|
||||
delivery mechanism, not on Paperclip's tier.
|
||||
|
||||
| Capability | A. Same machine | B. Server/VPS | C. Cloud | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Inbound delivery by public HTTPS callback | **`unsupported`** without a public origin | `untested` with a public HTTPS origin; `unsupported` without | `untested` | "For a public callback, distinguish having HTTPS from being reachable by the provider. A private-network HTTPS URL alone is not proof of public reachability." `CHAT-CONNECTOR-UX.md:77-79`. |
|
||||
| Inbound delivery by outbound socket or polling | `untested` | `untested` | `untested` | Provider-dependent. Establish the mechanism before asserting a prerequisite — `CHAT-CONNECTOR-UX.md:18-22`. |
|
||||
| Personal identity linking, membership approval | `untested` | `untested` | `untested` | Separate from resource setup; see `references/chat-and-email.md`. |
|
||||
|
||||
A self-hoster without a public origin is not out of options: choose a provider
|
||||
whose inbound path is a socket or polling, or put the instance behind a real
|
||||
public HTTPS origin. Do not tell them Cloud is the only answer, and do not embed
|
||||
a server tutorial in the wizard — link maintained setup documentation
|
||||
(`CHAT-CONNECTOR-UX.md:81-84`).
|
||||
|
||||
## Two findings that change what you should tell an operator
|
||||
|
||||
These are reproducible product behaviours at commit `9335b7db10`, not advice.
|
||||
Both are filed for product engineering; until they change, the skill's guidance
|
||||
has to account for them.
|
||||
|
||||
### F1 — a pasted MCP server can widen its own permissions after approval
|
||||
|
||||
On every catalog refresh, newly discovered entries are added to the connection's
|
||||
active managed profile unless `quarantineNewEntries` is set — and for a
|
||||
**user-pasted URL** it is explicitly set to `false`
|
||||
(`server/src/services/tool-access.ts:12721`, in the same expression that flags
|
||||
the connection `unverifiedServer: true`). Curated gallery apps and
|
||||
Paperclip-managed cloud connectors *do* quarantine. The least-trusted class does
|
||||
not.
|
||||
|
||||
Executed: a connection finished with exactly two tools enabled and access limited
|
||||
to one agent. The provider then advertised `export_notes`, which returns every
|
||||
note body in one response. After one refresh, with no operator action:
|
||||
`policy test ⇒ allow / allow_profile`, `new-tools review queue: pendingCount 0`.
|
||||
Risk tier does not gate it either — a newly advertised `delete_note`, correctly
|
||||
classified `destructive`, was also auto-allowed.
|
||||
|
||||
**What this means for the skill's advice.** "Start writes at Off and promote
|
||||
deliberately" is not durable on this build: the server decides. If you paste a
|
||||
URL you do not control, re-check the effective profile after every refresh, and
|
||||
treat the connection's tool set as provider-controlled rather than
|
||||
operator-controlled.
|
||||
|
||||
### F2 — the board "Test as agent" call runs with the board's authority
|
||||
|
||||
`executeTestCall` builds a session with `actorType: "user"` and the board user's
|
||||
ID. Agent tool profiles bind to agents, so the board user is not subject to them.
|
||||
|
||||
Executed: testing as an **ungranted** agent returned
|
||||
`HTTP 200 {"decision":"allowed"}` and the read succeeded. Testing `create_note`
|
||||
as an agent whose policy denied it returned `allowed` — **and the file was
|
||||
created on the provider.** The policy simulator, asked the same questions with
|
||||
`actorType: "agent"`, answers correctly.
|
||||
|
||||
**What this means for the skill's advice.** A board Test call is not evidence
|
||||
about an agent, in either direction: it can pass where the agent would be denied,
|
||||
and it can perform a write the agent's own policy forbids. Use the policy
|
||||
simulator (`POST /api/companies/{c}/tools/policy/test` with `actorType: "agent"`)
|
||||
for permission questions, and a real agent run for execution questions.
|
||||
|
||||
### F10 — the connect wizard grants the policy but not the install
|
||||
|
||||
`POST …/tools/apps/{id}/finish` with `access: {agentIds: [...]}` creates the
|
||||
tool-profile binding but **no `tool_connection_install` row**. Agent readiness
|
||||
requires both (`server/src/services/connection-intents.ts:241-248`), so the tool
|
||||
never enters the agent's session. Both wizard-created connections, on both
|
||||
instances, showed `installs: []` alongside a correct `allowedToolNames`. The
|
||||
board's access view showed the tool allowed and the connection green; the agent
|
||||
got `needs_user_action` and could not run.
|
||||
|
||||
**What this means for the skill's advice.** After finishing the wizard, check
|
||||
`GET /api/tool-connections/{id}/installs` — not just the effective profile.
|
||||
`PUT …/installs` is the one-call fix. This is exactly the "Connected badge, agent
|
||||
still cannot do the job" failure the skill names in **Use This When**, reproduced
|
||||
on the path the skill recommends.
|
||||
|
||||
## Deferred
|
||||
|
||||
| Item | Reason | Owner |
|
||||
| --- | --- | --- |
|
||||
| Cloud parity for every row above | No Cloud instance was reachable, and the self-hosted-first priority defers it | Operator with a Cloud instance, after self-hosted |
|
||||
| OAuth (DCR / CIMD / own client) on any shape | Needs an authorized provider account | Operator with a provider account |
|
||||
| `local_stdio` execution | The live `supportMatrix` says shape A supports it, which is not the same as running it | Follow-up under [PAP-18464](/PAP/issues/PAP-18464) |
|
||||
| Chat and email inbound delivery | Out of scope for the first pass | Follow-up under [PAP-18464](/PAP/issues/PAP-18464) |
|
||||
| Shape-B write execution and provider-side readback | The shape-B provider is read-only | Operator with a writable third-party provider |
|
||||
| Link-local denial | Source-cited only; no live call | Low priority — the guard is unconditional |
|
||||
|
||||
## Source commits
|
||||
|
||||
| Source | Revision | Read |
|
||||
| --- | --- | --- |
|
||||
| Paperclip App | `9335b7db10` on `master` | 19 September 2026 |
|
||||
| `doc/connections/CONNECTOR-PLAYBOOK.md`, `doc/connections/CHAT-CONNECTOR-UX.md` | merged by PR #13675 at `c9e867797939fe069278c4ae660f86f7692ed866` | 19 September 2026 |
|
||||
|
||||
Re-read these before relying on a line number. The runbook moved 214 lines in
|
||||
PR #13675 alone; if a citation disagrees with the commit in front of you, the
|
||||
commit wins and you record the drift.
|
||||
@@ -2,7 +2,7 @@
|
||||
"schemaVersion": 1,
|
||||
"packageName": "@paperclipai/skills-catalog",
|
||||
"packageVersion": "0.3.1",
|
||||
"generatedAt": "2026-08-11T15:05:34.526Z",
|
||||
"generatedAt": "2026-09-19T16:58:28.123Z",
|
||||
"skills": [
|
||||
{
|
||||
"id": "paperclipai:bundled:docs:doc-maintenance",
|
||||
@@ -1114,6 +1114,112 @@
|
||||
"url": "https://github.com/mvanhorn/last30days-skill/tree/v3.3.0/skills/last30days"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "paperclipai:optional:software-development:author-connector-definition",
|
||||
"key": "paperclipai/optional/software-development/author-connector-definition",
|
||||
"kind": "optional",
|
||||
"category": "software-development",
|
||||
"slug": "author-connector-definition",
|
||||
"name": "author-connector-definition",
|
||||
"description": "Author one Paperclip catalog connector definition: probe the provider MCP metadata, fix the five connection axes, add official branding, edit the generator source, regenerate, and get the catalog tests green with evidence. Mechanical authoring only; not research approval, not PR submission.",
|
||||
"path": "catalog/optional/software-development/author-connector-definition",
|
||||
"entrypoint": "SKILL.md",
|
||||
"trustLevel": "markdown_only",
|
||||
"compatibility": "compatible",
|
||||
"defaultInstall": false,
|
||||
"recommendedForRoles": [
|
||||
"engineer"
|
||||
],
|
||||
"requires": [
|
||||
"git",
|
||||
"curl",
|
||||
"node",
|
||||
"pnpm"
|
||||
],
|
||||
"tags": [
|
||||
"mcp",
|
||||
"connectors",
|
||||
"app-definition",
|
||||
"catalog",
|
||||
"codegen"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "skill",
|
||||
"sizeBytes": 20964,
|
||||
"sha256": "ecdf045a004da99c6ed9a3384b6000151d6965564d2ed1543c76c94b336811b7"
|
||||
},
|
||||
{
|
||||
"path": "references/catalog-contract.md",
|
||||
"kind": "reference",
|
||||
"sizeBytes": 13589,
|
||||
"sha256": "f8a659b8c5790fd7c7b6d0a7bac1d3e16e175072f18c97ce3e5db1924f9abda3"
|
||||
},
|
||||
{
|
||||
"path": "references/live-acceptance.md",
|
||||
"kind": "reference",
|
||||
"sizeBytes": 5670,
|
||||
"sha256": "24afcc38cfc9e56ca692d6d10e6eeba11d1ca41de939c47cb1caed42afba7928"
|
||||
},
|
||||
{
|
||||
"path": "references/offline-verification.md",
|
||||
"kind": "reference",
|
||||
"sizeBytes": 9519,
|
||||
"sha256": "ca38bcddf376aa1bc2ea5f392032c37cda204af21a412ec4b6e984343087c2e2"
|
||||
}
|
||||
],
|
||||
"contentHash": "sha256:c7a267d390dee25ac65750b46f9aaad69993e08eb42c8133bf52a05701a6953a"
|
||||
},
|
||||
{
|
||||
"id": "paperclipai:optional:software-development:connect-agent-tools",
|
||||
"key": "paperclipai/optional/software-development/connect-agent-tools",
|
||||
"kind": "optional",
|
||||
"category": "software-development",
|
||||
"slug": "connect-agent-tools",
|
||||
"name": "connect-agent-tools",
|
||||
"description": "Get a Paperclip agent working with an outside tool, self-hosted or Cloud: start from the workflow and its proof, establish the deployment and its real reachability, take the smallest supported connection path, and verify a real agent run with provider-side readback. Not for catalog authoring.",
|
||||
"path": "catalog/optional/software-development/connect-agent-tools",
|
||||
"entrypoint": "SKILL.md",
|
||||
"trustLevel": "markdown_only",
|
||||
"compatibility": "compatible",
|
||||
"defaultInstall": false,
|
||||
"recommendedForRoles": [
|
||||
"engineer",
|
||||
"operations"
|
||||
],
|
||||
"requires": [
|
||||
"curl"
|
||||
],
|
||||
"tags": [
|
||||
"mcp",
|
||||
"connectors",
|
||||
"integrations",
|
||||
"connections",
|
||||
"verification"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"kind": "skill",
|
||||
"sizeBytes": 26730,
|
||||
"sha256": "7fa19dc00395a5b8214f1a725c259dd1aa97ce580a43d0f8b8a13d2586c83bbb"
|
||||
},
|
||||
{
|
||||
"path": "references/chat-and-email.md",
|
||||
"kind": "reference",
|
||||
"sizeBytes": 4945,
|
||||
"sha256": "c014394e04b46bf34460296936febfe8497926faf9f87ed47c27377acc0f2371"
|
||||
},
|
||||
{
|
||||
"path": "references/deployment-support-matrix.md",
|
||||
"kind": "reference",
|
||||
"sizeBytes": 20738,
|
||||
"sha256": "fa7b868c2c3997e5433ad4845738d7092dc2a7f621e420d7a28dbf1f29e7c2b5"
|
||||
}
|
||||
],
|
||||
"contentHash": "sha256:ab06d5fcaa0ce39865e53d9ee45345cc07d72f4be51ce201d13b195e4f8872ba"
|
||||
},
|
||||
{
|
||||
"id": "paperclipai:optional:software-development:prepare-mcp-integration",
|
||||
"key": "paperclipai/optional/software-development/prepare-mcp-integration",
|
||||
|
||||
@@ -24,6 +24,8 @@ const EXPECTED_OPTIONAL_KEYS = [
|
||||
"paperclipai/optional/finance/ramp",
|
||||
"paperclipai/optional/product/design-critique",
|
||||
"paperclipai/optional/research/last30days",
|
||||
"paperclipai/optional/software-development/author-connector-definition",
|
||||
"paperclipai/optional/software-development/connect-agent-tools",
|
||||
"paperclipai/optional/software-development/prepare-mcp-integration",
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user