Private-first publish: one permanent private link, explicit make_public, unified scopes#525
Conversation
…te_url, explicit make_public Publish and add_revision are now content-only on every surface (CLI, MCP, REST). They take no visibility input and return exactly one link, `private_url`, the login-walled clean viewer at `/v/<artifactId>` for a Workspace Member. There is no `share` input and no `shared` output anywhere; the misleading viewer_url/ shared flip is gone. Going public is a separate, explicit, revocable step: `make_public` (MCP) / `agent-paste make-public` (CLI), with `revoke_access_link` as the go-private verb. The `/artifacts/<id>` console is never returned to an agent or user. Private Link is a plain `_authed` route, not an access_links row: nothing to sign, nothing to leak, nothing to revoke. The public `/al/<publicId>` resolve gate is untouched, so existing Share Links keep resolving. A new shared ArtifactLiveViewer backs both the console and the clean `/v` viewer. make_public now reuses an Artifact's one active (non-revoked, unexpired) Share Link instead of minting a duplicate, so an Artifact has at most one live Share Link — making the "mints or reuses the one Share Link" contract true in code, not just in copy. revision links are never deduped. Unify the scope vocabulary: one set `read`/`publish`/`admin` shared verbatim by API and MCP. The old MCP-only `write`/`share` names are gone, and the mcpScopesToApiScopes/apiScopesToMcpScopes translation layer is deleted (MCP scopes are the member's stored API scopes verbatim, per ADR 0079). The four agent access-link routes require `publish` (managing your own Artifact's public access is content authority); `admin` is dashboard-only and no MCP tool needs it. web.accessLinks.lockdown.* stay `admin`. Supersedes ADR 0085 (publish-returns-one-viewer-url, which made the link flip and surfaced the lie); amends ADR 0084 (output shape) and ADR 0079 (scope vocabulary unified). Adds ADR 0086. Specs are updated as the source of truth (api/web/cli/ephemeral-publish, CONTEXT vocab, docs/mcp.md, apex agent docs). OpenAPI goldens regenerated. Early-alpha break: CLI and MCP ship in lockstep; the deployed MCP server `instructions` text still teaches `share:true` and updates on deploy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tifacts console The /v clean viewer's own header Wordmark linked to /artifacts/$artifactId — the dashboard console page that is explicitly never to be handed to a user. Point it at /dashboard (brand home) instead. The console stays reachable only from the dashboard's own artifact list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t a revoked link If a client reuses the same idempotency key across a revoke (make_public → revoke_access_link → make_public with the same key), the create step replays the now-revoked link from its idempotency record and mint fails on the dead link. Real MCP clients use monotonic JSON-RPC ids and the CLI uses random UUIDs so neither collides, but the failure mode is sharp: a revoke must never lock you out of going public again. createAndMintAccessLink now retries once on a salted idempotency key when the first create→mint fails. The salted create runs the command fresh, so for a share link it reuses the artifact's active link (the DB-layer findActiveShareLink path) or mints a new one. The happy path never retries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
We kept tripping over what the link guarantees, so write it down where the specs are the source of truth. The Private Link (`/v/<artifactId>`) is permanent and stable: derived only from the Artifact id with no token/signature/expiry, and add_revision republishes into the same id, so it never changes across revisions and live-updates to the latest Published Revision. It is always private (member only; publish never grants public access) and stops resolving only when the Artifact is deleted or swept by Auto Deletion — the expires_at in the publish response is the Artifact's content lifetime, not a link expiry. Going public is the separate, revocable Share Link. Documented in CONTEXT.md (Private Link + Share Link vocab), docs/specs/api.md (publish behavior), and ADR 0086 (decision trail). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 25 minutes and 26 seconds. Learn how PR review limits work. To continue reviewing without waiting, enable usage-based billing in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (3)
📒 Files selected for processing (86)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
First-pass review
Risk: high
Decision: needs human review
Ticket triage
- Intended change: Rebuild publish around private-first semantics: publish/
add_revisionare content-only and always return a stable member-only Private Link (/v/<artifactId>asprivate_url). Public access requires an explicitmake_public/make-publicstep. Removesshare/--share/shared/access_link_urlfrom publish surfaces. Unifies API+MCP scopes toread/publish/admin(dropswrite/sharetranslation layer). Adds ADR 0086; supersedes ADR 0085. - Scope match: Aligns with the spirit of AP-304 (explicit sharing) but goes further — visibility is fully decoupled from publish rather than opt-in via
--share. No Linear issue is linked in the PR; consider attaching a ticket for traceability.
Review findings
Blocking: None found in critical paths (publish coordinator, MCP tools, access-links workflow, scope checks, /v viewer). The core security fix — publish no longer mints or implies a public link — is implemented correctly.
Non-blocking:
/v/$artifactIdfirst-login edge case — The route authenticates vialoadAuthedSessionFnbut does not callprovisionWebMemberSessionFn(unlike_authedroutes). A brand-new user signing in directly via a private link may hit 403 and see the generic empty state instead of the viewer. Not a data leak, but worth fixing or documenting.- MCP
createAndMintAccessLinkretries on any first failure — The:rsalted-key retry runs for all failures, not only revoked-link replay. Happy path is fine; consider narrowing to mint failures. - Server-side idempotency replay for revoked share links — Idempotency replay can return a revoked link ID; MCP compensates client-side but API/CLI callers with stable keys may not. Worth a server-side fix to remove the client hack.
- Missing
/v/route tests —ArtifactLiveVieweris tested on the dashboard route; the new clean/v/handoff route has no dedicated coverage. - Deploy coordination — PR notes deployed prod MCP
instructionsstill teachshare:trueuntil next deploy. Merge should ship MCP + API + docs together.
Merge checklist
| Check | Status |
|---|---|
| Ticket linked | |
| Scope matches intent | ✅ Private-first model is coherent end-to-end |
| Checks green | ✅ Validate, Postgres smoke, CodeQL, Secret scan all pass |
| Tests/docs appropriate | ✅ Broad test + spec/ADR updates; /v/ route tests are a gap |
| No blocking findings | ✅ |
| No high-risk areas | ❌ Auth/authorization/permissions + breaking public contracts |
| Merge-safe for automation | ❌ |
Why human review
This PR touches authentication, authorization, and permissions (scope unification, MCP tool scope gates, login-walled viewer), is security-sensitive (publish visibility model), and makes breaking contract changes across CLI, MCP, REST, and OpenAPI (~88 files). The implementation looks sound and CI is green, but the blast radius warrants a human pass before merge — especially scope migration for existing MCP tokens/API keys and coordinated prod deploy of MCP instructions.
Sent by Cursor Automation: First Pass PR Reviewer
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8335df4. Configure here.
…st-login provisioning, /v tests Cursor Bugbot (blocking, Medium) + first-pass review findings: - **Revision-link retry duplicates (Bugbot).** createAndMintAccessLink's salted-key retry ran for both make_public and create_revision_link. Share links dedupe on create (findActiveShareLink), so the retry is idempotent; revision links do NOT dedupe, so a create-ok/mint-fail retry inserted a second revision link for the same revision. Scope the retry to `type:'share'` only; revision links return the original failure. Test asserts create_revision_link does not retry. - **/v first-login provisioning (finding #1).** /v is a standalone handoff route, not under _authed, so it never provisioned the workspace member. A brand-new user signing in directly via a private link had a valid token but no member row, so the owner-scoped artifact read missed and showed the empty state. Provision via webSessionQuery in the loader before the artifact read. - **/v route tests (finding #4).** New apps/web/test/v-route.test.tsx pins the gate: unauthed → redirect + no artifact read; authed → provision-then-read in order; chromeless viewer renders; redirect path works. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the thorough pass. Addressed in
Acknowledged, not changed in this PR:
All green: |
|
Traceability + follow-up tickets (per review):
|
|
agent-paste PR preview resources were cleaned up. The shared Preview GitHub Environment is retained for future preview deploys. |
0.1.5 was published to npm before #525 (private-first publish) merged, so the released CLI parses the old `artifact_url` publish response while the live server now returns `private_url`. Every authenticated publish fails client-side with `cli_error … expected string … artifact_url`, even though the artifact is created server-side — stranding the link and causing retry duplicates. The fix is already on main (#525 swapped artifact_url->private_url in the shared runPublish/PublishResult contract); 0.1.5 just predates it. Bump the version so cli-release cuts cli-v0.1.6 and re-publishes from current source. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lta, agent read-back (ADR 0088/0089/0090) (#529) * docs(adr): record Git-like revision model (commit chain + jobs-reconstructed delta) ADR 0086 retroactively captures the shipped workspace-scoped content-addressed blob dedup (no prior ADR existed; it was only in data-model.md/api.md + commit dea091f). ADR 0087 decides the next step: revisions.parent_revision_id + tree inheritance (partial-manifest publish, unlisted paths inherit the parent tree by reference) so an agent can express "change this file" instead of the whole tree, plus server-reconstructed intra-file delta (unified diff for text, whole-blob fallback for binary). Reconstruction runs in jobs, not upload: upload is write-only against R2 today (sole op ARTIFACTS.put), while jobs already does the read-decrypt-transform-reencrypt-write shape for Bundle generation. This keeps content and the ADR 0063 encryption boundary untouched. Chunk stores, per-block AEAD, Range serving, global dedup, and dropping encryption are deferred. Spec/CONTEXT edits land with the implementation, per the spec-is-source-of-truth rule. Staged plan in docs/ops/git-like-revisions-todo.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(db,contracts): add revision commit chain + partial-manifest upload contract (ADR 0087 stages 1-2) Stage 1 (schema): revisions.parent_revision_id nullable column with a deferrable composite self-FK on (workspace_id, artifact_id, parent_revision_id) -> revisions(workspace_id, artifact_id, id), ON DELETE SET NULL (parent_revision_id), plus revisions_parent_idx. The composite target structurally pins a parent to the same Workspace and Artifact; the column-list SET NULL nulls only the pointer (plain SET NULL would violate the NOT NULL workspace_id/artifact_id). Deferrable because claim-reparent bulk-rewrites workspace_id across all revisions inside deferred constraints. Threaded through the Revision type, insert mapper, and mapRevision; draft creation writes NULL (Stage 3 populates from base_revision_id). Migration 0024 is idempotent (journal-less runner) and verified on PGlite + snapshot regenerated. Stage 2 (contract): CreateUploadSessionRequest gains optional base_revision_id, deleted_paths, and a per-file patch descriptor {base_sha256, format:"unified", result_sha256}. A superRefine enforces the structural rules (patch/deleted_paths require base_revision_id; deleted_paths unique; a path cannot be both uploaded and deleted; format must be unified). Stateful checks and the tree-inheritance merge / diff reconstruction are deferred to Stages 3-4. OpenAPI golden regenerated; round- trip tests added. Also fixes a pre-existing Sha256Hex /u-flag leak that serialized an invalid "^...$/u" pattern into the published upload OpenAPI (now clean in all 6 spots), and folds the ADR 0087 spec-source-of-truth updates into data-model.md (column + index), api.md (request fields + rules), and CONTEXT.md (Revision parent relationship). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(api,db): tree inheritance at finalize for revision commit chain (ADR 0087 stage 3) Publishing against base_revision_id now inherits the base Revision's unchanged blob-backed files instead of re-uploading them: a one-file change yields a full artifact_files tree with one new blob and parent_revision_id set. The merge runs at finalize (mergeBaseRevisionTree), recomputes file_count/size_bytes from the merged tree, and re-runs validateUpload (caps + entrypoint) against it. Stateful validation deferred from the Stage 2 contract now lands server-side (6 new repo error codes -> invalid_request): published-base-only, same workspace (base_revision_not_found) and same artifact (base_revision_artifact_mismatch, fired before the composite parent FK would 500), deleted-path-in-base, patch base_sha256 match, and blob-backed-only inheritance (a revision-scoped base path is not refcount-protected, so it is rejected rather than dangled). Patch descriptors (patch_base_sha256/patch_result_sha256) are recorded and validated on upload_session_files with the diff uploaded as a revision object (sha256 omitted from the signed PUT), but finalize fails loud (patch_reconstruction_unavailable) until jobs reconstruction lands in Stage 4 - otherwise the raw diff bytes would be served as the file. A file may not declare both a whole-file sha256 and a patch. Carriers: upload_sessions.base_revision_id + deleted_paths, the two patch columns on upload_session_files (migration 0025, idempotent). Specs updated alongside. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(upload,db,storage): synchronous intra-file patch reconstruction at finalize (ADR 0087 stage 4) Apply an agent-uploaded unified diff to the base blob synchronously at finalize, in the upload worker, before the new Revision commits. A patch that cannot apply fails the finalize call with patch_conflict (HTTP 422, "patch_conflict: <path>: <reason>") so the agent re-submits a corrected diff; a broken patch never becomes a servable Revision. The reconstructed result is an ordinary content-addressed blob, so content/bundles/GC are unchanged and no migration is needed. - packages/storage: hand-rolled byte-exact unified-diff applier (no diff dep: jsdiff's UTF-16 round-trip breaks the raw-byte result_sha256 check) + workspace-blob read/write helpers (blob AAD) and a revision-file read helper. - packages/db: RevisionReconstructor adapter (RepositoryOptions, wired in createPostgresRuntime + local MVP harness), invoked from mergeBaseRevisionTree before any DB write; result blob + content_blobs row commit with the draft; caps run on the reconstructed result size; removes the Stage 3 patch_reconstruction_unavailable gate; conflict -> patch_conflict, infra failures -> storage_unavailable. - contracts/worker-runtime: new patch_conflict ErrorCode (422), MCP status map + publishChain, route errors, openapi goldens; also maps the previously 500-falling-back finalize codes (caps, expired, incomplete) for MCP. - upload: widen R2 binding with get; surface the conflict path+reason as the error message. - scripts: smoke-local-patch.mjs drives the real reconstruction path (local + preview). Verified byte-exact serve + patch_conflict on hosted preview. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: run the patch reconstruction smoke locally and against PR preview (ADR 0087 stage 4) Stage 4's unit/integration tests use a fake reconstructor, so the only checks that exercise the real decrypt -> apply diff -> hash-verify -> re-encrypt -> serve path are the smoke. Wire it into both gates: - ci.yml: `pnpm smoke:local:patch` after the existing local smoke (in-memory MVP, every PR via Validate). - pr-preview.yml: `node scripts/smoke-local-patch.mjs pr` against the deployed PR preview Workers, reusing the per-PR deploy outputs + harness secret. smoke-local-patch.mjs now supports local/preview/pr targets with env resolution mirroring scripts/smoke-hosted.mjs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: renumber Git-revision ADRs to 0087/0088 after merge collision Merged PR #525 claimed ADR 0086 for "publish is content-only, private-first". This branch's earlier work also created an 0086 (workspace-scoped blob dedup) and an 0087 (revision commit chain + reconstructed delta). Renumber this branch's pair to 0087 (blob dedup) and 0088 (revision delta) so 0086 stays the merged publish-privacy ADR, and sweep every cross-reference (ADR bodies, specs, migrations, code comments, CI smoke steps) to the new numbers. Reference-only: no schema, contract, or logic change. Full suite green (typecheck 39/39, test 39/39, openapi:check). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(api,cli,mcp,contracts): agent file read-back + CLI incremental-revise diff client (ADR 0089) Stage 5 of the git-like revision model: give an agent without a working copy a way to read back exactly what's stored so it can produce a correct unified-diff revise, and make the CLI send only what changed. - sha256 on Agent View file entries (optional, non-breaking add) - new member-authed GET /v1/artifacts/{id}/file-content in the api worker: it decrypts the owning member's plaintext and returns { path, sha256, size_bytes, content_type, is_binary, body? }. Oversize text and binary files omit the body; oversize short-circuits before touching R2. Any decrypt/storage failure maps to storage_unavailable (503), never a 500. - MCP read_file tool forwarding to it; api-client artifacts.readFile() - CLI: per-artifact manifest cache (0600), a byte-exact unified-diff generator that self-checks (re-applies its own diff and verifies the digest before attaching it; a generator bug degrades to a whole-blob upload, never a finalize conflict), incremental revise wiring, a `pull` verb, and single-shot full-republish fallback when the cached base is unusable - finalize now carries the precise base-* repository kind as the error detail so the CLI self-heal fires for all base-unusable conditions, not just patch conflicts (the 5 base-* kinds collapse to invalid_request on the wire) ADR 0089 records the api-decrypts-member-plaintext trust-boundary decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(adr): renumber revision stack to 0088/0089/0090 (0087 taken by public-artifacts) Main merged ADR 0087 (public-artifacts-and-unlisted-share-links, #528) while this revision stack was off-branch, so the prior renumber onto 0087/0088 collided. Shift the stack up one and restore main's 0087 index row: workspace-scoped blob dedup 0087 -> 0088 revision commit chain + delta 0088 -> 0089 agent file read-back 0089 -> 0090 Renames the three ADR files and rewrites every in-tree reference (filename tokens + bare "ADR 00NN" comments across code, migrations, specs, CI, and CONTEXT.md). Bare "ADR 0087" references that mean main's public-artifacts ADR (CONTEXT.md, project-status.md, 0086) are preserved untouched. README index re-adds the 0087 public-artifacts row dropped during the rebase. Also drops four dead imports in apps/cli/src/index.ts (PublishResultShape, formatBytes, hyperlink, paint) left unused by the Stage 5 work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(adr): record ADR 0091 — shared revise engine + literal multi-edit tools Design for @agent-paste/revise-core: a pure applyEdits core, a RevisionReader read-side seam (twin of PublishTransport), and a reviseOnePath orchestrator that both the CLI `edit` verb and an MCP `multi_edit` tool drive, plus a rebuilt MCP `add_revision` that preserves the artifact title (fixing the "Revision" overwrite bug) and sends a verified patch. Strict fail-fast; moves diffWithSelfCheck out of apps/cli so MCP can share it; finalize render_mode inheritance invariant. Records the planned spec in docs/ops/git-like-revisions-todo.md (live cli/mcp specs update when the code lands). Builds on ADR 0090; reverses its "diff half stays CLI-only" deferral. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(upload): allow delete-only revise (empty files) against a base revision A partial-manifest revise where every remaining file is unchanged but some paths are deleted produced an empty files manifest. Both CreateUploadSessionRequest (files.min(1)) and validateUpload (files.length === 0 -> file_count_cap_exceeded) rejected it, so delete-only revises failed instead of inheriting the base tree and dropping the paths. Make the min-1 file rule conditional on the publish kind: a whole publish (no base_revision_id) still requires at least one file; a base delta may send zero files as long as it deletes at least one path. validateUpload mirrors this for the partial-manifest path; the merged tree is still re-checked with the whole-tree caps (entrypoint + total size) at finalize. Bugbot finding on PR #529. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): fall back to full publish on no-op delta; guard claim-token leak + LCS blowup - runPublish drops the base and sends a whole-blob manifest when the revise plan produces no changed files and no deletions, so an unchanged working tree revise no longer sends an empty delta the server rejects (bugbot). - publish-format only treats the claim token in the URL query string as a leak, not a coincidental fragment substring (CodeRabbit). - unified-diff-gen returns null instead of attempting an LCS over more than MAX_LCS_CELLS cells, so a pathological diff falls back to a whole blob rather than pinning a core (CodeRabbit). - Corrected the pull/read-back comment that wrongly claimed base64 bytes ride in the JSON; oversize text/binary is metadata-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(api): infer is_binary from content type on the oversize file-content path An oversize file is returned as metadata without reading R2, so its bytes are never inspected. Previously is_binary defaulted to false, mislabeling an oversize binary as text. Now the flag is derived from the stored content type (non-text/* => binary), so a client keying on the flag does not try to inline binary bytes. body stays absent on this branch (CodeRabbit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(db): return committed revision counts on finalize fast path; document FK scope - The finalized-session fast path now reads the committed revision and reports its file_count/size_bytes, so a repeat finalize returns the merged-tree counts rather than the pre-merge session counts (CodeRabbit). - Documented that migration 0024's column-scoped ON DELETE SET NULL on parent_revision_id is authoritative and that Drizzle cannot express the column list, so the snapshot's unscoped form is expected drift, not a bug to "fix" toward the snapshot (CodeRabbit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(storage,docs): preserve UTF-8 BOM in decodeUtf8Strict; tighten patch sha256 spec - decodeUtf8Strict keeps a leading UTF-8 BOM (ignoreBOM) so valid BOM-prefixed text round-trips and is not rejected as binary; fatal is passed explicitly for the Worker TS lib option type (CodeRabbit). - api.md: a patched per-file entry's size_bytes is the diff byte length and the entry carries no whole-file sha256; the sha256 rule now scopes to whole-file entries (CodeRabbit). - Test-only: cover the BOM round-trip + invalid-UTF-8 reject, read_file omits revision_id from the query when absent, and isolate the non-unified-patch contract test from sha256. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>



What & why
Started as "publish an HTML report and give me a link." Publishing with
--sharemade it public without consent on a private-first platform, and the latershared:falseon revise was a lie. This rebuilds the publish surface around the right model:/v/<artifactId>): permanent, stable, member-only, always private. The one link every publish returns (private_url). Derived only from the Artifact id — no token, signature, or expiry — andadd_revisionrepublishes into the same id, so it never changes across revisions and live-updates in place. Stops resolving only when the Artifact is deleted or swept by Auto Deletion./al/<id>#...): the public counterpart, off by default, minted only by an explicitmake_public(MCP) /agent-paste make-public(CLI). Revocable any time withrevoke_access_link— kills public access without touching the Artifact, its data, its revisions, or its Private Link.add_revisionare content-only: noshareinput, nosharedoutput, noaccess_link_url. The/artifacts/<id>console is never returned to an agent or user.Supersedes ADR 0085, amends ADR 0084 and ADR 0079; adds ADR 0086. Specs are the source of truth and updated to match.
Key decisions
_authedroute, not anaccess_linksrow — nothing to sign, leak, or revoke. The public/al/resolve gate is untouched, so existing Share Links keep resolving.make_publicreuses the one active Share Link instead of minting duplicates (an Artifact has at most one live Share Link). Makes the "mints or reuses the one Share Link" contract true in code.read/publish/admin, shared verbatim by API and MCP. The old MCP-onlywrite/sharenames and themcpScopesToApiScopes/apiScopesToMcpScopestranslation layer are deleted. The four agent access-link routes requirepublish(managing your own Artifact's public access is content authority);adminis dashboard-only.Verified end-to-end on preview
private_url→/v/<id>, noshare/shared/access_link_urlmake_public)make_public→ no-login/al/URL that resolves 200; second call reuses the same single linkmake_publicmints a fresh public link (fixed an idempotency-replay edge where a reused key could point mint at a revoked link)/vviewer no longer links to the banned consolepnpm verify+pnpm test:coveragegreen (floors held). OpenAPI goldens regenerated.Follow-up (not in this PR)
The deployed prod MCP server's
instructionstext still teachesshare:trueon publish; it updates on the next prod deploy and should land in lockstep with merge.🤖 Generated with Claude Code
Note
High Risk
Breaking publish and MCP contracts, changes to who can mint public links and what URLs agents hand users, plus new member viewer routing—core access-control and agent-integration surface.
Overview
Private-first publish replaces the old
viewer_url/share/sharedmodel: every publish path (REST, CLI, MCP, sharedrunPublish) returns onlyprivate_url— the login-walled clean viewer at/v/<artifactId>— and no longer accepts visibility flags or mints Share Links during publish. The API publish coordinator drops all share-link creation/reuse logic;signPublishResultemitsprivate_urlinstead ofartifact_url.Going public is explicit: MCP
make_public(replacingcreate_share_link) and CLIagent-paste make-publiccreate or reuse the one Share Link and return its signed public URL, with a salted idempotency retry when a replayed create points at a revoked link.Web: new authenticated route
/v/$artifactIdfor the handoff viewer; sharedArtifactLiveViewer(live updates) is used by both/vand the dashboard/artifactsconsole (management only, never returned by agents).Scopes: one vocabulary
read/publish/admin—mcp.whoamireturns member API scopes verbatim (no MCP translation layer); link tools align withpublishfor managing your artifact’s public access.Docs & ADRs:
CONTEXT.md, agent/docs surfaces, CLI/MCP READMEs, and ADR 0086 (supersedes 0085); amendments to 0079 and 0084. OpenAPI/contracts useprivate_urlon publish results.Reviewed by Cursor Bugbot for commit 8335df4. Bugbot is set up for automated code reviews on this repo. Configure here.