Skip to content

fix(workcontext): resource owner is an ASCII allowlist; owner-blind readers listed; joint-owner refusal named (r128) - #847

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
fix/owner-ascii-allowlist
Oct 26, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
fix/owner-ascii-allowlist

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Follow-ups to #846 (merged at c1d1430) from security review r128. Its four minors are fixed, applying the maximum-security posture.

Changes

f1 and f2: resource_owner_id is an ASCII allowlist.

  • The pattern is now ^[!-)+-~]*$, with max_bytes = 512 unchanged. It accepts printable ASCII other than space and *.
  • The quantifier is *, not +, because an empty owner means the scope selects no owner. The allowlist needs no empty-string carve-out.
  • The old category-exclusion pattern let through characters that print as nothing or as another letter: U+034F, U+FE0F, U+3164, U+115F, U+2800, NFD accents, and Cyrillic and fullwidth lookalikes. Go and Python protovalidate also disagreed on unassigned code points and noncharacters (U+0378, U+FFFF, U+FDD0).
  • The vectors live in workcontext/testdata/resource_owner_vectors.json: 4 accepted and 36 refused. TestResourceOwnerSchema reads them.
  • Python check: protovalidate-python 2.1.0 (Rust-backed) with protobuf 7.36.1, run against the regenerated work_context_pb2, matches on 40 of 40 vectors.
    • The Python run is local and is not wired into CI. To get past the buf.validate descriptor clash, I registered protovalidate's own validate.proto through a probe shim. Committing that shim to CI would be a workaround.
  • Controls: against main's schema, Python mismatches 13 vectors and Go fails 10.
  • Fleet check:
    • No code in any mirrored repo assigns resource_owner_id yet; only test fixtures do (principal-u, principal-owner), and they fit.
    • The solution-principal contract makes the owner a stable UUID per (organization, solution).
    • The saas starters use UUID primary keys.
    • All of these fit the allowlist.

f3: the reader table in docs/work-context.md is complete.

f4: InheritResourceOwner refusals and the confined read.

  • A scope covered only by several owners' rows together is now refused as "owner-scoped parent: … covered only across resource owners [P Q] together, never under one; split it into one child per owner". It used to say "not covered".
  • The read-dropped case (parent [doc:read, doc:write@P] gives doc:{read,write}@P) is documented as intended fail-closed behaviour. An operation invoked under an owner-scoped capability is confined to that owner's resources, and the operation declares a single scope per kind.
  • Four new table cases cover this.

Classification

Fix, at the owner of each behaviour: the schema, core's inheritance helper, and core's docs. No hack.

Evidence

  • make buf-lint is clean.
  • make buf-breaking: no breaking change against origin/main.
  • go build ./... and go vet on workcontext and runnable are ok.
  • go test -race -count=1 passes on workcontext, workcontext/conformance, runnable, composition, moduleupdate, policy, policy/testharness and solutionhost.
  • The Python contract tests (CI step) pass: OK.
  • Bindings were regenerated with codefly generate proto for both Go and Python. The buf.lock bump from the companion's dependency update was reverted.

Not verified

  • The TypeScript protovalidate behaviour.
  • The reader table was read at mirror HEADs, not at deployed pins.

🤖 Generated with Claude Code

…eaders listed; joint-owner refusal named (r128)

resource_owner_id is now ^[!-)+-~]*$ under max_bytes 512: printable ASCII
other than space and "*". The category-exclusion pattern let invisible and
lookalike characters through (U+034F, U+FE0F, U+3164, U+115F, U+2800, NFD
accents, Cyrillic and fullwidth lookalikes), and Go and Python protovalidate
disagreed on unassigned code points and noncharacters. The vectors live in
workcontext/testdata/resource_owner_vectors.json; Go protovalidate and
Python protovalidate 2.1.0 agree on all 40.

InheritResourceOwner names the reason when only several owners' rows
together cover a scope ("covered only across resource owners ... split it
into one child per owner") instead of "not covered", and documents and
tests that an inherited owner applies to the whole scope: under
[doc:read, doc:write@P], doc:{read,write} becomes doc:{read,write}@p, an
intended fail-closed confinement.

docs/work-context.md lists the owner-blind readers r128 found (the
authority gateway issuer, module-document-store, module-runtime,
saas-sdk-python, the saas-starter accounts adapters) and says exactly what
authority #18 (open) changes: the local issuer refuses owner-naming issue
and delegation requests by name; the gateway issuer stays owner-blind.

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