diff --git a/CONTEXT.md b/CONTEXT.md index af551d99..0333cbdf 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -241,7 +241,7 @@ _Avoid_: Token format, credential shape, credential prefix, bearer credential fo **Access Link Signed URL**: -The shareable URL form of an **Access Link**, shaped `https://app.agent-paste.sh/al/{publicId}#{blob}` where `blob` is a base64url-encoded binary payload containing the signing-key generation, expiration, allowed scopes, and HMAC signature. The payload is carried in the URL fragment so it never reaches any server-side log, and the signature is the credential — the `access_links` row holds no secret material. An authorized **Workspace Member** or **Agent Credential** with read and share **Scopes** mints a fresh URL on demand; re-minting produces a new URL with a new expiration. +The shareable URL form of an **Access Link**, shaped `https://app.agent-paste.sh/al/{publicId}#{blob}` where `blob` is a base64url-encoded binary payload containing the signing-key generation, expiration, allowed scopes, and HMAC signature. The payload is carried in the URL fragment so it never reaches any server-side log, and the signature is the credential — the `access_links` row holds no secret material. An authorized **Workspace Member** or **Agent Credential** with read and publish **Scopes** mints a fresh URL on demand; re-minting produces a new URL with a new expiration. _Avoid_: link token, access link secret, credential @@ -254,15 +254,15 @@ _Avoid_: Owner, author A named permission that authorizes an actor to perform a class of action within a **Workspace**. There is exactly one **Scope** vocabulary, shared verbatim by the API and the MCP surface (no per-surface names, no translation layer to keep in sync). The three **Scopes** are: - `read` — view your own **Artifacts**, **Revisions**, and **Access Links**. -- `publish` — change your own content: create, revise, and delete **Artifacts**, and manage public access to your own **Artifact** (make it public, list, and revoke its **Access Links**). This is the full agent write surface. -- `admin` — a **Member-Only Scope** for account/workspace management only (**Agent Credential** lifecycle, **Workspace** settings, **Audit Event** reads, billing). It does **not** grant publishing or public-access actions. +- `publish` — change your own content: create, revise, and delete **Artifacts**, and manage unauthenticated access to your own **Artifact** (share, list, and revoke its **Access Links**; in the planned **Public Artifact** model, select or clear its **Public Version**). This is the full agent write surface. +- `admin` — a **Member-Only Scope** for account/workspace management only (**Agent Credential** lifecycle, **Workspace** settings, **Audit Event** reads, billing). It does **not** grant content or unauthenticated access actions. A **Workspace Member** authenticated for direct workspace control (the dashboard) is implicitly granted every **Scope**, including `admin`. An **Agent Credential** (created by `agent-paste login`) and an MCP member's delegated set are capped at `publish` and `read` (never `admin`), so every agent surface is structurally below the dashboard ceiling. A member's MCP **Scopes** are their stored API **Scopes** verbatim, derived in `api`, never carried in the OAuth token (ADR 0079). _Avoid_: Role, capability, write/share (old MCP-only names — unified into `publish`) **Member-Only Scope**: -A **Scope** that only a **Workspace Member** can hold via direct workspace authentication (the dashboard); it cannot be granted to an **Agent Credential** and cannot be carried by tokens issued for delegated agent surfaces such as the CLI or MCP. The only **Member-Only Scope** is `admin`: it authorizes **Agent Credential** lifecycle management, **Audit Event** reads, **Workspace** settings, and billing. It is distinct from `publish`, which covers all content and public-access actions on your own **Artifacts** and is available to agents. +A **Scope** that only a **Workspace Member** can hold via direct workspace authentication (the dashboard); it cannot be granted to an **Agent Credential** and cannot be carried by tokens issued for delegated agent surfaces such as the CLI or MCP. The only **Member-Only Scope** is `admin`: it authorizes **Agent Credential** lifecycle management, **Audit Event** reads, **Workspace** settings, and billing. It is distinct from `publish`, which covers content changes and unauthenticated sharing actions on your own **Artifacts** and is available to agents. _Avoid_: Admin scope, restricted scope @@ -327,13 +327,13 @@ _Avoid_: Remote localStorage, app database, permanent storage **Private Link**: -The login-walled clean viewer (`/v/`) for a **Workspace Member**, returned by every **Publish** as `private_url`. No management chrome. It is **permanent and stable**: the URL is built only from the **Artifact** id with no token, signature, or **Expiration** baked in, and `add_revision` republishes into the same id, so the same link keeps working and live-updates to the latest **Published Revision** — it never changes when content is revised. It is **member-only and always private**: there is no public mode and **Publish** never grants public access. It stops resolving only when the **Artifact** itself is gone (deleted or swept by **Auto Deletion**), which is a property of the **Artifact**'s lifetime, not the link. To hand the same content to someone without a login, the **Member** mints a separate, revocable **Share Link**. +The login-walled clean viewer (`/v/`) for a **Workspace Member**, returned by every **Publish** as `private_url`. No management chrome. It is **permanent and stable**: the URL is built only from the **Artifact** id with no token, signature, or **Expiration** baked in, and `add_revision` republishes into the same id, so the same link keeps working and live-updates to the latest **Published Revision** — it never changes when content is revised. It is **member-only and always private**: there is no public mode and **Publish** never grants unauthenticated access. It stops resolving only when the **Artifact** itself is gone (deleted or swept by **Auto Deletion**), which is a property of the **Artifact**'s lifetime, not the link. To hand the same content to someone without a login, the **Member** mints a separate, revocable **Share Link**. _Avoid_: Artifact URL, console link, /artifacts page, dashboard link, permalink (it is stable, but say "permanent member link" not "permalink", which we reserve against for public links) **Access Link**: A revocable, unlisted, high-entropy grant for reading an **Artifact** without tenant authentication. **Share Links** and **Revision Links** are Access Link types. An Access Link is the durable grant; an **Access Link Signed URL** is the URL string minted from that grant. -_Avoid_: Artifact URL, content URL, dashboard URL +_Avoid_: Public Link, Artifact URL, content URL, dashboard URL **Access Link Lockdown**: @@ -342,13 +342,48 @@ _Avoid_: Disable sharing, private mode, emergency revoke **Platform Lockdown**: -A platform-initiated state that blocks all link resolution for either a single **Artifact** or an entire **Workspace**, applied by the operator to respond to abuse reports, takedown requests, or external safety flags. A **Workspace**-scoped **Platform Lockdown** also suspends every **Agent Credential** in the **Workspace**. +A platform-initiated state that blocks all platform-controlled link resolution, public resolution, and public asset access for either a single **Artifact** or an entire **Workspace**, applied by the operator to respond to abuse reports, takedown requests, or external safety flags. It is the hard public takedown path for **Public Artifacts**: unlike **Public Offline**, it blocks the **Public Resolver** and **Public Version Assets** and uses cache purge or deny controls where available. A **Workspace**-scoped **Platform Lockdown** also suspends every **Agent Credential** in the **Workspace**. _Avoid_: Suspension, ban, freeze, admin lock **Share Link**: -A type of **Access Link** that resolves to the latest **Published Revision** of an **Artifact**. It opens the **Artifact Viewer** and can receive **Publish Updates**. It is the **public** counterpart to the **Private Link**: a no-login URL, **off by default**, created only by the explicit make-public step (`make_public` on MCP, `agent-paste make-public` on the CLI), which mints or reuses the **one** Share Link an **Artifact** has and returns its **Access Link Signed URL**. **Publish** never creates one. It is **revocable at any time** (`revoke_access_link`) and may expire, so avoid calling it a permalink; revoking it kills public access without touching the **Artifact**, its data, its **Revisions**, or its **Private Link**. -_Avoid_: Artifact Console, public app link, permalink, Revision Content URL +A type of **Access Link** that resolves to the latest **Published Revision** of an **Artifact**. It opens the **Artifact Viewer** and can receive **Publish Updates**. It is the unlisted counterpart to the **Private Link**: a no-login URL, **off by default**, created only by an explicit sharing step, which mints or reuses the **one** Share Link an **Artifact** has and returns its **Access Link Signed URL**. **Publish** never creates one. It is the control-oriented unauthenticated path: it does not create a **Public URL** or **Public Version Assets** and is not the aggressive edge-cache surface. It is **revocable at any time** (`revoke_access_link`) and may expire, so avoid calling it public or a permalink; revoking it kills unlisted access without touching the **Artifact**, its data, its **Revisions**, or its **Private Link**. +_Avoid_: Public Link, Artifact Console, public app link, permalink, Revision Content URL + + +**Public Artifact**: +An **Artifact** intentionally configured for broad unauthenticated viewing through a **Public URL**, not merely shared through a high-entropy **Access Link Signed URL**. This is planned domain language from ADR 0087, not the shipped Share Link behavior described by ADR 0086. It is the distribution-oriented unauthenticated path: the first public action atomically allocates its **Public ID** and selects its initial **Public Version**. When online, it resolves through a selected **Public Version** and its **Public Version Assets** are designed for broad edge caching. When **Public Offline**, the permalink is retained but broad public viewing is unavailable. +_Avoid_: Share Link, Access Link, Unlisted Link, secret link + + +**Public Version**: +The **Published Revision** currently selected for broad unauthenticated viewing of a **Public Artifact**. +_Avoid_: Latest Revision, Live Version, public pointer + + +**Public Offline**: +The reversible soft-control state where a **Public Artifact** keeps its **Public URL** and **Public ID** but has no selected **Public Version**, so broad unauthenticated public resolution is temporarily unavailable. Selecting a **Public Version** brings it back online without changing the permalink. It is not a hard takedown guarantee for already-cached **Public Version Assets**. +_Avoid_: Delete, revoke, Access Link Lockdown, Platform Lockdown, rotate Public ID + + +**Public URL**: +The stable browser URL for broad unauthenticated viewing of a **Public Artifact**, shaped `/p/{publicId}`. It remains reserved while the **Public Artifact** is **Public Offline** and resolves through the **Public Resolver**. +_Avoid_: Public Link, Share Link, Access Link Signed URL, Revision Content URL + + +**Public ID**: +The high-entropy identifier carried by a **Public URL**, distinct from the **Artifact** id. +_Avoid_: Artifact id, slug, title, human-readable name + + +**Public Resolver**: +The small mutable resolution layer behind a **Public URL**. It maps a **Public ID** to either the selected **Public Version** or **Public Offline** and must change quickly when the public pointer changes. It is not the long-lived cache boundary for **Untrusted Content**. +_Avoid_: CDN asset, static page, public file, Public Version Asset + + +**Public Version Asset**: +An immutable **Untrusted Content** response for one file path in the **Published Revision** selected by a **Public Version**. It can be cached aggressively because selecting a different **Public Version** or going **Public Offline** changes the **Public Resolver** instead of mutating the asset. +_Avoid_: live asset, latest asset, mutable public file **Expiration**: @@ -479,9 +514,9 @@ _Avoid_: tenant filter, RLS shim, scoped map - Only one finalized **Draft Revision** can wait for **Publish** on an **Artifact** - Finalizing an **Upload Session** fails when another **Draft Revision** is already waiting for **Publish** - **Draft Revisions** are visible to **Workspace Members** in management surfaces -- **Draft Revisions** are visible to **Agent Credentials** with write **Scope** +- **Draft Revisions** are visible to **Agent Credentials** with publish **Scope** - A read **Scope** does not grant access to **Draft Revisions** -- **Agent Credentials** with write **Scope** can discard **Draft Revisions** +- **Agent Credentials** with publish **Scope** can discard **Draft Revisions** - Discarding a **Draft Revision** does not affect the **Published Revision** - **Private Links** and **Access Links** never resolve to **Draft Revisions** - **Private Links** and **Access Links** do not resolve for an **Unpublished Artifact** @@ -527,15 +562,16 @@ _Avoid_: tenant filter, RLS shim, scoped map - **Access Link Lockdown** does not prevent changing **Access Link** **Expiration** - A **Platform Lockdown** is operator-initiated and cannot be set or lifted by a **Workspace Member** or an **Agent Credential** - A **Platform Lockdown** has scope `Artifact` or `Workspace` -- A **Platform Lockdown** at `Artifact` scope blocks the **Private Link** and every **Access Link** for one **Artifact** -- A **Platform Lockdown** at `Workspace` scope blocks the **Private Link** and **Access Link** resolution for every **Artifact** in the **Workspace** +- A **Platform Lockdown** at `Artifact` scope blocks the **Private Link**, every **Access Link**, the **Public Resolver**, and **Public Version Assets** for one **Artifact** +- A **Platform Lockdown** at `Workspace` scope blocks **Private Link**, **Access Link**, **Public Resolver**, and **Public Version Asset** access for every **Artifact** in the **Workspace** - A **Platform Lockdown** at `Workspace` scope suspends every **Agent Credential** in the **Workspace** - A **Platform Lockdown** is reversible by the operator - A **Platform Lockdown** does not delete bytes or **Revisions** +- A **Platform Lockdown** uses cache purge and deny controls for **Public Version Assets** where available - A **Platform Lockdown** does not auto-expire in the MVP - A **Platform Lockdown** creates an **Audit Event** visible to **Workspace Members** - A **Platform Lockdown** is not exposed through the public API, SDK, or CLI -- A **Platform Lockdown** is distinct from **Access Link Lockdown**: it is operator-initiated, also blocks the **Private Link**, and at `Workspace` scope suspends **Agent Credentials** +- A **Platform Lockdown** is distinct from **Access Link Lockdown**: it is operator-initiated, also blocks the **Private Link**, **Public Resolver**, and **Public Version Assets**, and at `Workspace` scope suspends **Agent Credentials** - An **Operator** is a **Workspace Member** whose authenticated email is on the platform operator allowlist - An **Operator** acts with platform-wide authority only on operator-only routes; on every other route the identity is a normal **Workspace Member** - An **Operator** identity cannot be assumed by an **Agent Credential**: operator-only routes reject **Agent Credential** authentication @@ -544,23 +580,53 @@ _Avoid_: tenant filter, RLS shim, scoped map - A **Platform Lockdown** can only be set or lifted by an **Operator** - A **Share Link** resolves to the latest **Published Revision** of an **Artifact** - A **Share Link** is an **Access Link** type, not a synonym for **Access Link** -- The signed URL minted from a **Share Link** is the public, no-login handoff produced by the make-public step +- The signed URL minted from a **Share Link** is an unlisted, no-login handoff produced by an explicit sharing step - A **Revision Link** resolves to exactly one **Revision** - A **Revision Link** can continue resolving to an older **Revision** after a newer **Revision** is published - A **Revision Link** can be revoked without deleting its **Revision** - **Retention** can make a **Revision Link** stop resolving without revoking it - The base REST/CLI **Publish Result** includes the **Artifact** id, **Revision** id, authenticated **Private Link** (`private_url`), direct signed **Revision Content URL**, public **Agent View** URL, expiration, and **Bundle Availability** - **Publish** is content-only and private-first on every surface (CLI, MCP, REST): it accepts no visibility input and returns exactly one link, the **Private Link** as `private_url`. There is no `share`/`--share` input and no `shared` output bit -- A **Share Link** is created only by the explicit make-public step (`make_public` on MCP, `agent-paste make-public` on the CLI), never by **Publish** +- A **Share Link** is created only by an explicit sharing step, never by **Publish** - MCP publish tools do not create a **Revision Link** unless the agent explicitly calls **Create Revision Link** - **Access Link Lockdown** blocks creating or resolving **Access Links**; **Publish** is content-only and never an **Access Link** operation - **Revision Links** are not created for **Draft Revisions** - **Share Links** always resolve to the latest **Published Revision** - Additional **Revision Links** can be created for an already published **Revision** - Additional **Revision Links** can target any retained published **Revision** -- **Publish** never creates a **Share Link**; making an **Artifact** public is the separate `make_public` step, which mints or reuses the one revocable **Share Link** and returns its **Access Link Signed URL** -- The make-public step fails if the **Share Link** cannot be created (for example under **Access Link Lockdown**) without affecting the **Published Revision** -- A public **Artifact** keeps one stable **Share Link**: `make_public` reuses an active **Share Link** before creating one, so the public URL stays the same across revisions and live-updates to the latest **Published Revision** +- **Publish** never creates a **Share Link**; sharing an **Artifact** without login is a separate explicit step, which mints or reuses the one revocable **Share Link** and returns its **Access Link Signed URL** +- The sharing step fails if the **Share Link** cannot be created (for example under **Access Link Lockdown**) without affecting the **Published Revision** +- An unlisted shared **Artifact** keeps one stable **Share Link**: the sharing step reuses an active **Share Link** before creating one, so the unlisted URL stays the same across revisions and live-updates to the latest **Published Revision** +- A **Share Link** is the unauthenticated path to choose when revocation and takedown control matter more than broad distribution +- A **Share Link** does not create a **Public URL** or **Public Version Assets** +- A **Share Link** is not the aggressive edge-cache surface +- **Public Artifact**, **Public Version**, **Public URL**, **Public ID**, **Public Resolver**, **Public Version Asset**, and **Public Offline** are planned public-distribution terms from ADR 0087; shipped CLI/MCP behavior still creates **Share Links** for no-login latest-moving handoff until the implementation specs and routes change +- A **Public Artifact** has zero or one **Public Version** at a time +- A **Public Artifact** has one stable **Public URL** +- A **Public Artifact** is created by the first public action on an **Artifact** +- The first public action atomically allocates the **Public ID**, creates the **Public URL**, and selects the initial **Public Version** +- There is no reserved **Public URL** state before the first **Public Version** is selected +- A **Public Artifact** is the unauthenticated path to choose when broad distribution and traffic spikes matter more than strict cache-level revocation +- A **Public URL** carries a **Public ID**, not the **Artifact** id +- A **Public URL** has no slug +- A **Public URL** resolves through the **Public Resolver** +- The **Public Resolver** resolves through the selected **Public Version** +- A **Public Version** resolves to exactly one **Published Revision** +- **Public Version Assets** are immutable for one **Published Revision** +- **Public Version Assets** are the aggressive edge-cache surface for broad public traffic +- Selecting a **Public Version** is the action that makes that **Published Revision** eligible for **Public Version Assets** +- **Publish**, **Share Link** creation, and **Revision Link** creation do not make **Untrusted Content** eligible for **Public Version Assets** +- The **Public Resolver** is not cached aggressively; it must use short cache lifetime or explicit purge on **Public Version** and **Public Offline** changes +- A **Public Artifact** with no selected **Public Version** is **Public Offline** +- **Public Offline** preserves the **Public URL** and **Public ID** +- **Public Offline** prevents the **Public URL** from resolving broad public content without affecting the **Artifact**, **Revisions**, **Private Link**, **Share Link**, or other **Access Links** +- **Public Offline** is a soft public-distribution control, not a hard takedown guarantee for already-cached **Public Version Assets** +- Selecting a **Public Version** brings a **Public Offline** **Public Artifact** back online without changing the **Public URL** +- **Publish Updates** do not advance a **Public Version** +- Selecting a new **Public Version** is an explicit action, not an automatic side effect of **Publish** +- An **Agent Credential** requires publish **Scope** to select a **Public Version** +- An **Agent Credential** requires publish **Scope** to put a **Public Artifact** **Public Offline** +- Moving a **Public Artifact** to a new **Public Version** does not change its **Public URL**, **Private Link**, or **Share Link** - The CLI and MCP **Publish Result** both surface one `private_url`; the CLI still carries the full **Publish Result** (IDs, `private_url`, exact **Revision Content URL**, **Agent View** URL, **Bundle** status) in its JSON for automation - A **Publish Result** includes separate human-view links and agent-view links - A **Publish Result** includes **Bundle Availability** even when the **Bundle** is not ready @@ -636,7 +702,8 @@ _Avoid_: tenant filter, RLS shim, scoped map - An **Audit Event** has exactly one **Change Summary** - **Audit Retention** is separate from **Usage Policy** - **Audit Events** are visible only to **Workspace Members** in the MVP -- **Unpublished Artifact** creation, **Publish**, **Deletion**, **Draft Revision** discard, **Retention** removals, **Display Metadata** changes, **Safety Warnings**, durable **Usage Policy** enforcement, **Agent Credential** changes, **Agent Credential Revocation**, **Access Link** changes, and **Access Link Lockdown** changes create **Audit Events** +- **Unpublished Artifact** creation, **Publish**, **Deletion**, **Draft Revision** discard, **Retention** removals, **Display Metadata** changes, **Safety Warnings**, durable **Usage Policy** enforcement, **Agent Credential** changes, **Agent Credential Revocation**, **Access Link** changes, **Access Link Lockdown** changes, **Public Version** changes, and **Public Offline** changes create **Audit Events** +- **Public Version** and **Public Offline** **Audit Events** include a redacted **Change Summary** with the **Public ID**, previous **Published Revision** id or null, new **Published Revision** id or null, actor, and calling surface - Routine **Upload Cleanup** does not create **Audit Events** - **Upload Cleanup** creates **Audit Events** when it removes stale **Unpublished Artifact** management state - A **Workspace** can have zero or more **Agent Credentials** @@ -657,15 +724,14 @@ _Avoid_: tenant filter, RLS shim, scoped map - **Agent Credential Revocation** stops future use of the **Agent Credential** - **Agent Credential Revocation** does not revoke **Artifacts** or **Access Links** created with it - An **Agent Credential** requires a read **Scope** to read private **Artifacts** -- An **Agent Credential** requires a share **Scope** to manage **Access Links** -- An **Agent Credential** requires read and share **Scopes** to create **Access Links** -- An **Agent Credential** requires read and share **Scopes** to mint **Access Link Signed URLs** -- An **Agent Credential** requires a share **Scope** to change **Access Link Lockdown** -- A share **Scope** does not imply a read **Scope** -- A write **Scope** does not imply a share **Scope** -- **Publish** requires write and read **Scopes**; requested **Access Link** creation additionally requires share **Scope** -- **Upload Sessions** require a write **Scope**, not a share **Scope** -- A write-only **Agent Credential** can prepare a **Draft Revision** for another actor to **Publish** +- An **Agent Credential** requires publish **Scope** to manage **Access Links** +- An **Agent Credential** requires read and publish **Scopes** to create **Access Links** +- An **Agent Credential** requires read and publish **Scopes** to mint **Access Link Signed URLs** +- An **Agent Credential** requires publish **Scope** to change **Access Link Lockdown** +- A publish **Scope** does not imply a read **Scope** +- The **Publish** action requires publish and read **Scopes** +- **Upload Sessions** require publish **Scope**, not read **Scope** +- A publish-only **Agent Credential** can prepare a **Draft Revision** for another actor to **Publish** - A **Creator** is recorded for an **Artifact** but does not own it - A **Creator** is recorded before first **Publish** when an **Unpublished Artifact** is created - A **Creator** remains recorded after **Agent Credential Revocation** or **Agent Credential** **Expiration** @@ -691,7 +757,7 @@ _Avoid_: tenant filter, RLS shim, scoped map - Any **Agent Credential** with the right **Scope** in the owning **Workspace** can update **Display Metadata** - Updating a known **Artifact** does not require a read **Scope** - Updating **Display Metadata** for a known **Artifact** does not require a read **Scope** -- **Publish** requires write and read **Scopes**; share **Scope** is required only when the actor requests a **Share Link** +- The **Publish** action requires publish and read **Scopes** - An **Artifact** contains **Untrusted Content** - An **Artifact** can have zero or more **Safety Warnings** - A **Revision** can have zero or more **Safety Warnings** @@ -793,6 +859,34 @@ _Avoid_: tenant filter, RLS shim, scoped map > **Domain expert:** "Yes — JavaScript is allowed but remains **Untrusted Content**." > **Dev:** "When an **Artifact** is updated, should existing links change?" > **Domain expert:** "No — **Private Links** and **Share Links** stay stable and show the latest **Published Revision**." +> **Dev:** "Is a **Share Link** a public URL?" +> **Domain expert:** "No — a **Share Link** is unlisted access; a **Public Artifact** is intentionally published for broad viewing." +> **Dev:** "Does **Publish** automatically update a **Public Artifact**?" +> **Domain expert:** "No — a **Public Artifact** changes only when a new **Public Version** is selected." +> **Dev:** "Does selecting a new **Public Version** change the **Public URL**?" +> **Domain expert:** "No — the **Public URL** is stable; it resolves through whichever **Public Version** is currently selected." +> **Dev:** "Does the **Public URL** expose the **Artifact** id?" +> **Domain expert:** "No — it carries a separate **Public ID**." +> **Dev:** "Should a **Public URL** include a title slug?" +> **Domain expert:** "No — the **Public ID** is the canonical URL segment." +> **Dev:** "Can an agent reserve a **Public URL** before choosing a **Public Version**?" +> **Domain expert:** "No — the first public action atomically creates the **Public URL** and selects the initial **Public Version**." +> **Dev:** "Can an agent move a **Public Artifact** to the latest **Published Revision** on every publish?" +> **Domain expert:** "Only when it takes the explicit action to select a new **Public Version**; ordinary **Publish** does not move public viewing." +> **Dev:** "Is selecting a **Public Version** human-only?" +> **Domain expert:** "No — an **Agent Credential** can select a **Public Version** with publish **Scope**." +> **Dev:** "If a public page needs to come down briefly, do we delete or rotate the **Public URL**?" +> **Domain expert:** "No — put the **Public Artifact** **Public Offline**. The **Public URL** and **Public ID** stay reserved, and selecting a **Public Version** brings it back online." +> **Dev:** "Does **Public Offline** affect private or unlisted access?" +> **Domain expert:** "No — it only stops the **Public URL** from serving broad public content. **Private Links**, **Share Links**, and other **Access Links** are separate controls." +> **Dev:** "Do we cache the stable **Public URL** hard?" +> **Domain expert:** "No — cache immutable **Public Version Assets** aggressively. Keep the **Public Resolver** short-lived or explicitly purged so pointer changes and **Public Offline** take effect quickly." +> **Dev:** "Should I make something **Public** if I might need strict takedown later?" +> **Domain expert:** "No — use a **Share Link** when revocation and takedown control matter. Use **Public** when broad distribution and traffic-spike handling are the priority." +> **Dev:** "Does **Public Offline** mean every cached public asset disappears immediately?" +> **Domain expert:** "No — it is a soft public-distribution control for the **Public Resolver**, not a hard takedown guarantee for already-cached **Public Version Assets**." +> **Dev:** "What is the hard takedown path for a **Public Artifact**?" +> **Domain expert:** "**Platform Lockdown**. It is operator-only and blocks the **Public Resolver** and **Public Version Assets**, using cache purge and deny controls where available." > **Dev:** "Can a **Private Link** be pinned to an older **Revision**?" > **Domain expert:** "No — a **Private Link** always follows the latest **Published Revision**." > **Dev:** "Can viewers see files while an update is still uploading?" @@ -804,9 +898,9 @@ _Avoid_: tenant filter, RLS shim, scoped map > **Dev:** "Can **Workspace Members** see a **Draft Revision**?" > **Domain expert:** "Yes — management surfaces can show drafts, but viewing links remain published-only." > **Dev:** "Can a read-only **Agent Credential** see **Draft Revisions**?" -> **Domain expert:** "No — draft access is a management capability tied to write **Scope**." +> **Domain expert:** "No — draft access is a management capability tied to publish **Scope**." > **Dev:** "Can an agent discard a **Draft Revision**?" -> **Domain expert:** "Yes — an **Agent Credential** with write **Scope** can discard it without affecting the **Published Revision**." +> **Domain expert:** "Yes — an **Agent Credential** with publish **Scope** can discard it without affecting the **Published Revision**." > **Dev:** "Does discarding a **Draft Revision** create an **Audit Event**?" > **Domain expert:** "Yes — finalized draft state is durable management state." > **Dev:** "Can only the original **Creator** update an **Artifact**?" @@ -840,7 +934,7 @@ _Avoid_: tenant filter, RLS shim, scoped map > **Dev:** "Can an agent change **Access Link** **Expiration** during **Access Link Lockdown**?" > **Domain expert:** "Yes — expiration can change, but lockdown still prevents access." > **Dev:** "Can an agent publish a new **Revision** while **Access Link Lockdown** is active?" -> **Domain expert:** "Yes — **Publish** is content-only and can create a new **Revision**. Lockdown blocks creating new **Access Links**, so the separate make-public step (the **Share Link**) and explicit **Revision Links** fail while it is active." +> **Domain expert:** "Yes — **Publish** is content-only and can create a new **Revision**. Lockdown blocks creating new **Access Links**, so the separate sharing step for a **Share Link** and explicit **Revision Links** fail while it is active." > **Dev:** "Can another agent use a **Share Link** without an **Agent Credential**?" > **Domain expert:** "Yes — a **Share Link** grants read-only access to the **Agent View** and published files." > **Dev:** "Can another agent use a **Revision Link** to inspect an exact **Revision**?" @@ -894,21 +988,21 @@ _Avoid_: tenant filter, RLS shim, scoped map > **Dev:** "Do **Unpublished Artifacts** count against creation limits?" > **Domain expert:** "Yes — **Usage Policy** controls their creation too." > **Dev:** "What is the simplest way for an agent to share a folder?" -> **Domain expert:** "Two steps. **Publish** the folder — it is content-only and private, and returns the **Private Link** as `private_url`. Then, only if the user wants a public no-login link, call the make-public step (`make_public` on MCP, `agent-paste make-public` on the CLI). That mints or reuses the **Artifact**'s one **Share Link** and returns its **Access Link Signed URL**." +> **Domain expert:** "Two steps. **Publish** the folder — it is content-only and private, and returns the **Private Link** as `private_url`. Then, only if the user wants an unlisted no-login link, run the explicit sharing step. That mints or reuses the **Artifact**'s one **Share Link** and returns its **Access Link Signed URL**." > **Dev:** "What does an agent get back after **Publish**?" > **Domain expert:** "One `private_url` — the login-walled `/v/` viewer. The CLI also carries the full **Publish Result** in its JSON — IDs, `private_url`, direct **Revision Content URL**, **Agent View** URL, **Bundle** status, and any **Safety Warnings** — for automation. There is no `shared` bit and no `share` input." > **Dev:** "Does **Publish** make an **Artifact** shareable by default?" -> **Domain expert:** "No — **Publish** is content-only and private on every surface; nothing is reachable without login. Going public is the separate `make_public` step, and `private_url` is always the authenticated **Private Link**." -> **Dev:** "What if the **Share Link** cannot be created during the make-public step?" -> **Domain expert:** "`make_public` fails without touching the **Published Revision** or its **Private Link**." +> **Domain expert:** "No — **Publish** is content-only and private on every surface; nothing is reachable without login. Unlisted sharing is a separate explicit step, and `private_url` is always the authenticated **Private Link**." +> **Dev:** "What if the **Share Link** cannot be created during the sharing step?" +> **Domain expert:** "The sharing step fails without touching the **Published Revision** or its **Private Link**." > **Dev:** "Can a **Share Link** be pinned to the current **Published Revision**?" > **Domain expert:** "No — **Share Links** always follow the latest **Published Revision**; use a **Revision Link** to pin one." > **Dev:** "Can an agent create a **Share Link** without publishing again?" -> **Domain expert:** "Yes — the make-public step works on any already-published **Artifact**; it never needs a re-publish." +> **Domain expert:** "Yes — the sharing step works on any already-published **Artifact**; it never needs a re-publish." > **Dev:** "Can an agent create a **Share Link** while **Access Link Lockdown** is active?" > **Domain expert:** "No — lockdown prevents creating new **Access Links**." > **Dev:** "Does **Publish** create a pinned link for the exact **Revision**?" -> **Domain expert:** "No — **Publish** returns only the latest-following **Private Link**. For a public latest-moving link an agent runs the make-public step to mint the **Share Link**; for a pinned URL of the exact **Revision** it calls **Create Revision Link**." +> **Domain expert:** "No — **Publish** returns only the latest-following **Private Link**. For an unlisted latest-moving link an agent runs the sharing step to mint the **Share Link**; for a pinned URL of the exact **Revision** it calls **Create Revision Link**." > **Dev:** "Can an agent create another **Revision Link** for the same **Revision**?" > **Domain expert:** "Yes — additional **Revision Links** can be created for separate audiences." > **Dev:** "Can an agent create an additional **Revision Link** during **Access Link Lockdown**?" @@ -943,22 +1037,20 @@ _Avoid_: tenant filter, RLS shim, scoped map > **Domain expert:** "Yes — credential lifecycle changes are security-relevant." > **Dev:** "Can a publishing **Agent Credential** read private **Artifacts**?" > **Domain expert:** "Only if it has a read **Scope**." -> **Dev:** "Can any write-capable **Agent Credential** manage **Access Links**?" -> **Domain expert:** "No — managing **Access Links** requires a share **Scope**." -> **Dev:** "Can a share-only **Agent Credential** create **Access Links**?" -> **Domain expert:** "No — minting **Access Link Signed URLs** requires both read and share **Scopes**." -> **Dev:** "Can a share-only **Agent Credential** mint **Access Link Signed URLs**?" -> **Domain expert:** "No — minting **Access Link Signed URLs** requires both read and share **Scopes**." -> **Dev:** "Does share **Scope** include read **Scope**?" +> **Dev:** "Can any publishing **Agent Credential** manage **Access Links**?" +> **Domain expert:** "Yes — publish **Scope** manages **Access Links**." +> **Dev:** "Can a publish-only **Agent Credential** create **Access Links**?" +> **Domain expert:** "No — creating **Access Links** requires both read and publish **Scopes**." +> **Dev:** "Can a publish-only **Agent Credential** mint **Access Link Signed URLs**?" +> **Domain expert:** "No — minting **Access Link Signed URLs** requires both read and publish **Scopes**." +> **Dev:** "Does publish **Scope** include read **Scope**?" > **Domain expert:** "No — **Scopes** are independent." -> **Dev:** "Does write **Scope** include share **Scope**?" -> **Domain expert:** "No — **Publish** requires write and read. Share is a separate power required only for creating or minting **Access Links**." -> **Dev:** "Can an **Agent Credential** publish with write **Scope** but no read or share **Scope**?" -> **Domain expert:** "No — **Publish** requires write and read **Scopes**. It requires share **Scope** only when a **Share Link** is requested." -> **Dev:** "Does creating an **Upload Session** require a share **Scope**?" -> **Domain expert:** "No — **Upload Sessions** create drafts, so write **Scope** is enough." +> **Dev:** "Can an **Agent Credential** publish with publish **Scope** but no read **Scope**?" +> **Domain expert:** "No — the **Publish** action requires publish and read **Scopes**." +> **Dev:** "Does creating an **Upload Session** require read **Scope**?" +> **Domain expert:** "No — **Upload Sessions** create drafts, so publish **Scope** is enough." > **Dev:** "Can one agent upload a draft and another publish it?" -> **Domain expert:** "Yes — a write-only **Agent Credential** can prepare a **Draft Revision** for another actor to **Publish**." +> **Domain expert:** "Yes — a publish-only **Agent Credential** can prepare a **Draft Revision** for another actor to **Publish**." > **Dev:** "Can an **Upload Session** expire?" > **Domain expert:** "Yes — after **Expiration**, it can no longer be used." > **Dev:** "Does **Retention** clean up expired **Upload Sessions**?" @@ -970,11 +1062,11 @@ _Avoid_: tenant filter, RLS shim, scoped map > **Dev:** "What if **Upload Cleanup** removes a stale **Unpublished Artifact**?" > **Domain expert:** "That creates an **Audit Event** because management state changed." > **Dev:** "Can an **Agent Credential** update a known **Artifact** without reading it first?" -> **Domain expert:** "Yes — update authority comes from the write **Scope**, not the read **Scope**." +> **Domain expert:** "Yes — update authority comes from the publish **Scope**, not the read **Scope**." > **Dev:** "Can an **Agent Credential** publish without read **Scope**?" > **Domain expert:** "No — **Publish** returns a **Revision Content URL** and **Agent View** URL, so read **Scope** is required." > **Dev:** "Does updating **Display Metadata** require a read **Scope**?" -> **Domain expert:** "No — write **Scope** is enough for a known **Artifact**." +> **Domain expert:** "No — publish **Scope** is enough for a known **Artifact**." > **Dev:** "Can uploaded JavaScript call arbitrary external APIs?" > **Domain expert:** "Not by default — the **Execution Policy** restricts external network access." > **Dev:** "Does **Execution Policy** only apply to HTML?" @@ -1013,6 +1105,8 @@ _Avoid_: tenant filter, RLS shim, scoped map > **Domain expert:** "No — **Audit Event** reads require a **Member-Only Scope**, which only a dashboard-authenticated **Workspace Member** carries. An **Agent Credential** cannot hold it; CLI and MCP tokens cannot carry it." > **Dev:** "Do **Access Link** changes create **Audit Events**?" > **Domain expert:** "Yes — they are unauthenticated access grants, so lifecycle changes are security-relevant." +> **Dev:** "Do **Public Version** changes and **Public Offline** changes create **Audit Events**?" +> **Domain expert:** "Yes — they are important public access events. The **Change Summary** records the **Public ID**, old and new **Published Revision** ids or null, actor, and calling surface." > **Dev:** "Do **Audit Events** store raw uploaded content or secrets?" > **Domain expert:** "No — they store redacted **Change Summaries**." > **Dev:** "Does **Publish** wait for deep content scanning?" diff --git a/docs/adr/0052-agent-view-discovery-from-access-link-signed-urls.md b/docs/adr/0052-agent-view-discovery-from-access-link-signed-urls.md index 41fa7ade..28b97644 100644 --- a/docs/adr/0052-agent-view-discovery-from-access-link-signed-urls.md +++ b/docs/adr/0052-agent-view-discovery-from-access-link-signed-urls.md @@ -2,14 +2,14 @@ Status: Accepted. Renumbered and rewritten from the duplicate ADR 0043 after [ADR 0047](./0047-access-link-signed-url-with-fragment-encoded-payload.md) replaced code-bearing Access Links with fragment-encoded signed URLs. Amended 2026-06-11: Access Link Signed URLs remain the unauthenticated recipient discovery model, but publish surfaces no longer create or return Share Links by default. -The **Access Link Signed URL** minted from a **Share Link** is the unauthenticated human handoff URL for live-updating Artifact Viewers when a caller explicitly creates a public/shareable link. A receiving agent discovers the **Agent View** from the same **Access Link Signed URL** that a human opens: parse `https://app.agent-paste.sh/al/{publicId}#{blob}`, preserve the fragment, and call `POST /v1/access-links/resolve` with `{ public_id, blob }`. The response is the **Agent View** plus short-lived content-gateway URLs from [ADR 0028](./0028-signed-url-tokens-for-content-gateway-authorization.md). There are no code-scoped bearer routes and no `Link: rel="agent-view"` header on `content` responses, because `content` sees only the derived content token and cannot reconstruct the fragment credential. +The **Access Link Signed URL** minted from a **Share Link** is the unauthenticated human handoff URL for live-updating Artifact Viewers when a caller explicitly creates an unlisted/shareable link. A receiving agent discovers the **Agent View** from the same **Access Link Signed URL** that a human opens: parse `https://app.agent-paste.sh/al/{publicId}#{blob}`, preserve the fragment, and call `POST /v1/access-links/resolve` with `{ public_id, blob }`. The response is the **Agent View** plus short-lived content-gateway URLs from [ADR 0028](./0028-signed-url-tokens-for-content-gateway-authorization.md). There are no code-scoped bearer routes and no `Link: rel="agent-view"` header on `content` responses, because `content` sees only the derived content token and cannot reconstruct the fragment credential. ## Consequences - The distributed handoff string is the full **Access Link Signed URL** from ADR 0047, including its fragment. Agents must not drop the fragment when normalizing or logging URLs. - `POST /v1/access-links/resolve` is the unauthenticated Agent View discovery endpoint. It accepts the `public_id` path segment and fragment `blob`; every invalid, expired, revoked, locked, retained, or deleted case returns the generic `not_found` envelope from [ADR 0036](./0036-error-envelope-and-generic-404-boundary.md). - `GET /v1/artifacts/{id}/agent-view` remains the authenticated member-or-key-scoped surface and is the publisher's follow-up handle. It is never returned through an unauthenticated surface, so the artifact id stays out of distributed link strings. -- User-facing **Publish Result** surfaces should return the authenticated **Artifact URL** by default. They return `access_link_url`, the **Access Link Signed URL** minted from a **Share Link**, only after explicit public/shareable link creation. `agent_view_url` is for the publishing actor, not for unauthenticated recipients. Recipients derive their **Agent View** through the resolve endpoint above when they receive an **Access Link Signed URL**. +- User-facing **Publish Result** surfaces should return the authenticated **Artifact URL** by default. They return `access_link_url`, the **Access Link Signed URL** minted from a **Share Link**, only after explicit unlisted/shareable link creation. `agent_view_url` is for the publishing actor, not for unauthenticated recipients. Recipients derive their **Agent View** through the resolve endpoint above when they receive an **Access Link Signed URL**. - The CLI and internal `api-client` accept an Access Link Signed URL wherever an artifact read URL is expected. They parse `{ publicId, blob }`, call resolve, and then follow the returned content-gateway URLs. - The **Content Origin** does not emit `Link: rel="agent-view"` for Access Link responses. A `Link` header would either omit the fragment credential and fail, or reintroduce a server-visible bearer URL, which ADR 0047 explicitly removed. - No `GET /v1/r/{code}/agent-view` or `GET /v1/s/{code}/agent-view` endpoints are created. The `publicId` alone is log-safe but not a credential. diff --git a/docs/adr/0079-mcp-scopes-derived-from-member-role-not-workos-token.md b/docs/adr/0079-mcp-scopes-derived-from-member-role-not-workos-token.md index 2c90febc..ad1b5fc1 100644 --- a/docs/adr/0079-mcp-scopes-derived-from-member-role-not-workos-token.md +++ b/docs/adr/0079-mcp-scopes-derived-from-member-role-not-workos-token.md @@ -2,7 +2,7 @@ Status: Accepted. Supersedes the scope-granting mechanism of [ADR 0061](./0061-mcp-worker-with-oauth-only-via-auth0-dcr.md). The MCP transport, OAuth-via-AuthKit, CIMD/DCR registration, resource-indicator audience binding, the twelve-tool surface, and the `write`/`read`/`share` scope vocabulary are all retained from ADR 0061 unchanged. Only the **source** of a caller's granted scopes changes: from a WorkOS-issued `scope` token claim (which AuthKit does not and cannot issue) to a set derived from the caller's **Workspace Member** role inside `api`. -> **Amendment (private-first publish, [ADR 0086](./0086-private-first-publish.md)):** The `write`/`read`/`share` MCP scope vocabulary referenced throughout this ADR has since been **unified with the API scope vocabulary** (`read`/`publish`/`admin`). There is now **one** scope set shared by `api` and MCP: a member's MCP scopes **are** their stored API scopes verbatim. The translation layer described below (`apiScopesToMcpScopes` / `mcpScopesToApiScopes` in `packages/contracts/src/mcp/scopes.ts`) has been **removed** — there is nothing to map. Scope meaning is now canonical: `read` = view your stuff; `publish` = change your stuff, **including managing an Artifact's own public access** (make_public, list and revoke that Artifact's links — these are `publish`-scope actions, **not** `admin`); `admin` = account/workspace management (API keys, settings, audit, billing). The decision below (scopes are derived from the member, not the WorkOS token; the Worker verifies issuer + audience only and pre-flight-gates against `mcp.whoami`) **still holds** — only the vocabulary and the now-deleted translation step changed. Read references to `write`/`share` below as `publish`/`publish` respectively, and `mcp.whoami` now returns the member's API scopes verbatim. +> **Amendment (private-first publish, [ADR 0086](./0086-private-first-publish.md)):** The `write`/`read`/`share` MCP scope vocabulary referenced throughout this ADR has since been **unified with the API scope vocabulary** (`read`/`publish`/`admin`). There is now **one** scope set shared by `api` and MCP: a member's MCP scopes **are** their stored API scopes verbatim. The translation layer described below (`apiScopesToMcpScopes` / `mcpScopesToApiScopes` in `packages/contracts/src/mcp/scopes.ts`) has been **removed** — there is nothing to map. Scope meaning is now canonical: `read` = view your stuff; `publish` = change your stuff, **including managing an Artifact's own unauthenticated access** (make_public, list and revoke that Artifact's links — these are `publish`-scope actions, **not** `admin`); `admin` = account/workspace management (API keys, settings, audit, billing). The decision below (scopes are derived from the member, not the WorkOS token; the Worker verifies issuer + audience only and pre-flight-gates against `mcp.whoami`) **still holds** — only the vocabulary and the now-deleted translation step changed. Read references to `write`/`share` below as `publish`/`publish` respectively, and `mcp.whoami` now returns the member's API scopes verbatim. ADR 0061 assumed the MCP consent screen would request from `{write, read, share}` and that WorkOS would mint those into the access token's `scope` claim, which both `apps/mcp` and `api` would read. That assumption is wrong for how WorkOS AuthKit actually works, and the entire authenticated MCP surface is non-functional as a result. diff --git a/docs/adr/0086-publish-is-content-only-private-first.md b/docs/adr/0086-publish-is-content-only-private-first.md index dce5cb9c..51c2243d 100644 --- a/docs/adr/0086-publish-is-content-only-private-first.md +++ b/docs/adr/0086-publish-is-content-only-private-first.md @@ -1,5 +1,11 @@ # Publish Is Content-Only and Private-First; Going Public Is a Separate Step +> **Planned terminology amendment:** [ADR 0087](./0087-public-artifacts-and-unlisted-share-links.md) +> reserves **Public Artifact** for a future CDN-backed public distribution model +> and reclassifies the current Share Link handoff as unlisted. Until that model is +> implemented, this ADR still describes shipped CLI/MCP behavior: `make_public` +> and `agent-paste make-public` mint or reuse the Artifact's one Share Link. + agent-paste is private-first: an **Artifact** is for its owner until they decide otherwise. The publish surfaces must honor that by default and never expose anything by URL that the caller did not explicitly ask to expose. @@ -35,15 +41,15 @@ Link** (when private) and the public **Share Link** (when shared), surfaced thro only from the Artifact id — no token, signature, or expiry — and `add_revision` republishes into the same id, so the link never changes across revisions and live-updates to the latest Published Revision. It is member-only (publish never - grants public access) and stops resolving only when the Artifact itself is deleted - or swept by Auto Deletion. The `expires_at` in the publish response is the - Artifact's content lifetime, not a link expiry. The mental model: a permanent, - private, internal link that is always there; making it public is a separate, - revocable Share Link. -- **Going public is a separate, explicit verb.** `make_public` (MCP) and - `agent-paste make-public` (CLI), replacing `create_share_link`, mint or reuse the - one revocable **Share Link** (`access_links.type='share'`) and return its public, - no-login **Access Link Signed URL**. This is the only way an Artifact becomes + grants unauthenticated access) and stops resolving only when the Artifact itself + is deleted or swept by Auto Deletion. The `expires_at` in the publish response + is the Artifact's content lifetime, not a link expiry. The mental model: a + permanent, private, internal link that is always there; unauthenticated sharing + is a separate, revocable Share Link. +- **Creating unauthenticated access is a separate, explicit verb.** `make_public` + (MCP) and `agent-paste make-public` (CLI), replacing `create_share_link`, mint or + reuse the one revocable **Share Link** (`access_links.type='share'`) and return + its no-login **Access Link Signed URL**. This is the only way an Artifact becomes reachable without login. - **Revocation is independent of content.** `revoke_access_link` kills a Share Link (or Revision Link) without touching the Artifact, its data, its revisions, or its diff --git a/docs/adr/0087-public-artifacts-and-unlisted-share-links.md b/docs/adr/0087-public-artifacts-and-unlisted-share-links.md new file mode 100644 index 00000000..e992ca84 --- /dev/null +++ b/docs/adr/0087-public-artifacts-and-unlisted-share-links.md @@ -0,0 +1,93 @@ +# Public Artifacts and Unlisted Share Links + +Status: Planned. Current shipped CLI/MCP behavior still treats `make_public` / +`agent-paste make-public` as Share Link minting until the implementation specs +and routes are updated. + +## Context + +ADR 0086 made publish private-first and moved unauthenticated handoff into a +separate `make_public` step that mints the Artifact's one revocable Share Link. +That fixed accidental public-by-flag publishing, but it reused "public" for two +different jobs: + +- unlisted, revocable handoff to a specific audience +- broad public distribution that should survive traffic spikes and benefit from + aggressive edge caching + +Those jobs have different control and caching expectations. A user iterating on +an Artifact may need a no-login URL that can be revoked cleanly. A user sharing a +finished Artifact with many people needs a stable permalink and CDN-shaped cost +profile. Treating both as "public" makes the product and implementation lie. + +## Decision + +- **Share Links are unlisted.** A Share Link remains an Access Link that follows + the latest Published Revision, opens the Artifact Viewer, and can receive Live + Updates. It is the control-oriented unauthenticated path: revocable, + expirable, not a permalink, and not the aggressive edge-cache surface. +- **Public Artifacts are a separate planned distribution model.** A Public + Artifact has a stable ID-only Public URL shaped `/p/{publicId}`. The Public ID + is separate from the Artifact id and has no slug. +- **The first public action is atomic.** It allocates the Public ID, creates the + Public URL, and selects the initial Public Version in one durable action. + There is no reserved Public URL state before the first Public Version is + selected. +- **Public Versions are frozen.** A Public Version points at one Published + Revision. Ordinary Publish Updates do not move it. Moving the public pointer is + an explicit action available to Agent Credentials with publish Scope. +- **Public Offline is soft.** Clearing the selected Public Version keeps the + Public URL and Public ID reserved while stopping the Public Resolver from + serving broad public content. It is for owner/agent control, not hard abuse or + legal takedown. +- **Public Resolver and Public Version Assets are separate cache surfaces.** The + Public Resolver is mutable and must change quickly through short cache lifetime + or explicit purge. Public Version Assets are immutable for one Published + Revision and are the aggressive edge-cache surface for broad traffic. +- **Platform Lockdown is the hard public takedown path.** Operator-only Platform + Lockdown blocks the Public Resolver and Public Version Assets, using cache + purge and deny controls where available. It remains distinct from Access Link + Lockdown and Public Offline. +- **Public pointer changes are audit-worthy.** Public Version changes and Public + Offline changes create Audit Events with a redacted Change Summary containing + the Public ID, old and new Published Revision ids or null, actor, and calling + surface. + +## Consequences + +- The current `make_public` / `agent-paste make-public` name becomes misleading: + it creates an unlisted Share Link today, not the future Public Artifact model. + A follow-up implementation should choose explicit verbs before shipping true + public distribution, for example `share` / `create_share_link` for unlisted and + `make_public` / `select_public_version` for true public. +- Existing shipped specs and user docs remain current until that implementation + lands: publish is private-first, Share Link creation is explicit, and Access + Link Signed URLs remain the only shipped no-login latest-moving handoff. +- The Public Artifact model needs schema, API, CLI, MCP, cache, audit, and + operator-lockdown work before it can be described as shipped behavior in + `docs/specs/`. +- Public should be chosen for broad distribution and traffic spikes. Unlisted + Share Links should be chosen when revocation and takedown control matter more + than cache-level distribution. + +## Considered Options + +- **Keep calling Share Links public.** Rejected. It hides the cache and + revocation tradeoff and makes CDN-backed distribution sound safer to revoke + than it is. +- **Use signed URLs for public distribution.** Rejected. Signed URLs are the + right shape for unlisted grants, but broad public distribution needs a stable + permalink and immutable cacheable assets. +- **Let Publish move the public pointer automatically.** Rejected. Public + versions should be frozen so the public page does not live-update while an + agent iterates. +- **Add slugs to Public URLs.** Rejected for the canonical URL. Slugs create + uniqueness and rename concerns without adding enough product value. The Public + ID is the canonical segment. + +## What this ADR is not + +- Not an implementation of Public Artifacts. +- Not a change to the current Access Link Signed URL model from ADR 0047. +- Not a change to the current shipped `make_public` command until a follow-up + spec and implementation PR changes it. diff --git a/docs/adr/README.md b/docs/adr/README.md index 476fad33..1df77fd0 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -39,7 +39,8 @@ This directory is the decision log for agent-paste: it records _why_ choices wer - [ADR 0083](./0083-local-repository-backend-enforces-run-scope.md) records that the local in-memory repository backend now enforces the **Run Scope** ([ADR 0070](./0070-repository-core-ports-and-adapters.md)) through a **Scoped View**, as a deliberate test-surface bug detector rather than a faithful RLS emulator. Under a workspace Run Scope a foreign read returns nothing (RLS-faithful), a foreign `insert` throws (loud and self-labeling, the one `set()` path), and every other foreign mutation no-ops; the platform Run Scope is unfiltered. It only closes a gap in the local backend's enforcement and does not change the production isolation model ([ADR 0044](./0044-workspace-isolation-via-postgres-rls.md) Postgres RLS is unchanged). - [ADR 0084](./0084-cli-and-mcp-share-one-publish-path.md) records that the **CLI** and **MCP** are two transports over one publish path: both call `runPublish` in `@agent-paste/api-client`, differing only behind a four-method `PublishTransport` seam (CLI over the HTTPS `ApiClient`, MCP over Worker service bindings). It forbids reintroducing a surface-specific publish implementation — the divergence that shipped the no-link-on-MCP and draft-`list_artifacts`-500 bugs. The shared module is exposed on the Worker-safe `@agent-paste/api-client/publish` subpath so the MCP bundle never pulls the Node-only `ApiClient`. The publish output is `{title, private_url, expires_at, upload_stats?}` with no `shared` field, per [ADR 0086](./0086-publish-is-content-only-private-first.md). It does not merge the two binaries; login/logout/upgrade, ephemeral provisioning, idempotency-key derivation, and output rendering stay caller-specific. - [ADR 0085](./0085-publish-returns-one-viewer-url.md) — **Status: Superseded by [ADR 0086](./0086-publish-is-content-only-private-first.md).** It recorded that publish (both surfaces, through [ADR 0084](./0084-cli-and-mcp-share-one-publish-path.md)) returns one `viewer_url` plus a `shared` boolean, private by default, where `viewer_url` switched between the authenticated **Private Link** and the public **Share Link**'s signed URL. ADR 0086 retired that switching field and the `share`/`shared` convention: the switching link lied on revise (it reported `shared:false` while a live Share Link still served the page) and the `share` flag put public-by-flag on the content-publish call. **Viewer URL** is removed from [`CONTEXT.md`](../../CONTEXT.md). -- [ADR 0086](./0086-publish-is-content-only-private-first.md) supersedes [ADR 0085](./0085-publish-returns-one-viewer-url.md): publish is content-only and private-first. `publish_artifact`, `add_revision`, and `agent-paste publish` accept no visibility input and return exactly one link — the **Private Link**, surfaced as `private_url`, a login-walled clean viewer at `/v/` for the owning **Workspace Member** (never the **Artifact Console** at `/artifacts/`). The `share`/`--share` inputs and the `shared` output bit are removed from every surface (CLI, MCP, the REST `PublishRevisionRequest` body, and `runPublish`); the server `PublishResult` renames `viewer_url`/`artifact_url` to `private_url` and drops `access_link_url`. Going public is a separate explicit verb: `make_public` (MCP) and `agent-paste make-public` (CLI), replacing `create_share_link`, mint or reuse the one revocable **Share Link** and return its no-login **Access Link Signed URL**. `revoke_access_link`, `list_access_links`, and `create_revision_link` are unchanged; the [ADR 0047](./0047-access-link-signed-url-with-fragment-encoded-payload.md) Access Link grant model is untouched. [`CONTEXT.md`](../../CONTEXT.md) deletes **Viewer URL**, renames **Artifact URL** to **Artifact Console**, and retargets **Private Link** at the `/v` viewer. Amends [ADR 0084](./0084-cli-and-mcp-share-one-publish-path.md)'s output-shape note. +- [ADR 0086](./0086-publish-is-content-only-private-first.md) supersedes [ADR 0085](./0085-publish-returns-one-viewer-url.md): publish is content-only and private-first. `publish_artifact`, `add_revision`, and `agent-paste publish` accept no visibility input and return exactly one link — the **Private Link**, surfaced as `private_url`, a login-walled clean viewer at `/v/` for the owning **Workspace Member** (never the **Artifact Console** at `/artifacts/`). The `share`/`--share` inputs and the `shared` output bit are removed from every surface (CLI, MCP, the REST `PublishRevisionRequest` body, and `runPublish`); the server `PublishResult` renames `viewer_url`/`artifact_url` to `private_url` and drops `access_link_url`. Creating unauthenticated Share Link access is a separate explicit verb: `make_public` (MCP) and `agent-paste make-public` (CLI), replacing `create_share_link`, mint or reuse the one revocable **Share Link** and return its no-login **Access Link Signed URL**. `revoke_access_link`, `list_access_links`, and `create_revision_link` are unchanged; the [ADR 0047](./0047-access-link-signed-url-with-fragment-encoded-payload.md) Access Link grant model is untouched. [`CONTEXT.md`](../../CONTEXT.md) deletes **Viewer URL**, renames **Artifact URL** to **Artifact Console**, and retargets **Private Link** at the `/v` viewer. Amends [ADR 0084](./0084-cli-and-mcp-share-one-publish-path.md)'s output-shape note. +- [ADR 0087](./0087-public-artifacts-and-unlisted-share-links.md) records the planned split between unlisted Share Links and true Public Artifacts. Current shipped behavior is still ADR 0086: `make_public` / `agent-paste make-public` mint or reuse a Share Link. The future Public Artifact model gets a stable ID-only `/p/{publicId}` Public URL, frozen Public Version, soft Public Offline control, cacheable Public Version Assets, and operator-only Platform Lockdown for hard takedown. - [ADR 0021](./0021-id-based-r2-object-key-layout.md) is amended for revision file keys. The ADR originally described env-scoped file keys; shipped revision files and upload PUT targets use the legacy `artifacts/{artifactId}/revisions/{revisionId}/files/{path}` prefix. Derived bundles and env-scoped purge prefixes remain env-scoped. Current shapes are in [`docs/specs/data-model.md`](../specs/data-model.md#r2-object-key-layout). - [ADR 0062](./0062-two-layer-cache-for-hot-path-auth-lookups.md) is amended for the L2 synthetic cache URL. The ADR originally used `https://cache.agent-paste.internal/{namespace}/{key}`; the shipped helper uses `https://agent-paste.internal/cache/{namespace}/{key}`. Current behavior is in [`docs/specs/architecture.md`](../specs/architecture.md#auth-lookup-cache). - [`packages/contracts`](../../packages/contracts) and [`docs/specs/contracts.md`](../specs/contracts.md) are the canonical MVP implementation contract for Zod schemas, ID formats, and the route registry. ADRs provide rationale; contracts provide field-level implementation shape. diff --git a/docs/ops/project-status.md b/docs/ops/project-status.md index 73a7e079..86da64ae 100644 --- a/docs/ops/project-status.md +++ b/docs/ops/project-status.md @@ -210,11 +210,17 @@ Highest-signal gaps: `agent-paste publish` take no visibility input and return one link, `private_url` — the login-walled `/v/` clean viewer (the server `PublishResult` renamed `artifact_url`→`private_url` and dropped - `access_link_url`/`shared`). Going public is the separate explicit verb - `make_public` (MCP) / `agent-paste make-public` (CLI), renamed from - `create_share_link`, which mints or reuses the one revocable Share Link and - returns its no-login Access Link Signed URL. ADR 0085 (one switching + `access_link_url`/`shared`). Unlisted no-login sharing is the separate + explicit verb `make_public` (MCP) / `agent-paste make-public` (CLI), renamed + from `create_share_link`, which mints or reuses the one revocable Share Link + and returns its no-login Access Link Signed URL. ADR 0085 (one switching `viewer_url` + `shared`) is superseded. +- True Public Artifacts are planned, not shipped. ADR 0087 / AP-330 reserves + **Public Artifact** for the future CDN-backed distribution model with a stable + ID-only `/p/{publicId}` URL, frozen Public Version, soft Public Offline control, + and hard Platform Lockdown path. Until that implementation lands, shipped + unauthenticated latest-moving handoff remains the explicit Share Link created by + `make_public` / `agent-paste make-public`. - File-bytes hash-reputation malware scanner: cancelled/removed. Llama Guard and Cloudflare URL Scanner still support the ephemeral advisory/abuse path when configured, alongside built-in warning metadata. Containment is the trust diff --git a/docs/specs/api.md b/docs/specs/api.md index 6a31759d..796a41f2 100644 --- a/docs/specs/api.md +++ b/docs/specs/api.md @@ -200,7 +200,7 @@ link publish returns. It is **permanent and stable**: the URL is derived only from the Artifact id with no token, signature, or expiry, and `add_revision` republishes into the same id, so the link 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 +only; publish never grants unauthenticated access) and stops resolving only when the Artifact itself is deleted or swept by Auto Deletion — a property of the Artifact's lifetime, not the link. The `expires_at` in `PublishResult` is the Artifact's content lifetime, not a link expiry. The dashboard-only **Artifact @@ -210,10 +210,11 @@ response, expires with its signed token, and does not Live Update. Direct `usercontent` HTML is inert raw byte delivery unless it is loaded through the controlled Artifact Viewer iframe. MCP publish tools (`publish_artifact`, `add_revision`) and CLI `publish` run the same publish path and return the same -shape: `private_url`, title, expiry, and upload stats. Making an Artifact public -is a separate explicit step — `make_public` (MCP) and `agent-paste make-public` -(CLI) — which mints or reuses the one revocable **Share Link** and returns its -public, no-login **Access Link Signed URL**. Creating a `share` Access Link is +shape: `private_url`, title, expiry, and upload stats. Creating an unlisted +no-login handoff is a separate explicit step — `make_public` (MCP) and +`agent-paste make-public` (CLI) — which currently mints or reuses the one +revocable **Share Link** and returns its no-login **Access Link Signed URL**. +Creating a `share` Access Link is idempotent on the Artifact, not just on the request key: if the Artifact already has an active (non-revoked, unexpired) Share Link, create returns that same link instead of minting a duplicate, so an Artifact has at most one live Share Link. @@ -257,10 +258,10 @@ Human operators and rotation agents use WorkOS operator auth or Cloudflare Acces Publishing without `--artifact-id` creates a new Artifact. Publishing with an existing `artifact_id` creates and publishes a new Revision for that Artifact. The previous `revision_content_url` continues to point at the older Revision. -Publish never makes the Artifact public; a Share Link is created only by the -separate `make_public` / `agent-paste make-public` step. Its Access Link Signed -URL is the user-facing public URL. The `private_url` remains the authenticated -clean-viewer link for Workspace members. +Publish never creates unauthenticated access; a Share Link is created only by +the separate `make_public` / `agent-paste make-public` step. Its Access Link +Signed URL is the user-facing unlisted no-login URL. The `private_url` remains +the authenticated clean-viewer link for Workspace members. Workspace-wide publish deduplication starts only for new hash-aware uploads after the digest-manifest contract shipped. There is no historical backfill of legacy diff --git a/docs/specs/architecture.md b/docs/specs/architecture.md index 3701dab6..7ba24eab 100644 --- a/docs/specs/architecture.md +++ b/docs/specs/architecture.md @@ -126,8 +126,8 @@ Key invariants: ## Read And Share Flow Default human handoff is the authenticated Private Link (`private_url`, the -`/v/` clean viewer); publish is content-only and never makes an -Artifact public. Public or shareable handoff requires an explicit, separate +`/v/` clean viewer); publish is content-only and never creates +unauthenticated access. Unlisted no-login handoff requires an explicit, separate make-public step that mints a revocable Share Link; its Access Link Signed URL opens the Artifact Viewer for the latest Published Revision. Direct signed content URLs are delivery URLs for one exact Revision. @@ -154,7 +154,7 @@ Access Link rules: - The shareable credential lives in the URL fragment, not in the path or query string. -- An Access Link Signed URL minted from a Share Link grants public access to the Artifact Viewer. +- An Access Link Signed URL minted from a Share Link grants unlisted no-login access to the Artifact Viewer. - A Share Link resolves to the latest Published Revision and can Live Update through that viewer. - A Revision Link resolves to exactly one Revision and does not Live Update. diff --git a/docs/specs/cli.md b/docs/specs/cli.md index 9508728e..3b6cc0b4 100644 --- a/docs/specs/cli.md +++ b/docs/specs/cli.md @@ -43,9 +43,9 @@ automatic; flags override detection. - `publish` is content-only and private: it emits one handoff link, `private_url` (the login-walled clean viewer at `/v/` for a Workspace Member) — the same field the MCP server returns. There is no `--share` input and no - `shared` output bit. Making an Artifact public is the separate `make-public` - command, which mints or reuses the one Share Link and prints its no-login - Access Link Signed URL. + `shared` output bit. Creating an unlisted no-login handoff is the separate + `make-public` command, which currently mints or reuses the one Share Link and + prints its no-login Access Link Signed URL. - Errors in `json` mode are emitted on **stderr** as `{ "error": { "code", "message", "docs?" } }` (no `schema_version` — it is an error envelope, not a result). diff --git a/docs/specs/local-dev.md b/docs/specs/local-dev.md index ffa240a7..89f2184a 100644 --- a/docs/specs/local-dev.md +++ b/docs/specs/local-dev.md @@ -208,7 +208,7 @@ The first local vertical slice is complete when: 1. A Workspace and local CLI credential can be created locally. 2. `agent-paste whoami` succeeds after `pnpm cli:dev login`. 3. CLI can publish a folder with `index.html`. -4. Publish is content-only and private: it prints the `private_url` (`/v/` clean viewer) as `View`. Making an Artifact public is the separate `make-public` step (MCP `make_public`). +4. Publish is content-only and private: it prints the `private_url` (`/v/` clean viewer) as `View`. Unlisted no-login sharing is the separate `make-public` step (MCP `make_public`). 5. CLI JSON output includes `artifact_id`, `revision_id`, `private_url`, `revision_content_url`, `agent_view_url`, and `expires_at` for automation. There is no `share` input and no `shared` output. 6. `private_url` opens the authenticated `/v/` clean viewer in the local harness, while `revision_content_url` serves raw Revision bytes under the content origin with direct HTML scripts disabled. 7. `agent_view_url` returns Agent View JSON with full per-file URLs. diff --git a/docs/specs/mvp.md b/docs/specs/mvp.md index db2b7b4b..a39c6430 100644 --- a/docs/specs/mvp.md +++ b/docs/specs/mvp.md @@ -100,12 +100,12 @@ Publish returns: Publish is content-only and private. `private_url` is the login-walled clean viewer at `/v/` for the owning Workspace Member and is the only handoff link publish returns; there is no `share` input and no `shared` output. -Making an Artifact public is the separate `make_public` / `agent-paste -make-public` step, which mints or reuses the one Share Link and returns its -no-login Access Link Signed URL. `revision_content_url` remains a direct signed -content URL for the exact Revision. Direct `usercontent` HTML is inert raw byte -delivery unless it is loaded through the controlled Artifact Viewer iframe. The -content token lives in the path. +Creating an unlisted no-login handoff is the separate `make_public` / +`agent-paste make-public` step, which currently mints or reuses the one Share +Link and returns its no-login Access Link Signed URL. `revision_content_url` +remains a direct signed content URL for the exact Revision. Direct `usercontent` +HTML is inert raw byte delivery unless it is loaded through the controlled +Artifact Viewer iframe. The content token lives in the path. `agent_view_url` is public and signed. It returns a JSON manifest for the same revision. @@ -247,5 +247,5 @@ The MVP is buildable when the API-key publish loop works end to end. Phase 3 mem - `agent-paste publish ./demo.html` uploads a single HTML file. - Human-facing publish output returns the `private_url` (`/v/` clean viewer) as `View`. - JSON/REST publish output also carries `artifact_id`, `revision_id`, `private_url`, `revision_content_url`, `agent_view_url`, and `expires_at` for automation. There is no `share` input and no `shared` output. -- `private_url` is the authenticated `/v/` clean viewer; making an Artifact public is the separate `make-public` step (which mints or reuses the one Share Link's no-login Access Link Signed URL); `revision_content_url` is raw byte delivery for one Revision; and `agent_view_url` returns JSON with full per-file URLs. +- `private_url` is the authenticated `/v/` clean viewer; unlisted no-login sharing is the separate `make-public` step (which currently mints or reuses the one Share Link's no-login Access Link Signed URL); `revision_content_url` is raw byte delivery for one Revision; and `agent_view_url` returns JSON with full per-file URLs. - Expired artifacts stop resolving and their bytes are cleaned up. diff --git a/docs/specs/phases.md b/docs/specs/phases.md index c3ed56b8..628d56fe 100644 --- a/docs/specs/phases.md +++ b/docs/specs/phases.md @@ -27,7 +27,7 @@ Goal: prove the artifact handoff loop. - Storage: private R2. - Metadata: Postgres through Cloudflare Hyperdrive using Drizzle. - Publish: single HTML file or folder with `index.html`. -- Output: publish is content-only and private. Human-facing publish prints the `private_url` (`/v/` clean viewer) as `View`; CLI JSON output includes `artifact_id`, `revision_id`, `private_url`, direct signed `revision_content_url`, public signed `agent_view_url`, and `expires_at`. There is no `share` input and no `shared` output; making an Artifact public is the separate `make-public` step. +- Output: publish is content-only and private. Human-facing publish prints the `private_url` (`/v/` clean viewer) as `View`; CLI JSON output includes `artifact_id`, `revision_id`, `private_url`, direct signed `revision_content_url`, public signed `agent_view_url`, and `expires_at`. There is no `share` input and no `shared` output; unlisted no-login sharing is the separate `make-public` step. - Agent View: simple JSON with full per-file URLs. - Retention: default `30d`, max `90d`, scheduled cleanup. - Operator: WorkOS `/v1/web/admin/*` lockdown routes; non-production smokes use the harness secret. diff --git a/docs/specs/use-cases.md b/docs/specs/use-cases.md index bb5bf8c9..22735d59 100644 --- a/docs/specs/use-cases.md +++ b/docs/specs/use-cases.md @@ -31,13 +31,14 @@ committing temporary files, creating a gist, or deploying to a preview host. | Govern agent output | A team needs to know what agents published, when it expires, and how to revoke access if needed. | Attach Artifacts to Workspaces, Access Links, Audit Events, Auto Deletion, and lockdown controls. | | Embed artifact handoff | A product needs artifact storage and a manifest protocol without building the whole platform itself. | Expose CLI, MCP, Agent View, and documented contracts that can be built on by another platform. | -For the iteration use case, public/shareable browser URLs must come from an -explicit Share Link, minted by the separate make-public step (`agent-paste -make-public`, MCP `make_public`); publish itself is content-only and private and -never makes an Artifact public. The direct `revision_content_url` is exact -Revision content; it is useful for one-shot inspection but it does not advance -when the agent publishes a later Revision. The `private_url` (`/v/` -clean viewer) is the default authenticated Workspace view publish returns. +For the iteration use case, no-login shareable browser URLs must come from an +explicit unlisted Share Link, minted by the separate make-public step +(`agent-paste make-public`, MCP `make_public`); publish itself is content-only +and private and never creates unauthenticated access. The direct +`revision_content_url` is exact Revision content; it is useful for one-shot +inspection but it does not advance when the agent publishes a later Revision. +The `private_url` (`/v/` clean viewer) is the default authenticated +Workspace view publish returns. ## Primary Audiences diff --git a/docs/specs/web.md b/docs/specs/web.md index e547a3d7..26d2c5ab 100644 --- a/docs/specs/web.md +++ b/docs/specs/web.md @@ -51,12 +51,12 @@ On every authed request, `api` verifies the forwarded WorkOS access token and re Read-only Workspace Members can list Access Links workspace-wide and per Artifact. Access Link management mutations on your own Artifact - create -(make public), mint, and revoke - require the `publish` scope: making an -Artifact public and revoking that public access are part of the same content -authority that creates and revises it. Workspace-wide Access Link Lockdown set -and Lockdown lift remain `admin` (an account/workspace management action, not a -per-Artifact one). Member `scopes` are always resolved from the database, not -WorkOS token text. +(create unlisted Share Link), mint, and revoke - require the `publish` scope: +creating unlisted no-login access and revoking that access are part of the same +content authority that creates and revises it. Workspace-wide Access Link +Lockdown set and Lockdown lift remain `admin` (an account/workspace management +action, not a per-Artifact one). Member `scopes` are always resolved from the +database, not WorkOS token text. After first provisioning, `POST /v1/auth/web/callback` receives the default credential plaintext once. The dashboard stores it only in client memory for the first-run card. The secret is never persisted, never written to logs, and never retrievable from `api`.