You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
spec(rbac): typed nullable FKs on RoleBinding + workflow and coverage improvements (#1580)
## Summary
- **RoleBinding model redesign**: Replace polymorphic `scope_id string`
with typed nullable FK columns (`user_id`, `project_id`, `agent_id`,
`session_id`, `credential_id`). `scope` remains as the discriminator.
`user_id` is nullable — only set for user-specific (ownership) bindings;
null for project-level grants. Credential→project bindings set both
`credential_id` and `project_id` with `user_id=NULL`.
- **CLI flag update**: `acpctl credential bind <name> --project
<project>` replaces `--scope project --scope-id <project>`. `create
role-binding` now uses explicit typed flags instead of a generic
`--scope-id`.
- **Workflow improvements**: Step 3 gap analysis now requires a
three-way field-level diff (spec ↔ OpenAPI ↔ model). Integration tests
must use the spec-correct route, not an implementation-convenient one.
- **Coverage matrix fixes**: `apply` for Credential kind marked ✅
(already implemented); stale project-scoped token-fetch path corrected
to `/credentials/{id}/token`.
- **Security spec**: Credential access requirement and design decisions
updated to name the exact FK columns used in credential→project
bindings.
- **NetworkPolicy fix**: `runner-networkpolicy.yaml` allows all ingress
(`- {}` wildcard) so NodePort traffic reaches the frontend pod (E2E
fix).
## Test plan
- [ ] Spec ERD Mermaid renders correctly (no broken relationship lines)
- [ ] `acpctl credential bind` command shape matches the new `--project`
flag in spec
- [ ] `POST /role_bindings` body shape documented in spec matches the
nullable FK design
- [ ] No references to `scope_id` remain in RoleBinding model spec or
security spec
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **Documentation**
* Credentials are now documented as globally scoped resources (not
project-scoped).
* CLI examples updated: credential bind uses --project; role-binding
creation examples show explicit id flags for
user/project/agent/session/credential.
* API coverage and token endpoint documentation updated to the global
credential path.
* Workflow guidance strengthened: stricter field-level
spec↔model↔OpenAPI validation and tests.
* **Behavior Changes**
* RBAC/credential access semantics clarified: credential grants resolved
via credential-scoped role bindings for project access.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Ambient Code Bot <bot@ambient-code.local>
Copy file name to clipboardExpand all lines: specs/api/ambient-model.spec.md
+57-30Lines changed: 57 additions & 30 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,7 +18,7 @@ The Ambient API server provides a coordination layer for orchestrating fleets of
18
18
-**Message** — a single AG-UI event in the LLM conversation. Append-only; the canonical record of what happened in a session.
19
19
-**Inbox** — a persistent message queue on an Agent. Messages survive across sessions and are drained into the start context at the next run.
20
20
-**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.
21
-
-**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.
21
+
-**RoleBinding** — binds a Role to a subject (user or project) at a given scope. Ownership and access for all Kinds is expressed through RoleBindings. The subject and scope are each represented as typed nullable FKs — exactly one FK is non-null, determined by `scope`.
22
22
23
23
The stable address of an agent is `{project_name}/{agent_name}`. It holds the inbox and links to the active session.
@@ -781,7 +786,7 @@ DELETE /api/ambient/v1/credentials/{cred_id} soft d
781
786
GET /api/ambient/v1/credentials/{cred_id}/token fetch raw token — restricted to credential:token-reader
782
787
```
783
788
784
-
> **Note:**`credential bind` (via `POST /role_bindings` with `scope=credential`) is planned but not yet implemented.
789
+
> **Note:**`credential bind` (via `POST /role_bindings` with `scope=credential`, `credential_id`, and `project_id`) is planned but not yet implemented.
785
790
786
791
`token` is accepted on `POST` and `PATCH` but **never returned** by standard read endpoints.
787
792
`GET .../token` is gated by `credential:token-reader`. See
@@ -816,26 +821,48 @@ When a runner fetches a credential, the response payload shape is consistent acr
816
821
817
822
## RBAC
818
823
824
+
### RoleBinding — Nullable FK Design
825
+
826
+
`RoleBinding` is a typed nullable FK table. Each row has exactly one non-null FK, determined by `scope`. There is no polymorphic `scope_id` string — every FK points to a real table with referential integrity.
827
+
828
+
|`scope` value | Non-null FK | Meaning |
829
+
|---|---|---|
830
+
|`global`|_(none)_| Role applies across the entire platform |
831
+
|`project`|`project_id`| Role applies within a specific project |
832
+
|`agent`|`agent_id`| Role applies to a specific agent |
833
+
|`session`|`session_id`| Role applies to a specific session run |
834
+
|`credential`|`credential_id`| Role governs access to a specific credential |
835
+
836
+
`user_id` is a **separate, independently nullable FK** — it identifies the user who holds the binding when the grant is user-specific. It is null when the grant is project-level (not tied to a specific human):
837
+
838
+
| Use case |`user_id`| scope FK | Meaning |
839
+
|---|---|---|---|
840
+
| User A owns Credential Y |`user_id=A`|`credential_id=Y`| A can CRUD credential Y |
841
+
| Credential Y bound to Project X |`user_id=NULL`|`credential_id=Y` + `project_id=X`| Project X can access credential Y |
842
+
| User A is project:owner of Project X |`user_id=A`|`project_id=X`| A owns project X |
843
+
| Global platform:admin grant |`user_id=A`|_(none)_| A has platform-wide admin |
844
+
845
+
For credential→project bindings, both `credential_id` and `project_id` are non-null. This is the one exception to the "single FK per row" pattern — a credential binding names both the credential (the resource) and the project (the recipient). `user_id` is null because the grant is not user-specific; it applies to the entire project.
846
+
819
847
### Scopes
820
848
821
-
| Scope | Meaning |
822
-
|---|---|
823
-
|`global`| Applies across the entire platform |
824
-
|`project`| Applies to all resources in a project (Agents, Sessions) and Credentials bound to the project |
825
-
|`agent`| Applies to one Agent and all its sessions |
826
-
|`session`| Applies to one session run only |
827
-
|`credential`|Grants access to a specific Credential (used to bind credentials to projects)|
849
+
| Scope |FK set |Meaning |
850
+
|---|---|---|
851
+
|`global`|_(none)_|Applies across the entire platform |
852
+
|`project`|`project_id`|Applies to all resources in a specific project |
853
+
|`agent`|`agent_id`|Applies to a specific Agent and all its sessions |
854
+
|`session`|`session_id`|Applies to one session run only |
855
+
|`credential`|`credential_id`| Governs access to a specific Credential |
828
856
829
857
Effective permissions = union of all applicable bindings (global ∪ project ∪ agent ∪ session). No deny rules.
830
858
831
859
#### Credential Access — Global with RoleBinding Grants
832
860
833
-
Credentials are global resources. Access is granted via RoleBindings — bind a credential to a
834
-
Project, Agent, or Session scope. At session start, the resolver lists all credentials the
835
-
caller has access to (via RoleBindings) and returns matching credentials for each requested
836
-
provider. A single Credential can be shared across multiple Projects without duplication.
837
-
See [Security Spec — Credential Access via RoleBindings](../security/security.spec.md#requirement-credential-access-via-rolebindings) for
838
-
runtime authorization semantics.
861
+
Credentials are global resources. A credential is made accessible to a Project by creating a RoleBinding with `scope=credential`, `credential_id=<cred>`, `project_id=<project>`, and `user_id=NULL`. At session start, the resolver finds all `scope=credential` bindings where `project_id` matches the session's project and returns the matching credentials.
862
+
863
+
A single Credential can be shared across multiple Projects by creating one binding per project — no duplication of the Credential record.
864
+
865
+
See [Security Spec — Credential Access via RoleBindings](../security/security.spec.md#requirement-credential-access-via-rolebindings) for runtime authorization semantics.
839
866
840
867
### Built-in Roles
841
868
@@ -1127,7 +1154,7 @@ This structure means you can define and compose bespoke agent suites — entire
1127
1154
|---|---|
1128
1155
| 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. |
1129
1156
| Agent.prompt is mutable | Prompt editing is a routine operational task. RBAC controls who can change it. No versioning overhead. |
1130
-
| 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. |
1157
+
| Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `agent_id=<id>`, `user_id=<owner>`). Enables multi-owner and delegated ownership consistently across all Kinds. |
1131
1158
| One active Session per Agent | Avoids concurrent conflicting runs; start is idempotent |
1132
1159
| Inbox on Agent, not Session | Messages persist across re-ignitions; addressed to the agent, not the run |
1133
1160
| Inbox drained at start | Unread messages become part of the start context; session picks up where things left off |
|**Credentials — token fetch**| ✅ `GET /projects/{id}/credentials/{cred_id}/token`| ✅ `GetToken()` in `credential_extensions.go`| ✅ `credential token <id>`| Gated by `credential:token-reader`; granted to runner SA by operator |
1265
+
|**Credentials — token fetch**| ✅ `GET /credentials/{cred_id}/token`| ✅ `GetToken()` in `credential_extensions.go`| ✅ `credential token <id>`| Gated by `credential:token-reader`; granted to runner SA by operator |
Copy file name to clipboardExpand all lines: specs/security/security.spec.md
+8-7Lines changed: 8 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -270,11 +270,11 @@ API endpoints, and provider enum definitions, see the
270
270
271
271
### Requirement: Credential Access via RoleBindings
272
272
273
-
Credentials SHALL be global resources. Access SHALL be granted via RoleBindings with
274
-
`credential` scope — bind a credential to a Project to make it available to all agents in
275
-
that Project. At session start, the resolver SHALL list all credentials the session's
276
-
Project has access to (via RoleBindings) and return the matching credential for each
277
-
requested provider.
273
+
Credentials SHALL be global resources. Access SHALL be granted via a RoleBinding with
274
+
`scope=credential`, `credential_id=<cred>`, and `project_id=<project>` — `user_id` is
275
+
null because the grant is project-level, not user-specific. At session start, the resolver
276
+
SHALL list all `scope=credential`RoleBindings where `project_id` matches the session's
277
+
project and return the matching credential for each requested provider.
278
278
279
279
This follows the Kubernetes resource model:
280
280
@@ -386,8 +386,9 @@ These endpoints MUST validate the caller is cluster-internal to prevent token ex
386
386
387
387
| Decision | Rationale |
388
388
|----------|-----------|
389
-
| 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. |
390
-
| 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. |
389
+
| Agent ownership via RBAC, not a hardcoded FK | Ownership is expressed as a RoleBinding (`scope=agent`, `agent_id=<id>`, `user_id=<owner>`). Enables multi-owner and delegated ownership consistently across all Kinds. |
390
+
| Credential is global, bound via RoleBindings | Credentials are global resources. Access is granted by a RoleBinding with `scope=credential`, `credential_id=<cred>`, `project_id=<project>`, `user_id=NULL`. A single credential can be shared across multiple Projects without duplication. |
391
+
| RoleBinding uses typed nullable FKs, not a polymorphic scope_id string | Each FK (`user_id`, `project_id`, `agent_id`, `session_id`, `credential_id`) is nullable. `scope` discriminates which FK identifies the bound resource. Enables real referential integrity constraints; `user_id` is null for non-user grants (e.g. project-level credential access). |
391
392
| 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. |
392
393
| Five-scope RBAC (`global`, `project`, `agent`, `session`, `credential`) | Credential access is explicit via RoleBindings with `credential` scope. Enables cross-project sharing without credential duplication. |
393
394
| 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. |
0 commit comments