Skip to content

feat(auth): make artifacts private to their owner with three sharing levels - #18

Merged
juancarlosm merged 16 commits into
mainfrom
feat/artifact-sharing-levels
Oct 31, 2026
Merged

juancarlosm merged 16 commits into
mainfrom
feat/artifact-sharing-levels

Conversation

@juancarlosm

Copy link
Copy Markdown
Contributor

Summary

Artifacts are private to their owner unless shared. Each artifact has an owner, the principal that published version 1, and one of three sharing levels:

Level Wire value Who opens it with the link
Private (default) private The owner
Organization account_required Anyone signed in to the installation
Public public_link Anyone, without signing in

Only the owner sees an artifact in lists. Human administrators also list service-owned artifacts, lower any level and delete any artifact, but cannot read a member's Private one.

Spec: project/spec/artifact-sharing-levels-spec.md. Decision: project/spec/decisions/0032-owner-private-artifacts.md.

Changes

  • Storage: owner_principal_id on SQLite (schema 15), Postgres (migration 0014) and D1 (v12), backfilled from the version 1 publisher. The access CHECK allows private. List queries filter by owner.
  • Reads: every artifact-scoped read returns the same 404 as a missing artifact when the principal cannot read it. This covers HTTP, MCP, comments, content bootstrap, comparisons and the Git clone token.
  • Changes: publishing, capture, relink, restore, tags, review mode and raising the level require control, on top of the existing capability gates. Lowering is also open to human administrators. Bulk comment clear and deleting others' comments with manage:any require control.
  • Defaults: HTTP, MCP, linked artifacts and the CLI create Private artifacts. The CLI gains --organization, which conflicts with --public. The local API token counts as a service principal.
  • Dispatch: sending a Private artifact's threads to an agent returns 409 ARTIFACT_PRIVATE. Mailbox bundles resolve threads by id instead of by listing artifacts.
  • Config: ARTIFACT_SERVER_ORGANIZATION_NAME / Helm configuration.organizationName appears in the session response as installation.organizationName.
  • Review UI: a General access selector in the Share modal. The Public links admin action reads "Stop public access".
  • Ledger: AUTH-032 to AUTH-037 and DSP-014 are behavior_verified. SCP-008, AUTH-002, AUTH-008, AUTH-030, CMT-006/007/008, PRS-005 and ADM-005 are reworded.

Behaviour change for existing installations

  • No existing artifact changes level, so everything stays reachable by link.
  • Each person's lists shrink to their own artifacts.
  • Artifacts published with a shared service key show up for administrators only.
  • The external @plannotator/agent-bridge package finds dispatched threads by listing artifacts, so service-key agents using it will see empty bundles until it is updated.

Test Plan

  • pnpm verify:iteration: everything passes except:
    • Timeouts: three CLI-spawn tests time out under parallel load. They pass alone and in a --maxWorkers=8 run (438/438).
    • Not run locally: test:compact-compose, test:external-storage-compose and test:helm. The local Docker driver has no buildx attestation or arm64 emulation, so CI proves them.
  • pnpm verify:external-storage-runtime: 23/23 against Postgres and MinIO.
  • Cloudflare: 48/48. Playwright: 53/53.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p

juancarlosm and others added 13 commits October 30, 2026 18:25
Every artifact gets an owner (the version 1 publisher) and one of three
levels: private (default), organization (account_required) and public
(public_link). Only the owner lists an artifact. Adds AUTH-032 to
AUTH-037 and DSP-014 as specified, records decision 0032, and rewords
SCP-008, AUTH-008, CMT-006, CMT-007 and ADM-005.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
Every artifact stores owner_principal_id, backfilled from the version 1
publisher on SQLite, Postgres and D1, and the access CHECK accepts
private. Repository lists return the viewer's own artifacts, plus
service-owned ones for human administrators.

Mailbox bundles resolve dispatched threads by id, so a service-key
agent no longer depends on listing a member's artifacts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
A principal sees an artifact it controls, or one shared beyond Private
when it holds read authority; otherwise the artifact reads as missing.
Only the owner, or a human administrator for a service-owned artifact,
controls it. The owner or any human administrator may lower its level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
Every artifact-scoped read checks visibility after loading the
artifact: get, versions, files, archive, media, comparisons, actions,
comments, content bootstrap, preview leases, linked live origins and
the Git clone token, over HTTP and MCP. A principal that cannot read
receives the same 404 as for a missing artifact. HTTP and MCP inputs
accept the private level.

The spec exempts dispatch records and agent presence, which carry no
artifact data, and gives anonymous requests the same sign-in
requirement for Private as for Organization.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
HTTP and MCP lists, search, tag filters and comment sort return the
reader's artifacts only; an administrator's list adds service-owned
ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
…elete

Publishing, capture, relink, restore, tags, review mode and raising
the level require control of the artifact on top of the existing
capability gates. Lowering is also open to any human administrator,
who may delete any artifact, Private ones included. Comment writes
need visibility; deleting others' comments with manage:any, and bulk
clearing, need control or a human administrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
HTTP, MCP, linked artifacts and the CLI create Private artifacts by
default; the CLI gains --organization beside --public, and the two
conflict. MCP capabilities list three levels. The local API token
counts as a service principal, so the local administrator reads and
lists what it publishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
Agents connect with service keys and cannot read a Private artifact,
so creating a dispatch for its threads fails with 409 ARTIFACT_PRIVATE
and creates nothing. A sender that cannot read the artifact receives
the usual not-found response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
ARTIFACT_SERVER_ORGANIZATION_NAME, at most 64 characters without
control characters, reaches the session response as
installation.organizationName. Helm exposes it as
configuration.organizationName, and Compose passes it through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
A General access selector replaces Manage access and saves on change,
confirming in place before Public and returning to the saved level on
failure. Non-owners see it disabled; administrators may only lower.
At Public a Public link row comes first. Management controls follow
the artifact's owner, and the admin bulk action reads Stop public
access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
AUTH-032 to AUTH-037 and DSP-014 move to behavior_verified with the
local, browser and external-storage runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwpo6f39DCmJkJwz5ahK1p
@juancarlosm
juancarlosm merged commit ceb787c into main Oct 31, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant