diff --git a/BOOKMARKS.md b/BOOKMARKS.md index d2db657e3..30586ad1d 100644 --- a/BOOKMARKS.md +++ b/BOOKMARKS.md @@ -25,10 +25,11 @@ Desired state of the system, organized by capability domain. | Spec | Domain | Purpose | |------|--------|---------| -| [Ambient Data Model](specs/sessions/ambient-model.spec.md) | sessions | Platform-wide data model: projects, agents, sessions, credentials, RBAC | +| [Ambient Data Model](specs/api/ambient-model.spec.md) | api | Platform-wide data model: projects, agents, sessions, credentials, RBAC | | [Control Plane](specs/control-plane/control-plane.spec.md) | control-plane | CP architecture, runner structure, K8s provisioning | | [Runner](specs/agents/runner.spec.md) | agents | Runner subprocess lifecycle, bridges, gRPC/HTTP endpoints | | [MCP Server](specs/integrations/mcp-server.spec.md) | integrations | MCP tool definitions, sidecar and public endpoint modes | +| [Security](specs/security/security.spec.md) | security | Identity boundaries, credential authorization, per-session isolation, design decisions | Feature specs remain in numbered directories under `specs/` (e.g., `specs/001-*/spec.md`). diff --git a/docs/internal/design/README.md b/docs/internal/design/README.md index 9c6da9c60..8942ffc53 100644 --- a/docs/internal/design/README.md +++ b/docs/internal/design/README.md @@ -52,6 +52,7 @@ A spec is **complete** when it is unambiguous enough that two engineers reading | `ambient-model.spec.md` | All platform Kinds: Session, Agent, Project, Inbox, Role, RoleBinding, User | | `control-plane.spec.md` | Control plane gRPC protocol, session fan-out, runner contract | | `mcp-server.spec.md` | MCP server tools, annotation state, sidecar transport | +| `security.spec.md` | Identity boundaries, credential authorization, per-session SA isolation | ### Guide files (`*.guide.md`) @@ -65,7 +66,7 @@ Guides are **living documents**. Every time the workflow runs and something is d | Guide | Paired spec | |---|---| -| `workflows/sessions/ambient-model.workflow.md` | `specs/sessions/ambient-model.spec.md` | +| `workflows/sessions/ambient-model.workflow.md` | `specs/api/ambient-model.spec.md` | | `workflows/control-plane/control-plane.workflow.md` | `specs/control-plane/control-plane.spec.md` | | `workflows/integrations/mcp-server.workflow.md` | `specs/integrations/mcp-server.spec.md` | @@ -133,6 +134,6 @@ Every time the loop stops because something was wrong, the documents get better. ## Reading Order for a New Contributor 1. This README — the why -2. `specs/sessions/ambient-model.spec.md` — what the platform is +2. `specs/api/ambient-model.spec.md` — what the platform is 3. `workflows/sessions/ambient-model.workflow.md` — how changes are made 4. The context file for the component you are working on diff --git a/skills/devflow/SKILL.md b/skills/devflow/SKILL.md index f2089d1f0..974a7759f 100644 --- a/skills/devflow/SKILL.md +++ b/skills/devflow/SKILL.md @@ -78,7 +78,7 @@ Every component has a spec file. Load the spec for the component being changed: | Component | Spec | Guide | |-----------|------|-------| -| Data Model / API / CLI / RBAC | `specs/sessions/ambient-model.spec.md` | `workflows/sessions/ambient-model.workflow.md` | +| Data Model / API / CLI / RBAC | `specs/api/ambient-model.spec.md` | `workflows/sessions/ambient-model.workflow.md` | ### 2b. Modify the Spec @@ -354,7 +354,7 @@ But prefer fixing the lint/format issue instead. | `specs/standards/backend/conventions.spec.md` | SDK generation | | `specs/standards/control-plane/conventions.spec.md` | CP fan-out, runner contract | | `specs/standards/frontend/conventions.spec.md` | Frontend build, React Query | -| `specs/sessions/ambient-model.spec.md` | Data model spec (source of truth) | +| `specs/api/ambient-model.spec.md` | Data model spec (source of truth) | | `workflows/sessions/ambient-model.workflow.md` | Implementation workflow (wave-based) | --- diff --git a/specs/sessions/ambient-model.spec.md b/specs/api/ambient-model.spec.md similarity index 86% rename from specs/sessions/ambient-model.spec.md rename to specs/api/ambient-model.spec.md index b17f6a003..cc37268b7 100755 --- a/specs/sessions/ambient-model.spec.md +++ b/specs/api/ambient-model.spec.md @@ -17,7 +17,7 @@ The Ambient API server provides a coordination layer for orchestrating fleets of - **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. - **Inbox** — a persistent message queue on an Agent. Messages survive across sessions and are drained into the start context at the next run. -- **Credential** — a project-scoped secret. Stores a Personal Access Token or equivalent for an external provider (GitHub, GitLab, Jira, Google). Consumed by runners at session start. All agents in the project share the project's credentials automatically. +- **Credential** — a global secret. Stores a Personal Access Token or equivalent for an external provider (GitHub, GitLab, Jira, Google, Vertex AI, Kubeconfig). Consumed by runners at session start. Bound to Projects via RoleBindings — a single Credential can be shared across multiple Projects without duplication. - **RoleBinding** — binds a Resource to a Role at a given scope (`global`, `project`, `agent`, `session`). Ownership and access for all Kinds is expressed through RoleBindings. The stable address of an agent is `{project_name}/{agent_name}`. It holds the inbox and links to the active session. @@ -162,21 +162,20 @@ erDiagram string ID PK string user_id FK string role_id FK - string scope "global | project | agent | session" + string scope "global | project | agent | session | credential" string scope_id "empty for global" time created_at time updated_at time deleted_at } - %% ── Credential (project-scoped PAT/token store) ────────────────────────── + %% ── Credential (global PAT/token store, bound via RoleBindings) ────────── Credential { string ID PK "KSUID" - string project_id FK - string name "human-readable; unique within project" + string name "human-readable; globally unique" string description - string provider "github | gitlab | jira | google" + string provider "github | gitlab | jira | google | vertex | kubeconfig" string token "write-only; stored encrypted" string url "nullable; service instance URL" string email "nullable; required for Jira" @@ -210,7 +209,7 @@ erDiagram Project ||--o{ ProjectSettings : "has" Project ||--o{ Agent : "owns" - Project ||--o{ Credential : "owns" + RoleBinding }o--o| Credential : "grants_access" Project ||--o{ ScheduledSession : "owns" User ||--o{ RoleBinding : "bound_to" @@ -417,17 +416,17 @@ The `acpctl` CLI mirrors the API 1-for-1. Every REST operation has a correspondi | `POST /sessions/{id}/tasks/{task_id}/stop` | `acpctl session tasks stop ` | 🔲 planned | | `GET /sessions/{id}/tasks/{task_id}/output` | `acpctl session tasks output ` | 🔲 planned | -#### Credentials (Project-Scoped) +#### Credentials (Global) | REST API | `acpctl` Command | Status | |---|---|---| -| `GET /projects/{id}/credentials` | `acpctl credential list` | 🔲 planned | -| `GET /projects/{id}/credentials?provider={p}` | `acpctl credential list --provider

