Skip to content

fix(workcontext): owner scope follow-ups from review r109 of #845 - #846

Merged
antoinetoussaint-byte merged 2 commits into
mainfrom
fix/owner-scope-followups
Oct 26, 2026
Merged

antoinetoussaint-byte merged 2 commits into
mainfrom
fix/owner-scope-followups

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Follow-ups to #845 (merged at 8af4b78) from security review r109: the three minor findings, plus the wording fix for the major one.

Changes

F2: owner validation matches what the schema states. resource_owner_id now uses max_bytes = 512. The old max_len counted runes, so a 2048-byte owner passed. The pattern is now ^[^\s\p{Z}\p{C}*]*$, which also refuses \v, NBSP, U+3000, U+2028, zero-width characters, BOM, C0/C1 controls, DEL and private-use characters. Go enforces the schema through protovalidate at mint and at decode. TestResourceOwnerSchema covers 21 refusals (including a 2048-byte owner, a vertical tab, NBSP, ZWSP and controls) and 4 acceptances. Control: restoring the old generated descriptor fails 12 of those subtests.

F3: conformance vectors for the owner rule. workcontext.Fixtures gains three fixtures:

  • owner-delegated (accepted): a hop narrowing within the owner.
  • owner-dropped-by-hop (rejected): a genuinely signed hop that drops the owner.
  • owner-changed-by-hop (rejected): a genuinely signed hop that changes the owner.

Both rejections require ErrInvalid with "widens authority". The authority builds its shared Python/TS claims.json from this kit, and consumers run conformance.Run against it. So an owner-blind second implementation now fails. TestVerifyRefusesAHopThatDropsOrChangesTheOwner is the verify-side Go test. Decode's structural list names the two refusals. Control: removing the owner if in ScopeContained fails both fixtures, the verify test, and every conformance kit test.

F4: the refusal names the real reason, and a runtime can mint under an owner-scoped parent.

  • The refusal now names where the owner was written: the declaration, the installation binding, or the composition's scope-slot selection. The selection is now checked in validateSelectedScope, which used to ignore the owner. Each refusal reads "owner-scoped parent: a resource owner is inherited from the capability the operation is invoked under …" and no longer says "never declared".
  • New workcontext.InheritResourceOwner(requested, parent):
    • keeps a scope the parent covers as written;
    • inherits the owner when exactly one owner covers the scope;
    • fails closed with ErrInvalid and a named reason when no owner covers it, when several do ("mint one child per owner"), or when the scope already names another owner.
    • It returns copies and never mutates its inputs.
  • Tests:
    • a 9-case table;
    • an end-to-end mint: under an owner-scoped parent the raw owner-less operation scope is refused by Child, while the inherited one mints and verifies.
  • Documented in docs/work-context.md (new section "Scopes and the resource owner") and docs/runnable.md.

F1 wording. docs/work-context.md and the ScopeContained comment now limit "fails closed" to checks built on ScopeContained/ScopesAttenuate. They state that a reader inspecting WorkScopeV1 fields itself must refuse (or grant nothing from) an owner-selected scope until it compares owners. The doc lists the known readers:

  • authority hop.go flatten
  • authority Python Scope/scope_contained
  • authority Python codefly_hop._flatten
  • module-annotations
  • module-evaluation
  • module-record-store
  • cli execution recorder

Reuse

  • InheritResourceOwner is built entirely on ScopeContained. It adds no second containment rule.
  • The fixtures reuse the kit's authority, seals and verifier. The existing re-sign closure became fixtureResign so both fixture builders share it.
  • The refusal text is one constant shared by the three origins.

Classification

Fix, at the place that owns each behaviour: the schema, core's conformance kit, and core's runnable validation. No hack.

Evidence

  • make buf-lint: clean. make buf-breaking: "no breaking change against origin/main".
  • go build ./... ok. go vet ./workcontext/... ./runnable/... ok.
  • go test -race -count=1 passes on: workcontext, workcontext/conformance, runnable, composition, moduleupdate, policy, policy/testharness, solutionhost. ./internal/ciguard/... passed under -race before the merge with main.
  • Python contract tests (the CI step, protobuf 7.36.1): 16 OK against the regenerated binding.
  • Bindings were regenerated with codefly generate proto --local and with --template buf.gen.python.yaml. The buf.lock bump the companion's dep update made was reverted.

Not verified

  • Whether Python protovalidate agrees with Go on the new pattern (r109's open question). The probe hit the same buf.validate descriptor clash and version mismatches. Core's Python package is bindings only.
  • The TypeScript reader's unknown-field handling.
  • Fleet deployments on core older than v0.9.0.

Left for owners

codefly-dev/authority: the owner_aware issuance gate, and making the hop flatten and the Python/TS twins owner-aware. All of this must land before the authority mints any owner-selected scope. When the authority bumps core and regenerates claims.json, it picks up the new fixtures.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 2 commits October 25, 2026 22:48
resource_owner_id is bounded in bytes (max_bytes 512, not 512 runes) and
its pattern refuses Unicode separators, control, format (zero-width) and
private-use characters, not only ASCII whitespace. protovalidate enforces
the schema at mint and decode; tests cover a 2048-byte owner, a vertical
tab, a non-breaking space, a zero-width space and control characters.

The conformance kit gains owner vectors: owner-delegated (a hop narrowing
within the owner, accepted), and owner-dropped-by-hop / owner-changed-by-hop
(genuinely signed hops refused with "widens authority"), so a second
implementation that ignores the owner fails the kit. A verify-side test
feeds Verify the rewritten chains directly.

An owner in an operation's authority is refused with its origin
(declaration, installation binding, composition's selection) and the
reason, "owner-scoped parent". workcontext.InheritResourceOwner is how a
runtime mints an operation child under an owner-scoped parent: it keeps
what the parent covers as written, inherits the one covering owner, and
refuses no owner, several owners, or a replaced owner.

The fail-closed claim in docs/work-context.md and ScopeContained is
narrowed to checks built on ScopeContained, and the doc lists the readers
known to inspect scopes field by field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant