Skip to content

Commit e700a08

Browse files
devonartisclaude
andcommitted
docs: align delegation framing with non-strict subset behavior
Closes #41 Comments and docs in 9 places claimed delegation enforces strict narrowing ("strict subset", "only narrow", "narrower-scoped"). The actual code in authz.ScopeIsSubset is a non-strict containment check: equal scopes pass. Same-scope delegation is a deliberate pattern (e.g., fan-out to workers carrying the parent's full authority) and is verified by the SDK acceptance suite (Story 8: "Delegate All Scope (No Narrowing)"). Wording template applied: "cannot widen; equal or narrower is accepted." The two source-file docstrings (deleg_svc.go, scope.go) get a longer explanation noting same-scope delegation is intentional, with a back-reference to issue #41 per golang.md's rule on commenting non-obvious design choices. Changes (12 distinct edits across 9 files): Source code (3 edits, 2 files) - internal/deleg/deleg_svc.go:6 "narrower-scoped token" → "scope-attenuated token (equal or narrower)" - internal/deleg/deleg_svc.go:12 "strict subset" → "cannot widen; equal or narrower" (+ rationale + #41 ref) - internal/authz/scope.go:74-78 "scopes can only narrow, never expand" → "requested scopes cannot widen allowed scopes" (+ rationale + #41 ref) README (2 edits) - README.md:39 "permissions only narrow, never expand" → "permissions cannot widen, only equal or narrower" - README.md:43 "delegate narrower-scoped tokens" → "delegate scope-attenuated tokens (equal or narrower)" Docs (6 edits, 5 files) - docs/security-topology.md:30 "scopes can only narrow, never escalate" → "scopes cannot widen; equal or narrower is accepted" - docs/architecture.md:212 "delegates a narrower-scoped token" → "delegates a scope-attenuated token (equal or narrower)" - docs/roles.md:97 "Create a narrower-scoped token" → "Create a scope-attenuated token (equal or narrower)" - docs/common-tasks.md:681 docstring "narrower-scoped token" → "scope-attenuated token (equal or narrower)" - docs/common-tasks.md:842 error table "strict subset of your scope" → "does not widen your scope (equal or narrower is accepted)" - docs/integration-patterns.md:2095 checklist "always narrows scope" → "does not widen scope (equal or narrower)" Diagram (1 edit, 1 SVG, 2 text elements) - docs/diagrams/security-topology.svg:191-192 "Scopes can only narrow / Never escalate" → "Scopes cannot widen / Equal or narrower" Verified already-correct (no change needed): - docs/api.md:1057 "same or narrower, never wider" — accurate - ErrScopeViolation = "delegated scope exceeds delegator scope" — accurate (talks about exceeding, not failing-to-narrow) Local gates run pre-push: build, vet, format, contamination, unit-tests (all packages), go-mod-verify — all PASS. Lint/gosec/govulncheck run authoritatively in CI. Co-authored-by: Claude <noreply@anthropic.com>
1 parent b8789f2 commit e700a08

9 files changed

Lines changed: 22 additions & 15 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ Traditional IAM was built for humans and long-running services — not for AI ag
3636
| Traditional IAM for agents | AgentWrit |
3737
|---|---|
3838
| Agents get static API keys or service account credentials designed for long-running services | Each agent requests a token scoped to one task |
39-
| Credentials are over-permissioned because scoping per-task is manual and fragile | Scope attenuation is automatic — permissions only narrow, never expand |
39+
| Credentials are over-permissioned because scoping per-task is manual and fragile | Scope attenuation is automatic — permissions cannot widen, only equal or narrower |
4040
| Leaked credential exposes everything the service account can access | Leaked token exposes one task, already expiring in minutes |
4141
| Revoking a static key means rotating it everywhere it's used | Revocation is instant at 4 levels — token, agent, task, or delegation chain |
4242
| No visibility into which agent used which credential for which task | Every credential event is audited per-agent, per-task in a tamper-evident hash chain |
43-
| No native concept of agent-to-agent delegation | Delegation is built in — Agent A can delegate narrower-scoped tokens to Agent B with full chain tracking |
43+
| No native concept of agent-to-agent delegation | Delegation is built in — Agent A can delegate scope-attenuated tokens to Agent B (equal or narrower) with full chain tracking |
4444

4545
> **What the audit trail covers:** The broker logs credential lifecycle events — issue, renew, revoke, delegate, release, auth failures, and scope violations. It does not see what the agent does with the token at the resource server.
4646

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ sequenceDiagram
209209

210210
### Delegation Flow
211211

212-
Agent A delegates a narrower-scoped token to Agent B:
212+
Agent A delegates a scope-attenuated token to Agent B (equal or narrower scope; widening is rejected):
213213

214214
```mermaid
215215
sequenceDiagram

docs/common-tasks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@ import requests
678678
BROKER = "http://localhost:8080"
679679

680680
def delegate_token(broker, my_token, delegate_agent_id, scope, ttl=60):
681-
"""Delegate a narrower-scoped token to another agent."""
681+
"""Delegate a scope-attenuated token to another agent (equal or narrower)."""
682682
resp = requests.post(
683683
f"{broker}/v1/delegate",
684684
headers={"Authorization": f"Bearer {my_token}"},
@@ -839,7 +839,7 @@ try {
839839
| Status | Meaning | Action |
840840
|--------|---------|--------|
841841
| 400 | Invalid request (bad format, missing fields) | Verify `delegate_to` is a valid SPIFFE ID and `scope` is an array |
842-
| 403 | Scope escalation or widening attempted | Ensure delegated scope is a strict subset of your scope |
842+
| 403 | Scope escalation or widening attempted | Ensure delegated scope does not widen your scope (equal or narrower is accepted) |
843843
| 404 | Delegate agent not found in broker | Verify the agent's SPIFFE ID is correct and it has registered |
844844
| 401 | Your token is invalid or expired | Renew or re-register to get a fresh token |
845845

docs/diagrams/security-topology.svg

Lines changed: 2 additions & 2 deletions
Loading

docs/integration-patterns.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2092,7 +2092,7 @@ When implementing AgentWrit patterns, verify:
20922092
### Scope and Delegation
20932093

20942094
- [ ] Scope ceilings are enforced at the broker level
2095-
- [ ] Delegation always narrows scope (never escalates)
2095+
- [ ] Delegation does not widen scope (equal or narrower)
20962096
- [ ] Delegation depth is limited (maximum 5 hops)
20972097
- [ ] Scope format is validated: `action:resource:identifier`
20982098
- [ ] Wildcard scope (`*`) is used narrowly and intentionally

docs/roles.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ write:logs:agent-run-42
9494
| `POST /v1/token/validate` | External services check this token to decide if the agent is authorized |
9595
| `POST /v1/token/renew` | Extend the session — same scope, same original TTL, old token revoked first |
9696
| `POST /v1/token/release` | Self-revoke when the task is done |
97-
| `POST /v1/delegate` | Create a narrower-scoped token for another registered agent |
97+
| `POST /v1/delegate` | Create a scope-attenuated token (equal or narrower) for another registered agent |
9898

9999
**What the agent cannot do:** call any admin or app endpoint. It has no `admin:*` or `app:*` scopes. The broker enforces this.
100100

docs/security-topology.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Scopes only move in one direction: down. Every boundary is enforced at issuance
2727

2828
- **Challenge-response** — Ed25519 keypair per agent instance. No shared secrets at the agent level.
2929
- **Hash-chain audit** — tamper-evident trail with 24 event types. Each record hashes the previous.
30-
- **Scope attenuation** — scopes can only narrow, never escalate. Delegation preserves the original principal.
30+
- **Scope attenuation** — scopes cannot widen; equal or narrower is accepted. Delegation preserves the original principal.
3131
- **Token TTL** — default 5 minutes, max 24 hours (configurable). Per-app override available. Revocable at 4 levels.
3232

3333
---

internal/authz/scope.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ func scopeCovers(requested, allowed string) bool {
7272
// least one scope in allowed. A scope is covered when its action and
7373
// resource match and either the identifiers are equal or the allowed
7474
// identifier is the wildcard "*". This enforces the attenuation rule:
75-
// scopes can only narrow, never expand.
75+
// requested scopes cannot widen allowed scopes. Equal is accepted
76+
// (same-scope delegation is a deliberate pattern); narrower is accepted;
77+
// broader is rejected. See issue #41 for why this is not a strict-subset
78+
// check.
7679
func ScopeIsSubset(requested, allowed []string) bool {
7780
for _, req := range requested {
7881
covered := false

internal/deleg/deleg_svc.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
// Package deleg provides scope-attenuated token delegation with chain
44
// verification and depth limiting.
55
//
6-
// Delegation allows an authenticated agent to issue a narrower-scoped token
7-
// to another registered agent. The delegated token carries a delegation
6+
// Delegation allows an authenticated agent to issue a scope-attenuated
7+
// token to another registered agent (equal or narrower scope; widening
8+
// is rejected). The delegated token carries a delegation
89
// chain that records the full provenance (who delegated what scope, and
910
// when). Delegation depth is capped at [maxDelegDepth] (5) to prevent
1011
// unbounded chains.
1112
//
12-
// The delegated scope must be a strict subset of the delegator's scope
13-
// (attenuation only — scopes can never expand).
13+
// The delegated scope cannot widen the delegator's scope; equal or
14+
// narrower is accepted. Same-scope delegation is a deliberate pattern
15+
// (e.g., fan-out to workers carrying the parent's full authority) and
16+
// is allowed by [authz.ScopeIsSubset]. See issue #41 for why this is
17+
// not a strict-subset check.
1418
package deleg
1519

1620
import (

0 commit comments

Comments
 (0)