` | 🔲 planned | -| `POST /projects/{id}/credentials` | `acpctl credential create --name --provider

--token [--url ] [--email ] [--description ]` | 🔲 planned | -| `GET /projects/{id}/credentials/{cred_id}` | `acpctl credential get ` | 🔲 planned | -| `PATCH /projects/{id}/credentials/{cred_id}` | `acpctl credential update [--token ] [--description ]` | 🔲 planned | -| `DELETE /projects/{id}/credentials/{cred_id}` | `acpctl credential delete --confirm` | 🔲 planned | -| `GET /projects/{id}/credentials/{cred_id}/token` | `acpctl credential token ` | 🔲 planned | +| `GET /credentials` | `acpctl credential list [--provider

]` | 🔲 planned | +| `POST /credentials` | `acpctl credential create --name --provider

--token [--url ] [--email ] [--description ]` | 🔲 planned | +| `GET /credentials/{cred_id}` | `acpctl credential get ` | 🔲 planned | +| `PATCH /credentials/{cred_id}` | `acpctl credential update [--token ] [--description ]` | 🔲 planned | +| `DELETE /credentials/{cred_id}` | `acpctl credential delete --confirm` | 🔲 planned | +| `GET /credentials/{cred_id}/token` | `acpctl credential token ` | 🔲 planned | +| `POST /role_bindings` | `acpctl credential bind --scope project --scope-id ` | 🔲 planned | #### RBAC @@ -459,7 +458,7 @@ The `acpctl` CLI mirrors the API 1-for-1. Every REST operation has a correspondi |---|---| | `Project` | `name`, `description`, `prompt`, `labels`, `annotations` | | `Agent` | `name`, `prompt`, `labels`, `annotations`, `inbox` (seed messages) | -| `Credential` | `name`, `description`, `provider`, `token` (env var reference), `url`, `email`, `labels`, `annotations` — created in current project context | +| `Credential` | `name`, `description`, `provider`, `token` (env var reference), `url`, `email`, `labels`, `annotations` — global resource; use `credential bind` to grant project access | `Agent` resources in `.ambient/teams/` files also carry an `inbox` list of seed messages. On apply, any message in the list is posted to the agent's inbox if an identical message (same `from_name` + `body`) does not already exist there. @@ -733,21 +732,25 @@ GET /api/ambient/v1/sessions/{id}/tasks/{task_id}/output get POST /api/ambient/v1/sessions/{id}/tasks/{task_id}/stop stop background task ``` -### Credentials (Project-Scoped) +### Credentials (Global) + +Credentials are global resources. Access to credentials is granted via RoleBindings — bind a +credential to a Project, Agent, or Session scope to make it available to runners in that scope. ``` -GET /api/ambient/v1/projects/{id}/credentials list credentials in this project -GET /api/ambient/v1/projects/{id}/credentials?provider={provider} filter by provider -POST /api/ambient/v1/projects/{id}/credentials create a credential -GET /api/ambient/v1/projects/{id}/credentials/{cred_id} read credential (metadata only; token never returned) -PATCH /api/ambient/v1/projects/{id}/credentials/{cred_id} update credential -DELETE /api/ambient/v1/projects/{id}/credentials/{cred_id} soft delete -GET /api/ambient/v1/projects/{id}/credentials/{cred_id}/token fetch raw token — restricted to credential:token-reader +GET /api/ambient/v1/credentials list credentials (filtered by caller's RoleBindings) +GET /api/ambient/v1/credentials?provider={provider} filter by provider +POST /api/ambient/v1/credentials create a credential +GET /api/ambient/v1/credentials/{cred_id} read credential (metadata only; token never returned) +PATCH /api/ambient/v1/credentials/{cred_id} update credential +DELETE /api/ambient/v1/credentials/{cred_id} soft delete +GET /api/ambient/v1/credentials/{cred_id}/token fetch raw token — restricted to credential:token-reader ``` -`token` is accepted on `POST` and `PATCH` but **never returned** by the standard read endpoints. It is stored encrypted in the database. The database row is the authoritative store; a future Vault integration can be adopted by pointing the row at a Vault path without changing the API surface. - -`GET /projects/{id}/credentials/{cred_id}/token` is the **only** endpoint that returns the raw token. It is gated by the `credential:token-reader` role — a platform-internal role granted only to runner service accounts at session start. Human users and service accounts do not hold this role by default. Credential CRUD is governed by the caller's project-level role (e.g. `project:owner`, `project:editor`). +`token` is accepted on `POST` and `PATCH` but **never returned** by standard read endpoints. +`GET .../token` is gated by `credential:token-reader`. See +[Security Spec — Token Reader Role Grant](../security/security.spec.md#requirement-token-reader-role-grant) for +runtime authorization semantics. #### Provider Enum @@ -757,6 +760,8 @@ GET /api/ambient/v1/projects/{id}/credentials/{cred_id}/token fetch | `gitlab` | GitLab.com or self-hosted | Personal Access Token | optional; required for self-hosted | — | | `jira` | Jira Cloud (Atlassian) | API Token | required (Atlassian instance URL) | required (used in Basic auth) | | `google` | Google Cloud / Workspace | Service Account JSON serialized to string | — | — | +| `vertex` | Vertex AI (GCP) | GCP service account key | — | — | +| `kubeconfig` | Kubernetes clusters | Kubeconfig file serialized to string | — | — | #### Token Response Shape (Runner) @@ -780,29 +785,21 @@ When a runner fetches a credential, the response payload shape is consistent acr | Scope | Meaning | |---|---| | `global` | Applies across the entire platform | -| `project` | Applies to all resources in a project (Agents, Sessions, Credentials) | +| `project` | Applies to all resources in a project (Agents, Sessions) and Credentials bound to the project | | `agent` | Applies to one Agent and all its sessions | | `session` | Applies to one session run only | +| `credential` | Grants access to a specific Credential (used to bind credentials to projects) | Effective permissions = union of all applicable bindings (global ∪ project ∪ agent ∪ session). No deny rules. -#### Credential Access — Project-Scoped by Default - -Credentials belong to a project. All agents in the project share the project's credentials automatically — no explicit sharing or per-credential RoleBindings needed. At session start, the resolver lists all credentials in the agent's project and returns the matching credential for each requested provider. +#### Credential Access — Global with RoleBinding Grants -This follows the Kubernetes resource model: - -| Ambient | Kubernetes Analogy | Relationship | -|---------|-------------------|-------------| -| Project | Namespace | Isolation boundary, owns child resources | -| Agent | Deployment | Mutable definition, runs workloads | -| Session | Pod | Ephemeral execution, created from Agent | -| Credential | Secret | Project-scoped secret, available to all workloads in the namespace | - -Named patterns: -- **Project Robot Account** — credential created in a project; all agents use it automatically. -- **Multi-project credential** — create the same credential (same PAT) in multiple projects. Each project gets its own Credential record. -- **No credential** — projects without credentials simply run sessions without provider integrations. +Credentials are global resources. Access is granted via RoleBindings — bind a credential to a +Project, Agent, or Session scope. At session start, the resolver lists all credentials the +caller has access to (via RoleBindings) and returns matching credentials for each requested +provider. A single Credential can be shared across multiple Projects without duplication. +See [Security Spec — Credential Access via RoleBindings](../security/security.spec.md#requirement-credential-access-via-rolebindings) for +runtime authorization semantics. ### Built-in Roles @@ -817,7 +814,9 @@ Named patterns: | `agent:editor` | Update prompt and metadata on a specific Agent | | `agent:observer` | Read a specific Agent and its sessions | | `agent:runner` | Minimum viable pod credential: read agent, push messages, send inbox | -| `credential:token-reader` | Fetch the raw token via `GET /projects/{id}/credentials/{cred_id}/token`. Granted only to runner service accounts at session start. Human users do not hold this role. | +| `credential:owner` | Full CRUD on credentials the user created. Bind credentials to projects the user has `project:owner` on. | +| `credential:viewer` | Read metadata (not token) on credentials bound to projects the user has access to. | +| `credential:token-reader` | Fetch the raw token via `GET /credentials/{cred_id}/token`. Granted only to runner service accounts at session start. Human users do not hold this role. | ### Permission Matrix @@ -825,13 +824,15 @@ Named patterns: |---|---|---|---|---|---|---|---| | `platform:admin` | full | full | full | full | full | full | full | | `platform:viewer` | read/list | read/list | read/list | — | read/list | read | read/list | -| `project:owner` | full | full | full | full | full | read | project+agent bindings | -| `project:editor` | read | create/update/ignite | read/list | send/read | create/update/delete | read | — | -| `project:viewer` | read | read/list | read/list | — | read/list | read | — | +| `project:owner` | full | full | full | full | manage bindings | read | project+agent bindings | +| `project:editor` | read | create/update/ignite | read/list | send/read | — | read | — | +| `project:viewer` | read | read/list | read/list | — | — | read | — | | `agent:operator` | — | update/ignite | read/list | send/read | — | — | — | | `agent:editor` | — | update | — | — | — | — | — | | `agent:observer` | — | read | read/list | — | — | — | — | | `agent:runner` | — | read | read | send | — | — | — | +| `credential:owner` | — | — | — | — | create/update/delete + bind | — | — | +| `credential:viewer` | — | — | — | — | read/list (metadata only) | — | — | | `credential:token-reader` | — | — | — | — | token: read | — | — | ### RBAC Endpoints @@ -851,9 +852,13 @@ GET /api/ambient/v1/users/{id}/role_bindings GET /api/ambient/v1/projects/{id}/role_bindings GET /api/ambient/v1/projects/{id}/agents/{agent_id}/role_bindings GET /api/ambient/v1/sessions/{id}/role_bindings +GET /api/ambient/v1/credentials/{cred_id}/role_bindings ``` -The `credential:token-reader` role is granted to the runner service account by the platform at session start. It is never granted via user-facing `POST /role_bindings`. It is a platform-internal binding managed by the operator. Credential CRUD is governed by the caller's project-level role — `project:owner` and `project:editor` can create/update/delete credentials; `project:viewer` can list/read metadata. +The `credential:token-reader` role is platform-internal. Credential CRUD is governed by +RoleBindings with `credential` scope. See +[Security Spec — Token Reader Role Grant](../security/security.spec.md#requirement-token-reader-role-grant) for +grant semantics and runtime authorization rules. --- @@ -876,9 +881,14 @@ GET /api/ambient/v1/projects/{id}/scheduled-sessions/{sched_id}/runs ### Generic Proxy -All backend paths not mapped to a native `/api/ambient/v1/...` endpoint are forwarded verbatim to the backend service. The API server authenticates the caller, injects service credentials, then proxies the request — preserving method, path, query string, body, and response status. +All backend paths not mapped to a native `/api/ambient/v1/...` endpoint are forwarded +verbatim to the backend service. See +[Security Spec — Proxy Authentication](../security/security.spec.md#requirement-proxy-authentication) for +authentication and credential injection behavior. -This allows SDK and CLI clients to reach the full backend surface through a single authenticated endpoint without requiring every backend route to be natively implemented in the API server. Routes listed here are candidates for future native spec entries. +This allows SDK and CLI clients to reach the full backend surface through a single +authenticated endpoint without requiring every backend route to be natively implemented in +the API server. Routes listed here are candidates for future native spec entries. #### Project Configuration (proxied) @@ -955,7 +965,7 @@ Every first-class Kind carries two JSONB columns: | `labels` | Queryable key/value tags. Use for filtering, grouping, and selection. | `{"env": "prod", "team": "platform", "tier": "critical"}` | | `annotations` | Freeform key/value metadata. Use for tooling notes, human remarks, external references. | `{"last-reviewed": "2026-03-21", "jira": "PLAT-123", "owner-slack": "@mturansk"}` | -**Kinds with `labels` + `annotations`:** User, Project, Agent, Session, Credential +**Kinds with `labels` + `annotations`:** User, Project, Agent, Session, Credential (global) **Kinds without:** Inbox (ephemeral message queue), SessionMessage (append-only event stream), Role, RoleBinding (RBAC internals — structured by design) @@ -1080,10 +1090,6 @@ This structure means you can define and compose bespoke agent suites — entire | Agent is project-scoped, not global | Simplicity. An agent's identity and prompt are contextual to the project it serves. No indirection via a global registry. | | Agent.prompt is mutable | Prompt editing is a routine operational task. RBAC controls who can change it. No versioning overhead. | | Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `scope_id=agent_id`). Enables multi-owner and delegated ownership consistently across all Kinds. | -| Credential is project-scoped, like a Kubernetes Secret | Credentials live inside a project. All agents in the project share them automatically. Duplication across projects is intentional and explicit — each project gets its own Credential record, same as Kubernetes Secrets in different Namespaces. | -| Credential token is write-only | Prevents token exfiltration via the standard REST API. Raw token only surfaced to runners via the runtime credentials path, not to end users. | -| Four-scope RBAC (`global`, `project`, `agent`, `session`) | Credential access is implicit via project membership — no dedicated `credential` scope needed. Simpler model with fewer moving parts. | -| Credential CRUD governed by project roles | `project:owner` and `project:editor` can manage credentials. No separate `credential:owner` / `credential:reader` roles — project roles cover it. | | One active Session per Agent | Avoids concurrent conflicting runs; start is idempotent | | Inbox on Agent, not Session | Messages persist across re-ignitions; addressed to the agent, not the run | | Inbox drained at start | Unread messages become part of the start context; session picks up where things left off | @@ -1091,12 +1097,12 @@ This structure means you can define and compose bespoke agent suites — entire | Sessions created only via start | Sessions are run artifacts; direct `POST /sessions` does not exist | | Every layer carries a `prompt` | Project.prompt = workspace context; Agent.prompt = who the agent is; Session.prompt = what this run does; Inbox = prior requests. Pokes roll downhill. | | `SessionMessage` is append-only | Canonical record of the LLM conversation; never edited or deleted | -| `agent:editor` role | Allows prompt updates without full operator access | -| `agent:runner` role | Pods get minimum viable credential: read agent definition, push session messages, send inbox | -| Union-only permissions | No deny rules — simpler mental model for fleet operators | | CLI mirrors API 1-for-1 | Every endpoint has a corresponding command; status tracked explicitly | | This document is the spec | A reconciler will compare the spec (this doc) against code status and surface gaps | | `labels` / `annotations` are JSONB, not strings | Enables GIN-indexed key/value queries (`@>` operator) without joins; every row carries its own metadata without a separate EAV table. `labels` = queryable tags; `annotations` = freeform notes. Applied to first-class Kinds: User, Project, Agent, Session. Not applied to Inbox, SessionMessage, Role/RoleBinding. | +| Credential is global, not project-scoped | Eliminates duplication when the same PAT is used across multiple Projects. Access controlled via RoleBindings with `credential` scope. A single Credential can be shared across Projects without creating copies. | + +Security and credential design decisions (RBAC scoping, write-only tokens, role catalog rationale) are in the [Security Spec — Design Decisions](../security/security.spec.md#design-decisions). --- @@ -1112,7 +1118,13 @@ acpctl credential create --name my-gitlab-pat --provider gitlab \ echo "$GITLAB_PAT" | acpctl credential create --name my-gitlab-pat --provider gitlab \ --token @- --url https://gitlab.myco.com -# List credentials +# Bind credential to a project (grants access to all agents in the project) +acpctl credential bind my-gitlab-pat --scope project --scope-id my-project + +# Bind the same credential to another project (no duplication) +acpctl credential bind my-gitlab-pat --scope project --scope-id other-project + +# List credentials (filtered by caller's RoleBindings) acpctl credential list # NAME PROVIDER URL CREATED # my-gitlab-pat gitlab https://gitlab.myco.com 2026-03-31 @@ -1136,24 +1148,23 @@ spec: ``` ```sh -acpctl project my-project acpctl apply -f credential.yaml -# credential/platform-gitlab-pat created (in project my-project) +# credential/platform-gitlab-pat created + +# Then bind to the desired project +acpctl credential bind platform-gitlab-pat --scope project --scope-id my-project ``` --- ## Design Decisions — Credential -| Decision | Rationale | -|----------|-----------| -| Credential is project-scoped | Follows the Kubernetes Secret-in-Namespace pattern. All agents in the project share credentials implicitly. No RoleBindings needed for sharing within a project. | -| Token stored in database, encrypted at rest | Single authoritative store. A future Vault integration can be adopted by pointing the DB row at a Vault path without changing the API surface. | -| `google` token serialized as a string | Service Account JSON is serialized into the single `token` field. Keeps the schema uniform across all providers. | -| No validation on creation | First-use error is acceptable. Avoids a network call to the provider at creation time and the failure modes that come with it. | -| Credential rotation is user-managed | Users update the token via `PATCH` or `acpctl credential update`. No platform-side rotation or expiry tracking. | -| No migration utility for existing K8s Secrets | Users re-enter credentials via the new API. The old Secret-based path is removed when the new API is live. | -| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs for each project, not share their personal credentials. Each project gets its own Credential records. | +Credentials are global resources, not project-scoped. This eliminates duplication when the same +PAT is used across multiple Projects. Access is controlled via RoleBindings — bind a credential +to a project scope to grant access to all agents in that project. + +See the [Security Spec — Design Decisions](../security/security.spec.md#design-decisions) for credential +design rationale (storage, rotation, provider serialization, migration). --- @@ -1184,8 +1195,8 @@ _Last updated: 2026-04-28. Use this as the authoritative index — click into co | **Projects — labels/annotations** | ✅ PATCH accepts `labels`/`annotations` | ✅ fields on `Project` type; `ProjectAPI.Update(patch map[string]any)` | ⚠️ no dedicated subcommand | | | **RBAC — roles** | ✅ | ✅ `RoleAPI` | ✅ `create role` only; list/get not exposed | | | **RBAC — role bindings** | ✅ | ✅ `RoleBindingAPI` | ✅ `create role-binding` only; list/delete not exposed | | -| **Credentials — CRUD** | 🔲 | 🔲 | 🔲 `credential list/get/create/update/delete` | Project-scoped; not yet implemented | -| **Credentials — token fetch (runner)** | 🔲 `GET /projects/{id}/credentials/{cred_id}/token` | 🔲 | n/a | Gated by `credential:token-reader`; granted to runner SA by operator | +| **Credentials — CRUD** | 🔲 | 🔲 | 🔲 `credential list/get/create/update/delete` | Global; bound to Projects via RoleBindings; not yet implemented | +| **Credentials — token fetch (runner)** | 🔲 `GET /credentials/{cred_id}/token` | 🔲 | n/a | Gated by `credential:token-reader`; granted to runner SA by operator | | **ScheduledSessions — CRUD** | ✅ scheduledSessions plugin | ✅ `ScheduledSessionAPI.{List,Get,Create,Update,Delete,GetByName}` | ✅ `scheduled-session list/get/create/update/delete` | | | **ScheduledSessions — lifecycle** | ✅ suspend/resume/trigger/runs handlers | ✅ `ScheduledSessionAPI.{Suspend,Resume,Trigger,Runs}` | ✅ `scheduled-session suspend/resume/trigger/runs` | | | **Generic proxy — project config** | ✅ proxy plugin (`plugins/proxy`); forwards non-`/api/ambient/` paths to `BACKEND_URL` | n/a | 🔲 raw HTTP fallback | Permissions, keys, MCP servers, secrets, feature flags | @@ -1193,7 +1204,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 — Credential kind** | n/a | 🔲 | 🔲 | Planned; token sourced from env var in YAML | +| **Declarative apply — Credential kind** | n/a | 🔲 | 🔲 | Planned; global resource; token sourced from env var in YAML | | **Declarative apply — ScheduledSession kind** | n/a | 🔲 | 🔲 | Planned; schedule and agent reference in YAML | ### Labels/Annotations — SDK Ergonomics Gap @@ -1227,21 +1238,16 @@ All Kinds with `labels`/`annotations` store them as JSON strings in the DB (`*st AGENT_ID=$(acpctl agent list --project-id test-cred-1 -o json | python3 -c "import sys,json; print(json.load(sys.stdin)['items'][0]['id'])") echo "AGENT_ID=$AGENT_ID" - # 3. Credential (apply from file — only working path) + # 3. Credential (global resource) printf 'kind: Credential\nname: github-pat-test\nprovider: github\ntoken: %s\ndescription: test\n' \ "$(cat ~/projects/secrets/github.ambient-pat.token)" > /tmp/cred.yaml acpctl apply -f /tmp/cred.yaml && rm /tmp/cred.yaml - CRED_ID=$(acpctl get credentials -o json | python3 -c "import sys,json; print(next(i['id'] for i in json.load(sys.stdin)['items'] if i['name']=='github-pat-test'))") - echo "CRED_ID=$CRED_ID" - - # 4. Role binding - ROLE_ID=$(acpctl get roles -o json | python3 -c "import sys,json; print(next(i['id'] for i in json.load(sys.stdin)['items'] if i['name']=='credential:token-reader'))") - MY_USER=$(acpctl whoami | awk '/^User:/{print $2}') - echo "ROLE_ID=$ROLE_ID MY_USER=$MY_USER" + # 4. Bind credential to project + acpctl credential bind github-pat-test --scope project --scope-id test-cred-1 - acpctl create role-binding --user-id "$MY_USER" --role-id "$ROLE_ID" \ - --scope agent --scope-id "$AGENT_ID" + CRED_ID=$(acpctl credential list -o json | python3 -c "import sys,json; print(next(i['id'] for i in json.load(sys.stdin)['items'] if i['name']=='github-pat-test'))") + echo "CRED_ID=$CRED_ID" # 5. Start session SESSION_ID=$(acpctl start github-agent --project-id test-cred-1 \ diff --git a/specs/index.spec.md b/specs/index.spec.md index a20c3acbf..44be1c90e 100644 --- a/specs/index.spec.md +++ b/specs/index.spec.md @@ -79,9 +79,11 @@ specs/ | Domain | Covers | |--------|--------| -| `sessions/` | Lifecycle, initialization, status, messages, events, data model | +| `api/` | Platform data model: projects, agents, sessions, credentials, RBAC, CLI | +| `sessions/` | Lifecycle, initialization, status, messages, events | | `agents/` | Agent model, runtime registry, prompts, runners | | `control-plane/` | Reconciliation, operator, scheduling | | `frontend/` | UI rendering, session views, markdown, navigation | | `integrations/` | MCP, Gerrit, external services | +| `security/` | Identity boundaries, credential authorization, per-session isolation | | `standards/` | Cross-cutting engineering constraints by component | diff --git a/specs/security/security.spec.md b/specs/security/security.spec.md new file mode 100644 index 000000000..2e908a13b --- /dev/null +++ b/specs/security/security.spec.md @@ -0,0 +1,407 @@ +# Security Specification + +## Purpose + +The Ambient Code Platform runs agentic AI sessions inside Kubernetes. Each session is a +pod that executes an LLM-powered runner, accesses external services (Vertex AI, GitHub, +Jira), and stores results via the API server. + +This specification defines who can do what. Six identity boundaries govern the platform: +an SRE-managed Control Plane that reconciles state across Projects, per-session +ServiceAccounts that isolate runner pods from each other, user SSO tokens that scope +runner authorization to the creating user, global credentials bound to Projects via +RoleBindings (Vertex AI, GitHub/GitLab/Jira/etc.), and a Project-scoped build agent +SA for OpenShift CI/CD workflows. + +**Critical gap:** all runner sessions in a Project share a ServiceAccount with unscoped +Secret access. Any session can read another session's runner tokens. This spec closes +that gap with per-session Roles restricted by `resourceNames`. + +**Terminology:** Each Project is realized as a single Kubernetes namespace. This spec +uses "Project" for the Ambient isolation boundary and "namespace" only when referring +to the Kubernetes primitive directly. + +## Accounts and Tokens + +### Control Plane Identities + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| `ambient-control-plane` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Long-lived (token Secret) | Watches API server, reconciles sessions/projects to K8s, writes status back | +| `ambient-control-plane` OIDC token | OAuth2 client_credentials | SRE | API server | Auto-refreshed (30s buffer) | CP authenticates to API server for session/credential CRUD | + +### Platform Service Identities + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| `backend-api` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Backend API: manages CRs, mints session tokens, validates user tokens | +| `frontend` | K8s ServiceAccount | SRE | Cluster (ClusterRole) | Pod lifetime | Frontend: TokenReview and SubjectAccessReview only | + +### Session Runtime Identities + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| `ambient-session-` | K8s ServiceAccount | SRE (created by operator) | Project (Role) | Session lifetime | Per-session runner identity; scoped to own secrets and session CR | +| Runner bot token | K8s TokenRequest | SRE (minted by operator) | Session-specific | Mounted + refreshed by kubelet | Runner authenticates to K8s API and API server for status/credential ops | +| Runner AGUI token | UUID | SRE (generated per session) | Session-specific | Session lifetime | Authenticates inbound AG-UI requests to runner pod (bearer validation) | +| CP RSA-encrypted session token | RSA + OIDC exchange | SRE | Session-specific | On-demand (per request) | Runner fetches API token from CP `/token` endpoint using encrypted session ID | + +### User Authentication + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| User SSO token | OIDC (Red Hat SSO) | User | User's RBAC scope | SSO session TTL | User authenticates to frontend/backend; propagated as `caller_token` to runner | + +### Credentials (Global, Bound via RoleBindings) + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| `Credential(provider=vertex)` | GCP service account key | User | Global (bound to Projects via RoleBindings) | Until rotated | Vertex AI LLM inference; stored in API server, materialized as K8s Secret per Project | +| `Credential(provider=github)` | PAT or GitHub App token | User | Global (bound to Projects via RoleBindings) | Until rotated | Git operations; fetched at runtime, written to ephemeral storage, cleared per turn | +| `Credential(provider=gitlab)` | PAT | User | Global (bound to Projects via RoleBindings) | Until rotated | GitLab repository access | +| `Credential(provider=jira)` | API token | User | Global (bound to Projects via RoleBindings) | Until rotated | Jira issue tracking integration | +| `Credential(provider=google)` | OAuth2 token | User | Global (bound to Projects via RoleBindings) | Until rotated | Google Workspace integrations | +| `Credential(provider=kubeconfig)` | Kubeconfig | User | Global (bound to Projects via RoleBindings) | Until rotated | Cross-cluster Kubernetes operations | + +### Build Agent Identity (Proposed) + +| Identity | Type | Owner | Scope | Lifetime | Purpose | +|----------|------|-------|-------|----------|---------| +| `ambient-agent` | K8s ServiceAccount | SRE | Single Project (Role) | Long-lived | OpenShift build agent: BuildConfig, ImageStream, deploy within one Project | + +## Requirements + +### Requirement: Control Plane Identity Isolation + +The Control Plane SA SHALL be the only identity that spans Projects. Runner containers +MUST NOT mount or inherit the CP token. The CP SHALL create per-session SAs with scoped +tokens rather than sharing its own. + +#### Scenario: Runner cannot access CP token + +- GIVEN a runner pod in a Project +- WHEN the pod enumerates available ServiceAccount tokens +- THEN no CP token is present in the pod's filesystem or environment + +#### Scenario: CP reconciles across Projects + +- GIVEN the Control Plane is running +- WHEN a new session is created in any Project +- THEN the CP reconciles the session to Kubernetes resources in that Project's namespace +- AND uses its own cluster-scoped SA for cross-Project operations + +### Requirement: Vertex AI Credential Scoping + +Vertex AI credentials SHALL be global resources bound to Projects via RoleBindings. +The credential token MUST be write-only in the API (never returned in GET responses). +The runner SHALL fetch credentials at runtime via authenticated API calls. + +#### Scenario: Credential write-only enforcement + +- GIVEN a user creates a `Credential(provider=vertex)` and binds it to a Project +- WHEN another user calls `GET /credentials/{id}` +- THEN the response contains metadata but the `token` field is absent + +#### Scenario: Credential materialization + +- GIVEN a Project has a Vertex credential +- WHEN a runner pod is provisioned in that Project +- THEN the CP resolves the credential and writes the service account key into a K8s Secret +- AND the runner pod mounts this secret for `GOOGLE_APPLICATION_CREDENTIALS` + +#### Scenario: Credential rotation + +- GIVEN a Vertex credential is updated via the API +- WHEN the next session is provisioned in that Project +- THEN the CP re-resolves the credential and writes the updated key + +### Requirement: User Token Propagation + +The runner SHALL operate with the creating user's authorization context. The runner +MUST NOT access resources the creating user cannot access. + +#### Scenario: User SSO token passed to runner + +- GIVEN a user authenticates via SSO and creates a session +- WHEN a human interacts via AG-UI +- THEN their bearer token is passed through as `caller_token` +- AND the runner uses this token for API calls, falling back to the bot token only if expired + +#### Scenario: Cross-user credential access blocked + +- GIVEN user A creates a session +- WHEN user B's token is used to access user A's session credentials +- THEN the backend returns 403 Forbidden + +#### Scenario: Bot token scoped to session + +- GIVEN a runner pod with a bot token +- WHEN the bot token is used for API calls +- THEN access is restricted to the specific session's resources within the Project + +### Requirement: Integration Credential Isolation + +Integration credentials SHALL be global resources. Access SHALL be controlled via +RoleBindings — a credential is only accessible to runners in Projects it has been +bound to. Credential tokens SHALL be write-only in the API. + +#### Scenario: Unbound credential access blocked + +- GIVEN a GitHub credential exists but is not bound to Project B +- WHEN a runner in Project B attempts to fetch that credential +- THEN the request is denied + +#### Scenario: Runner fetches credential at runtime + +- GIVEN a GitHub credential is bound to a Project +- WHEN a runner pod in that Project requests the credential token +- THEN the token is returned via the restricted endpoint +- AND the runner writes it to ephemeral storage +- AND the credential is cleared after each turn + +#### Scenario: Token fetch restricted to cluster-internal callers + +- GIVEN a valid credential token request +- WHEN the caller is not cluster-internal +- THEN the request is denied to prevent token exfiltration + +### Requirement: MCP Credential Lifecycle + +MCP server credentials SHALL follow the same RoleBinding-scoped access model as other +integration credentials. The Control Plane SHOULD support dynamic credential updates +without requiring full pod restarts. + +#### Scenario: Sidecar mode credential update + +- GIVEN an MCP sidecar running alongside a runner +- WHEN the Project's MCP credentials are updated +- THEN the CP triggers a pod rolling restart with updated environment + +#### Scenario: Pod mode credential update (proposed) + +- GIVEN an MCP server running as an independent Pod +- WHEN the Project's MCP credentials are updated +- THEN the CP updates the MCP Pod configuration without affecting the runner + +### Requirement: Per-Session Service Account Isolation + +Each session MUST have a ServiceAccount that can only access its own resources. +Sessions MUST NOT be able to read other sessions' runner tokens from K8s Secrets. + +#### Scenario: Session cannot read another session's secrets + +- GIVEN Session A and Session B running in the same Project +- WHEN Session A attempts to read Session B's runner token Secret +- THEN the request is denied by RBAC (`resourceNames` restriction) + +#### Scenario: Per-session Role restricts Secret access + +- GIVEN a new session is created +- WHEN the operator provisions the session SA +- THEN the Role restricts Secret access to `ambient-runner-token-` and shared read-only secrets +- AND AgenticSession access is restricted to `` + +#### Scenario: NetworkPolicy isolates session pods + +- GIVEN a session pod is running +- WHEN another session's pod attempts to connect +- THEN the NetworkPolicy blocks the traffic +- AND only the session's own pods and the Control Plane can communicate + +#### Scenario: Shared secrets mounted read-only + +- GIVEN Project-wide secrets exist (e.g., Vertex credentials) +- WHEN a session pod needs access +- THEN the secrets are mounted as read-only volumes +- AND they are not accessible via the K8s API from the session SA + +### Requirement: Per-Session SA Target State + +Each session SA SHALL be restricted to the following resources: + +| Resource | Allowed Names | Verbs | +|----------|--------------|-------| +| Secrets | `ambient-runner-token-`, shared secrets (read-only mount) | get | +| Pods | Labeled `ambient-code/session=` | get, list, watch | +| AgenticSessions | `` | get, update (status only) | +| SelfSubjectAccessReviews | (any) | create | + +### Requirement: Build Agent SA Scoping (OpenShift) + +The build agent SA SHALL be bound to a single Project. It MUST NOT access other +Projects, nodes, or cluster-scoped resources. It MUST NOT create or modify CRDs, +ClusterRoles, or ClusterRoleBindings. + +#### Scenario: Build agent deploys within Project + +- GIVEN a build agent SA bound to a Project +- WHEN the agent triggers a BuildConfig +- THEN the build runs within that Project's namespace +- AND images are pushed to the internal registry via `system:image-builder` + +#### Scenario: Build agent cannot escalate + +- GIVEN a build agent SA bound to a Project +- WHEN the agent attempts to create a ClusterRole +- THEN the request is denied + +### Requirement: Build Agent Permissions + +The build agent SA SHALL have the following permissions within its Project: + +| API Group | Resources | Verbs | +|-----------|-----------|-------| +| `build.openshift.io` | `buildconfigs`, `buildconfigs/instantiate`, `builds`, `builds/log` | get, list, watch, create, update, patch, delete | +| `image.openshift.io` | `imagestreams`, `imagestreamtags`, `imagestreamimages` | get, list, watch, create, update, patch, delete | +| `apps` | `deployments`, `statefulsets`, `replicasets` | get, list, watch, create, update, patch, delete | +| `""` (core) | `pods`, `pods/log`, `services`, `configmaps`, `secrets`, `persistentvolumeclaims`, `serviceaccounts`, `events` | get, list, watch, create, update, patch, delete | +| `route.openshift.io` | `routes` | get, list, watch, create, update, patch, delete | +| `batch` | `jobs`, `cronjobs` | get, list, watch, create, update, patch, delete | +| `networking.k8s.io` | `networkpolicies` | get, list, watch, create, update, patch, delete | +| `rbac.authorization.k8s.io` | `roles`, `rolebindings` | get, list, watch, create, update, patch, delete | + +Additionally requires the built-in `system:image-builder` role for internal registry push access. + +## Credential Authorization Model + +This section defines how credentials are authorized at runtime. For credential Kind schemas, +API endpoints, and provider enum definitions, see the +[Ambient Data Model Spec](../api/ambient-model.spec.md). + +### Requirement: Credential Access via RoleBindings + +Credentials SHALL be global resources. Access SHALL be granted via RoleBindings with +`credential` scope — bind a credential to a Project to make it available to all agents in +that Project. At session start, the resolver SHALL list all credentials the session's +Project has access to (via RoleBindings) and return the matching credential for each +requested provider. + +This follows the Kubernetes resource model: + +| Ambient | Kubernetes Analogy | Relationship | +|---------|-------------------|--------------| +| Project | Namespace | Isolation boundary | +| Agent | Deployment | Mutable definition, runs workloads | +| Session | Pod | Ephemeral execution, created from Agent | +| Credential | Secret (cross-namespace) | Global resource, bound to Projects via RoleBindings | + +Named patterns: +- **Project Robot Account** — credential created globally and bound to a Project; all agents in the Project use it automatically. +- **Multi-Project credential** — bind the same credential to multiple Projects via separate RoleBindings. No duplication of the Credential record. +- **No credential** — Projects without credential bindings run sessions without provider integrations. + +#### Scenario: All agents access bound credentials + +- GIVEN a GitHub credential is bound to a Project via RoleBinding +- WHEN any agent in that Project starts a session +- THEN the runner can fetch the GitHub credential token + +#### Scenario: Unbound credential not accessible + +- GIVEN Project A and Project B exist, and a credential is bound only to Project A +- WHEN an agent in Project B requests credentials +- THEN only credentials bound to Project B are returned + +### Requirement: Token Reader Role Grant + +The `credential:token-reader` role SHALL be granted to the runner service account by the +platform at session start. It MUST NOT be granted via user-facing `POST /role_bindings`. +It is a platform-internal binding managed by the operator. + +Credential CRUD SHALL be governed by the `credential:owner` role. Users with +`credential:owner` can create, update, and delete credentials they own and bind them +to Projects where they hold `project:owner`. Users with `credential:viewer` can read +metadata (not tokens) on credentials bound to Projects they have access to. + +#### Scenario: Runner can fetch token + +- GIVEN a runner SA with `credential:token-reader` bound at session start +- WHEN the runner calls `GET /credentials/{cred_id}/token` +- THEN the raw token is returned + +#### Scenario: Human user cannot fetch token + +- GIVEN a human user without `credential:token-reader` +- WHEN they call `GET /credentials/{cred_id}/token` +- THEN the request is denied with 403 + +### Requirement: Proxy Authentication + +All backend paths not mapped to a native `/api/ambient/v1/...` endpoint SHALL be forwarded +verbatim to the backend service. The API server SHALL authenticate the caller, inject +service credentials, then proxy the request — preserving method, path, query string, body, +and response status. + +Runner-internal endpoints (called by runner pods at runtime): +- `POST /api/projects/{p}/agentic-sessions/{s}/github/token` — get a GitHub token for a session +- `GET /api/projects/{p}/agentic-sessions/{s}/credentials/{provider}` — fetch credential by provider +- `POST /api/projects/{p}/agentic-sessions/{s}/runner/feedback` — runner feedback + +These endpoints MUST validate the caller is cluster-internal to prevent token exfiltration. + +#### Scenario: External caller blocked from runner endpoints + +- GIVEN an external client with a valid token +- WHEN they call a runner-internal endpoint +- THEN the request is denied because the caller is not cluster-internal + +## Security Boundary Summary + +``` ++------------------------------------------------------------------+ +| Cluster | +| | +| +---------------------------+ +-----------------------------+ | +| | ambient-code (platform) | | Project A | | +| | | | | | +| | [Control Plane] | | [Session A Pod] | | +| | SA: ambient-control-plane| | SA: ambient-session-aaa | | +| | - watches API server | | - own secrets only | | +| | - reconciles to K8s | | - own session CR only | | +| | - writes status back | | - user's SSO token | | +| | | | - bound vertex cred | | +| | [API Server] | | +------------------+ | | +| | SA: (pod identity) | | | MCP sidecar/pod | | | +| | - PostgreSQL backend | | | - integration | | | +| | - Credential store | | | - creds from API | | | +| | - RBAC enforcement | | +------------------+ | | +| | | | | | +| | [Backend] | | [Session B Pod] | | +| | SA: backend-api | | SA: ambient-session-bbb | | +| | - user token passthrough| | - ISOLATED from A | | +| | - credential RBAC | | - own secrets only | | +| +---------------------------+ +-----------------------------+ | +| | ++------------------------------------------------------------------+ +``` + +**Key Invariants:** +1. No runner session can access another session's secrets or tokens +2. No runner session can operate beyond the user's own authorization scope +3. Integration credentials are global, bound to Projects via RoleBindings, and fetched at runtime, never baked in +4. The Control Plane SA is the only identity that spans Projects +5. MCP lifecycle (sidecar vs. pod) is determined by operational requirements, not security compromise + +## Design Decisions + +| Decision | Rationale | +|----------|-----------| +| Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `scope_id=agent_id`). Enables multi-owner and delegated ownership consistently across all Kinds. | +| Credential is global, bound via RoleBindings | Credentials are global resources. Access is granted by binding a credential to a Project via a RoleBinding with `credential` scope. A single credential can be shared across multiple Projects without duplication. | +| Credential token is write-only | Prevents token exfiltration via the standard REST API. Raw token only surfaced to runners via the runtime credentials path, not to end users. | +| Five-scope RBAC (`global`, `project`, `agent`, `session`, `credential`) | Credential access is explicit via RoleBindings with `credential` scope. Enables cross-project sharing without credential duplication. | +| Credential CRUD governed by credential roles | `credential:owner` manages CRUD and bindings. `credential:viewer` reads metadata. Self-service: users create their own credentials without admin intervention. | +| `agent:runner` role | Pods get minimum viable credential: read agent definition, push session messages, send inbox. | +| Union-only permissions | No deny rules — simpler mental model for fleet operators. | +| Token stored in database, encrypted at rest | Single authoritative store. A future Vault integration can be adopted by pointing the DB row at a Vault path without changing the API surface. | +| `google` token serialized as a string | Service Account JSON is serialized into the single `token` field. Keeps the schema uniform across all providers. | +| No validation on creation | First-use error is acceptable. Avoids a network call to the provider at creation time and the failure modes that come with it. | +| Credential rotation is user-managed | Users update the token via `PATCH` or `acpctl credential update`. No platform-side rotation or expiry tracking. | +| No migration utility for existing K8s Secrets | Users re-enter credentials via the new API. The old Secret-based path is removed when the new API is live. | +| Dedicated tokens, not personal credentials | Users are expected to create dedicated Robot Accounts or PATs, not share their personal credentials. A single credential can be bound to multiple Projects. | + +## References + +- [Ambient Data Model Spec](../api/ambient-model.spec.md) — Credential/RBAC schemas, endpoints, provider enum +- [Security Standards](../standards/security/security.spec.md) +- [User Token Authentication ADR](../../docs/internal/adr/0002-user-token-authentication.md) diff --git a/workflows/integrations/mcp-server.workflow.md b/workflows/integrations/mcp-server.workflow.md index 2bb747b0c..83fbc9ace 100644 --- a/workflows/integrations/mcp-server.workflow.md +++ b/workflows/integrations/mcp-server.workflow.md @@ -461,5 +461,5 @@ Lessons learned: - Full per-tool schemas, return shapes, and error tables: `specs/integrations/mcp-server.spec.md` - Annotation key conventions and fleet state protocol: `docs/internal/proposals/agent-fleet-state-schema.md` - Agent visual language (how purple SEND/WAIT blocks map to MCP tools): `docs/internal/proposals/agent-script-visual-language.md` -- Platform data model: `specs/sessions/ambient-model.spec.md` +- Platform data model: `specs/api/ambient-model.spec.md` - Component pipeline and wave pattern: `workflows/sessions/ambient-model.workflow.md` diff --git a/workflows/sessions/ambient-model.workflow.md b/workflows/sessions/ambient-model.workflow.md index e50ad437d..4249cd9d9 100755 --- a/workflows/sessions/ambient-model.workflow.md +++ b/workflows/sessions/ambient-model.workflow.md @@ -64,7 +64,7 @@ Checklist: ### Step 2 — Read the Spec -Read `specs/sessions/ambient-model.spec.md` in full. +Read `specs/api/ambient-model.spec.md` in full. Extract and hold in working memory: @@ -453,7 +453,7 @@ The old Gin/K8s backend (`components/backend/`) is covered by `.claude/context/b | Artifact | Location | Owner | | --------------------- | ---------------------------------------------------- | ----------------- | -| Spec | `specs/sessions/ambient-model.spec.md` | Human / consensus | +| Spec | `specs/api/ambient-model.spec.md` | Human / consensus | | This workflow | `workflows/sessions/ambient-model.workflow.md` | Updated each run | | OpenAPI spec | `components/ambient-api-server/openapi/openapi.yaml` | API wave | | Generated SDK | `components/ambient-sdk/go-sdk/` | SDK wave |