From 6138bd1fa72e799880b67f3e6a1a9a011a7fc731 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:33:48 -0700 Subject: [PATCH 1/9] docs: tighten agent-facing copy so every token is earned Cut generic explanation and repeated boilerplate from the public agent guide, llms.txt, the agent skill, CLI help and README, the apex docs pages, the MCP guide and Worker README, and the MCP tool descriptions sent on every tools/list. Keep only what an agent cannot infer: whoami exit semantics, device login, API key precedence, ephemeral blocks and claim, artifact ID shape, entrypoint inference, exclusions, exit codes, JSON shapes, MCP output shapes, scope requirements, and edit fail-loud rules. Drop the "no viewer, no share link" negatives that defend against surfaces that no longer exist, and the retired-tools section. Relax the CLI help test that pinned a sentence to asserting the field name it cares about. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- apps/apex/src/agents.ts | 79 +++----- apps/apex/src/docs/pages/artifact-model.ts | 14 +- apps/apex/src/docs/pages/billing.ts | 16 +- apps/apex/src/docs/pages/cli.ts | 28 +-- apps/apex/src/docs/pages/dashboard.ts | 17 +- apps/apex/src/docs/pages/ephemeral.ts | 8 +- apps/apex/src/docs/pages/getting-started.ts | 14 +- apps/apex/src/docs/pages/limits.ts | 16 +- apps/apex/src/docs/pages/mcp.ts | 22 +- apps/apex/src/docs/pages/safety.ts | 30 ++- apps/apex/src/docs/pages/sharing.ts | 12 +- apps/apex/src/llms.ts | 28 ++- apps/cli/README.md | 99 ++++----- apps/cli/src/help.ts | 96 ++++----- apps/cli/test/index.test.ts | 2 +- apps/mcp/README.md | 69 +++---- docs/mcp.md | 214 +++++++------------- packages/contracts/src/mcp/registry.ts | 20 +- skills/agent-paste/SKILL.md | 62 +++--- 20 files changed, 338 insertions(+), 510 deletions(-) diff --git a/README.md b/README.md index aa1eff77..b5b0d581 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ In a sandbox or SSH session, replace `login` with `login --device-code`. Keep that process running and approve its displayed URL and code in your own browser, then run `npx @zaks-io/agent-paste whoami --json` before publishing. Device login needs network access to WorkOS and the API, but no browser in the -sandbox. See the [remote login guide](apps/cli/README.md#remote-login). +sandbox. See the [remote login guide](apps/cli/README.md#agent-quick-path). Expected output: diff --git a/apps/apex/src/agents.ts b/apps/apex/src/agents.ts index 466adce7..08f424e5 100644 --- a/apps/apex/src/agents.ts +++ b/apps/apex/src/agents.ts @@ -3,18 +3,13 @@ import { API_BASE_URL, APP_BASE_URL, MCP_BASE_URL, SKILL_INSTALL_CMD } from "./c export const AGENTS_MD = `# agent-paste for agents -Agent Paste turns a file or directory into a top-level website. Use it when the -next step should be a URL instead of a deploy, repository, zip, screenshot, or -local server. +agent-paste publishes a file or directory as a website and returns one URL that +opens without login. Use it when the next step should be a link instead of a +deploy, zip, screenshot, or local server. -Every publish returns one no-login \`url\` on the Artifact's own capability -subdomain. There is no iframe, viewer wrapper, or second sharing step. Revising -the same Artifact keeps the URL. +Default to the CLI. Use MCP only when the host cannot run commands. -Default to the CLI. Use MCP only when the host can connect to remote MCP but -cannot run commands. - -## Install the skill +## Skill \`\`\`sh ${SKILL_INSTALL_CMD} @@ -22,19 +17,17 @@ ${SKILL_INSTALL_CMD} ## CLI -Check authentication first: - \`\`\`sh npx @zaks-io/agent-paste whoami --json \`\`\` -\`whoami\` exits 0 when signed out; inspect \`authenticated: false\`. -Use \`npx @zaks-io/agent-paste login\` locally or add \`--device-code\` in a -sandbox. Keep device login running while the user approves the URL and code -from stderr, then check \`whoami\` again. Existing credentials, including an -injected \`AGENT_PASTE_API_KEY\`, work without another login. +\`whoami\` exits 0 even when signed out, so check \`authenticated\`. If it is +false, run \`login\` where a browser is available or \`login --device-code\` in a +sandbox. Device login prints a URL and code on stderr; keep it running until the +user approves, then run \`whoami\` again. An \`AGENT_PASTE_API_KEY\` env var also +authenticates and takes precedence over stored credentials. -Publish a file or directory: +Publish: \`\`\`sh npx @zaks-io/agent-paste publish ./path --json @@ -42,34 +35,30 @@ npx @zaks-io/agent-paste publish ./path --json Return \`url\` to the user. -Revise the Artifact at the same URL: +Revise at the same URL: \`\`\`sh npx @zaks-io/agent-paste publish ./path --artifact-id 01234-56789-abcde-fghjd --json \`\`\` -Use the artifact ID with \`--artifact-id\`, \`pull\`, or \`edit\`. -Full URLs also work. Updates require Workspace access. +The artifact ID is the first label of the URL hostname. \`--artifact-id\`, +\`pull\`, and \`edit\` accept the ID or the full URL. -If login is unavailable and static accountless output satisfies the task, or -the user explicitly asks for accountless publish: +When login is unavailable, or the user asks for accountless publishing: \`\`\`sh npx @zaks-io/agent-paste publish ./path --ephemeral --json \`\`\` -Return \`url\`. Return \`claim_url\` too when the human wants to keep and own the -upload. Unclaimed ephemeral Artifacts expire after 24 hours and render with -scripts, connections, forms, frames, objects, and workers blocked. Claiming -promotes the same Artifact URL to the claimed execution policy. - -If copied instructions include \`--claim-code \`, preserve it on the -ephemeral publish command. It is attribution, not part of the Artifact URL. +Return \`url\`. Also return \`claim_url\` if the user wants to keep the Artifact. +Unclaimed ephemeral Artifacts expire in 24 hours and serve static HTML only: +scripts, fetch, forms, frames, and workers are blocked. Claiming keeps the URL +and lifts those blocks. If the instructions you were given include +\`--claim-code \`, keep it on the command. -Directory publish preserves relative paths. Entrypoint inference is -\`index.html\`, \`index.md\`, \`README.md\`, then the only file. Otherwise pass -\`--entrypoint \`. Folder publishing excludes \`.git/\`, \`node_modules/\`, -\`.DS_Store\`, \`.env\`, and \`.env.*\`. +Directory publish keeps relative paths and skips \`.git\`, \`node_modules\`, +\`.DS_Store\`, and \`.env*\`. The entrypoint is \`index.html\`, \`index.md\`, +\`README.md\`, or the only file; otherwise pass \`--entrypoint \`. ## Result @@ -84,35 +73,31 @@ Directory publish preserves relative paths. Entrypoint inference is } \`\`\` -The URL is an unguessable bearer locator with at least 95 bits of entropy. It opens without login and -serves the latest Published Revision. Authenticated publishes can use inline -scripts, external HTTPS dependencies, root-relative assets, fetch, secure -WebSockets, and dedicated workers. Service workers are blocked on every -Artifact host. +Signed-in Artifacts run as ordinary top-level pages: inline scripts, external +HTTPS dependencies, fetch, WebSockets, and dedicated workers all work. Service +workers are blocked everywhere. ## MCP -Connect to \`${MCP_BASE_URL}\`, complete OAuth, and call \`whoami\`. +Connect to \`${MCP_BASE_URL}\` with OAuth and call \`whoami\`. - Create: \`publish_artifact\` -- Revise: \`add_revision\` or \`multi_edit\` +- Revise: \`add_revision\` (whole file) or \`multi_edit\` (literal find/replace) - Read: \`list_artifacts\`, \`read_artifact\`, \`read_file\`, \`list_revisions\` - Manage: \`delete_artifact\`, \`update_display_metadata\` -Use the artifact ID in \`artifact_id\`; full URLs also work. -MCP and CLI publishing return the same \`url\` contract. +MCP publishes text only; folders and binary files need the CLI. \`artifact_id\` +accepts the ID or the full URL. ## Reading this site -Every page on https://agent-paste.sh answers \`Accept: text/markdown\` with a clean -Markdown twin of that page. Each twin also has a direct address: /index.md, +Every page answers \`Accept: text/markdown\` and has a \`.md\` twin: /index.md, /docs.md, /docs/{slug}.md, /about.md, /how-it-works.md, /terms.md, /privacy.md. ## Links - Dashboard: ${APP_BASE_URL} - API auth metadata: ${API_BASE_URL}/auth.md -- Human docs: https://agent-paste.sh/docs -- Markdown docs: https://agent-paste.sh/docs.md +- Docs: https://agent-paste.sh/docs.md - Full corpus: https://agent-paste.sh/llms-full.txt `; diff --git a/apps/apex/src/docs/pages/artifact-model.ts b/apps/apex/src/docs/pages/artifact-model.ts index 2bc74463..b3e3fe9a 100644 --- a/apps/apex/src/docs/pages/artifact-model.ts +++ b/apps/apex/src/docs/pages/artifact-model.ts @@ -15,13 +15,13 @@ export const ARTIFACT_MODEL_DOC: DocsPage = { columns: ["Object", "Meaning"], rows: [ ["Workspace", "Tenant that owns Artifacts, members, policy, and Audit Events."], - ["Artifact", "Durable folder-like package containing one or more files."], + ["Artifact", "Folder-like package of one or more files."], ["Revision", "Immutable saved state of an Artifact."], - ["Published Revision", "Revision currently visible at the Artifact URL."], - ["Artifact URL", "Unguessable top-level capability website returned by publish."], - ["Artifact Console", "Login-walled management page at `/artifacts/`; never returned by publish."], + ["Published Revision", "Revision currently served at the Artifact URL."], + ["Artifact URL", "Unguessable website returned by publish."], + ["Artifact Console", "Login-walled management page at `/artifacts/`."], ["Agent View", "Machine-readable Artifact and Revision metadata with per-file URLs."], - ["Bundle", "Downloadable archive of a complete Revision tree."], + ["Bundle", "Downloadable archive of a complete Revision."], ], }, ], @@ -32,7 +32,7 @@ export const ARTIFACT_MODEL_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Artifact and Revision IDs are management identities. The Artifact URL uses an independent random capability ID, so it does not expose either management ID.", + text: "Artifact and Revision IDs are management identities. The Artifact URL uses a separate random capability ID, so it exposes neither.", }, { kind: "code", @@ -47,7 +47,7 @@ export const ARTIFACT_MODEL_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Publishing to an existing Artifact creates a new Published Revision and rewrites the capability manifest in place. The URL stays unchanged. Draft Revisions are never served there.", + text: "Publishing to an existing Artifact creates a new Published Revision at the same URL. Draft Revisions are never served.", }, ], }, diff --git a/apps/apex/src/docs/pages/billing.ts b/apps/apex/src/docs/pages/billing.ts index a77daf50..39aee41a 100644 --- a/apps/apex/src/docs/pages/billing.ts +++ b/apps/apex/src/docs/pages/billing.ts @@ -5,7 +5,7 @@ export const BILLING_DOC: DocsPage = { slug: "billing", title: "Billing and Plans", shortTitle: "Billing", - summary: "Hosted billing is enabled: your Plan sets write allowance, retention, and caps.", + summary: "Your Plan sets write allowance, retention, and caps. Reads are always free.", sections: [ { id: "plans", @@ -18,7 +18,7 @@ export const BILLING_DOC: DocsPage = { }, { kind: "paragraph", - text: "Shared caps: 100 files per Revision, 100 lifetime published Revisions per Artifact, 60 actor requests per minute, and 300 Workspace burst requests per minute.", + text: "Shared caps: 100 files per Revision, 100 published Revisions per Artifact, 60 requests per minute per actor, 300 per minute per Workspace.", }, ], }, @@ -28,11 +28,7 @@ export const BILLING_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Open `/billing` in the dashboard and choose the Pro interval. The dashboard creates a Stripe Checkout session and redirects you to Stripe.", - }, - { - kind: "paragraph", - text: "After a successful Checkout return, the dashboard activates Pro synchronously, refreshes the billing cache, and shows the updated Plan. Stripe webhooks and daily reconciliation keep local entitlement state converged after that.", + text: "Open `/billing` in the dashboard and choose a Pro interval. Checkout runs on Stripe; Pro activates as soon as you return, and webhooks plus daily reconciliation keep it in sync after that.", }, ], }, @@ -42,11 +38,11 @@ export const BILLING_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "The billing page can open Stripe Customer Portal for subscription management and cancellation. It also lists Stripe invoices with hosted invoice and PDF links when Stripe provides them.", + text: "The billing page opens the Stripe Customer Portal for changes and cancellation, and lists invoices with hosted and PDF links.", }, { kind: "paragraph", - text: "Reads are always free. Billing changes write allowance and feature limits; it does not meter recipients opening an Artifact URL.", + text: "Billing changes write allowance and limits only. Recipients opening an Artifact URL are never metered.", }, ], }, @@ -56,7 +52,7 @@ export const BILLING_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Stripe `active`, `trialing`, and `past_due` subscriptions map to Pro. Canceled, unpaid, incomplete, expired, paused, or missing subscriptions map to Free.", + text: "Stripe `active`, `trialing`, and `past_due` map to Pro. Everything else, including no subscription, maps to Free.", }, ], }, diff --git a/apps/apex/src/docs/pages/cli.ts b/apps/apex/src/docs/pages/cli.ts index 07c32273..4991c95f 100644 --- a/apps/apex/src/docs/pages/cli.ts +++ b/apps/apex/src/docs/pages/cli.ts @@ -14,17 +14,21 @@ export const CLI_DOC: DocsPage = { kind: "table", columns: ["Command", "Purpose"], rows: [ - ["`agent-paste login`", "Authenticate through browser OAuth."], - ["`agent-paste login --device-code`", "Authenticate from a sandbox with approval in your own browser."], + ["`agent-paste login`", "Authenticate through the browser."], + ["`agent-paste login --device-code`", "Authenticate from a sandbox; approve in your own browser."], ["`agent-paste logout`", "Revoke and remove the stored credential."], ["`agent-paste whoami --json`", "Report authentication, Workspace, actor, and scopes."], - ["`agent-paste publish `", "Publish a file or folder and return one top-level `url`."], + ["`agent-paste publish `", "Publish a file or folder and return `url`."], ["`agent-paste pull `", "Read one stored file."], ["`agent-paste edit `", "Apply literal edits and publish a Revision."], ["`agent-paste version`", "Print the CLI version."], ["`agent-paste upgrade`", "Update a standalone binary install."], ], }, + { + kind: "paragraph", + text: "`--json` writes one object to stdout with `schema_version`; progress and errors go to stderr. Exit codes: 0 success, 1 generic, 2 authentication, 3 quota, 4 validation, 5 not found, 6 network or server. `agent-paste help publish` and `help pull` list flags and JSON fields.", + }, ], }, { @@ -38,11 +42,7 @@ export const CLI_DOC: DocsPage = { }, { kind: "paragraph", - text: "Keep device login running while the user approves the URL and code from stderr in their browser, then check whoami again. Device login needs access to WorkOS and the API, but no local browser.", - }, - { - kind: "paragraph", - text: "Existing credentials work without another login. AGENT_PASTE_API_KEY takes precedence over stored credentials. If authentication is unavailable, use --ephemeral for accountless static output or report the blocker.", + text: "`whoami` exits 0 even when signed out, so check `authenticated`. Device login prints a URL and code on stderr and needs network access to WorkOS and the API but no local browser. Keep it running until the user approves, then run `whoami` again. An `AGENT_PASTE_API_KEY` env var also authenticates and takes precedence over stored credentials.", }, ], }, @@ -57,22 +57,16 @@ export const CLI_DOC: DocsPage = { }, { kind: "paragraph", - text: "Publish returns a URL that opens without login. Use its artifact ID with `--artifact-id`, `pull`, or `edit`. Full URLs also work. Updates keep the same URL and require Workspace access.", + text: "The returned `url` opens without login and stays the same across updates. Its artifact ID is the first label of the hostname; `--artifact-id`, `pull`, and `edit` accept the ID or the full URL. Updates require Workspace access.", }, { kind: "code", language: "text", code: '✓ Published "report"\n\n View https://01234-56789-abcde-fghjd.agent-paste.link/\n Expires \n\n Update agent-paste publish ./report --artifact-id 01234-56789-abcde-fghjd\n\n → open https://01234-56789-abcde-fghjd.agent-paste.link/', }, - ], - }, - { - id: "ephemeral", - title: "Accountless publish", - blocks: [ { kind: "paragraph", - text: "Run `whoami --json` first. When login is unavailable and static accountless output meets the task, or when explicitly requested, use `publish --ephemeral --json`. Return `url`; return `claim_url` too when the human wants to keep the upload. Ephemeral HTML is static until claimed.", + text: "When login is unavailable, `publish --ephemeral --json` publishes without an account. See [Ephemeral](/docs/ephemeral).", }, ], }, @@ -82,7 +76,7 @@ export const CLI_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Directory publish preserves relative paths and skips `.git`, `node_modules`, `.DS_Store`, and `.env*`. Entrypoint inference is `index.html`, `index.md`, `README.md`, then the only file. Otherwise pass `--entrypoint `.", + text: "Directory publish keeps relative paths and skips `.git`, `node_modules`, `.DS_Store`, and `.env*`. The entrypoint is `index.html`, `index.md`, `README.md`, or the only file; otherwise pass `--entrypoint `. Pass `--render-mode html|markdown|text|image|audio|video` only when inference is wrong.", }, ], }, diff --git a/apps/apex/src/docs/pages/dashboard.ts b/apps/apex/src/docs/pages/dashboard.ts index 89d2bc1e..739bb378 100644 --- a/apps/apex/src/docs/pages/dashboard.ts +++ b/apps/apex/src/docs/pages/dashboard.ts @@ -4,7 +4,7 @@ export const DASHBOARD_DOC: DocsPage = { slug: "dashboard", title: "Dashboard", shortTitle: "Dashboard", - summary: "The dashboard is the human control plane for Workspaces, Artifacts, billing, and settings.", + summary: "The human control plane for Workspaces, Artifacts, billing, and settings.", sections: [ { id: "sign-in", @@ -12,7 +12,7 @@ export const DASHBOARD_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Open [app.agent-paste.sh](https://app.agent-paste.sh) or run `agent-paste login`. Both use WorkOS authentication. For a CLI in a sandbox or SSH session, use `agent-paste login --device-code` and approve from your own browser. See [remote login](/docs/cli#remote-login).", + text: "Open [app.agent-paste.sh](https://app.agent-paste.sh) or run `agent-paste login`. From a sandbox or SSH session, use `agent-paste login --device-code` and approve in your own browser. See [remote login](/docs/cli#remote-login).", }, ], }, @@ -26,13 +26,10 @@ export const DASHBOARD_DOC: DocsPage = { rows: [ ["`/dashboard`", "Workspace overview, Usage Policy, recent Artifacts, and recent Audit Events."], ["`/artifacts`", "Artifact list with status, pinning, Bundle state, URL, and last publish time."], - [ - "`/artifacts/{artifactId}`", - "Artifact detail, capability URL, Revisions, Bundle state, warnings, and delete action.", - ], - ["`/keys`", "Dashboard member credential list, create, and revoke controls."], - ["`/audit`", "Workspace Audit Event list."], - ["`/settings`", "Workspace name and default retention settings."], + ["`/artifacts/{artifactId}`", "Artifact detail, URL, Revisions, Bundle state, warnings, and delete."], + ["`/keys`", "Create and revoke member credentials."], + ["`/audit`", "Workspace Audit Events."], + ["`/settings`", "Workspace name and default retention."], ["`/billing`", "Plan, remaining writes, Checkout, Portal, and invoices."], ], }, @@ -44,7 +41,7 @@ export const DASHBOARD_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "The claim page accepts `/claim#`. It requires a signed-in human and promotes the Ephemeral Workspace's Artifact into that member's Personal Workspace.", + text: "`/claim#` requires a signed-in human and moves the ephemeral Artifact into that member's Personal Workspace.", }, ], }, diff --git a/apps/apex/src/docs/pages/ephemeral.ts b/apps/apex/src/docs/pages/ephemeral.ts index 040bbba8..2e1ec46e 100644 --- a/apps/apex/src/docs/pages/ephemeral.ts +++ b/apps/apex/src/docs/pages/ephemeral.ts @@ -12,11 +12,11 @@ export const EPHEMERAL_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Run `whoami --json` first. A sandbox without a local browser can authenticate with `login --device-code` and human approval. Use `--ephemeral` when login is unavailable and static accountless output meets the task, or the user explicitly requests accountless publishing.", + text: "Check `whoami --json` first; a sandbox can usually still sign in with `login --device-code`. Use `--ephemeral` when login is unavailable or the user asks for accountless publishing.", }, { kind: "paragraph", - text: "Ephemeral is an unclaimed Workspace with low write caps, 24-hour Auto Deletion, and `noindex`. Its Artifact URL renders static content while scripts, connections, forms, frames, objects, and workers stay blocked.", + text: "Ephemeral Artifacts live in an unclaimed Workspace with low write caps, delete after 24 hours, and are `noindex`. They serve static content: scripts, fetch, forms, frames, objects, and workers are blocked.", }, ], }, @@ -31,7 +31,7 @@ export const EPHEMERAL_DOC: DocsPage = { }, { kind: "paragraph", - text: "Return `url` for immediate viewing. Return `claim_url` too when the human wants to keep and own the Artifact.", + text: "Return `url`. Also return `claim_url` if the user wants to keep the Artifact.", }, ], }, @@ -41,7 +41,7 @@ export const EPHEMERAL_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "The Claim Token lives only in the `claim_url` hash. It is never placed in the Artifact URL or a query string. The signed-in browser session that redeems it selects the destination Workspace.", + text: "The Claim Token lives only in the `claim_url` hash, never in the Artifact URL or a query string. Redeeming it in a signed-in browser moves the Artifact into that member's Workspace, keeps the URL, and lifts the script and network blocks.", }, ], }, diff --git a/apps/apex/src/docs/pages/getting-started.ts b/apps/apex/src/docs/pages/getting-started.ts index bfe8dc9d..7dbac9cf 100644 --- a/apps/apex/src/docs/pages/getting-started.ts +++ b/apps/apex/src/docs/pages/getting-started.ts @@ -5,7 +5,7 @@ export const GETTING_STARTED_DOC: DocsPage = { slug: "getting-started", title: "Getting Started", shortTitle: "Start", - summary: "Check auth, publish, and hand off one Artifact URL.", + summary: "Check auth, publish, and hand back one URL.", sections: [ { id: "install", @@ -18,7 +18,7 @@ export const GETTING_STARTED_DOC: DocsPage = { }, { kind: "paragraph", - text: "Use `npx` for one-shot Node.js 24+ runs or install `@zaks-io/agent-paste` globally for repeated use.", + text: "`npx` needs Node.js 24+. Install `@zaks-io/agent-paste` globally for repeated use.", }, ], }, @@ -28,7 +28,7 @@ export const GETTING_STARTED_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Run `whoami --json`; signed-out results exit 0. If authenticated:false, use `login` locally or `login --device-code` in a sandbox. Keep it running while the user approves the URL/code from stderr, then check `whoami` again. See [remote login](/docs/cli#remote-login).", + text: "`whoami --json` exits 0 even when signed out, so check `authenticated`. If false, run `login` where a browser is available or `login --device-code` in a sandbox. See [remote login](/docs/cli#remote-login).", }, ], }, @@ -38,7 +38,7 @@ export const GETTING_STARTED_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Install the repository's portable skill directly into Claude Code and Codex. It teaches agents the CLI-first publish, revise, ephemeral, and MCP workflows.", + text: "The skill teaches Claude Code and Codex the publish, revise, ephemeral, and MCP workflows.", }, { kind: "code", @@ -53,7 +53,7 @@ export const GETTING_STARTED_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Publish walks the file tree, uploads changed bytes, finalizes a Revision, and returns `url`.", + text: "Publish uploads the file tree and returns `url`, which opens without login.", }, { kind: "code", @@ -62,7 +62,7 @@ export const GETTING_STARTED_DOC: DocsPage = { }, { kind: "paragraph", - text: "Use `--artifact-id 01234-56789-abcde-fghjd` to update the same website. Full URLs also work. Omit the flag to create a new Artifact.", + text: "Pass `--artifact-id 01234-56789-abcde-fghjd` (or the full URL) to update the same website. Omit it to create a new Artifact.", }, ], }, @@ -77,7 +77,7 @@ export const GETTING_STARTED_DOC: DocsPage = { }, { kind: "paragraph", - text: "Use this when login is unavailable and static accountless output meets the task, or when explicitly requested. Return `url` for viewing and `claim_url` when the human wants to keep the upload. The Artifact expires after 24 hours. Until it is claimed, scripts and connections stay blocked.", + text: "Use this when login is unavailable or the user asks for accountless publishing. Return `url`, plus `claim_url` if the user wants to keep the upload. The Artifact expires in 24 hours and blocks scripts and network until claimed. See [Ephemeral](/docs/ephemeral).", }, ], }, diff --git a/apps/apex/src/docs/pages/limits.ts b/apps/apex/src/docs/pages/limits.ts index 33008586..f49b924d 100644 --- a/apps/apex/src/docs/pages/limits.ts +++ b/apps/apex/src/docs/pages/limits.ts @@ -4,7 +4,7 @@ export const LIMITS_DOC: DocsPage = { slug: "limits", title: "Limits and Retention", shortTitle: "Limits", - summary: "Billing-enabled limits keep handoffs useful without turning the service into permanent storage.", + summary: "Plan caps and Auto Deletion keep handoffs useful without becoming permanent storage.", sections: [ { id: "plan-limits", @@ -33,15 +33,15 @@ export const LIMITS_DOC: DocsPage = { kind: "list", items: [ "100 files per Revision.", - "100 lifetime published Revisions per Artifact.", + "100 published Revisions per Artifact, lifetime.", "60 authenticated requests per minute per actor.", "300 authenticated requests per minute per Workspace.", - "Pinned Artifact cap: 50 per Workspace.", + "50 pinned Artifacts per Workspace.", ], }, { kind: "paragraph", - text: "Authenticated clients can read their effective machine-readable caps from `GET /v1/usage-policy`.", + text: "Authenticated clients can read their effective caps from `GET /v1/usage-policy`.", }, ], }, @@ -51,11 +51,7 @@ export const LIMITS_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Every Artifact has Auto Deletion. Free Workspaces default to 3 days and can choose up to 7 days. Pro Workspaces default to 30 days and can choose up to 90 days. Ephemeral Artifacts auto-delete after 24 hours unless claimed.", - }, - { - kind: "paragraph", - text: "Pinned Artifacts are exempt from Auto Deletion while they remain pinned, subject to the Workspace cap. Non-current Revisions are retained according to policy and Revision links stop working after a retained Revision is removed.", + text: "Every Artifact has Auto Deletion. Free Workspaces default to 3 days and can choose up to 7; Pro defaults to 30 and can choose up to 90. Ephemeral Artifacts delete after 24 hours unless claimed. Pinned Artifacts are exempt while pinned.", }, ], }, @@ -65,7 +61,7 @@ export const LIMITS_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "The daily allowance counts new Artifacts. New Revisions of an existing Artifact do not count against the daily new-Artifact allowance, but each Artifact still has a 100 lifetime Revision ceiling.", + text: "The daily allowance counts new Artifacts only. Revisions of an existing Artifact are free up to its 100-Revision lifetime ceiling.", }, ], }, diff --git a/apps/apex/src/docs/pages/mcp.ts b/apps/apex/src/docs/pages/mcp.ts index 5c770523..5181590c 100644 --- a/apps/apex/src/docs/pages/mcp.ts +++ b/apps/apex/src/docs/pages/mcp.ts @@ -12,7 +12,7 @@ export const MCP_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Connect `https://mcp.agent-paste.sh`, complete OAuth, then call `whoami`. Use MCP when the host cannot run the CLI.", + text: "Add `https://mcp.agent-paste.sh` as a remote MCP server, complete OAuth, then call `whoami`. Use MCP only when the host cannot run the CLI.", }, ], }, @@ -24,16 +24,16 @@ export const MCP_DOC: DocsPage = { kind: "table", columns: ["Tool", "Purpose"], rows: [ - ["`whoami`", "Return the member, Workspace, and derived scopes."], - ["`publish_artifact`", "Publish a new text-only Artifact and return its top-level `url`."], - ["`add_revision`", "Publish a Revision of an existing Artifact at the same `url`."], - ["`multi_edit`", "Edit one stored file and publish a Revision."], - ["`list_artifacts`", "List Workspace Artifacts."], - ["`read_artifact`", "Read the latest Agent View."], - ["`read_file`", "Read one stored file or its metadata."], - ["`list_revisions`", "List Artifact Revisions."], + ["`whoami`", "Return the member, Workspace, and scopes."], + ["`publish_artifact`", "Publish a new text Artifact and return its `url`."], + ["`add_revision`", "Publish a new body for an existing Artifact; same `url`."], + ["`multi_edit`", "Literal find/replace in one stored file; same `url`."], + ["`list_artifacts`", "List Workspace Artifacts (`data[].id`)."], + ["`read_artifact`", "Read the latest Agent View with per-file URLs."], + ["`read_file`", "Read one stored file's text and sha256."], + ["`list_revisions`", "List Revisions (`items[].revision_id`)."], ["`delete_artifact`", "Delete an Artifact."], - ["`update_display_metadata`", "Update the Artifact title."], + ["`update_display_metadata`", "Rename an Artifact."], ], }, ], @@ -44,7 +44,7 @@ export const MCP_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Use the artifact ID, such as `01234-56789-abcde-fghjd`, in `artifact_id`. Full URLs also work. Updates require Workspace access. MCP publishes text; use the CLI for folders and binary files.", + text: "`artifact_id` accepts the ID (`01234-56789-abcde-fghjd`) or the full URL. MCP publishes text only; folders, binary files, and ephemeral publishing need the CLI. Updates require Workspace access.", }, ], }, diff --git a/apps/apex/src/docs/pages/safety.ts b/apps/apex/src/docs/pages/safety.ts index b8e49a04..7815049c 100644 --- a/apps/apex/src/docs/pages/safety.ts +++ b/apps/apex/src/docs/pages/safety.ts @@ -4,7 +4,7 @@ export const SAFETY_DOC: DocsPage = { slug: "safety", title: "Safety and Content Isolation", shortTitle: "Safety", - summary: "agent-paste treats uploaded work as untrusted content and isolates serving from control-plane auth.", + summary: "Uploaded work is untrusted content, served apart from the control plane.", sections: [ { id: "what-not-to-publish", @@ -12,23 +12,19 @@ export const SAFETY_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Every published Artifact has an unguessable capability URL. Anyone holding that URL can read it until the Artifact expires, is disabled, or is deleted. Treat Artifact URLs as sensitive.", - }, - { - kind: "paragraph", - text: "Do not upload secrets or other people's data. In particular:", + text: "Anyone holding an Artifact URL can read it until the Artifact expires, is disabled, or is deleted. Treat the URL as sensitive. Do not upload:", }, { kind: "list", items: [ "Tokens, passwords, private keys, `.env` files, or any other credential.", - "Personal or customer data you are not authorized to share or required to protect.", - "Anything you would not be comfortable handing to whoever holds the link.", + "Personal or customer data you are not authorized to share.", + "Anything you would not hand to whoever holds the link.", ], }, { kind: "paragraph", - text: "If you publish a secret by mistake, rotate it and delete or revoke the Artifact. Deletion can make content unreachable before every backup, cache, or queued cleanup job has finished.", + text: "If you publish a secret by mistake, rotate it and delete the Artifact. Deletion makes content unreachable before every cache and cleanup job finishes.", }, ], }, @@ -38,7 +34,7 @@ export const SAFETY_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Every Artifact is served top-level from its own `{capability}.agent-paste.link` origin, never the dashboard or API origin. R2 stays private and clients never receive direct storage URLs. The authentication cookie is host-only and is not sent to Artifact hosts.", + text: "Each Artifact is served top-level from its own `{capability}.agent-paste.link` origin, never from the dashboard or API origin. Storage stays private; clients never receive direct storage URLs. The auth cookie is host-only and never reaches Artifact hosts.", }, ], }, @@ -48,11 +44,11 @@ export const SAFETY_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "The content origin verifies signed tokens, expiration, scope, denylist state, and requested path. Authorization failures return generic not found responses.", + text: "The content origin verifies signed tokens, expiration, scope, denylist state, and requested path. Authorization failures return a generic not found.", }, { kind: "paragraph", - text: "Claimed Artifact HTML runs top-level with a compatibility-oriented CSP that allows inline scripts, external HTTPS dependencies, data and blob assets, dedicated workers, fetch, and secure WebSockets. `frame-ancestors 'none'` prevents another site from putting it back inside an iframe.", + text: "Signed-in Artifact HTML runs with a permissive CSP: inline scripts, external HTTPS dependencies, data and blob assets, dedicated workers, fetch, and secure WebSockets. `frame-ancestors 'none'` keeps other sites from framing it. Service workers are blocked on every Artifact host.", }, ], }, @@ -62,7 +58,7 @@ export const SAFETY_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Unclaimed ephemeral HTML renders top-level with scripts, connections, forms, frames, objects, and workers blocked. Claiming the Artifact rewrites its capability with the claimed execution policy.", + text: "Unclaimed ephemeral HTML blocks scripts, connections, forms, frames, objects, and workers. Claiming keeps the URL and switches it to the signed-in policy.", }, ], }, @@ -72,7 +68,7 @@ export const SAFETY_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "agent-paste does not certify uploaded content as safe. Artifacts can be deleted or disabled without exposing private storage URLs.", + text: "agent-paste does not certify uploaded content as safe. Artifacts can be deleted or disabled at any time.", }, ], }, @@ -83,10 +79,10 @@ export const SAFETY_DOC: DocsPage = { { kind: "list", items: [ - "Stored credentials are non-recoverable after creation.", + "Stored credentials cannot be recovered after creation.", "Claim Tokens ride the URL hash and are stored hashed.", - "Capability hostnames are bearer locators and must not be logged in full.", - "Tokens, signed URLs, and credential secret material must not be logged.", + "Capability hostnames are bearer locators; do not log them in full.", + "Tokens, signed URLs, and credential secrets are never logged.", ], }, ], diff --git a/apps/apex/src/docs/pages/sharing.ts b/apps/apex/src/docs/pages/sharing.ts index a38b7352..9ca415dd 100644 --- a/apps/apex/src/docs/pages/sharing.ts +++ b/apps/apex/src/docs/pages/sharing.ts @@ -4,7 +4,7 @@ export const SHARING_DOC: DocsPage = { slug: "sharing", title: "Artifact URLs", shortTitle: "URLs", - summary: "Every publish returns one top-level website URL.", + summary: "Every publish returns one website URL.", sections: [ { id: "one-url", @@ -12,17 +12,13 @@ export const SHARING_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Publish returns `url`, the complete unguessable capability URL that opens without login. Its hostname is the 23-character capability ID under `agent-paste.link`. It is the Artifact itself, not an app viewer or redirect.", + text: "Publish returns `url`. It opens without login, and its hostname is the 23-character artifact ID under `agent-paste.link`. Send it to the recipient as is.", }, { kind: "code", language: "text", code: "https://01234-56789-abcde-fghjd.agent-paste.link/", }, - { - kind: "paragraph", - text: "There is no iframe, sandbox, Access Link wrapper, private viewer URL, or separate visibility command. Send `url` to the recipient.", - }, ], }, { @@ -31,7 +27,7 @@ export const SHARING_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Use the artifact ID with `--artifact-id` to update the same website. Full URLs also work. Updates require Workspace access; the URL alone grants no write access.", + text: "Pass the artifact ID or full URL to `--artifact-id` to update the same website. Updates require Workspace access; the URL alone grants read only.", }, ], }, @@ -41,7 +37,7 @@ export const SHARING_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "The hostname carries at least 95 bits of random entropy and acts as the bearer locator. The content Worker validates its manifest, signed authorization, expiry, denylist, and requested path before serving encrypted R2 bytes.", + text: "The hostname carries at least 95 bits of random entropy and is the only credential a reader needs. The content Worker checks the manifest, signed authorization, expiry, denylist, and requested path before serving encrypted R2 bytes.", }, ], }, diff --git a/apps/apex/src/llms.ts b/apps/apex/src/llms.ts index 80103f2e..e7684e8a 100644 --- a/apps/apex/src/llms.ts +++ b/apps/apex/src/llms.ts @@ -2,32 +2,28 @@ import { API_BASE_URL, APP_BASE_URL, SKILL_INSTALL_CMD } from "./copy"; const LLMS_TXT_BASE = `# agent-paste -> Publish agent-generated files or directories and return one top-level Artifact URL. +> Publish a file or directory as a website and get back one URL that opens without login. Start with the agent guide unless you are implementing a protocol client. ## Agent Entry Points -- Agent skill for Claude Code and Codex: \`${SKILL_INSTALL_CMD}\` +- Skill for Claude Code and Codex: \`${SKILL_INSTALL_CMD}\` - [Agent guide](/agents.md): CLI-first publish, auth, ephemeral, claim, and MCP. -- [CLI reference](/docs/cli.md): Commands, publish modes, JSON output, pull, and edit. -- [Ephemeral publish](/docs/ephemeral.md): Accountless 24h publish and claim. -- [MCP server](/docs/mcp.md): OAuth-only remote MCP for hosted agents without CLI access. -- [Safety docs](/docs/safety.md): Content isolation and sharing safety. -- [Protocol auth metadata](${API_BASE_URL}/auth.md): auth.md client implementation. -- [Full docs corpus](/llms-full.txt): Complete public docs. +- [CLI reference](/docs/cli.md): Commands, flags, JSON output, pull, and edit. +- [Ephemeral publish](/docs/ephemeral.md): Accountless 24-hour publish and claim. +- [MCP server](/docs/mcp.md): OAuth-only remote MCP for hosts without a shell. +- [Safety](/docs/safety.md): What not to publish and how content is isolated. +- [Protocol auth metadata](${API_BASE_URL}/auth.md): For auth.md client implementers. +- [Full docs corpus](/llms-full.txt) -## Markdown Twins - -Every HTML page also answers \`Accept: text/markdown\` with its Markdown twin, and -each twin has a direct address: /index.md, /about.md, /how-it-works.md, /docs.md, -/docs/{slug}.md, /terms.md, /privacy.md. +Every HTML page answers \`Accept: text/markdown\` and has a \`.md\` twin: +/index.md, /about.md, /how-it-works.md, /docs.md, /docs/{slug}.md, /terms.md, /privacy.md. ## Human Entry Points - [Dashboard](${APP_BASE_URL}): Workspaces, Artifacts, and billing. -- [Human docs](/docs): Product docs for humans and agents. -- [Markdown docs](/docs.md): Markdown index of public docs. +- [Docs](/docs) `; const LLMS_PRICING_SECTION = ` @@ -35,7 +31,7 @@ const LLMS_PRICING_SECTION = ` - [Pricing](/pricing): Free vs Pro. - [Billing dashboard](${APP_BASE_URL}/billing): Checkout and Portal. -- [Pricing markdown](/pricing.md): Markdown twin of the pricing page. +- [Pricing markdown](/pricing.md) `; export function renderLlmsTxt(billingEnabled: boolean): string { diff --git a/apps/cli/README.md b/apps/cli/README.md index 4657b608..c518462f 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -19,14 +19,12 @@ npx @zaks-io/agent-paste publish ./report → open https://01234-56789-abcde-fghjd.agent-paste.link/ ``` -The URL opens without login. Use its artifact ID with `--artifact-id`, `pull`, -or `edit`. Full URLs also work. Updates keep the same URL -and require Workspace access. +The URL opens without login and stays the same across updates. Its artifact ID +is the first label of the hostname; `--artifact-id`, `pull`, and `edit` accept +the ID or the full URL. Updates require Workspace access. -## Migrating to 0.2 - -Version 0.2 removes `set-visibility` and replaces `private_url` and -`unlisted_url` with one `url` field. JSON output uses `schema_version: "2"`. +The npm package requires Node.js 24. Without a global install, prefix commands +with `npx @zaks-io/agent-paste`. ## Agent quick path @@ -35,59 +33,40 @@ agent-paste whoami --json agent-paste publish --json ``` -`whoami` exits `0` when signed out, so inspect `authenticated` in its JSON. Run -`agent-paste login` when browser login is possible on the same machine. In a -sandbox or SSH session, use `agent-paste login --device-code` and have the user -approve the displayed code in their browser. Keep the command running until it -finishes, then check `whoami` again. Use accountless publish only when login is -unavailable or explicitly requested: - -```sh -agent-paste publish --ephemeral --json -``` - -Return `url` to the user. Ephemeral output also contains `claim_url` for the -optional keep and ownership step. - -## Remote login +`whoami` exits 0 even when signed out, so check `authenticated` in its JSON. +If false, run `agent-paste login` where a browser is available. In a sandbox or +SSH session, run `agent-paste login --device-code`: it prints a URL and code on +stderr, and needs network access to WorkOS and the API but no local browser. +Keep it running until the user approves, then run `whoami` again. An +`AGENT_PASTE_API_KEY` env var also authenticates and takes precedence over +stored credentials. -Run this in the sandbox or remote shell: +When login is unavailable, or the user asks for accountless publishing: ```sh -agent-paste login --device-code -agent-paste whoami --json +agent-paste publish --ephemeral --json ``` -Keep login running while you approve the URL and code from stderr in your own -browser. The CLI saves a publish/read credential in the sandbox; check -`whoami --json` after success. No local browser or callback port is needed. - -Device login needs access to WorkOS and the API. Existing credentials work -without another login; `AGENT_PASTE_API_KEY` takes precedence over stored -credentials. If authentication is unavailable, report the blocker or use -`--ephemeral` when accountless static output meets the task. - -Without an installed CLI, prefix commands with `npx @zaks-io/agent-paste`. -The npm package requires Node.js 24. +Return `url`. Ephemeral output also has `claim_url` for the optional keep step. ## Commands | Command | Purpose | | -------------------------------------------------------- | ---------------------------------------------------- | -| `agent-paste login` | Authenticate through browser PKCE. | +| `agent-paste login` | Authenticate through the browser. | | `agent-paste login --device-code` | Authenticate from a sandbox or remote shell. | -| `agent-paste logout` | Remove the stored CLI session. | +| `agent-paste logout` | Remove the stored credential. | | `agent-paste whoami --json` | Report authentication, Workspace, actor, and scopes. | -| `agent-paste publish ` | Publish a new Artifact website. | +| `agent-paste publish ` | Publish a new Artifact. | | `agent-paste publish --artifact-id ` | Revise an Artifact at the same URL. | | `agent-paste publish --ephemeral` | Accountless 24-hour publish. | -| `agent-paste pull ` | Read one file; see pull help for output and URLs. | +| `agent-paste pull ` | Read one stored file. | | `agent-paste edit --edits ` | Apply literal edits and publish a Revision. | | `agent-paste version` | Print the installed version. | -| `agent-paste upgrade` | Install a selected release tag. | +| `agent-paste upgrade` | Install a release tag (standalone binary). | -Run `agent-paste help publish` for the full agent-oriented publish guide. -Run `agent-paste help pull` for remote-path and local redirection examples. +`agent-paste help publish` and `agent-paste help pull` cover flags, JSON +fields, and recipes. ## Publish JSON @@ -115,27 +94,29 @@ Run `agent-paste help pull` for remote-path and local redirection examples. `publish --ephemeral --json` also includes `claim_token`, `claim_url`, `workspace_id`, `api_key_id`, and `claim_token_id`. -## Files and entrypoints +Version 0.2 removed `set-visibility` and replaced `private_url` and +`unlisted_url` with `url`. -`` may be one file or a directory. Directory publishing preserves relative -paths and skips `.git`, `node_modules`, `.DS_Store`, `.env`, and `.env.*`. -Symlinks are followed only when the resolved target stays inside the published -directory and does not cross an excluded path. +## Files and entrypoints -The CLI selects `index.html`, `index.md`, `README.md`, or the only file as the -entrypoint. For any other multi-file directory, pass `--entrypoint `. +`` may be a file or a directory. Directory publish keeps relative paths +and skips `.git`, `node_modules`, `.DS_Store`, and `.env*`. Symlinks are +followed only when the target stays inside the directory and is not an excluded +path. -Use `--render-mode html|markdown|text|image|audio|video` only when inference is -not correct. +The entrypoint is `index.html`, `index.md`, `README.md`, or the only file. Any +other multi-file directory needs `--entrypoint `. Pass +`--render-mode html|markdown|text|image|audio|video` only when inference is +wrong. ## Output and exit behavior -`--json` reserves stdout for one machine-readable object. Progress and errors go -to stderr. `--quiet` suppresses human success output. `--color` and `--no-color` -override TTY detection. +`--json` reserves stdout for one object; progress and errors go to stderr. +`--quiet` suppresses the human summary. `--color` and `--no-color` override TTY +detection. -Exit codes are `0` success, `1` generic, `2` authentication, `3` quota, -`4` validation, `5` not found, and `6` network/server failure. +Exit codes: `0` success, `1` generic, `2` authentication, `3` quota, `4` +validation, `5` not found, `6` network or server failure. -The package bundles its application code into `dist/index.js` and keeps one -pinned runtime dependency, `@openclaw/fs-safe`, for root-bounded local reads. +The package bundles its application code into `dist/index.js` with one pinned +runtime dependency, `@openclaw/fs-safe`, for root-bounded local reads. diff --git a/apps/cli/src/help.ts b/apps/cli/src/help.ts index b28de866..d2fb894b 100644 --- a/apps/cli/src/help.ts +++ b/apps/cli/src/help.ts @@ -12,69 +12,55 @@ Usage: agent-paste version [--json] agent-paste upgrade [] -Agent publish quick path: - 1. Run agent-paste whoami --json. Signed-out results exit 0; - parse "authenticated": false. - 2. If signed out: agent-paste login locally, or agent-paste login --device-code in a - sandbox. Keep it running while the user approves the URL/code from stderr. - 3. Check whoami again, then agent-paste publish --json. Return url. - 4. For accountless static output: agent-paste publish --ephemeral --json. - Return claim_url when the user wants to keep it. - -Every publish returns one top-level Artifact URL. It opens without login and -stays stable across updates. Use its artifact ID with --artifact-id, pull, or edit. -Full URLs also work. +Agent quick path: + 1. agent-paste whoami --json. Exits 0 even when signed out; check "authenticated". + 2. If false: agent-paste login (browser available) or agent-paste login --device-code + (sandbox). Device login prints a URL and code on stderr; keep it running + until the user approves, then run whoami again. + 3. agent-paste publish --json. Return url. + 4. No login available: agent-paste publish --ephemeral --json. + Return claim_url too when the user wants to keep it. + +Every publish returns one URL that opens without login and stays the same +across updates. Its artifact ID is the first label of the hostname; --artifact-id, +pull, and edit accept the ID or the full URL. Output: - --json Machine-readable JSON on stdout (stable, carries schema_version). + --json One machine-readable object on stdout, with schema_version. --quiet Suppress the human summary; errors and exit code still apply. - --color Force colour/rich output; --no-color forces plain. + --color Force rich output; --no-color forces plain. Default: rich on a TTY, plain when piped or NO_COLOR/CI is set. `; export const PULL_HELP_TEXT = `agent-paste pull help -Read one remote file stored inside an Artifact. Plain mode writes an inline text -body to stdout. --json writes metadata and a content URL; binary or oversized -files omit body and can be fetched from that URL. is relative to -the Artifact root, not a local destination. +Read one file stored in an Artifact. is relative to the Artifact +root. Plain mode writes the text body to stdout. --json writes metadata and a +content URL; binary or oversized files omit body and are fetched from that URL. Usage: agent-paste pull [--revision-id ] [--json] -Full URLs also work. - Recipes: - Save a remote file locally: - agent-paste pull 01234-56789-abcde-fghjd index.html > ./index.html - - Hash a remote file without saving it: - agent-paste pull 01234-56789-abcde-fghjd index.html | shasum -a 256 + agent-paste pull 01234-56789-abcde-fghjd index.html > ./index.html + agent-paste pull 01234-56789-abcde-fghjd index.html | shasum -a 256 `; export const PUBLISH_HELP_TEXT = `agent-paste publish help -Start: - Run agent-paste whoami --json; signed-out results exit 0. - If authenticated:false, use agent-paste login locally or agent-paste login --device-code in a sandbox. - Keep device login running while the user approves the URL/code from stderr, - then check whoami again. Use --ephemeral for accountless static output. +Start with agent-paste whoami --json. It exits 0 even when signed out; if +"authenticated" is false, run agent-paste login (browser available) or +agent-paste login --device-code (sandbox), approve the URL and code it prints +on stderr, then run whoami again. Recipes: - Signed-in publish: - agent-paste publish --json - - Revise an existing Artifact at the same URL: - agent-paste publish --artifact-id 01234-56789-abcde-fghjd --json - - Accountless 24-hour publish: - agent-paste publish --ephemeral --json - Add --claim-code only when copied instructions include it. + agent-paste publish --json + agent-paste publish --artifact-id 01234-56789-abcde-fghjd --json # revise, same URL + agent-paste publish --ephemeral --json # no login, 24 hours What to hand back: - url Top-level Artifact capability URL. It opens without login. - claim_url Ephemeral keep/claim link. Include it when the human wants to keep - or claim the upload. + url The Artifact. Opens without login. + claim_url Ephemeral only. Include it when the user wants to keep the upload. JSON fields: publish --json returns: @@ -85,20 +71,18 @@ JSON fields: { claim_token, claim_url, workspace_id, api_key_id, claim_token_id } Path behavior: - may be a file or directory. Directory publish uploads every included - file except .git, node_modules, .DS_Store, and .env*. Symlinks are followed - only when their target stays inside the published directory and is not an - excluded path. Relative paths are preserved, so the entrypoint can load - sibling JS, CSS, JSON, and assets. Entrypoint defaults to index.html, - index.md, README.md, then the only file. A multi-file directory without an - inferred entrypoint fails; pass --entrypoint . - - --artifact-id Update an Artifact by artifact ID. Full URLs also work. + may be a file or directory. Directory publish keeps relative paths and + skips .git, node_modules, .DS_Store, and .env*. Symlinks are followed only + when the target stays inside the directory and is not an excluded path. The + entrypoint is index.html, index.md, README.md, or the only file; any other + multi-file directory needs --entrypoint . + +Flags: + --artifact-id Revise an existing Artifact. Accepts the ID or the full URL. --title Set the Artifact title. - --entrypoint Override the entrypoint file within . + --entrypoint Entrypoint file within . --render-mode html | markdown | text | image | audio | video - --ephemeral Accountless 24-hour publish with the same top-level page behavior - and a one-time claim_url. - --claim-code Optional attribution for --ephemeral. Preserve it when copied - Agent Paste instructions include one. + --ephemeral Accountless 24-hour publish. Static until claimed via claim_url. + --claim-code Attribution for --ephemeral. Keep it when the user's + instructions include one. `; diff --git a/apps/cli/test/index.test.ts b/apps/cli/test/index.test.ts index 84ad0f77..650aafa7 100644 --- a/apps/cli/test/index.test.ts +++ b/apps/cli/test/index.test.ts @@ -133,7 +133,7 @@ describe("cli command dispatch", () => { expect(stdoutValues(stdout)).toEqual( expect.arrayContaining([expect.stringContaining("agent-paste publish ")]), ); - expect(stdoutValues(stdout).join("")).toContain('parse "authenticated": false'); + expect(stdoutValues(stdout).join("")).toContain("authenticated"); }); it("routes a --help flag on a subcommand to help instead of running it", async () => { diff --git a/apps/mcp/README.md b/apps/mcp/README.md index 06e29431..3e8dc0fb 100644 --- a/apps/mcp/README.md +++ b/apps/mcp/README.md @@ -1,27 +1,27 @@ # mcp -OAuth-only hosted MCP Worker for agent-paste. - -User-facing MCP guide: [`docs/mcp.md`](../../docs/mcp.md). Use MCP when an agent -can connect to a remote MCP server but cannot run the CLI. +OAuth-only hosted MCP Worker for agent-paste. Agent-facing guide: +[`docs/mcp.md`](../../docs/mcp.md). Responsibilities: - Streamable HTTP MCP transport. -- OAuth protected resource metadata. -- OAuth authorization-server metadata facade for compatibility clients. -- WorkOS JWT bearer verification. -- Forwarding to allowlisted `api` and `upload` named RPC entrypoints. -- Text-only MCP tool surface (ten tools). - -Contracts: [ADR 0061](../../docs/adr/0061-mcp-worker-with-oauth-only-via-auth0-dcr.md) and [`packages/contracts/src/mcp/registry.ts`](../../packages/contracts/src/mcp/registry.ts). - +- OAuth protected-resource and authorization-server metadata. +- WorkOS JWT bearer verification. The bearer terminates here; tool calls forward + only the verified subject to allowlisted named RPC entrypoints on `api` and + `upload`, whose public HTTP routes reject MCP bearers. +- Ten text-only tools, defined in + [`packages/contracts/src/mcp/registry.ts`](../../packages/contracts/src/mcp/registry.ts) + and implemented in `src/tools.ts` with schema validation, scope checks, and + API error mapping. + +Decision trail: [ADR 0061](../../docs/adr/0061-mcp-worker-with-oauth-only-via-auth0-dcr.md). Host onboarding and smoke commands: [`docs/ops/runbook-mcp-hosts.md`](../../docs/ops/runbook-mcp-hosts.md). -Current endpoints: +Endpoints: -- `POST /` - Streamable HTTP MCP transport (JSON-RPC; optional SSE responses) -- `GET /` - endpoint metadata for humans and agents opening the MCP URL +- `POST /` Streamable HTTP MCP transport (JSON-RPC; optional SSE responses) +- `GET /` endpoint metadata - `GET /healthz` - `GET /.well-known/mcp/server-card.json` - `GET /.well-known/oauth-protected-resource` @@ -29,37 +29,18 @@ Current endpoints: - `GET /.well-known/openid-configuration` - `GET /openapi.json` -Transport auth is OAuth-bearer only via WorkOS JWT verification. The bearer -terminates at this Worker. Tool calls pass only the verified WorkOS subject to -allowlisted named RPC entrypoints on `api` and `upload`; their public HTTP routes -reject MCP bearers. The current ten-tool surface is defined in -`packages/contracts/src/mcp/registry.ts` and implemented in `src/tools.ts` with -schema validation, scope checks, and API error mapping. - -## Tools - -Ten tools, gated by MCP capabilities (`read`, `publish`, `admin`) derived by -`api` from the authenticated Workspace Member. WorkOS AuthKit tokens carry -standard OAuth scopes; they do not directly grant these scopes. Canonical -contract: [`packages/contracts/src/mcp/registry.ts`](../../packages/contracts/src/mcp/registry.ts). +## Scopes -| Tool | Scopes | Purpose | -| ------------------------- | --------------- | ----------------------------------------------------------------------- | -| `whoami` | (none) | Authenticated member, workspace, and derived scopes. | -| `list_artifacts` | `read` | List Artifacts in the workspace. | -| `read_artifact` | `read` | Latest Agent View for an Artifact. | -| `read_file` | `read` | Read one stored Artifact file for edit/revise workflows. | -| `list_revisions` | `read` | List Revisions for an Artifact. | -| `publish_artifact` | `publish, read` | Publish a new text Artifact and return its top-level capability URL. | -| `add_revision` | `publish, read` | Add and publish a Revision while keeping the Artifact URL stable. | -| `multi_edit` | `publish, read` | Apply literal edits to one stored text file and publish a new Revision. | -| `delete_artifact` | `publish` | Delete an Artifact. | -| `update_display_metadata` | `publish` | Update an Artifact's display title. | +Tools are gated by `read` and `publish`, taken from the authenticated Workspace +Member in `api`. WorkOS tokens carry standard OAuth scopes and do not grant +these directly. -`publish_artifact` and `add_revision` return one `url`: the unguessable, -no-login, top-level Artifact capability URL. It has no app viewer or iframe and -stays stable across Revisions. Publish output also returns `artifact_id` and -`revision_id` for immediate follow-up reads or revisions. +| Scope | Tools | +| ----------------- | ---------------------------------------------------------------- | +| none | `whoami` | +| `read` | `list_artifacts`, `read_artifact`, `read_file`, `list_revisions` | +| `publish`, `read` | `publish_artifact`, `add_revision`, `multi_edit` | +| `publish` | `delete_artifact`, `update_display_metadata` | ## Local verification diff --git a/docs/mcp.md b/docs/mcp.md index b16ff29c..75104e2d 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,62 +1,40 @@ # MCP For Agents -The hosted MCP server is the path for agents that can connect to remote MCP -servers but cannot run the `agent-paste` CLI. It gives those agents the same -Artifact handoff model: publish generated work, read Agent Views, and add -Revisions without shell access. - -Production endpoint: +The hosted MCP server is for agents that can connect to remote MCP but cannot +run the `agent-paste` CLI. It publishes text Artifacts, reads Agent Views, and +adds Revisions without a shell. ```text https://mcp.agent-paste.sh ``` -Opening that URL directly returns endpoint metadata for humans and agents. -Protocol calls use `POST /` with Streamable HTTP JSON-RPC and an OAuth bearer -token; a browser GET is not the tool transport. - -MCP clients can discover the transport and supported capability families from -`https://mcp.agent-paste.sh/.well-known/mcp/server-card.json`. The card reports -the environment-specific Streamable HTTP endpoint and advertises tools; this -server does not expose MCP resources or prompts. - -## When To Use MCP - -Use MCP when: +`POST /` is the Streamable HTTP transport. `GET /` returns endpoint metadata, +and `/.well-known/mcp/server-card.json` advertises the transport and tools. The +server exposes no MCP resources or prompts. -- The agent runs inside a hosted tool that cannot install npm packages, spawn a - local process, or read a local keychain. -- The host supports remote MCP servers with OAuth. -- The work product is text that can be published through an MCP tool call. -- Another agent needs to inspect an existing Artifact through Agent View instead - of scraping a browser page. +## When to use MCP -Use the CLI instead when the agent can run commands and needs to publish a file -tree, binary files, images, audio, video, or a complete static folder. +Use MCP when the host cannot install packages, spawn a process, or read a local +keychain, but does support remote MCP with OAuth. Use the CLI when commands are +available, and always for folders, binary files, images, audio, video, and +ephemeral publishing. -When a shell is available but its browser OAuth callback cannot work, use -`agent-paste login --device-code` and have the human approve from a separate -browser. Keep the CLI process running until approval completes, then check -`whoami --json`. See [CLI remote login](../apps/cli/README.md#remote-login). -Device login still needs access to the auth provider. MCP remains OAuth-only -and cannot authenticate through the CLI's stored key or `AGENT_PASTE_API_KEY`. +If a shell exists but browser OAuth cannot complete there, use +`agent-paste login --device-code` instead of MCP. See +[CLI remote login](../apps/cli/README.md#agent-quick-path). MCP is OAuth-only and +cannot use the CLI's stored credential or `AGENT_PASTE_API_KEY`. ## Connect -Add `https://mcp.agent-paste.sh` as a remote MCP server in the host, then -complete the OAuth flow when prompted. MCP uses OAuth only; dashboard cookies, -copied session tokens, and local CLI credentials do not authenticate MCP calls. +Add `https://mcp.agent-paste.sh` as a remote MCP server and complete OAuth. +Dashboard cookies, copied session tokens, and CLI credentials do not +authenticate MCP calls. OAuth discovery is at +`/.well-known/oauth-protected-resource`; the resource identifier is +`https://mcp.agent-paste.sh/` and clients should discover it automatically. -OAuth discovery is hosted at `/.well-known/oauth-protected-resource`. The root -MCP resource identifier is `https://mcp.agent-paste.sh/`; clients should discover -it automatically and should not require a manually configured OAuth resource. - -The WorkOS user must already belong to a Workspace. Signing in to the dashboard -or running `agent-paste login` creates the member row. Once connected, run the -`whoami` tool first; it reports the Workspace and the derived MCP capabilities -available to the agent. - -Common remote-MCP hosts: +The user must already belong to a Workspace. Signing in to the dashboard or +running `agent-paste login` once creates it. Call `whoami` first; it reports the +Workspace and the scopes available. | Host | Setup | | -------------- | ----------------------------------------------------------------------------------------------------- | @@ -66,110 +44,60 @@ Common remote-MCP hosts: | Claude.ai | Add a custom MCP connector with server URL `https://mcp.agent-paste.sh`. | | ChatGPT | Register the MCP connector with server URL `https://mcp.agent-paste.sh`. | -Host-specific OAuth and redirect notes live in +Host-specific OAuth and redirect notes: [`docs/ops/runbook-mcp-hosts.md`](./ops/runbook-mcp-hosts.md). -## What Agents Can Do - -MCP exposes ten tools: - -| Tool | Purpose | -| ------------------------- | ---------------------------------------------------------------------------------------------------------- | -| `whoami` | Return the authenticated member, Workspace, and derived scopes. | -| `publish_artifact` | Publish a new text-only Artifact and return its top-level capability URL. | -| `add_revision` | Revise an Artifact in place: publish a text-only Revision under the same stable link. Preserves the title. | -| `multi_edit` | Edit one file in an Artifact with literal find/replace (the Claude Edit model), publish it as a Revision. | -| `list_artifacts` | List Artifacts in the Workspace. | -| `read_artifact` | Read the latest Agent View for an Artifact. | -| `read_file` | Read one stored file's bytes back (member plaintext) so you can edit against the current content. | -| `list_revisions` | List Revisions for an Artifact. | -| `delete_artifact` | Delete an Artifact. | -| `update_display_metadata` | Update an Artifact display title. | - -Publishing tools return one `url`: the unguessable, no-login, top-level -Artifact capability URL. It has no app viewer or iframe and stays stable across -Revisions. Publish output also includes `artifact_id`, `revision_id`, `title`, -`expires_at`, and upload statistics. A no-op publish returns zeroed upload -statistics. Use the artifact ID, such as `01234-56789-abcde-fghjd`, in -`artifact_id`. Full URLs also work; existing `art_...` IDs remain supported. -Workspace authorization still applies. Responses retain canonical `artifact_id` -values. - -Output shapes to keep straight: - -- `list_artifacts` returns `data[]`; the Artifact ID field is `data[].id`. -- `read_artifact` returns the Agent View with `artifact_id`, `revision_id`, - `files[].url`, and optional `bundle`. -- `list_revisions` returns `items[]`; the Revision ID field is - `items[].revision_id`. - -`add_revision` runs through the shared revise engine (`@agent-paste/revise-core`, -[ADR 0091](../adr/0091-client-side-revise-engine-and-literal-edit-tools.md)): it -reads the base Revision and **preserves the existing title** (it takes no title -parameter and no longer overwrites the title with the literal `"Revision"`; rename -explicitly via `update_display_metadata`), and publishes the new body as a verified -patch under the Artifact's stable `url`. When the new body's `sha256` equals the -stored bytes it is a **no-op**: no Revision is minted and the call echoes the -unchanged link, title, and expiry. A -`render_mode` change publishes a whole-file fresh-entrypoint Revision (the one -meaningful whole-body replace). When the call's entrypoint is not in the base tree -it falls back to a whole-file publish under the same Artifact. The Revision inherits -the base's Render Mode unless the call sets one. - -`multi_edit` is the targeted-edit twin of `add_revision`, the parity match for the -CLI `edit` verb. It takes an `artifact_id`, a `path`, and an ordered `edits` array -of `{ old_string, new_string, replace_all? }` — the same shape as Claude's Edit -tool — and runs through the same `@agent-paste/revise-core` engine: it reads the -named file, applies the literal edits client-side, and publishes the result as a -Revision under the Artifact's stable `url`, preserving the title. Matching -is **literal and fail-loud**: each `old_string` must be non-empty and match exactly -once (set `replace_all` to change every occurrence); a not-found or ambiguous match -returns an `invalid_request` (HTTP 400) naming the offending edit index instead of -guessing, so the agent re-reads with `read_file` and retries. The server's stored -sha256 is the source of truth. Edits that reproduce the current bytes are a no-op -that mints no Revision and echoes the unchanged link, title, and expiry. - -## Capabilities - -MCP uses OAuth for authentication, but agent-paste does not trust OAuth scopes as -the source of product authorization. Capabilities come from the authenticated -Workspace Member in `api`, using one shared scope vocabulary (the same names the -API uses); MCP scopes are the member's stored API scopes verbatim, no translation: - -| Scope | Grants | Tools | -| --------- | ----------------------- | ---------------------------------------------------------------------------------------------- | -| `read` | View your own Artifacts | `whoami`, `list_artifacts`, `read_artifact`, `read_file`, `list_revisions` | -| `publish` | Change your own content | `publish_artifact`, `add_revision`, `multi_edit`, `delete_artifact`, `update_display_metadata` | - -`admin` exists but is dashboard-only (account/workspace management); no MCP tool -needs it. Today, normal Workspace members are provisioned with `read`, `publish`, -and `admin`. Future read-only roles can change in the database without changing -the MCP host connection. +## Tools + +| Tool | Scope | Purpose | +| ------------------------- | ----------------- | ------------------------------------------------------------------------------- | +| `whoami` | none | Authenticated member, Workspace, and scopes. | +| `publish_artifact` | `publish`, `read` | Publish a new text Artifact and return its `url`. | +| `add_revision` | `publish`, `read` | Publish a new body for an existing Artifact at the same `url`. Keeps the title. | +| `multi_edit` | `publish`, `read` | Literal find/replace in one stored file, published as a Revision. | +| `list_artifacts` | `read` | List Workspace Artifacts. Returns `data[]`; the ID is `data[].id`. | +| `read_artifact` | `read` | Latest Agent View: `artifact_id`, `revision_id`, `files[].url`, `bundle`. | +| `read_file` | `read` | One stored file's text and sha256, for editing against current content. | +| `list_revisions` | `read` | List Revisions. Returns `items[]`; the ID is `items[].revision_id`. | +| `delete_artifact` | `publish` | Delete an Artifact. | +| `update_display_metadata` | `publish` | Set an Artifact's title. | + +Publishing tools return `url` (opens without login, same across Revisions), +`artifact_id`, `revision_id`, `title`, `expires_at`, and upload statistics. +`artifact_id` inputs accept the short ID (`01234-56789-abcde-fghjd`), the full +URL, or a legacy `art_...` ID. Workspace authorization applies to every call. + +`add_revision` and `multi_edit` run through `@agent-paste/revise-core` +([ADR 0091](./adr/0091-client-side-revise-engine-and-literal-edit-tools.md)). +Both preserve the title; rename with `update_display_metadata`. A body or edit +set that reproduces the stored bytes is a no-op: no Revision is minted and the +call echoes the unchanged link, title, and expiry. `add_revision` inherits the +base Render Mode unless the call sets one; changing it publishes a fresh +entrypoint. `multi_edit` takes `artifact_id`, `path`, and an ordered `edits` +array of `{ old_string, new_string, replace_all? }`. Each `old_string` must +match exactly once unless `replace_all` is set; a miss or ambiguous match +returns `invalid_request` (HTTP 400) naming the edit index, so re-read with +`read_file` and retry. + +## Scopes + +OAuth authenticates the user; scopes come from the Workspace Member record in +`api`, not from the OAuth token. `read` covers `whoami`, `list_artifacts`, +`read_artifact`, `read_file`, and `list_revisions`. Publishing tools need `publish` +and `read`; `delete_artifact` and `update_display_metadata` need `publish`. Normal +members hold both. `admin` exists but no MCP tool needs it. ## Limits -- MCP publish is text-only today. -- Folder upload, binary upload, and Bundle download stay in the CLI. Dashboard - settings, billing, and lockdown controls stay in the dashboard. -- MCP is not an anonymous publish path. Agents with no account and no OAuth host - should first check CLI auth with `agent-paste whoami --json`; if no login is - available, they can use `agent-paste publish --ephemeral` through the CLI for - restricted accountless handoffs such as text, images, markdown, or static - sites. That path returns `url` for immediate no-login viewing and `claim_url` - only for optional keep/upgrade. -- Artifact lifetime follows Workspace Auto Deletion policy. MCP callers do not - choose TTL. - -## Retired tools - -Visibility and Access Link tools are gone. Every successful publish already -returns the no-login capability `url`, so agents do not need a second sharing -step. +- Text only. Folders, binary uploads, Bundle download, and ephemeral publishing + stay in the CLI; settings, billing, and lockdown stay in the dashboard. +- Artifact lifetime follows Workspace Auto Deletion. MCP callers do not choose + TTL. -## Deeper References +## References - [`apps/mcp/README.md`](../apps/mcp/README.md): Worker endpoints and implementation map. - [`packages/contracts/src/mcp/registry.ts`](../packages/contracts/src/mcp/registry.ts): canonical tool registry. -- [`docs/ops/runbook-mcp-hosts.md`](./ops/runbook-mcp-hosts.md): operator and host onboarding details. -- [`docs/adr/0061-mcp-worker-with-oauth-only-via-auth0-dcr.md`](./adr/0061-mcp-worker-with-oauth-only-via-auth0-dcr.md): MCP transport and OAuth decision. -- [`docs/adr/0079-mcp-scopes-derived-from-member-role-not-workos-token.md`](./adr/0079-mcp-scopes-derived-from-member-role-not-workos-token.md): capability source decision. +- [`docs/ops/runbook-mcp-hosts.md`](./ops/runbook-mcp-hosts.md): host onboarding. +- [ADR 0061](./adr/0061-mcp-worker-with-oauth-only-via-auth0-dcr.md): transport and OAuth decision. +- [ADR 0079](./adr/0079-mcp-scopes-derived-from-member-role-not-workos-token.md): scope source decision. diff --git a/packages/contracts/src/mcp/registry.ts b/packages/contracts/src/mcp/registry.ts index f0896b6d..1d4598bc 100644 --- a/packages/contracts/src/mcp/registry.ts +++ b/packages/contracts/src/mcp/registry.ts @@ -29,7 +29,7 @@ export const mcpToolContracts = [ { name: "publish_artifact", description: - "Publish a NEW text-only Artifact and return artifact_id, revision_id, and its top-level url. The URL is an unguessable capability, opens without login, and stays stable across revisions. Use this only for something not yet published. To CHANGE an existing Artifact, call add_revision with the artifact ID in artifact_id instead. Full URLs also work. Publishing an edit here creates a different Artifact on a different URL.", + "Publish a NEW text Artifact. Returns artifact_id, revision_id, and url, which opens without login and stays the same across Revisions. To change an existing Artifact use add_revision or multi_edit; publishing an edit here creates a second Artifact at a second URL.", auth: "mcp_oauth", requiredScopes: ["publish", "read"], idempotency: "optional_override", @@ -41,7 +41,7 @@ export const mcpToolContracts = [ { name: "add_revision", description: - "Update an existing Artifact. Pass its artifact ID in artifact_id; full URLs also work. Returns artifact_id, revision_id, and the same url, now showing the latest Revision. Use this instead of publish_artifact for updates.", + "Publish a new body for an existing Artifact. artifact_id accepts the ID or the full URL. Returns the same url now showing the new Revision. Keeps the title; a body identical to the stored bytes is a no-op and mints no Revision.", auth: "mcp_oauth", requiredScopes: ["publish", "read"], idempotency: "optional_override", @@ -53,7 +53,7 @@ export const mcpToolContracts = [ { name: "multi_edit", description: - "Edit one file inside an EXISTING Artifact with literal find/replace, then publish the result as a new Revision. Read the file first with read_file. Each old_string must match exactly once unless replace_all is set; misses and ambiguous matches fail loud. The Artifact's url stays stable and shows the newest Revision on refresh. A no-op mints no Revision.", + "Literal find/replace in one file of an existing Artifact, published as a new Revision at the same url. Call read_file first. Each old_string must match exactly once unless replace_all is set; a miss or ambiguous match fails with invalid_request naming the edit index. A no-op mints no Revision.", auth: "mcp_oauth", requiredScopes: ["publish", "read"], idempotency: "optional_override", @@ -77,7 +77,7 @@ export const mcpToolContracts = [ }, { name: "list_artifacts", - description: "List Artifacts in the authenticated workspace. Returns data[]; use data[].id as artifact_id.", + description: "List Artifacts in the Workspace. Returns data[]; use data[].id as artifact_id.", auth: "mcp_oauth", requiredScopes: ["read"], idempotency: "none", @@ -94,7 +94,7 @@ export const mcpToolContracts = [ { name: "read_artifact", description: - "Read an Artifact's latest Agent View by artifact_id. Full URLs also work. Returns artifact_id, revision_id, files[].url, and optional bundle metadata; file contents are not inlined.", + "Read an Artifact's latest Agent View. Returns artifact_id, revision_id, files[].url, and optional bundle metadata. File contents are not inlined; use read_file.", auth: "mcp_oauth", requiredScopes: ["read"], idempotency: "none", @@ -111,7 +111,7 @@ export const mcpToolContracts = [ { name: "read_file", description: - "Read one file's stored content from an Artifact so you can edit it and revise. Returns the decoded text body plus its sha256 for text files up to 10 MiB; for binary or larger files it returns sha256/size/is_binary with no body (fetch those via the file url or re-upload whole). Use the returned body as the base when producing an edited Revision; the sha256 is the exact base the server validates a diff against.", + "Read one stored file so you can edit it. Returns the text body and its sha256 for text files up to 10 MiB; binary or larger files return sha256, size, and is_binary with no body (fetch them from the file url).", auth: "mcp_oauth", requiredScopes: ["read"], idempotency: "none", @@ -132,7 +132,7 @@ export const mcpToolContracts = [ { name: "list_revisions", description: - "List Revisions for an Artifact. Returns items[]; use items[].revision_id when another tool needs a Revision ID.", + "List an Artifact's Revisions. Returns items[]; use items[].revision_id where a Revision ID is needed.", auth: "mcp_oauth", requiredScopes: ["read"], idempotency: "none", @@ -148,7 +148,7 @@ export const mcpToolContracts = [ }, { name: "delete_artifact", - description: "Delete an artifact.", + description: "Delete an Artifact.", auth: "mcp_oauth", requiredScopes: ["publish"], idempotency: "none", @@ -164,7 +164,7 @@ export const mcpToolContracts = [ }, { name: "update_display_metadata", - description: "Update artifact display title (description updates are not supported in this phase).", + description: "Set an Artifact's title.", auth: "mcp_oauth", requiredScopes: ["publish"], idempotency: "none", @@ -180,7 +180,7 @@ export const mcpToolContracts = [ }, { name: "whoami", - description: "Return the authenticated workspace member, workspace, and granted MCP scopes.", + description: "Return the authenticated member, Workspace, and MCP scopes.", auth: "mcp_oauth", requiredScopes: [], idempotency: "none", diff --git a/skills/agent-paste/SKILL.md b/skills/agent-paste/SKILL.md index 0297246d..89caaaeb 100644 --- a/skills/agent-paste/SKILL.md +++ b/skills/agent-paste/SKILL.md @@ -1,48 +1,46 @@ --- name: agent-paste -description: Publish files and folders as websites with agent-paste, or update existing published content. Use this skill for publishing and editing. For reading or downloading an agent-paste.link URL, fetch the supplied URL directly with curl or the available web tool; do not load this skill. Prefer the CLI for publishing when shell access is available, and hosted MCP otherwise. +description: Publish files and folders as websites with agent-paste, or revise content already published there. Prefer the CLI when shell access exists, hosted MCP otherwise. To read or download an agent-paste.link URL, fetch it directly; this skill is not needed. --- # agent-paste -Publish a file or folder as an unguessable, no-login website. Revising an Artifact updates the -same stable URL. +Publish a file or folder as an unguessable website that opens without login. Revising an Artifact +keeps its URL. -## Prefer HTML over Markdown for reports +## Write reports as HTML -When handing a human a report, plan, review, or summary, write it as HTML rather than Markdown so -it reads well when opened. A published page can use real typography, layout, color, tables, -inline SVG charts, collapsible sections, and images. Claimed Artifacts can also run inline +When handing a human a report, plan, review, or summary, publish HTML rather than Markdown. Use real +typography, tables, inline SVG charts, and collapsible sections. Signed-in Artifacts can run inline scripts, the Tailwind browser CDN, and HTTPS libraries; ephemeral Artifacts block scripts and -network connections, so keep those static. Lead with the finding, keep the data behind each claim -in the page, and open the URL once to check it before handing it over. +network, so keep those static. Lead with the finding, keep the supporting data on the page, and open +the URL once before handing it over. ## Publish -Use `agent-paste` if installed, otherwise run commands through `npx @zaks-io/agent-paste`. Use -`--json` for agent-consumed output and inspect `--help` instead of inventing flags. +Use `agent-paste` if installed, otherwise `npx @zaks-io/agent-paste`. Pass `--json` when you will +parse the output. Run `--help` for flags. ```sh agent-paste whoami --json agent-paste publish --json ``` -`whoami` exits 0 when signed out; inspect `authenticated: false`. Use `login` locally or -`login --device-code` in a sandbox. Keep device login running while the user approves the URL -and code from stderr, then check `whoami` again. Existing credentials, including an injected -`AGENT_PASTE_API_KEY`, work without another login. +`whoami` exits 0 even when signed out, so check `authenticated`. If false, run `login` where a +browser is available or `login --device-code` in a sandbox. Device login prints a URL and code on +stderr; keep it running until the user approves, then run `whoami` again. An `AGENT_PASTE_API_KEY` +env var also authenticates. -When login is unavailable and static accountless output meets the task, or the user requests it: +When login is unavailable, or the user asks for accountless publishing: ```sh agent-paste publish --ephemeral --json ``` -Preserve a caller-provided `--claim-code `. Return the result's `url`; it is the Artifact -and opens without login. For ephemeral results, return `claim_url` only for the optional keep and -ownership step. Ephemeral HTML is static until claimed: scripts, connections, forms, frames, -objects, and workers are blocked. Service workers remain blocked after claim. Do not seek a -separate share link, viewer URL, or visibility command. +Keep any `--claim-code ` the user's instructions include. Return `url`; it is the Artifact. +Return `claim_url` too if the user wants to keep the Artifact. Ephemeral HTML is static until +claimed: scripts, fetch, forms, frames, and workers are blocked. Service workers stay blocked after +claim. ## Revise instead of republishing @@ -52,19 +50,19 @@ agent-paste pull 01234-56789-abcde-fghjd --json agent-paste edit 01234-56789-abcde-fghjd --edits --json ``` -The artifact ID is the first part of its hostname. Full URLs also work. Updates require Workspace access. +The artifact ID is the first label of the URL hostname; the full URL works too. -`edit` accepts an ordered JSON array of `{ "old_string", "new_string", "replace_all"? }`. If a -literal match is absent or ambiguous, re-read and correct it. Never silently replace the whole -file. Confirm the returned `artifact_id` and stable `url` after every update. +`edit` takes an ordered JSON array of `{ "old_string", "new_string", "replace_all"? }`. Each +`old_string` must match exactly once unless `replace_all` is set; a miss or ambiguous match fails +with exit 4. Re-read the file and correct the edit rather than replacing the whole file. ## Safety and MCP -Publish only the requested path. Check folders, and the data embedded in a report, for -credentials, private source, customer data, and unrelated files even though the CLI excludes -common secret paths. Never expose API keys, login state, claim tokens, or credential files. Do not retry an indeterminate publish unless the CLI or read-back -proves whether it committed. +Publish only the requested path. The CLI skips `.git`, `node_modules`, `.DS_Store`, and `.env*`, +but still check folders and report data for credentials, private source, customer data, and +unrelated files. Never put API keys, login state, or claim tokens in published content. If a publish +fails indeterminately, check whether it committed before retrying. -Without shell access, connect to `https://mcp.agent-paste.sh` with OAuth and run `whoami` first. -Use `publish_artifact`, `add_revision`, or `multi_edit`. MCP is text-only; use the CLI for folders, -binary files, and accountless publishing. Use the artifact ID in `artifact_id`; full URLs also work. Full docs: . +Without a shell, connect to `https://mcp.agent-paste.sh` over OAuth and call `whoami` first. Use +`publish_artifact`, `add_revision`, or `multi_edit`. MCP is text-only; folders, binary files, and +ephemeral publishing need the CLI. Full guide: . From d8ffe2e7b29e1d61650f3d41507fccf8f763fe0a Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:35:40 -0700 Subject: [PATCH 2/9] test(apex): assert billing.md structure instead of pinning copy Co-Authored-By: Claude Fable 5.1 --- apps/apex/src/server.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/apex/src/server.test.ts b/apps/apex/src/server.test.ts index c1896572..155ed741 100644 --- a/apps/apex/src/server.test.ts +++ b/apps/apex/src/server.test.ts @@ -471,7 +471,7 @@ describe("billing-gated text assets", () => { expect(response.status).toBe(200); const body = await response.text(); expect(body).toContain("# Billing and Plans"); - expect(body).toContain("Stripe Checkout"); + expect(body).toContain("## Upgrade"); }); }); From 62e9f3da3d54c5ed1c49dd31ff74c5db345c6b74 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:49:03 -0700 Subject: [PATCH 3/9] docs: whoami needs no scope; art_ IDs work with --artifact-id Co-Authored-By: Claude Fable 5.1 --- apps/apex/src/agents.ts | 3 ++- apps/apex/src/docs/pages/cli.ts | 2 +- apps/cli/README.md | 2 +- apps/cli/src/help.ts | 2 +- docs/mcp.md | 4 ++-- skills/agent-paste/SKILL.md | 3 ++- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/apex/src/agents.ts b/apps/apex/src/agents.ts index 08f424e5..636dbeac 100644 --- a/apps/apex/src/agents.ts +++ b/apps/apex/src/agents.ts @@ -42,7 +42,8 @@ npx @zaks-io/agent-paste publish ./path --artifact-id 01234-56789-abcde-fghjd -- \`\`\` The artifact ID is the first label of the URL hostname. \`--artifact-id\`, -\`pull\`, and \`edit\` accept the ID or the full URL. +\`pull\`, and \`edit\` accept that ID, the \`art_...\` \`artifact_id\` from JSON +output, or the full URL. When login is unavailable, or the user asks for accountless publishing: diff --git a/apps/apex/src/docs/pages/cli.ts b/apps/apex/src/docs/pages/cli.ts index 4991c95f..82b9c723 100644 --- a/apps/apex/src/docs/pages/cli.ts +++ b/apps/apex/src/docs/pages/cli.ts @@ -57,7 +57,7 @@ export const CLI_DOC: DocsPage = { }, { kind: "paragraph", - text: "The returned `url` opens without login and stays the same across updates. Its artifact ID is the first label of the hostname; `--artifact-id`, `pull`, and `edit` accept the ID or the full URL. Updates require Workspace access.", + text: "The returned `url` opens without login and stays the same across updates. Its artifact ID is the first label of the hostname; `--artifact-id`, `pull`, and `edit` accept that ID, the `art_...` `artifact_id` from JSON output, or the full URL. Updates require Workspace access.", }, { kind: "code", diff --git a/apps/cli/README.md b/apps/cli/README.md index c518462f..ff603819 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -21,7 +21,7 @@ npx @zaks-io/agent-paste publish ./report The URL opens without login and stays the same across updates. Its artifact ID is the first label of the hostname; `--artifact-id`, `pull`, and `edit` accept -the ID or the full URL. Updates require Workspace access. +that ID, the `art_...` `artifact_id` from JSON output, or the full URL. Updates require Workspace access. The npm package requires Node.js 24. Without a global install, prefix commands with `npx @zaks-io/agent-paste`. diff --git a/apps/cli/src/help.ts b/apps/cli/src/help.ts index d2fb894b..6f4dde7e 100644 --- a/apps/cli/src/help.ts +++ b/apps/cli/src/help.ts @@ -23,7 +23,7 @@ Agent quick path: Every publish returns one URL that opens without login and stays the same across updates. Its artifact ID is the first label of the hostname; --artifact-id, -pull, and edit accept the ID or the full URL. +pull, and edit accept that ID, the art_... artifact_id from --json, or the full URL. Output: --json One machine-readable object on stdout, with schema_version. diff --git a/docs/mcp.md b/docs/mcp.md index 75104e2d..d0e89bdf 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -82,8 +82,8 @@ returns `invalid_request` (HTTP 400) naming the edit index, so re-read with ## Scopes OAuth authenticates the user; scopes come from the Workspace Member record in -`api`, not from the OAuth token. `read` covers `whoami`, `list_artifacts`, -`read_artifact`, `read_file`, and `list_revisions`. Publishing tools need `publish` +`api`, not from the OAuth token. `whoami` needs no scope. `read` covers +`list_artifacts`, `read_artifact`, `read_file`, and `list_revisions`. Publishing tools need `publish` and `read`; `delete_artifact` and `update_display_metadata` need `publish`. Normal members hold both. `admin` exists but no MCP tool needs it. diff --git a/skills/agent-paste/SKILL.md b/skills/agent-paste/SKILL.md index 89caaaeb..78c7b984 100644 --- a/skills/agent-paste/SKILL.md +++ b/skills/agent-paste/SKILL.md @@ -50,7 +50,8 @@ agent-paste pull 01234-56789-abcde-fghjd --json agent-paste edit 01234-56789-abcde-fghjd --edits --json ``` -The artifact ID is the first label of the URL hostname; the full URL works too. +The artifact ID is the first label of the URL hostname. The `art_...` `artifact_id` from JSON output +and the full URL work too. `edit` takes an ordered JSON array of `{ "old_string", "new_string", "replace_all"? }`. Each `old_string` must match exactly once unless `replace_all` is set; a miss or ambiguous match fails From 3929276d86b1cd8c211045a587fbe8fa1d469499 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:58:33 -0700 Subject: [PATCH 4/9] docs(adr): propose one Artifact ID and help-as-table-of-contents Co-Authored-By: Claude Fable 5.1 --- docs/adr/0098-one-artifact-id.md | 76 ++++++++++++++++++++ docs/adr/0099-help-is-a-table-of-contents.md | 58 +++++++++++++++ docs/adr/README.md | 2 + 3 files changed, 136 insertions(+) create mode 100644 docs/adr/0098-one-artifact-id.md create mode 100644 docs/adr/0099-help-is-a-table-of-contents.md diff --git a/docs/adr/0098-one-artifact-id.md b/docs/adr/0098-one-artifact-id.md new file mode 100644 index 00000000..724d0a4e --- /dev/null +++ b/docs/adr/0098-one-artifact-id.md @@ -0,0 +1,76 @@ +# One Artifact ID + +Status: Proposed. Amends [ADR 0094](./0094-capability-url-is-the-artifact-link.md) and [ADR 0096](./0096-shorter-base32-capability-ids.md). + +## Context + +An Artifact currently has two identities. The management ID (`art_` ULID) is +the primary key and appears in JSON output, MCP responses, Audit Events, and +dashboard URLs. The Capability ID (23-character grouped base32) is the +hostname label of the Artifact URL and is minted at first publish +(`upload-publish-workflow.ts` `setCapabilityIdIfMissing`). `ArtifactReference` +accepts either form plus the full URL. + +Agents see both and have to learn that they are interchangeable on input but +different on output. The split existed because a Capability ID did not exist +before first publish, and because possession grants read access while the +management ID is safe to log. + +Revision IDs (`rev_` ULID) carry a third shape for the same family of objects. + +## Decision + +1. The Capability ID is the Artifact ID. It is minted when the Artifact row is + created, not at first publish, and stored as the primary key. The Capability + Manifest is still written only at publish, so an unpublished ID resolves to + nothing on the content host. +2. Every surface emits that ID as `artifact_id`: CLI JSON, MCP results, Agent + View, list endpoints, Audit Events, and the dashboard route + `/artifacts/`. The `art_` form is emitted nowhere. +3. `ArtifactReference` keeps accepting `art_` IDs and full URLs on input so + agents and bookmarks holding old values keep working. Existing rows keep + their `art_` primary key and existing Capability ID; only new rows use the + unified key. Legacy 32-hex Capability IDs remain valid per ADR 0096. +4. Revision IDs adopt the same grouped base32 shape with no prefix. They are + not hostnames and carry no read capability; the shape is shared for + consistency only. Existing `rev_` values remain valid on input. +5. Because the Artifact ID is now credential material everywhere it appears, + the `api` and `web` Workers redact the `/artifacts/` path segment in + request logs the same way `content` already redacts the hostname label. + Audit Event rows are workspace-scoped and keep the full ID. + +## Rationale + +One ID removes a class of agent mistakes with no loss of function. Minting at +creation closes the only structural reason for the split. The remaining reason, +loggability, is a redaction rule on two routes rather than a second identity. + +The prefix carried no information an agent needed: the shape already +distinguishes an Artifact ID from a Revision ID by context, and the check +symbol catches transcription errors that the prefix never did. + +## Consequences + +- `docs/specs/cli.md`, `docs/specs/artifacts.md`, `CONTEXT.md`, and the MCP + contract descriptions drop the "full URLs and `art_` IDs also work" caveats + down to a single line about legacy input. +- `packages/tokens` content and agent-view codecs stop asserting `art_` and + `rev_` prefixes and validate the grouped base32 shape instead. +- The `artifacts.capability_id` column becomes redundant for new rows. It + stays for existing rows until a follow-up backfills `id` from it, which is a + separate migration with its own rollback plan. +- CLI JSON `schema_version` stays at `"2"`. Field names and types are + unchanged; only the ID format differs, and every consumer already accepts the + base32 form on input. +- Two-agent review loops revise one shared ID and read history through + `list_revisions` and `pull --revision-id`. The `LIFETIME_REVISION_CEILING` of + 100 is the practical limit on that loop and should be revisited separately. + +## Done + +- New Artifacts publish with `artifact_id` equal to the URL hostname label on + CLI, MCP, and dashboard surfaces. +- `pull`, `edit`, `--artifact-id`, and every MCP `artifact_id` input accept the + new ID, an `art_` ID, and a full URL. +- Request logs for `api` and `web` never contain a full Artifact ID. +- Specs and CONTEXT.md describe one Artifact ID and one Revision ID shape. diff --git a/docs/adr/0099-help-is-a-table-of-contents.md b/docs/adr/0099-help-is-a-table-of-contents.md new file mode 100644 index 00000000..d427ff35 --- /dev/null +++ b/docs/adr/0099-help-is-a-table-of-contents.md @@ -0,0 +1,58 @@ +# Help Is a Table of Contents + +Status: Proposed. + +## Context + +The agent-facing manual is spread across `/agents.md`, the repository skill, +the CLI README, `agent-paste help`, and the apex docs pages, and each carries +the same authentication, publish, and ephemeral paragraphs. Agents load all of +it up front whether or not the task needs it, and the copies drift. + +The MCP surface already has the shape that works: the `initialize` response +carries a short `instructions` string and each tool description is the help +for that tool, delivered when the tool is listed. + +## Decision + +1. `agent-paste help` prints a table of contents: one line per command and one + line per topic. It teaches nothing else. +2. Every command has `agent-paste help `. Cross-cutting topics have + `agent-paste help `: `auth`, `ephemeral`, `output`, and `ids`. +3. Every CLI error names the command or topic that answers it. An + unauthenticated `publish` exits 2 and prints the exact next command, + choosing `login --device-code` when no browser or TTY is available and + naming `--ephemeral` as the accountless alternative. +4. `/agents.md` and the skill shrink to: what agent-paste is, how to install + it, "run `agent-paste help`", the MCP pointer, and any taste guidance the + CLI cannot carry (the skill's HTML-over-Markdown rule). They do not restate + command help. +5. The apex docs pages remain the human reference and keep their current + scope. + +## Rationale + +Help delivered at the moment of need costs nothing until it is needed and is +never stale relative to the binary that prints it. A table of contents lets an +agent go straight from "I need help with X" to the help for X without reading +the manual for Y. Moving the auth dance into the error path removes the +paragraph that was repeated in nine places. + +## Consequences + +- `apps/cli/src/help.ts` becomes a map of topic to text with one renderer, and + `help` dispatch accepts any key in it. +- Error rendering in `render.ts` gains a `help_topic` field in the JSON error + envelope and a trailing "See: agent-paste help " line in plain mode. +- `/agents.md` and `SKILL.md` drop to roughly a third of their current length. +- Tests assert that every command and topic has a help entry and that every + error code maps to an existing topic; they do not pin help wording. + +## Done + +- `agent-paste help` lists every command and topic, and each listed key prints + its own page. +- `agent-paste publish` with no credentials exits 2 with a next command that + works when pasted. +- `/agents.md` and `SKILL.md` contain no command-level instructions beyond + "run `agent-paste help`". diff --git a/docs/adr/README.md b/docs/adr/README.md index 2af76b56..713a17be 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -47,6 +47,8 @@ This directory is the decision log for agent-paste: it records _why_ choices wer - [ADR 0093](./0093-capability-scoped-content-origins.md) introduced capability-scoped directory hosting and is amended by ADRs 0094, 0095, and 0096. Legacy `/v` content URLs and `/b` bundles remain valid until their signed tokens expire. - [ADR 0096](./0096-shorter-base32-capability-ids.md) shortens newly minted Capability IDs to a 23-character grouped Crockford-base32 form with 95 random bits and a check symbol. Legacy 32-character lowercase hexadecimal IDs remain permanent. - [ADR 0097](./0097-mcp-private-principal-handoff.md) terminates MCP OAuth bearers at the `mcp` Worker and passes only the verified WorkOS subject plus an allowlisted Route ID through named Cloudflare RPC entrypoints. Public `api` and `upload` routes reject MCP bearers; downstream member lookup, scopes, idempotency, and rate limits remain authoritative. +- [ADR 0098](./0098-one-artifact-id.md) (proposed) makes the Capability ID the Artifact ID, minted at row creation and emitted as `artifact_id` on every surface; `art_` and `rev_` prefixes are retired from output and remain valid on input. +- [ADR 0099](./0099-help-is-a-table-of-contents.md) (proposed) makes `agent-paste help` a table of contents with per-command and per-topic pages, moves the auth next-step into the `publish` error path, and shrinks `/agents.md` and the skill to pointers. - [ADR 0090](./0090-agent-file-read-back-api-decrypts-member-plaintext.md) closes the [ADR 0089](./0089-revision-commit-chain-tree-inheritance-and-server-reconstructed-delta.md) patch loop for agents that lack the working directory. Adds an optional `sha256` to `AgentViewFile`, a member-authed `GET /v1/artifacts/{id}/file-content?path=&revision_id=` read route in **`api`** returning `{ path, sha256, size_bytes, content_type, is_binary, body? }` (text body when UTF-8 and ≤10 MiB; oversize/binary return metadata only, oversize skips the R2 read), and an MCP `read_file` tool. The boundary-relevant decision: **`api` now decrypts artifact bytes and returns plaintext** to the owning Member — it does not widen the [ADR 0063](./0063-application-layer-encryption-for-artifact-bytes.md) confidentiality boundary (the Member already owns the artifact and can fetch the same bytes via the signed `url`; encryption defends the platform tier, not the owner), and `content` is untouched. The blob key is derived from the RLS-scoped row's `sha256` + the actor's workspace, never client input. Adds the **CLI diff client** (per-artifact manifest cache + working-dir diff → partial manifest with verified unified diffs; stale-base → full-publish fallback) and an `agent-paste pull` verb; MCP `add_revision` stays text-body-only ([ADR 0084](./0084-cli-and-mcp-share-one-publish-path.md)). Amends ADR 0063's "decrypt-on-read is `content`-only" note. - [ADR 0092](./0092-workspace-owned-feedback-with-operator-read.md) introduces product **Feedback**: a human-authored report owned by the **Workspace** it was submitted from (non-null `workspace_id`, RLS-scoped per [ADR 0044](./0044-workspace-isolation-via-postgres-rls.md)), not a platform-global table. Two submitters with an existing identity — a dashboard **Workspace Member** and an **Agent Credential** via CLI/MCP — drive one `POST /feedback` route on `api` that requires authentication but **no Scope** (even read-only credentials may file; reporting friction is the lowest-friction action). Each row carries free-text `body`, auto-attached **Feedback Context** (member route + artifact id; agent CLI/MCP version + command), an optional **Contact Email** (member session email, or the owning **Workspace**'s member email for an emailless credential), and a **Feedback Status** (`new` → `addressed`). Cross-tenant read for product insight is **Operator**-only under the platform **Run Scope**, reusing the [ADR 0046](./0046-operator-identity-and-web-admin-surface.md) operator surface; replies are sent manually. A fail-soft Cloudflare Email Worker notification fires to the verified `support@agent-paste.sh` on submit — the ingestion pipe into the operator's external tracker (Linear today) — with a per-actor throttle that suppresses (not drops) the email past the cap and flags the row, so the inbox can't be drowned and nothing is lost. Abuse reuses the existing escalation ladder (Actor Rate Limit → **Agent Credential Revocation** → **Platform Lockdown** [ADR 0040](./0040-platform-lockdown-for-operator-initiated-takedown.md), which suspends a workspace's credentials and thus blocks its feedback for free since the route requires auth); no feedback-specific mute is added. Day one is free-text only; adding a category later is a non-breaking additive migration. - [ADR 0091](./0091-client-side-revise-engine-and-literal-edit-tools.md) extracts a shared client-side **revise engine** (`@agent-paste/revise-core`) so the CLI and MCP express edits identically: a pure `applyEdits` (ordered literal `{old,new,replace_all?}`, `indexOf` not regex, unique-or-throw), a `RevisionReader` read-side seam (the twin of `PublishTransport`), and a `reviseOnePath` orchestrator (read base → apply → `diffWithSelfCheck` → partial-manifest `runPublish`). Three entry points flow through it: the CLI `edit` verb, an MCP `multi_edit` tool, and a **rebuilt** MCP `add_revision` that now reads the base and **preserves the artifact title** (fixing the bug where it overwrote it with `"Revision"`) and sends a verified patch (whole-file fallback only when `render_mode` changes the entrypoint or the diff is not smaller). Strict fail-fast: no-match / non-unique / binary or oversize base / `patch_conflict` are hard errors (no silent whole-blob fallback), with a single re-read+retry on a TOCTOU `patch_conflict`. Moves `diffWithSelfCheck` + the diff generator out of `apps/cli` into the package (MCP cannot import `apps/cli`), and makes finalize inherit `render_mode` as `session ?? base ?? infer(entrypoint)`. Reverses ADR 0090's "the diff half stays CLI-only" deferral; builds on [ADR 0090](./0090-agent-file-read-back-api-decrypts-member-plaintext.md) and [ADR 0084](./0084-cli-and-mcp-share-one-publish-path.md). From e7873d78d863884de91444c8d48e1e9e81962805 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:01:23 -0700 Subject: [PATCH 5/9] docs(adr): 0098 drops compatibility period; prod has one real member Co-Authored-By: Claude Fable 5.1 --- docs/adr/0098-one-artifact-id.md | 26 ++++++++++++++++---------- docs/adr/README.md | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/adr/0098-one-artifact-id.md b/docs/adr/0098-one-artifact-id.md index 724d0a4e..eb45f93f 100644 --- a/docs/adr/0098-one-artifact-id.md +++ b/docs/adr/0098-one-artifact-id.md @@ -27,13 +27,17 @@ Revision IDs (`rev_` ULID) carry a third shape for the same family of objects. 2. Every surface emits that ID as `artifact_id`: CLI JSON, MCP results, Agent View, list endpoints, Audit Events, and the dashboard route `/artifacts/`. The `art_` form is emitted nowhere. -3. `ArtifactReference` keeps accepting `art_` IDs and full URLs on input so - agents and bookmarks holding old values keep working. Existing rows keep - their `art_` primary key and existing Capability ID; only new rows use the - unified key. Legacy 32-hex Capability IDs remain valid per ADR 0096. +3. There is no compatibility period. Production checked on 2026-09-14 held + one real member with 10 live Artifacts and two unclaimed ephemeral + Artifacts due to expire the same day, so existing Artifact and Revision + rows are deleted before the change deploys. `ArtifactReference` accepts + the base32 ID and the full URL only. The `art_` and `rev_` shapes and the + legacy 32-hex Capability ID branch are removed from every contract, + database check, and content-host grammar. This supersedes the "legacy IDs + remain valid" clause of ADR 0096. 4. Revision IDs adopt the same grouped base32 shape with no prefix. They are not hostnames and carry no read capability; the shape is shared for - consistency only. Existing `rev_` values remain valid on input. + consistency only. 5. Because the Artifact ID is now credential material everywhere it appears, the `api` and `web` Workers redact the `/artifacts/` path segment in request logs the same way `content` already redacts the hostname label. @@ -53,12 +57,14 @@ symbol catches transcription errors that the prefix never did. - `docs/specs/cli.md`, `docs/specs/artifacts.md`, `CONTEXT.md`, and the MCP contract descriptions drop the "full URLs and `art_` IDs also work" caveats - down to a single line about legacy input. + entirely. - `packages/tokens` content and agent-view codecs stop asserting `art_` and `rev_` prefixes and validate the grouped base32 shape instead. -- The `artifacts.capability_id` column becomes redundant for new rows. It - stays for existing rows until a follow-up backfills `id` from it, which is a - separate migration with its own rollback plan. +- The `artifacts.capability_id` column is dropped; `artifacts.id` is the + hostname label. The migration truncates Artifact, Revision, and dependent + rows and rewrites the ID check constraints. No backfill exists. +- The 10 live Artifact URLs in the owner's Workspace stop resolving. Anything + worth keeping is republished after deploy. - CLI JSON `schema_version` stays at `"2"`. Field names and types are unchanged; only the ID format differs, and every consumer already accepts the base32 form on input. @@ -71,6 +77,6 @@ symbol catches transcription errors that the prefix never did. - New Artifacts publish with `artifact_id` equal to the URL hostname label on CLI, MCP, and dashboard surfaces. - `pull`, `edit`, `--artifact-id`, and every MCP `artifact_id` input accept the - new ID, an `art_` ID, and a full URL. + new ID or a full URL and reject `art_`, `rev_`, and 32-hex shapes. - Request logs for `api` and `web` never contain a full Artifact ID. - Specs and CONTEXT.md describe one Artifact ID and one Revision ID shape. diff --git a/docs/adr/README.md b/docs/adr/README.md index 713a17be..7e63fab4 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -47,7 +47,7 @@ This directory is the decision log for agent-paste: it records _why_ choices wer - [ADR 0093](./0093-capability-scoped-content-origins.md) introduced capability-scoped directory hosting and is amended by ADRs 0094, 0095, and 0096. Legacy `/v` content URLs and `/b` bundles remain valid until their signed tokens expire. - [ADR 0096](./0096-shorter-base32-capability-ids.md) shortens newly minted Capability IDs to a 23-character grouped Crockford-base32 form with 95 random bits and a check symbol. Legacy 32-character lowercase hexadecimal IDs remain permanent. - [ADR 0097](./0097-mcp-private-principal-handoff.md) terminates MCP OAuth bearers at the `mcp` Worker and passes only the verified WorkOS subject plus an allowlisted Route ID through named Cloudflare RPC entrypoints. Public `api` and `upload` routes reject MCP bearers; downstream member lookup, scopes, idempotency, and rate limits remain authoritative. -- [ADR 0098](./0098-one-artifact-id.md) (proposed) makes the Capability ID the Artifact ID, minted at row creation and emitted as `artifact_id` on every surface; `art_` and `rev_` prefixes are retired from output and remain valid on input. +- [ADR 0098](./0098-one-artifact-id.md) (proposed) makes the Capability ID the Artifact ID, minted at row creation and emitted as `artifact_id` on every surface; `art_`, `rev_`, and legacy 32-hex shapes are removed outright after a one-time wipe of existing Artifacts (production held one real member on 2026-09-14). - [ADR 0099](./0099-help-is-a-table-of-contents.md) (proposed) makes `agent-paste help` a table of contents with per-command and per-topic pages, moves the auth next-step into the `publish` error path, and shrinks `/agents.md` and the skill to pointers. - [ADR 0090](./0090-agent-file-read-back-api-decrypts-member-plaintext.md) closes the [ADR 0089](./0089-revision-commit-chain-tree-inheritance-and-server-reconstructed-delta.md) patch loop for agents that lack the working directory. Adds an optional `sha256` to `AgentViewFile`, a member-authed `GET /v1/artifacts/{id}/file-content?path=&revision_id=` read route in **`api`** returning `{ path, sha256, size_bytes, content_type, is_binary, body? }` (text body when UTF-8 and ≤10 MiB; oversize/binary return metadata only, oversize skips the R2 read), and an MCP `read_file` tool. The boundary-relevant decision: **`api` now decrypts artifact bytes and returns plaintext** to the owning Member — it does not widen the [ADR 0063](./0063-application-layer-encryption-for-artifact-bytes.md) confidentiality boundary (the Member already owns the artifact and can fetch the same bytes via the signed `url`; encryption defends the platform tier, not the owner), and `content` is untouched. The blob key is derived from the RLS-scoped row's `sha256` + the actor's workspace, never client input. Adds the **CLI diff client** (per-artifact manifest cache + working-dir diff → partial manifest with verified unified diffs; stale-base → full-publish fallback) and an `agent-paste pull` verb; MCP `add_revision` stays text-body-only ([ADR 0084](./0084-cli-and-mcp-share-one-publish-path.md)). Amends ADR 0063's "decrypt-on-read is `content`-only" note. - [ADR 0092](./0092-workspace-owned-feedback-with-operator-read.md) introduces product **Feedback**: a human-authored report owned by the **Workspace** it was submitted from (non-null `workspace_id`, RLS-scoped per [ADR 0044](./0044-workspace-isolation-via-postgres-rls.md)), not a platform-global table. Two submitters with an existing identity — a dashboard **Workspace Member** and an **Agent Credential** via CLI/MCP — drive one `POST /feedback` route on `api` that requires authentication but **no Scope** (even read-only credentials may file; reporting friction is the lowest-friction action). Each row carries free-text `body`, auto-attached **Feedback Context** (member route + artifact id; agent CLI/MCP version + command), an optional **Contact Email** (member session email, or the owning **Workspace**'s member email for an emailless credential), and a **Feedback Status** (`new` → `addressed`). Cross-tenant read for product insight is **Operator**-only under the platform **Run Scope**, reusing the [ADR 0046](./0046-operator-identity-and-web-admin-surface.md) operator surface; replies are sent manually. A fail-soft Cloudflare Email Worker notification fires to the verified `support@agent-paste.sh` on submit — the ingestion pipe into the operator's external tracker (Linear today) — with a per-actor throttle that suppresses (not drops) the email past the cap and flags the row, so the inbox can't be drowned and nothing is lost. Abuse reuses the existing escalation ladder (Actor Rate Limit → **Agent Credential Revocation** → **Platform Lockdown** [ADR 0040](./0040-platform-lockdown-for-operator-initiated-takedown.md), which suspends a workspace's credentials and thus blocks its feedback for free since the route requires auth); no feedback-specific mute is added. Day one is free-text only; adding a category later is a non-breaking additive migration. From 42e18f5541c253c38784629711652cad3df34c13 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:06:38 -0700 Subject: [PATCH 6/9] docs(ops): todo to stop minting a default API key at sign-in Co-Authored-By: Claude Fable 5.1 --- docs/ops/security-todo.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/ops/security-todo.md b/docs/ops/security-todo.md index 008cdace..13a5a8ff 100644 --- a/docs/ops/security-todo.md +++ b/docs/ops/security-todo.md @@ -68,6 +68,21 @@ stays fast and does not run the full bundle. which additionally pins `@cyclonedx/cyclonedx-esbuild@1` — re-verify all surfaces together. +- [ ] **Stop minting a "Default" API key at first sign-in.** Linear ticket + pending (connector was unauthorized when filed, 2026-09-14). + `ensureWebMember` in + `packages/db/src/repository/workflows/web-member-workflow.ts` creates a + never-expiring key with the member's full scopes the moment WorkOS + returns a new user, before they have done anything. Production on + 2026-09-14 held one drive-by account whose key has never been used. + The CLI does not depend on it: `agent-paste login` mints its own key + through `web.apiKeys.create` (`apps/cli/src/login.ts`). Decide the + trigger (first CLI login, first publish, or an explicit create on + `/keys`) and mint only then. Confirm nothing consumes the bootstrap + `secret` in `webAuthResponse`, then remove the mint and its + `api_key.created` event. Revoke the existing unused bootstrap key. + Done when a fresh sign-in yields zero API keys and `login` still works. + ## CLI Release supply-chain (AP-154) - [x] **Phase 1 (capture, non-blocking)** — `.github/workflows/cli-release.yml` From 0f447ed26a7ca5f79772fefb052e2230d0a93869 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:25:00 -0700 Subject: [PATCH 7/9] docs(ops): link default-key todo to AP-444 Co-Authored-By: Claude Fable 5.1 --- docs/ops/security-todo.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ops/security-todo.md b/docs/ops/security-todo.md index 13a5a8ff..eeddc6ad 100644 --- a/docs/ops/security-todo.md +++ b/docs/ops/security-todo.md @@ -68,8 +68,8 @@ stays fast and does not run the full bundle. which additionally pins `@cyclonedx/cyclonedx-esbuild@1` — re-verify all surfaces together. -- [ ] **Stop minting a "Default" API key at first sign-in.** Linear ticket - pending (connector was unauthorized when filed, 2026-09-14). +- [ ] **Stop minting a "Default" API key at first sign-in** (AP-444, filed + 2026-09-14). `ensureWebMember` in `packages/db/src/repository/workflows/web-member-workflow.ts` creates a never-expiring key with the member's full scopes the moment WorkOS From 3b8fff36d48a9a5f8738745f583316256f3eb2b0 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:57:45 -0700 Subject: [PATCH 8/9] docs: clarify execution policy and review follow-ups --- apps/apex/src/agents.ts | 2 +- apps/apex/src/docs/pages/safety.ts | 4 ++-- docs/adr/0096-shorter-base32-capability-ids.md | 4 ++++ docs/adr/README.md | 2 +- docs/ops/security-todo.md | 5 ++++- skills/agent-paste/SKILL.md | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/apps/apex/src/agents.ts b/apps/apex/src/agents.ts index 636dbeac..f83004ab 100644 --- a/apps/apex/src/agents.ts +++ b/apps/apex/src/agents.ts @@ -74,7 +74,7 @@ Directory publish keeps relative paths and skips \`.git\`, \`node_modules\`, } \`\`\` -Signed-in Artifacts run as ordinary top-level pages: inline scripts, external +Claimed Artifacts run as ordinary top-level pages: inline scripts, external HTTPS dependencies, fetch, WebSockets, and dedicated workers all work. Service workers are blocked everywhere. diff --git a/apps/apex/src/docs/pages/safety.ts b/apps/apex/src/docs/pages/safety.ts index 7815049c..798e9182 100644 --- a/apps/apex/src/docs/pages/safety.ts +++ b/apps/apex/src/docs/pages/safety.ts @@ -48,7 +48,7 @@ export const SAFETY_DOC: DocsPage = { }, { kind: "paragraph", - text: "Signed-in Artifact HTML runs with a permissive CSP: inline scripts, external HTTPS dependencies, data and blob assets, dedicated workers, fetch, and secure WebSockets. `frame-ancestors 'none'` keeps other sites from framing it. Service workers are blocked on every Artifact host.", + text: "Claimed Artifact HTML runs with a permissive CSP: inline scripts, external HTTPS dependencies, data and blob assets, dedicated workers, fetch, and secure WebSockets. `frame-ancestors 'none'` keeps other sites from framing it. Service workers are blocked on every Artifact host.", }, ], }, @@ -58,7 +58,7 @@ export const SAFETY_DOC: DocsPage = { blocks: [ { kind: "paragraph", - text: "Unclaimed ephemeral HTML blocks scripts, connections, forms, frames, objects, and workers. Claiming keeps the URL and switches it to the signed-in policy.", + text: "Unclaimed ephemeral HTML blocks scripts, connections, forms, frames, objects, and workers. Claiming keeps the URL and switches it to the claimed policy.", }, ], }, diff --git a/docs/adr/0096-shorter-base32-capability-ids.md b/docs/adr/0096-shorter-base32-capability-ids.md index e6994d0c..b8459b3f 100644 --- a/docs/adr/0096-shorter-base32-capability-ids.md +++ b/docs/adr/0096-shorter-base32-capability-ids.md @@ -2,6 +2,10 @@ Status: Accepted and implemented. Amends [ADR 0093](./0093-capability-scoped-content-origins.md) and [ADR 0094](./0094-capability-url-is-the-artifact-link.md). +[ADR 0098](./0098-one-artifact-id.md) proposes superseding this ADR's legacy-ID +compatibility guarantee. Until that proposal is implemented, the compatibility +rules below remain in effect. + ## Decision New Capability IDs contain 20 lowercase Crockford-base32 symbols from diff --git a/docs/adr/README.md b/docs/adr/README.md index 7e63fab4..13261eca 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -45,7 +45,7 @@ This directory is the decision log for agent-paste: it records _why_ choices wer - [ADR 0089](./0089-revision-commit-chain-tree-inheritance-and-server-reconstructed-delta.md) makes **Revisions** behave like Git commits so an agent can express "change this file" instead of the whole tree. Adds `revisions.parent_revision_id` and a `base_revision_id` + `deleted_paths` + partial-manifest publish contract where unlisted paths inherit the parent tree by reference (`api`-side merge, full `artifact_files` tree still materialized). Layers server-reconstructed intra-file delta on top: a changed file may be sent as a unified diff (text; whole-blob fallback for binary), reconstructed and re-hashed to a whole blob **synchronously at finalize in `upload`** so a patch that cannot apply fails the same publish call with an agent-visible `patch_conflict` (a broken patch never becomes a servable draft) and `content` plus the [ADR 0063](./0063-application-layer-encryption-for-artifact-bytes.md) encryption boundary are untouched. Builds on [ADR 0088](./0088-workspace-scoped-content-addressed-blob-deduplication.md); defers chunk stores, per-block AEAD, Range serving, global dedup, and dropping encryption. - [ADR 0094](./0094-capability-url-is-the-artifact-link.md) is the current publish-link architecture as amended by [ADR 0095](./0095-isolate-active-content-and-restore-ephemeral-execution-policy.md) and [ADR 0096](./0096-shorter-base32-capability-ids.md). Publish returns one durable per-Artifact `{id}.agent-paste.link` URL. Claimed Artifacts retain active-content compatibility, ephemeral Artifacts are static, and revising repoints the same hostname. The `/al` Access Link surface, app iframe, sandbox, and visibility commands are removed. Preview uses `{id}-preview.agent-paste.link`. - [ADR 0093](./0093-capability-scoped-content-origins.md) introduced capability-scoped directory hosting and is amended by ADRs 0094, 0095, and 0096. Legacy `/v` content URLs and `/b` bundles remain valid until their signed tokens expire. -- [ADR 0096](./0096-shorter-base32-capability-ids.md) shortens newly minted Capability IDs to a 23-character grouped Crockford-base32 form with 95 random bits and a check symbol. Legacy 32-character lowercase hexadecimal IDs remain permanent. +- [ADR 0096](./0096-shorter-base32-capability-ids.md) shortens newly minted Capability IDs to a 23-character grouped Crockford-base32 form with 95 random bits and a check symbol. Legacy 32-character lowercase hexadecimal IDs remain supported; proposed [ADR 0098](./0098-one-artifact-id.md) would remove that compatibility guarantee. - [ADR 0097](./0097-mcp-private-principal-handoff.md) terminates MCP OAuth bearers at the `mcp` Worker and passes only the verified WorkOS subject plus an allowlisted Route ID through named Cloudflare RPC entrypoints. Public `api` and `upload` routes reject MCP bearers; downstream member lookup, scopes, idempotency, and rate limits remain authoritative. - [ADR 0098](./0098-one-artifact-id.md) (proposed) makes the Capability ID the Artifact ID, minted at row creation and emitted as `artifact_id` on every surface; `art_`, `rev_`, and legacy 32-hex shapes are removed outright after a one-time wipe of existing Artifacts (production held one real member on 2026-09-14). - [ADR 0099](./0099-help-is-a-table-of-contents.md) (proposed) makes `agent-paste help` a table of contents with per-command and per-topic pages, moves the auth next-step into the `publish` error path, and shrinks `/agents.md` and the skill to pointers. diff --git a/docs/ops/security-todo.md b/docs/ops/security-todo.md index eeddc6ad..1ce5e57f 100644 --- a/docs/ops/security-todo.md +++ b/docs/ops/security-todo.md @@ -81,7 +81,10 @@ stays fast and does not run the full bundle. `/keys`) and mint only then. Confirm nothing consumes the bootstrap `secret` in `webAuthResponse`, then remove the mint and its `api_key.created` event. Revoke the existing unused bootstrap key. - Done when a fresh sign-in yields zero API keys and `login` still works. + Done when browser sign-in creates zero API keys, and first-time CLI + `login` through `web.apiKeys.create` provisions the Workspace and member + without a Default key, creates exactly one CLI key, and preserves that + key's `api_key.created` event. ## CLI Release supply-chain (AP-154) diff --git a/skills/agent-paste/SKILL.md b/skills/agent-paste/SKILL.md index 78c7b984..7abbb9e0 100644 --- a/skills/agent-paste/SKILL.md +++ b/skills/agent-paste/SKILL.md @@ -11,7 +11,7 @@ keeps its URL. ## Write reports as HTML When handing a human a report, plan, review, or summary, publish HTML rather than Markdown. Use real -typography, tables, inline SVG charts, and collapsible sections. Signed-in Artifacts can run inline +typography, tables, inline SVG charts, and collapsible sections. Claimed Artifacts can run inline scripts, the Tailwind browser CDN, and HTTPS libraries; ephemeral Artifacts block scripts and network, so keep those static. Lead with the finding, keep the supporting data on the page, and open the URL once before handing it over. From 3a082f1c6d3c123ee25e0c4553594865aa3c007b Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:05:54 -0700 Subject: [PATCH 9/9] docs: align key creation triggers and credential precedence --- docs/ops/security-todo.md | 6 +++--- skills/agent-paste/SKILL.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/ops/security-todo.md b/docs/ops/security-todo.md index 1ce5e57f..59e29dcb 100644 --- a/docs/ops/security-todo.md +++ b/docs/ops/security-todo.md @@ -76,9 +76,9 @@ stays fast and does not run the full bundle. returns a new user, before they have done anything. Production on 2026-09-14 held one drive-by account whose key has never been used. The CLI does not depend on it: `agent-paste login` mints its own key - through `web.apiKeys.create` (`apps/cli/src/login.ts`). Decide the - trigger (first CLI login, first publish, or an explicit create on - `/keys`) and mint only then. Confirm nothing consumes the bootstrap + through `web.apiKeys.create` (`apps/cli/src/login.ts`). Keep CLI key + creation on login and explicit key creation on `/keys`; browser sign-in + must not mint a key. Confirm nothing consumes the bootstrap `secret` in `webAuthResponse`, then remove the mint and its `api_key.created` event. Revoke the existing unused bootstrap key. Done when browser sign-in creates zero API keys, and first-time CLI diff --git a/skills/agent-paste/SKILL.md b/skills/agent-paste/SKILL.md index 7abbb9e0..ae2c30be 100644 --- a/skills/agent-paste/SKILL.md +++ b/skills/agent-paste/SKILL.md @@ -29,7 +29,7 @@ agent-paste publish --json `whoami` exits 0 even when signed out, so check `authenticated`. If false, run `login` where a browser is available or `login --device-code` in a sandbox. Device login prints a URL and code on stderr; keep it running until the user approves, then run `whoami` again. An `AGENT_PASTE_API_KEY` -env var also authenticates. +env var takes precedence over stored credentials. When login is unavailable, or the user asks for accountless publishing: