From ae2f36d6738873543f0ec24fd49edce4dc70666c Mon Sep 17 00:00:00 2001 From: John Sell Date: Thu, 11 Jun 2026 10:06:28 -0400 Subject: [PATCH] spec(projects): define Project preview URL allowlist --- specs/ambient-ui/ambient-ui.spec.md | 94 +++++++++++++++++++-- specs/api/ambient-model.spec.md | 107 ++++++++++++++++++------ specs/security/rbac-enforcement.spec.md | 54 ++++++++++++ 3 files changed, 226 insertions(+), 29 deletions(-) diff --git a/specs/ambient-ui/ambient-ui.spec.md b/specs/ambient-ui/ambient-ui.spec.md index 530c09bed..44a3ca44f 100644 --- a/specs/ambient-ui/ambient-ui.spec.md +++ b/specs/ambient-ui/ambient-ui.spec.md @@ -830,13 +830,26 @@ Sessions with an `ambient-code.io/ui/preview-url` annotation SHALL offer a live The preview iframe SHALL be hardened: - The `sandbox` attribute SHALL be set with minimal permissions (`allow-scripts allow-same-origin allow-forms`). Top-level navigation (`allow-top-navigation`) and popups (`allow-popups`) SHALL NOT be granted. -- The UI SHALL validate the preview URL against a configurable allowlist of trusted host patterns (e.g., `*.apps.rosa.example.com`, `*.apps.cluster.local`). URLs not matching the allowlist SHALL be rejected with an error message instead of rendered. -- A Content-Security-Policy `frame-src` directive SHALL restrict the iframe to the allowlisted hosts. +- The UI and BFF preview proxy SHALL validate the preview URL against the active Project's `spec.preview.allowedHosts` policy. URLs not matching the Project allowlist SHALL be rejected with an error message instead of rendered. +- The iframe SHALL load a session-scoped BFF route, for example `/api/projects/{projectId}/sessions/{sessionId}/preview?url=...`. The BFF SHALL fetch the Session server-side, read `ambient-code.io/ui/preview-url`, and accept the requested URL only when it equals that annotation value or carries a server-issued preview continuation token for the same Project, Session, and target URL. Arbitrary `url` parameters outside the Session preview context SHALL be rejected. +- The preview proxy SHALL issue continuation tokens only for URLs discovered from a previously authorized preview response or redirect chain. Continuation tokens SHALL be signed or stored server-side, short-lived, bound to Project and Session, and revalidated against the effective preview policy before use. +- For every proxied request, including redirects and injected-link navigations, the preview proxy SHALL revalidate the target URL against the effective preview policy. Token relay SHALL be recomputed per target host. +- The preview proxy SHALL forward the BFF's server-side SSO access token to the preview target only when the matching Project preview host entry has `forwardAmbientToken: true` and the platform token-relay policy permits that host. Hosts with `forwardAmbientToken: false` SHALL render without Ambient token relay. Custom connection tokens entered through the status-bar connection controls SHALL NOT be forwarded to preview targets. +- A Content-Security-Policy `frame-src` directive SHALL restrict iframes to the same-origin preview proxy and any platform-configured direct-frame hosts. Project-specific preview host trust is enforced server-side by the proxy. + +The session-scoped BFF route is required for preview rendering. `GET /api/projects/{projectId}/sessions/{sessionId}/preview?url=...` SHALL be the iframe `src`, fetch Session and Project state server-side, evaluate the requested URL against the Session annotation and the Project preview policy, proxy the authorized response, and return a 4xx response for malformed, untrusted, or out-of-session targets. Direct iframe navigation to the annotation URL SHALL NOT bypass this route. Links and redirects discovered inside a proxied preview SHALL be rewritten through the same route with a continuation token, so every subsequent navigation repeats Session binding, Project allowlist validation, and token-relay evaluation. + +Preview policy resolution during rollout SHALL be tri-state: +- If the API/SDK cannot expose Project `spec.preview`, preview trust MAY be read from deployment configuration as a read-only, deployment-scoped fallback. +- If the API/SDK exposes Project `spec.preview` but a Project has no explicit `spec.preview` field, the deployment fallback MAY be used for that Project. +- If a Project has explicit `spec.preview.allowedHosts`, including `[]`, Project policy is authoritative and overrides deployment fallback. + +Fallback entries imply `forwardAmbientToken: false`. #### Scenario: Preview mode activation - GIVEN a session with `ambient-code.io/ui/preview-url: "https://app.example.com"` and `ambient-code.io/ui/preview-title: "SSO Login v2"` -- AND the URL matches the configured preview host allowlist +- AND the URL matches the active Project's preview host allowlist - WHEN the user clicks "Open Preview" in the session detail - THEN a near-fullscreen overlay opens with the URL loaded in a sandboxed iframe - AND the overlay header shows the preview title, device size toggles (Desktop/Tablet/Mobile), and a Comment button @@ -844,11 +857,36 @@ The preview iframe SHALL be hardened: #### Scenario: Preview URL rejected (untrusted host) - GIVEN a session with `ambient-code.io/ui/preview-url: "https://evil.example.com"` -- AND the URL does not match the configured preview host allowlist +- AND the URL does not match the active Project's preview host allowlist - WHEN the user clicks "Open Preview" - THEN the preview does not render - AND an error message is displayed: "Preview URL is not on the trusted hosts allowlist" +#### Scenario: Preview renders without Ambient token relay + +- GIVEN a session with `ambient-code.io/ui/preview-url: "https://deploy-preview-123.netlify.app"` +- AND the active Project allowlist contains `pattern: "deploy-preview-*.netlify.app"` with `forwardAmbientToken: false` +- WHEN the user opens the preview +- THEN the preview proxy fetches the target without an Ambient `Authorization` header +- AND the preview renders if the target does not require Ambient authentication + +#### Scenario: Preview token relay requires explicit trust + +- GIVEN a session with `ambient-code.io/ui/preview-url: "https://pr-123.checkout.apps.rosa.example.com"` +- AND the active Project allowlist contains `pattern: "pr-*.checkout.apps.rosa.example.com"` with `forwardAmbientToken: true` +- AND platform token-relay policy permits that host pattern +- WHEN the user opens the preview +- THEN the preview proxy forwards the BFF's server-side SSO access token to the preview target +- AND no custom status-bar connection token is forwarded + +#### Scenario: Preview token relay denied by platform policy + +- GIVEN a Project preview allowlist entry with `forwardAmbientToken: true` +- AND the host pattern is outside the platform token-relay policy +- WHEN the UI or API attempts to save the Project preview entry +- THEN the save is rejected +- AND the preview target is not treated as trusted for Ambient token relay + #### Scenario: Device size emulation - GIVEN the preview overlay is open @@ -966,7 +1004,50 @@ No list-watch endpoint exists for sessions today. Polling is the interim mechani The Settings view SHALL provide project-scoped configuration management with tabbed sections. -**Tabs:** General (project metadata), Permissions (user/role management), API Keys (key lifecycle), Feature Flags (toggles with confirmation). +**Tabs:** General (project metadata), Preview (trusted preview hosts), Permissions (user/role management), API Keys (key lifecycle), Feature Flags (toggles with confirmation). + +### Requirement: Preview Trust Configuration + +The Settings view SHALL expose the active Project's `spec.preview.allowedHosts` policy. The Preview tab SHALL list trusted host patterns, show whether each pattern forwards the BFF's server-side SSO access token, and allow authorized users to add, edit, or remove entries. + +The UI SHALL persist Preview tab changes through the Project API by updating Project `spec.preview`. The UI SHALL NOT write preview trust policy to Project labels or annotations. + +If Project `spec.preview` API/SDK support is not available in a deployed version, the Preview tab SHALL show the effective deployment-scoped fallback policy as read-only or hide editing controls entirely. Editing becomes available only when Project `spec.preview` writes are supported. + +The UI SHALL render the same logical policy that can be applied by YAML: + +```yaml +apiVersion: ambient-code.io/v1alpha1 +kind: Project +metadata: + name: checkout +spec: + preview: + allowedHosts: + - pattern: "pr-*.checkout.apps.rosa.example.com" + forwardAmbientToken: true +``` + +#### Scenario: Project editor updates preview allowlist + +- GIVEN user A has `project:editor` on Project `checkout` +- WHEN user A adds `pr-*.checkout.apps.rosa.example.com` in Settings > Preview +- THEN the UI saves the entry to Project `spec.preview.allowedHosts` +- AND subsequent previews in Project `checkout` use the updated allowlist + +#### Scenario: Project viewer sees read-only preview settings + +- GIVEN user B has `project:viewer` on Project `checkout` +- WHEN user B opens Settings > Preview +- THEN the current preview host policy is visible +- AND controls that would create, update, or remove entries are disabled or hidden + +#### Scenario: Empty allowlist disables project previews + +- GIVEN Project `checkout` has `spec.preview.allowedHosts: []` +- WHEN a Session in `checkout` has an `ambient-code.io/ui/preview-url` annotation +- THEN opening the preview is rejected as untrusted +- AND the UI explains that no trusted preview hosts are configured for the Project #### Scenario: Feature flag toggle confirmation @@ -1117,6 +1198,7 @@ This section documents API endpoints and capabilities that this spec depends on | Dependency | Required By | Status | Interim | |------------|-------------|--------|---------| +| Project `spec.preview.allowedHosts` API and SDK support | Live Preview, Settings > Preview | Specified in Ambient Data Model; implementation planned | Use deployment-level preview env allowlist as read-only fallback until Project preview policy is available; fallback never enables Ambient token relay | | Annotation enrichment endpoint (resolve `ambient-code.io/jira/issue` etc. against bound credentials) | Annotation enrichment, Issues view status filtering | Not yet specified | Render raw annotation values as clickable chips | | `GET /credentials/{cred_id}/role_bindings` (scoped query) | Credential binding display | Planned, not implemented | Use generic `GET /role_bindings` filtered by `credential_id` | | Cross-resource search endpoint | Global search | Not planned | Client-side aggregation across multiple list endpoints | @@ -1133,6 +1215,8 @@ This section documents API endpoints and capabilities that this spec depends on | Annotation registry is a code enum (not dynamic) | Simplicity. Adding a new annotation type is a PR, not a config change. The set of annotations the UI understands should be deliberate and reviewed. | | Enrichment as graceful degradation | UI ships without enrichment API. Raw annotation values are useful on their own (clickable links). Enriched tooltips are additive. | | Cost as annotation, not API field | Cost is agent-computed and written as `ambient-code.io/cost/estimate`. No API-level cost computation. | +| Preview trust as Project spec, not annotation metadata or a singleton settings kind | Preview host trust is security-sensitive Project desired state. It belongs in typed Project `spec.preview` so it can be validated, authorized, edited via UI/API, and reconciled by `acpctl apply`. Session annotations only point to a preview URL; they do not grant trust. | +| Preview rendering trust separated from Ambient token relay trust | Many preview hosts should be frameable without receiving Ambient bearer tokens. `forwardAmbientToken` is explicit per host entry and may be constrained by platform token-relay policy. | | Tool metrics computed client-side | The API stores raw SessionMessages. Aggregating tool call stats is a UI concern, not an API concern. | | SSE for sessions, polling for rest | Sessions have real-time SSE streams. Credentials, schedules, and agents change infrequently — polling is sufficient and simpler. | | Single interaction pattern per entity | Agent rows: navigate to detail page. Session rows: navigate to detail page. Reduces cognitive load per Krug's "Don't Make Me Think." | diff --git a/specs/api/ambient-model.spec.md b/specs/api/ambient-model.spec.md index e6c9d86c1..ee15be54f 100644 --- a/specs/api/ambient-model.spec.md +++ b/specs/api/ambient-model.spec.md @@ -2,8 +2,9 @@ **Date:** 2026-03-20 **Status:** Active -**Last Updated:** 2026-06-03 — added Application (GitOps continuous sync for agent fleets); addressed review feedback: credential_id FK for remote auth, RoleBinding escalation rules, prune safety, health status semantics, gitops role grantability, sync engine kind filtering -**Previous:** 2026-05-12 — migrate Credentials from project-scoped to global routes (`/credentials`); remove `project_id` from model, OpenAPI, and SDK; add drop-column migration; update coverage matrix +**Last Updated:** 2026-06-11 — added kube-shaped Project `spec.preview` policy for project preview URL trust +**Previous:** 2026-06-03 — added Application (GitOps continuous sync for agent fleets); addressed review feedback: credential_id FK for remote auth, RoleBinding escalation rules, prune safety, health status semantics, gitops role grantability, sync engine kind filtering +**Earlier:** 2026-05-12 — migrate Credentials from project-scoped to global routes (`/credentials`); remove `project_id` from model, OpenAPI, and SDK; add drop-column migration; update coverage matrix **Workflow:** `../../workflows/sessions/ambient-model.workflow.md` — implementation waves, gap table, build commands, run log **Design:** `credentials-session.md` — full Credential Kind design spec and rationale @@ -13,7 +14,7 @@ The Ambient API server provides a coordination layer for orchestrating fleets of persistent agents across projects. The model is intentionally simple: -- **Project** — a workspace. Groups agents and provides shared context (`prompt`) injected into every agent start. +- **Project** — a workspace. Groups agents, provides shared context (`prompt`) injected into every agent start, and owns typed project configuration such as preview URL trust policy. - **Agent** — a project-scoped, mutable definition. Agents belong to exactly one Project. `prompt` defines who the agent is and is directly editable (subject to RBAC). - **Session** — an ephemeral Kubernetes execution run, created exclusively via agent start. Only one active Session per Agent at a time. - **Message** — a single AG-UI event in the LLM conversation. Append-only; the canonical record of what happened in a session. @@ -49,6 +50,7 @@ erDiagram string name string description string prompt "workspace-level context injected into every agent start" + jsonb spec "typed Project spec including preview" jsonb labels jsonb annotations string status @@ -57,16 +59,6 @@ erDiagram time deleted_at } - ProjectSettings { - string ID PK - string project_id FK - string group_access - string repositories - time created_at - time updated_at - time deleted_at - } - %% ── Agent (project-scoped, mutable) ────────────────────────────────────── Agent { @@ -259,7 +251,6 @@ erDiagram %% ── Relationships ──────────────────────────────────────────────────────── - Project ||--o{ ProjectSettings : "has" Project ||--o{ Agent : "owns" RoleBinding }o--o| Credential : "credential_id" Project ||--o{ ScheduledSession : "owns" @@ -314,7 +305,7 @@ An Application syncs **project-scoped fleet definitions** — a subset of resour | Kind | Sync Behavior | |---|---| -| `Project` | Created if `CreateProject=true` in `sync_options`; patched (description, prompt, labels, annotations) on subsequent syncs | +| `Project` | Created if `CreateProject=true` in `sync_options`; patched (metadata labels/annotations and declared `spec` fields, including `description`, `prompt`, and `preview`) on subsequent syncs | | `Agent` | Created or patched within the destination project; prompt, labels, annotations updated | | `Credential` | Created if not present; idempotent by name | | `RoleBinding` | Created if not present; idempotent by user+role+scope key. **Escalation-bound:** the sync engine can only create RoleBindings at or below the level of the service credential it uses (see Design Decisions). | @@ -430,6 +421,70 @@ Promotion is a git operation: merge the dev overlay changes into the release bra --- +## Project — Workspace And Project Configuration + +Project is the workspace boundary for Agents, Inbox messages, Sessions, and typed project configuration. The stable address is `metadata.name`, which is also the Ambient Project id today. + +Project manifests SHALL use a Kubernetes-style envelope: + +```yaml +apiVersion: ambient-code.io/v1alpha1 +kind: Project +metadata: + name: checkout + labels: + team: payments +spec: + description: "Checkout automation" + prompt: "This workspace owns checkout services." + preview: + allowedHosts: + - pattern: "pr-*.checkout.apps.rosa.example.com" + forwardAmbientToken: true + - pattern: "deploy-preview-*.netlify.app" + forwardAmbientToken: false +``` + +| Field | Notes | +|-------|-------| +| `apiVersion` | Required in declarative manifests. Initial value: `ambient-code.io/v1alpha1`. | +| `kind` | Required. Must be `Project`. | +| `metadata.name` | Required. The stable, unique Project name and Ambient Project id. | +| `metadata.labels` | Optional map for queryable Project tags. | +| `metadata.annotations` | Optional map for freeform Project metadata. Preview trust policy SHALL NOT be stored here. | +| `spec.description` | Nullable. Free-text Project description. Canonical in kube-shaped manifests; mirrored to legacy `description` responses during compatibility transition. | +| `spec.prompt` | Nullable. Workspace-level context injected into every Agent start in the Project. Canonical in kube-shaped manifests; mirrored to legacy `prompt` responses during compatibility transition. | +| `spec.preview.allowedHosts` | Optional list of trusted preview host entries for this Project. Omitted `preview` leaves preview settings unmanaged by the apply operation. An empty list clears the Project preview allowlist. | +| `spec.preview.allowedHosts[].pattern` | Required host glob pattern. The value is matched against URL host with optional port. It SHALL NOT include a URL scheme, path, query, fragment, username, or password. | +| `spec.preview.allowedHosts[].forwardAmbientToken` | Optional boolean, default `false`. When `true`, the preview proxy MAY forward the BFF's server-side SSO access token to matching preview targets, subject to the platform token relay policy. | + +Preview host patterns SHALL be explicit host patterns. The API SHALL reject wildcard-only patterns such as `*` and `*:*`, scheme-bearing patterns, path-scoped entries, and entries that cannot be parsed as host globs. Patterns are matched case-insensitively against normalized `URL.host`; preview URLs themselves MUST be absolute `http:` or `https:` URLs. Pattern validation is a server-side responsibility; UI and CLI validation MAY preflight but SHALL NOT be the only enforcement. + +`forwardAmbientToken` is a separate trust decision from allowing the preview to render. A Project MAY allow a host for preview rendering without allowing Ambient token relay. The platform deployment SHALL define a token-relay ceiling; if no ceiling is configured, every `forwardAmbientToken: true` entry SHALL be rejected. Project roles can opt in only within the platform-admin configured ceiling and cannot expand it. Relay SHALL be HTTPS-only, evaluated per request, never use service/internal tokens, and tokens SHALL NOT be logged or persisted. Entries with `forwardAmbientToken: false` do not need to match the token-relay ceiling, but are still subject to SSRF protections and preview host validation. + +If multiple `allowedHosts` entries match a preview host, the most-specific host pattern wins for `forwardAmbientToken` evaluation. Equal-specificity conflicts SHALL be rejected at save time. + +Project preview policy writes SHALL be authorized by Project role: `project:owner` and `project:editor` can update `spec.preview` for their Project; `project:viewer` can read but cannot modify it. Authenticated Project bootstrap creation MAY include `spec.preview` because Project creation atomically grants the caller `project:owner` for the new Project. Anonymous Project creation SHALL be rejected, and compatibility paths SHALL NOT persist preview trust unless the create succeeds with the owner binding in the same transaction. Clearing preview trust is done by updating `spec.preview.allowedHosts` to `[]`. + +### Storage Migration + +Project storage SHALL add persisted `spec` support for typed configuration while preserving existing `name`, `description`, `prompt`, `labels`, and `annotations` fields for backward compatibility. The final desired Project row shape SHALL keep Project identity and query fields on the Project row (`id`, `name`, lifecycle timestamps, `labels`, `annotations`) and store typed desired-state configuration in `spec` as JSONB or the database-native equivalent. Existing rows SHALL backfill `spec.description` and `spec.prompt` from the current top-level columns. During compatibility transition, writes to legacy `description` and `prompt` fields SHALL update the corresponding `spec` fields, and reads MAY continue to expose the legacy fields as mirrors of `spec.description` and `spec.prompt`. New typed configuration such as `preview.allowedHosts` SHALL live under Project `spec`, not in Project labels or annotations and not in a separate settings kind. + +The Project persistence model after migration SHALL include: +- `projects.name` as the non-null, immutable Project identity backing `metadata.name`, unique among non-deleted Projects; +- `projects.spec` as a non-null JSONB or database-native equivalent document backing the Kubernetes-style `spec` field, defaulting to `{}`; +- existing `projects.description` and `projects.prompt` columns only as compatibility mirrors during transition, with `spec.description` and `spec.prompt` canonical for kube-shaped manifests; +- no final project-scoped settings table, separate settings Kind, or denormalized settings row keyed by `project_id` for preview policy. + +Existing project-scoped settings storage is legacy implementation state, not desired API shape. Migration SHALL move any still-relevant settings into Project `spec` or delete them if they are no longer part of the desired model. Project preview policy SHALL NOT be sourced from legacy `group_access` or `repositories` fields, and no separate settings manifest Kind, REST resource, SDK type, or UI editing surface SHALL be exposed. + +The database SHALL enforce: +- a unique, non-deleted Project `name`; +- immutable `metadata.name` / Project id after creation; +- server-side validation of `spec.preview.allowedHosts`. + +--- + ## Agent — Project-Scoped Mutable Definition Agent is scoped to a Project. The stable address is `{project_name}/{agent_name}`. @@ -554,7 +609,7 @@ The `acpctl` CLI mirrors the API 1-for-1. Every REST operation has a correspondi | `GET /projects` | `acpctl get projects` | ✅ implemented | | `GET /projects/{id}` | `acpctl get project ` | ✅ implemented | | `POST /projects` | `acpctl create project --name [--description ]` | ✅ implemented | -| `PATCH /projects/{id}` | `acpctl project update [--name ] [--description ] [--prompt

]` | ✅ implemented | +| `PATCH /projects/{id}` | `acpctl project update [--description ] [--prompt

]` | ✅ implemented | | `DELETE /projects/{id}` | `acpctl delete project ` | ✅ implemented | | _(context switch)_ | `acpctl project ` | ✅ implemented | | _(context view)_ | `acpctl project current` | ✅ implemented | @@ -680,13 +735,13 @@ The `acpctl` CLI mirrors the API 1-for-1. Every REST operation has a correspondi ### `acpctl apply` — Declarative Fleet Management -`acpctl apply` reconciles Projects and Agents from declarative YAML files, mirroring `kubectl apply` semantics. It is the primary way to provision and update entire agent fleets from the `.ambient/teams/` directory tree. +`acpctl apply` reconciles Projects, Agents, Credentials, and RoleBindings from declarative YAML files, mirroring `kubectl apply` semantics. It is the primary way to provision and update entire agent fleets from the `.ambient/teams/` directory tree. #### Supported Kinds | Kind | Fields applied | |---|---| -| `Project` | `name`, `description`, `prompt`, `labels`, `annotations` | +| `Project` | `apiVersion`, `kind`, `metadata.name`, `metadata.labels`, `metadata.annotations`, `spec.description`, `spec.prompt`, `spec.preview.allowedHosts` | | `Agent` | `name`, `prompt`, `labels`, `annotations`, `inbox` (seed messages) | | `Credential` | `name`, `description`, `provider`, `token` (env var reference), `url`, `email`, `labels`, `annotations` — global resource; use `credential bind` to grant project access | @@ -703,7 +758,7 @@ acpctl apply -f - # read from stdin Each file may contain one or more YAML documents separated by `---`. Documents with unrecognised `kind` values are skipped with a warning. Apply behaviour per resource: -- **Project**: if a project with `name` already exists, `PATCH` it (description, prompt, labels, annotations). If it does not exist, `POST` to create it. +- **Project**: resolved by `metadata.name`. If a Project with that name already exists, `PATCH` the declared metadata and `spec` fields. If it does not exist, `POST` to create it. Applying `spec.preview.allowedHosts: []` clears the Project preview allowlist. Omitting `spec.preview` leaves preview settings unmanaged by that apply operation. - **Agent**: resolved within the current project context. If an agent with `name` already exists in the project, `PATCH` it (prompt, labels, annotations). If it does not exist, `POST` to create it. After upsert, post any inbox seed messages not already present. Output (default — one line per resource): @@ -834,6 +889,8 @@ DELETE /api/ambient/v1/projects/{id} delete project GET /api/ambient/v1/projects/{id}/role_bindings RBAC bindings scoped to this project ``` +Project routes SHALL accept and return the Kubernetes-style Project envelope (`apiVersion`, `kind`, `metadata`, `spec`). For project-scoped writes, `metadata.name` SHALL match the `{id}` path parameter when both are present. Mismatches SHALL return `400 Bad Request` with an error body identifying the conflicting fields and provided values; `409 Conflict` is reserved for conflicts with existing stored state. Field-level authorization applies to Project patches: `spec.preview` updates are allowed for `project:owner` and `project:editor`, while other Project fields retain their existing Project update authorization requirements. + ### Agents (Project-Scoped) ``` @@ -1142,9 +1199,9 @@ See [Security Spec — Credential Access via RoleBindings](../security/security. |---|---| | `platform:admin` | Full access to everything | | `platform:viewer` | Read-only across the platform | -| `project:owner` | Full control of a project and all its agents | -| `project:editor` | Create/update Agents, ignite, send messages | -| `project:viewer` | Read-only within a project | +| `project:owner` | Full control of a project, its preview policy, and all its agents | +| `project:editor` | Create/update Agents, ignite, send messages, update Project preview policy | +| `project:viewer` | Read-only within a project, including Project preview policy | | `agent:operator` | Ignite and message a specific Agent | | `agent:editor` | Update prompt and metadata on a specific Agent | | `agent:observer` | Read a specific Agent and its sessions | @@ -1162,7 +1219,7 @@ See [Security Spec — Credential Access via RoleBindings](../security/security. | `platform:admin` | full | full | full | full | full | full | full | full | | `platform:viewer` | read/list | read/list | read/list | — | read/list | read/list | read | read/list | | `project:owner` | full | full | full | full | manage bindings | local-only (own project) | read | project+agent bindings | -| `project:editor` | read | create/update/ignite | read/list | send/read | — | — | read | — | +| `project:editor` | read + update preview policy | create/update/ignite | read/list | send/read | — | — | read | — | | `project:viewer` | read | read/list | read/list | — | — | — | read | — | | `gitops:admin` | — | — | — | — | — | full (any destination) | — | — | | `gitops:viewer` | — | — | — | — | — | read/list | — | — | @@ -1523,7 +1580,7 @@ design rationale (storage, rotation, provider serialization, migration). ## Implementation Coverage Matrix -_Last updated: 2026-04-28. Use this as the authoritative index — click into component source to verify._ +_Last updated: 2026-06-11. Use this as the authoritative index — click into component source to verify._ | Area | API Server | Go SDK | CLI (`acpctl`) | Notes | |---|---|---|---|---| @@ -1547,6 +1604,7 @@ _Last updated: 2026-04-28. Use this as the authoritative index — click into co | **Inbox — mark-read/delete** | ✅ PATCH/DELETE `/inbox/{id}` | ✅ `InboxMessageAPI.{MarkRead,DeleteMessage}` | ✅ `inbox mark-read`, `inbox delete` | | | **Projects — CRUD** | ✅ | ✅ `ProjectAPI.{Get,List,Create,Update,Delete}` | ✅ `get/create/delete project`, `project set/current`, `project update` | | | **Projects — labels/annotations** | ✅ PATCH accepts `labels`/`annotations` | ✅ fields on `Project` type; `ProjectAPI.Update(patch map[string]any)` | ⚠️ no dedicated subcommand | | +| **Projects — preview allowlist** | 🔲 add kube-shaped Project `spec.preview.allowedHosts`, storage migration/backfill, validation, and field-level RBAC for Project preview policy updates | 🔲 Go/Python/TypeScript SDK models/builders for Project `metadata` and `spec.preview` | 🔲 `acpctl apply` support for kube-shaped Project manifests with `spec.preview.allowedHosts` | Persisted project preview URL trust policy | | **RBAC — roles** | ✅ full CRUD | ✅ `RoleAPI` | ✅ `create role`, `get roles`, `get roles `, `delete role` | | | **RBAC — role bindings** | ✅ full CRUD | ✅ `RoleBindingAPI` | ✅ `create role-binding`, `get role-bindings`, `get role-bindings `, `delete role-binding` | | | **RBAC — scoped role_bindings queries** | ✅ agents only; 🔲 users/projects/sessions/credentials | n/a | n/a | `GET /projects/{id}/agents/{agent_id}/role_bindings` implemented; other 4 scoped endpoints not yet | @@ -1559,6 +1617,7 @@ _Last updated: 2026-04-28. Use this as the authoritative index — click into co | **Generic proxy — auth integrations** | ✅ proxy plugin | n/a | n/a | GitHub/GitLab/Google/Jira/Gerrit/CodeRabbit/MCP OAuth flows | | **Generic proxy — cluster/platform** | ✅ proxy plugin | n/a | 🔲 `acpctl version`, `acpctl cluster-info` | cluster-info, version, health, LDAP, OOTB workflows | | **Declarative apply** | n/a | uses SDK | ✅ `apply -f`, `apply -k` | Upsert semantics; supports inbox seeding | +| **Declarative apply — Project spec.preview** | n/a | uses SDK | 🔲 `apply -f project.yaml` | Kube-shaped Project manifest with `metadata.name` and `spec.preview.allowedHosts` | | **Declarative apply — Credential kind** | n/a | uses SDK | ✅ `apply -f credential.yaml` | Global resource; token sourced from env var in YAML | | **Declarative apply — ScheduledSession kind** | n/a | 🔲 | 🔲 | Planned; schedule and agent reference in YAML | | **Applications — CRUD** | 🔲 planned | 🔲 planned | 🔲 planned | GitOps sync binding | diff --git a/specs/security/rbac-enforcement.spec.md b/specs/security/rbac-enforcement.spec.md index b951a129b..20bcbee36 100644 --- a/specs/security/rbac-enforcement.spec.md +++ b/specs/security/rbac-enforcement.spec.md @@ -112,6 +112,60 @@ The response is filtered to resources within their authorized scope. - THEN an empty list is returned with HTTP 200 - AND the response is not 403 +### Requirement: Project Preview Policy Authorization + +Project preview policy SHALL be authorized as a field-level Project update. +Project reads include `spec.preview` for callers who can read the Project. A +caller can create or update Project `spec.preview` when their effective bindings +grant `project:owner` or `project:editor` for the owning Project. A caller with +`project:viewer` can read the policy but cannot modify it. + +Project creation with `spec.preview` SHALL use the same bootstrap contract as +`POST /projects`: any authenticated caller may create a new Project, and the +Project plus caller `project:owner` RoleBinding are committed atomically. Because +the caller becomes owner in that same transaction, the create MAY persist +`spec.preview` when validation succeeds. Anonymous requests SHALL be rejected, +and compatibility paths SHALL NOT silently drop or persist preview trust unless +the Project create and owner binding both commit. Once the Project exists, +`spec.preview` changes require `project:owner` or `project:editor`. + +Project patches SHALL be authorized against every field they modify. A patch that +updates only `spec.preview` MAY be accepted for `project:editor`. A patch that +updates `spec.preview` and owner-only Project fields SHALL require authorization +for all modified fields; partial application is forbidden. + +Project preview policy is cleared by updating `spec.preview.allowedHosts` to +`[]`, not by deleting a separate settings resource. + +#### Scenario: Project editor updates preview policy + +- GIVEN user A has `project:editor` on proj-1 +- WHEN user A calls `PATCH /projects/proj-1` with only `spec.preview.allowedHosts` +- THEN the request is authorized +- AND the Project preview policy for proj-1 is updated + +#### Scenario: Bootstrap Project create can set preview policy atomically + +- GIVEN authenticated user A has no Project binding for project `proj-new` +- WHEN user A calls `POST /projects` with `metadata.name: "proj-new"` and `spec.preview.allowedHosts` +- THEN the Project is created +- AND a `project:owner` RoleBinding for user A is created in the same transaction +- AND the request persists `spec.preview` only if Project creation, owner binding creation, and preview policy validation all succeed + +#### Scenario: Project viewer cannot update preview policy + +- GIVEN user A has `project:viewer` on proj-1 +- WHEN user A calls `PATCH /projects/proj-1` with `spec.preview.allowedHosts` +- THEN the request returns 403 Forbidden +- AND the error body is generic + +#### Scenario: Mixed Project patch requires all field permissions + +- GIVEN user A has `project:editor` on proj-1 +- WHEN user A calls `PATCH /projects/proj-1` with `spec.preview.allowedHosts` and an owner-only Project field +- THEN the request returns 403 Forbidden +- AND neither field is updated + ### Requirement: User Auto-Provisioning The system SHALL automatically create a User record when a JWT-authenticated caller