doc/connections/CONNECTOR-PLAYBOOK.md
This playbook is the repeatable template for adding a vendor to the Apps catalog as data, not as a plugin. It follows the accepted connections framework in PAP-13211, the first-30 rollout matrix in PAP-2432, and the production validation scope in PAP-12373.
Use it when Paperclip acts on an external system through a governed connection: a stored credential, a capability catalog, access profiles and policy rules, and audit. Inbound integrations, such as an external client acting on Paperclip, use gateway or webhook guidance instead.
Every connector built with this playbook is a plane P2 connection — a resource token in the instance vault, acquired via the connect broker, never a sign-in authenticator. Before writing a connector, read Identity vs. connections for the P1/P2/P3 boundary and the D7 standing rule (sign-in tokens are never reused as resource tokens; id.paperclip.ing never stores resource tokens; no connections hub on the ID service).
A complete connector proposal produces:
company_secrets; never raw env values.Default to a catalog entry when the vendor can be represented as metadata plus a transport:
Use a plugin only when the integration needs code that cannot fit inside the common connection model:
A plugin may bundle one or more catalog entries, but it must still create normal applications, connections, credential refs, catalog entries, profiles, policies, and audit events. Plugin code must not bypass the gateway, policy engine, company_secrets, changed-action quarantine, or call-event audit log.
Classify the vendor before writing metadata. Use the PAP-2432 matrix terms so rollout planning, security review, and QA can compare providers consistently.
| Reuse path | Use when | Typical transport | Examples from the matrix |
|---|---|---|---|
| MCP-direct | The vendor exposes an official or stable MCP server whose tools map cleanly to Paperclip grants. | mcp_remote; local_stdio only for approved trusted templates. | Linear, Notion, Sentry, Vercel, Exa, Apify, Context7. |
| OpenAPI-shim | The vendor has a documented REST/OpenAPI surface but no stable MCP server, and a generated/thin shim can expose safe actions. | Shim service or approved template that presents an MCP-compatible catalog to Paperclip. | Datadog, Apollo, QuickBooks, Ramp/Brex, Zendesk. |
| Vendor-deep-wrapper | The vendor boundary depends on app-installation tokens, event validation, rich domain semantics, resource grants, or high-risk writes. | Vendor-specific wrapper behind the same connection model. | GitHub, Slack, Google Workspace writes, Atlassian, Microsoft 365, Cloudflare, Figma, Stripe, Salesforce, HubSpot, Intercom, PagerDuty. |
Record the classification in the proposal along with the transport and the reason a lighter path is or is not enough.
Choose one auth mode:
company_secrets ref.Credentials always live in company_secrets with redacted metadata and versioned material. The catalog entry records the secret binding shape, not the secret value:
{
"credentialSecretRefs": [
{
"configPath": "credentials.authorization",
"label": "Linear OAuth access token",
"required": true
}
],
"credentialRefs": [
{
"name": "Authorization",
"placement": "header",
"key": "Authorization",
"prefix": "Bearer ",
"secretId": "<resolved at connect time>"
}
]
}
Do not add durable vendor credentials to agent env, project env, runtime env, adapter config, issue comments, screenshots, logs, fixture JSON, or plugin config. Agents receive a run-scoped gateway token; Paperclip resolves the vendor credential server-side and audits the call.
Author an AppDefinition as the canonical data record for the app and every supported connection method. It must explain what the operator gets without exposing protocol details in prosumer surfaces. Developer docs can mention transport, MCP, shim, and gateway terms; the Apps gallery copy should use plain app/action language.
Capture:
key: stable lowercase app key, e.g. linear.name, logoUrl, tagline, description: user-facing metadata.methods: explicit combinations of transport (mcp_remote, rest_api, local_stdio), authKind (oauth, api_key, none), and ownership (platform_shared, platform_provisioned, customer, dcr).{namespace}/{slug} addresses.credentialFields: labels, vendor-call placement, header key, prefix, help URL, and required state. User-facing labels should be sanitized by the Apps UI copy layer. The saved value is always a company_secrets ref, not an env entry.oauth: provider key, scopes, authorization URL, token URL, metadata URL if applicable.urlPatterns: URLs that can identify this app during paste/import flows.recommendedDefaults: access and risk defaults, especially ask-first risk levels.availability: whether the connector is generally available, gated by deployment config, or needs vendor registration.Keep AppDefinition metadata deterministic and company-scoped at install time. Global catalog data names capabilities; company connection and grant rows hold the configured instance, subject/provider tenant, secret refs, resource filters, status, health, and audit history.
Every connector proposal needs resource filters before write actions are enabled. Filters are part of the connection configuration and must be enforced by the gateway or wrapper, not only by UI affordances.
Common filter dimensions:
The connection health and catalog discovery steps should fail or warn when required filters are absent for S3/S4 providers.
List each initial action before implementation. Do not rely on vendor tool names alone; Paperclip needs normalized metadata for review, policy, and audit.
For each action, capture:
Risk classes:
| Risk | Examples | Default |
|---|---|---|
read | Search, list, fetch metadata/content inside allowed resources. | Active when profile includes the app or read risk level. |
write | Create issue, add comment, update status, append block, trigger redeploy. | Ask-first unless the default profile or a reviewed policy narrows it further. New/changed write tools start quarantined when discovered after initial review. |
destructive | Delete, refund, cancel production deployment, send external message, broad tenant mutation. | Quarantined. Requires explicit operator review and usually a require_approval policy even after review. |
Changed-action quarantine is mandatory: if catalog refresh finds a new or schema-changed write/destructive action, the entry stays hidden from agents until an operator reviews and re-enables it. Do not mark a changed action active just because a previous action with a similar name was active.
The wizard path comes from auth mode and transport:
| Auth mode | Operator path | Stored result |
|---|---|---|
| OAuth | Gallery card -> Connect -> vendor consent -> callback -> configure filters -> health/catalog -> access defaults. | OAuth token material in company_secrets; connection metadata redacted. |
| API key | Gallery card -> paste key -> configure filters -> health/catalog -> access defaults. | Key material in company_secrets; no raw key returned after save. |
| App-installation | Gallery card -> install app/bot -> callback or paste installation identifier -> configure filters -> health/catalog -> access defaults. | Installation credential in company_secrets; installation account metadata redacted. |
| None | Gallery card -> configure allowed resources -> health/catalog -> access defaults. | No vendor secret; connection row still carries config and audit scope. |
The operator should see Apps, Connections, and Review language. Keep protocol language behind Developer/Advanced copy.
Governance is automatic because every catalog entry becomes a normal tool-access object:
disabled and quarantined deny immediately.Recommended defaults for a new catalog entry:
recommendedDefaults.askFirstRiskLevels to ["write", "destructive"] unless the connector is read-only.PAP-12373 owns real-vendor gallery smoke evidence and connector validation. Do not duplicate that issue's screenshot/evidence matrix in this playbook. A connector proposal should instead state exactly how it will be validated there:
If a gallery card cannot pass this path against a real vendor, de-list it or mark it unavailable until the missing auth, transport, or governance dependency is fixed.
Many vendors now expose an official hosted MCP server whose authorization
server is discovered from the MCP endpoint itself, instead of documenting fixed
OAuth URLs. For these connectors the manifest's oauth block is a hint at
most; the broker resolves endpoints at connect time:
GET <serverUrl> unauthenticated returns 401 with a WWW-Authenticate
header naming the protected-resource metadata URL (RFC 9728).GET /.well-known/oauth-protected-resource[/<path>] names the
authorization server(s).GET /.well-known/oauth-authorization-server (RFC 8414) yields
authorization_endpoint, token_endpoint, and — when the vendor supports
dynamic registration — registration_endpoint.The broker implements this in discoverOAuthEndpoints
(server/src/services/tool-access.ts), but discovery is not
unconditional. oauthEndpointsForConnection resolves endpoints in this
order:
defaults ship a complete pair
(authorizationEndpoint and tokenEndpoint), those are used
unconditionally. discoverOAuthEndpoints never runs in this case, so
endpoints stored on the connection's own OAuth config and 401 challenge
hints are not consulted at all.mcp_remote connections, the broker calls
discoverOAuthEndpoints, which first checks endpoints already stored on
the connection's own OAuth config (falling back field-by-field to the 401
challenge hints); a complete stored/hinted pair is used as-is — no
.well-known fetch.Consequence: complete manifest endpoint hints are authoritative, not
hints — they override even endpoints that an earlier discovery persisted
on the connection, and if they go stale the broker keeps using them. For
discovery-capable vendors, ship only serverUrl in defaults (as
notion.json does) so the broker discovers fresh endpoints at connect
time; add explicit authorizationEndpoint/tokenEndpoint only for vendors
that do not publish RFC 9728/8414 metadata, and then own keeping them
current.
Vendors whose authorization server advertises a registration_endpoint and
supports public clients (token_endpoint_auth_method: "none" plus PKCE S256)
need no pre-provisioned OAuth app at all. At first connect the broker
registers a client on the fly and stores it on the connection:
client_name Paperclip (<instance host>),
redirect_uris = the instance's own callback, grant_types
["authorization_code", "refresh_token"], response_types ["code"],
token_endpoint_auth_method "none".client_id is persisted in the connection's OAuth config and any
issued client_secret becomes a company_secrets ref. The registered
client is reused for every later authorize/refresh on that connection —
re-registering orphans prior grants on providers that bind grants to the
client.PAPERCLIP_TOOL_OAUTH_<PROVIDER>_CLIENT_ID/_SECRET are configured, the
broker uses them (customer ownership) and skips registration. List both
customer and dcr in the method's ownershipModes when the vendor
supports both.DCR needs neither Paperclip ID nor Paperclip Connect. DCR is always
instance-local (ratified in the PAP-14828 connector-service spec, section 10
item 8.4: "DCR is always instance-local; the service has no DCR involvement").
Each instance registers its own public client with the vendor and uses its own
/api/tools/oauth/callback redirect. Cloud-hosted and self-hosted instances
use the SAME path — the only per-instance difference is the hostname inside
the redirect URI. id.paperclip.ing authenticates operators only and never
holds resource tokens; connect.paperclip.ing is a fallback only for
providers that genuinely require a pre-registered public redirect, which a DCR
provider by definition does not.
Vendors restrict what redirect_uris a dynamic client may register. Record
the probed constraint in the AppDefinition redirectConstraints field and
enforce it before starting OAuth. The first supported value is
https-or-loopback-http (Notion's rule): HTTPS on any host — public or
private — or plain HTTP only on loopback (localhost, *.localhost, ::1,
127.0.0.0/8). A plain-HTTP non-loopback origin fails fast with
oauth_redirect_origin_unsupported ("This provider requires an HTTPS or
loopback origin. Configure TLS before connecting.") and a pointer to the TLS
deployment docs, instead of a confusing vendor-side invalid_redirect_uri.
Probe the constraint with real registration attempts before writing the
manifest — the redirect-URI rule and browser-reachability are independent
axes; a private HTTPS host can be fine even when plain HTTP is not.
Every connection doc — playbook appendix, proposal, or user-facing doc — must include all three of the following (they are part of the template below):
Copy this section into a connector proposal or implementation issue.
## Vendor
- App key:
- App name:
- Owner:
- First-30 classification: MCP-direct / OpenAPI-shim / vendor-deep-wrapper
- Reason for classification:
- Security tier: S1 / S2 / S3 / S4
- Plugin needed? No / Yes, because:
## Transport And Auth
- Transport:
- Endpoint or approved template:
- Auth mode: OAuth / API key / app-installation / none
- OAuth scopes or key scope:
- Credential owner: company / user-delegated / app-installation
- Secret storage: company_secrets refs only
- Revocation behavior:
## Connection Flow (mandatory)
- Sequence diagram: <mermaid source or rendered image — REQUIRED for every connection doc>
- Auth endpoints (exact paths):
- Authorize:
- Token:
- Registration (if DCR):
- Discovery (.well-known), if any:
- Paperclip callback: `/api/tools/oauth/callback` (or n/a)
- Redirect constraints (probed): none / https-or-loopback-http / requires-public-redirect
- Paperclip ID / Paperclip Connect involvement: <"none — DCR is instance-local; cloud and self-hosted use the same path" for RFC 7591 providers; otherwise name the role>
## Administrator Setup (mandatory)
- What the admin must register (callback URLs? client credentials? nothing for DCR?):
- Where to register it:
- Instance prerequisites (TLS, base URL, feature flags):
- How to verify the connection works:
## Resource Filters
- Required filters:
- Optional filters:
- Write-enabling filters:
- Filters enforced by:
## Manifest
- key:
- name:
- tagline:
- authKind:
- transportTemplate:
- credentialFields:
- oauth:
- urlPatterns:
- recommendedDefaults:
- availability:
## Actions
| Tool | Risk | Default status | Filters | Approval default | Audit fields | Negative case |
| --- | --- | --- | --- | --- | --- | --- |
| | read/write/destructive | active/quarantined/disabled | | allow/ask-first/block | | |
## Wizard Path
- User path:
- Configuration steps:
- Error states:
- Redacted metadata shown:
## Governance Defaults
- Default profile:
- Profile bindings:
- Policies:
- Quarantine rules:
- Rate limits:
## Validation Hook
- Real-vendor smoke issue:
- Connect evidence:
- Catalog evidence:
- Allowed read:
- Ask-first write:
- Denied/quarantined case:
- Revoke:
- Audit:
This dry run applies the template to Linear, one of the PAP-2432 Batch A providers.
linearmcp_remotehttps://mcp.linear.app/mcpread and write initially, with writes governed by profiles and ask-first policies.company_secrets refs; no token in agent env, project env, comments, logs, or screenshots.{
"key": "linear",
"name": "Linear",
"tagline": "Create, update and read tickets.",
"authKind": "oauth",
"transportTemplate": {
"transport": "mcp_remote",
"url": "https://mcp.linear.app/mcp"
},
"credentialFields": [],
"oauth": {
"provider": "linear",
"scopes": ["read", "write"],
"authorizationUrl": "https://linear.app/oauth/authorize",
"tokenUrl": "https://api.linear.app/oauth/token"
},
"urlPatterns": ["https://mcp.linear.app/*"],
"recommendedDefaults": {
"access": "all_agents",
"askFirstRiskLevels": ["write", "destructive"]
}
}
| Tool | Risk | Default status | Filters | Approval default | Audit fields | Negative case |
|---|---|---|---|---|---|---|
linear.search_issues | read | active after catalog review | workspace, team, project, label, status | allow when profile includes Linear reads | query summary, team/project ids, result count | Granted agent cannot search a disallowed team. |
linear.get_issue | read | active after catalog review | workspace, team, issue id | allow when profile includes Linear reads | issue id, team/project ids | Ungranted agent cannot list or invoke the tool. |
linear.create_issue | write | active only after review; changed versions quarantined | workspace, team, project, label | ask-first by default | team/project ids, title hash, created issue id | Missing project/team filter denies. |
linear.comment_issue | write | active only after review; changed versions quarantined | workspace, team, issue id | ask-first by default | issue id, comment body redaction summary | Agent cannot comment on a disallowed issue. |
linear.update_issue_status | write | active only after review; changed versions quarantined | workspace, team, issue id, allowed statuses | ask-first unless a trust rule covers exact shape | issue id, old/new status if returned | Revoked connection blocks retry. |
No destructive Linear action should ship in the first pass. If a future delete/archive/bulk-update action appears during catalog refresh, it starts quarantined and needs explicit SecurityEngineer review before any policy can expose it.
company_secrets and shows redacted workspace/account metadata.quarantineReason: "pending_review" and are hidden from agent tool lists.Linear's real-vendor evidence belongs in PAP-12373. The smoke pass should prove:
linear.create_issue opens ask-first review and only executes after approval.Connector proposals now target the versioned AppDefinition contract in packages/shared/src/types/app-definition.ts. Seed data is one JSON file per provider under packages/shared/src/app-definitions/; regenerate Wave 1 with pnpm connections:ingest-app-definitions. The generator parses all 99 captured templates, validates required placeholders, OAuth ownership modes, and API-key placement, and produces deterministic output for review. FIRST-30 remains authoritative for riskTier and requiredResourceFilters; managed ownership modes stay data-visible but runtime-hidden until availability is injected.
This dry run applies the template to Notion, the first MCP-direct connector to ship with RFC 7591 dynamic client registration (PAP-16637; server implementation PAP-16649, PR #11009). Unlike the Linear appendix, every endpoint and constraint below comes from a live request log, not vendor docs alone.
notionnotion-* tools map directly to Paperclip grants.server/src/services/mcp-http.ts already
handles. The FIRST-30 matrix's "thin wrapper for block/database policy" is
explicitly deferred; v1 enforcement is gateway policy plus filters-as-config.mcp_remotehttps://mcp.notion.com/mcp (Streamable HTTP; /sse fallback exists)notion.json deliberately ships only serverUrl — no
authorizationEndpoint/tokenEndpoint hints, which would otherwise take
precedence and be used verbatim (see "MCP-Direct Connections" above).dcr (default, zero setup) and customer
(env-registered classic integration via
PAPERCLIP_TOOL_OAUTH_NOTION_CLIENT_ID/_SECRET, which always wins when set).expires_in authoritative).
Refresh tokens rotate on every refresh — the old token is invalidated
(at most 2 valid per grant) and replaying a stale one can revoke the whole
grant, so the broker persists the rotated token before publishing the new
access token and serializes refresh per connection. Absolute expiry 180
days, inactivity expiry 30 days. invalid_grant on refresh is terminal:
clear tokens, require re-auth, never retry.client_secret are
company_secrets refs; the DCR client_id persists on the connection and
is reused — re-registering would orphan prior grants.notion-* tools from agent sessions and denies brokered execution on the
next gateway check.Paperclip ID / Paperclip Connect involvement: none — DCR is instance-local (PAP-14828 spec section 10 item 8.4); cloud-hosted and self-hosted use the same path. The only per-instance difference is the hostname in the redirect URI.
Auth endpoints (exact paths, from the live discovery chain):
| Role | Endpoint |
|---|---|
| MCP server | https://mcp.notion.com/mcp |
| Protected-resource metadata (RFC 9728) | https://mcp.notion.com/.well-known/oauth-protected-resource/mcp |
| AS metadata (RFC 8414) | https://mcp.notion.com/.well-known/oauth-authorization-server |
| Authorize | https://mcp.notion.com/authorize |
| Token (exchange + refresh) | https://mcp.notion.com/token |
| Registration (RFC 7591 DCR) | https://mcp.notion.com/register |
| Paperclip connect (wizard) | POST /api/companies/:companyId/tools/apps/connect |
| Paperclip OAuth start | POST /api/tools/oauth/:connectionId/start |
| Paperclip callback | GET /api/tools/oauth/callback |
Redirect constraints (probed): https-or-loopback-http.
sequenceDiagram
autonumber
actor U as User's browser
participant UI as Paperclip UI
/PAP/apps/connect?source=notion
participant S as Paperclip instance server
(cloud or self-hosted — same path)
participant M as mcp.notion.com
(MCP server + OAuth AS)
participant N as Notion web
(app.notion.com, notion.com)
U->>UI: Click "Connect" (deep link ?source=notion)
UI->>S: POST /companies/:id/tools/apps/connect { appKey: "notion" }
S->>M: GET /.well-known/oauth-protected-resource (RFC 9728)
M-->>S: authorization_servers → mcp.notion.com
S->>M: GET /.well-known/oauth-authorization-server (RFC 8414)
M-->>S: authorize / token / registration endpoints
alt First connect on this instance (no stored client, no env client)
S->>M: POST registration_endpoint (RFC 7591 DCR, public client, PKCE-only)
M-->>S: client_id (persisted, REUSED for every later connect)
else Client already known
S->>S: Reuse stored DCR client_id (or env-registered client if configured)
end
S-->>UI: auth.startUrl (authorize URL + PKCE S256 challenge + state)
UI->>U: Redirect browser to startUrl
U->>M: GET /authorize?client_id + code_challenge + state
M->>N: 302 to app.notion.com/install-integration
N->>N: notion.com/login (only if signed out)
N-->>U: Consent page: pick workspace, approve integration
U->>S: 302 to GET /api/tools/oauth/callback?code&state (instance's OWN callback)
S->>M: POST token_endpoint (code + code_verifier)
M-->>S: access_token (~8 h) + rotating refresh_token
S->>S: Store tokens as company_secrets refs (server-side only)
S-->>U: Redirect to wizard actions/review step (?oauth=connected)
Note over S,M: Later: agent runs reach notion-* tools via the managed MCP gateway.
Server refreshes ahead of use — each refresh ROTATES the refresh token.
The verified request sequence for a first connect:
GET https://mcp.notion.com/mcp → 401 with WWW-Authenticate naming
https://mcp.notion.com/.well-known/oauth-protected-resource/mcp.GET https://mcp.notion.com/.well-known/oauth-protected-resource/mcp →
200; authorization server https://mcp.notion.com, scope default.GET https://mcp.notion.com/.well-known/oauth-authorization-server →
200; /authorize, /token, /register; token_endpoint_auth_method
none supported; PKCE S256 supported.POST https://mcp.notion.com/register (RFC 7591).GET https://mcp.notion.com/authorize → Notion consent
(app.notion.com/install-integration, notion.com/login if signed out).POST https://mcp.notion.com/token for code exchange and every refresh.POST https://mcp.notion.com/mcp for MCP traffic.Redirect-URI probes against /register:
Probed redirect_uris value | Result |
|---|---|
http://paperclip-dev:3100/api/tools/oauth/callback | 400 invalid_redirect_uri — "Redirect URI must use HTTPS unless it is a loopback HTTP URI" |
https://paperclip-dev:3100/api/tools/oauth/callback | Accepted — private host is fine over HTTPS |
http://localhost:3100/api/tools/oauth/callback | Accepted |
http://127.0.0.1:3100/api/tools/oauth/callback | Accepted |
Hence redirectConstraints: "https-or-loopback-http" in notion.json, and
the broker's fail-fast oauth_redirect_origin_unsupported error for
plain-HTTP non-loopback origins.
PAPERCLIP_TOOL_OAUTH_NOTION_CLIENT_ID and
PAPERCLIP_TOOL_OAUTH_NOTION_CLIENT_SECRET; the env client always takes
precedence (customer ownership).enableApps experimental setting must be on for
/apps/* routes. The connecting user must be allowed to install
integrations in their Notion workspace./PAP/apps/connect?source=notion, complete the Notion
consent flow, and land on the wizard's actions step listing notion-*
tools. Then confirm an agent run sees Notion tools through the runtime MCP
gateway and that a write call (e.g. notion-create-pages) opens an
ask-first action request.The shipped packages/shared/src/app-definitions/notion.json (regenerate via
pnpm connections:ingest-app-definitions):
{
"schemaVersion": 1,
"slug": "notion",
"name": "Notion",
"description": "Read and update pages in your Notion workspace.",
"urlPatterns": ["https://mcp.notion.com/*"],
"methods": [
{
"key": "mcp-oauth",
"transport": "mcp_remote",
"auth": "oauth",
"ownershipModes": ["customer", "dcr"],
"defaults": { "serverUrl": "https://mcp.notion.com/mcp" },
"riskTier": "S3",
"requiredResourceFilters": ["workspace", "page", "database"]
}
],
"redirectConstraints": "https-or-loopback-http"
}
Notion's hosted server exposes ~20 notion-* tools. Representative risk
classes below; the full catalog review with per-tool defaults is PAP-16652
(P4), and changed-action quarantine applies as usual.
| Tool | Risk | Default status | Filters | Approval default | Audit fields | Negative case |
|---|---|---|---|---|---|---|
notion-search | read | active after catalog review; plan-gated by Notion (needs Notion AI) — may list but fail at call time | workspace | allow when profile includes Notion reads | query summary, result count | Ungranted agent cannot invoke. |
notion-fetch | read | active after catalog review | workspace, page, database | allow when profile includes Notion reads | page/database id | Fetch outside shared pages fails Notion-side and is audited. |
notion-create-pages | write | active only after review; changed versions quarantined | workspace, page, database | ask-first by default | parent id, title hash, created page id | Missing workspace/page filter denies. |
notion-update-page | write | active only after review; changed versions quarantined | workspace, page | ask-first by default | page id, redaction summary | Revoked connection blocks retry. |
notion-query-data-sources | read | active after catalog review | workspace, database | allow when profile includes Notion reads | data-source id, result count | Granted agent cannot query a disallowed database. |
No destructive Notion action ships in the first pass; any future delete/archive/bulk action starts quarantined pending SecurityEngineer review.
/PAP/apps/connect?source=notion (or the Notion gallery
card → Connect). The deep link POSTs connect immediately and redirects the
browser to auth.startUrl.GET /api/tools/oauth/callback;
Paperclip exchanges the code, stores token material in company_secrets,
and returns the operator to the wizard (?oauth=connected).notion-* tools appear
on the actions step.Error state: on a plain-HTTP non-loopback instance, step 1 fails fast with the TLS guidance error above — the operator never reaches Notion.
notion-create-pages, notion-update-page,
and comment writes; block any unreviewed destructive action.quarantineReason: "pending_review" and are hidden from agent tool lists.End-to-end evidence belongs to PAP-16654 (P6) and the PAP-12373 matrix:
https://paperclip-dev.tail29c1aa.ts.net/PAP/apps/connect?source=notion
with no pre-provisioned OAuth env vars (proves DCR).notion-* tools; new/changed risky actions are
quarantined.notion-create-pages opens ask-first review and executes only after
approval.