Skip to content

feat: role scopes limited to what an installation owns - #7

Merged
antoinetoussaint-byte merged 5 commits into
mainfrom
feat/installation-scoped-role-grants
Oct 26, 2026
Merged

antoinetoussaint-byte merged 5 commits into
mainfrom
feat/installation-scoped-role-grants

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Problem and resulting behavior

A solution role can now grant scopes on the exact resource ids its installation owns. An empty owned set grants nothing. Ordinary solution roles retain main's resource-owner principal selector; they never become unrestricted grants.

The coordinator's 26 October decision is implemented: an existing resource changes owner through AuthorizeTransfer first. There is no separate organization-admin attachment path. The existing transfer policy checks the serving module's current-owner attestation, the person's live organization admin grant on that resource, write through the target installation's roles, and capability provenance. Only after authorization succeeds does the same write transaction record the owned id. The store records the returned owner, retaining the original author, and must complete or compensate the change before exposing the resource.

AttachResource retains the module-created path: create on the whole kind, called by the kind's one serving module. Owner-selected create must select that installation's principal. Admin-only attachment is refused and names AuthorizeTransfer. Installation-agent creation and a solution creating its own kind remain supported.

Authority interface 0.3.1, composition 0.2.0. Existing consumers keep >=0.2.0 <0.4.0. Main has been merged without discarding other authors' commits.

Shared implementation and reuse

  • decide.RoleScopes resolves ordinary owner-selected roles and installation-owned exact-id rows. domain.Module.OwnedRoles and domain.Installation.Owned live in the shared authority core; the local issuer is a thin adapter.
  • Creation and authorized transfer reuse the same ownership mutation, exclusivity check and cap. Verification is fenced by revision, principal epochs, serving-module identity and live installation state under the Store write transaction.
  • DetachResource removes deleted ids and advances the authorization revision. Uninstall clears ownership; reinstall starts empty. No stale capability restores it.
  • Reused main's AuthorizeTransfer, personal resource-grant matching, role resolver, core verification, scope algebra, Store transactions, ListResources and existing conformance harness. No second authorization path or datasource interface was built; datasources remain SaaS-only.
  • Source proto comments, local Go/Connect bindings and Go/Python/TypeScript clients were regenerated through Codefly using companion 0.0.18. Reviewed generated-output digests were refreshed. No generated binding was edited by hand.

Named refusals

Read-only or admin-only attachment; create limited to an id or selecting a different owner; wrong or ambiguous serving module; wrong audience; superseded revision/provenance; no installation or multiple installations on creation; foreign/inactive transfer target; another installation's owned id; malformed ids; and more than 32 current ids per kind.

Transfer additionally refuses an agent, a capability not through the target installation, a current owner that is another solution principal/organization/foreign person/unknown/empty, absent personal admin on the exact resource, and absent write through target roles. Refusals do not mutate ownership. Ids are opaque and limited to 128 bytes, with whitespace, control-character and wildcard rules enforced.

Validation

  • go vet ./... and go test -race -count=1 ./... passed across all 11 Go modules, including shared authority core, local host, full shared conformance and tools/noproxygate.
  • Public issuance tests prove ordinary roles retain owner selection; authorized transfers add owned ids; missing live personal grants and cross-tenant targets do not; creation supports installation agents and tenant isolation.
  • Tests cover exact viewer ids, empty resolution, child narrowing, admin-only refusal, foreign/unknown current owner, installer-only and agent transfer refusal, cap, exclusivity, copy isolation, concurrent attachments, detach/reinstall invalidation, and revision changes between verification and the write transaction.
  • Python: 300 passed. TypeScript: 158 passed.
  • GitHub CI: all 15 checks passed at 337ab23edd05ca555a4aae1dceca220fd4c12636, including all 11 Go modules, the Go aggregate, and both language client jobs. Go run, client run.

Classification and follow-ups

Fix at the owning authorization seam. The former direct-admin attachment design is removed, not retained behind a flag or fallback.

  • Accounts owner: adopt/persist the shared core fields and pass the required conformance cases. Accounts, its durable adapter and deployed cells were not verified; local results do not establish SaaS parity.
  • Store and solution owners: integrate authorized transfer of configured resources, creation rollback and deletion through Codefly-generated clients, preferring Connect.
  • Authority/conformance owner: the existing descriptor-driven gRPC conformance harness remains transport/provenance follow-up debt; it is not a new production client or a separate grant policy. Local generated Connect bindings are regenerated here.
  • CLI owner: dependent authority-client repin to this published head and later merge-commit repins remain delivery work. Core #839 and CLI #1167 already carry the declaration path and have passing CI at their published heads.
  • Handbook #398: re-pinned to this final published head as part of this chain. Both PRs remain unmerged.

A solution's role could grant a consumed scope only on a whole kind, so a
viewer role that needed read on the collections its installation owns read
every collection in the tenant, and so did robin acting for the viewer.

A role now declares installation_owned_scopes (RoleV1 field 3): a kind and
actions, no ids. The authority resolves each, for the installation the role
is held on, into the same scope limited to the ids that installation owns; a
kind it owns nothing of grants nothing, never the whole kind. Ownership is
recorded by AuthorityService.AttachResource: the module serving the kind
presents the capability the creating call carried, and the owning
installation is read from its verified provenance, never named by the caller.
Child issuance is unchanged (rule A): the owner's current grants intersected
with the binding, parent limits never recovered.

The shared conformance suite gains InstallationResources: attachment from
provenance and its named refusals, a viewer of A reads exactly A's
resources, is refused B's by name, and a narrowed child never widens.

authority interface 0.3.0, composition interface 0.2.0. Bindings regenerated
with codefly generate proto / generate client (Go, TypeScript); the Python
client bindings could not be regenerated (CLI python generation fails to
resolve buf/validate), so they stay at the previous contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view r47)

B1: AttachResource now requires create on the whole kind (a read-only or
id-limited capability attaches nothing) and refuses an id another
installation of the tenant owns: attaching is for new resources, never for
existing ones. Both reviewer probes are conformance refusals.
M1: the capability's authorization revision is compared with the tenant's
under the state write lock, so a pre-uninstall capability never attaches into
the reinstalled installation.
M2: edge, hop and console require authority >=0.2.0 <0.4.0; they call nothing
new and resolve against a host still at 0.2.0.
M3: an installation owns at most 32 ids of one kind, each at most 128 bytes,
refused by name past either; capabilities stay bounded.
m4: library.codefly.yaml keeps the python entry and the 0.0.17 companion
until cli#1163 lets Python regenerate.
m5: only a kind's one serving module attaches; a kind two modules declare
attaches nothing.
m6: README versions; an installed solution serving its own kind attaches
through a person's capability addressed to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…w r58)

Coordinator decision: the only consumer configures collections it never
created, and the document store's collection kind declares no create. So
AttachResource accepts create on the whole kind (a new resource) or admin on
that id, by name or on the whole kind (an administrator attaching an existing
one). Read alone still attaches nothing, and an id another installation owns
is still refused.

F1: DetachResource, called by the kind's one serving module when it deletes a
resource; the 32 bound counts what is owned now, and detaching moves the
organisation's revision. The interface states that a store rolls back a create
whose attach is refused and detaches what it deletes.
F2: ids follow core's scope-value rule (non-empty, trimmed, no NUL, CR, LF or
tab) and carry no "*"; they are opaque, compared byte for byte.
F3: the actions read ("create", "admin") are named in the interface doc.
F5: positive tests for the solution-own-kind and installation-agent
provenance paths.
F7: the README states that local ownership is in memory, for development.

Conformance adds: an admin of X attaches X and A's viewer then reads it; a
reader of X cannot; an admin of X cannot attach Y; an admin of B's id cannot
take it into A; a detached resource leaves its owner and attaches again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte

Copy link
Copy Markdown
Collaborator Author

Runtime #311's real local-authority admission witness is green with the development cache but fails on a cold runner. I reproduced cold startup with an isolated CODEFLY_HOME: the selected authority c9cef74 asks for go-grpc 0.1.59, whose download returns HTTP 404. Current authority main 4ebd5be still pins 0.1.59 in local and hop. Public service-go-grpc v0.1.66 has Linux amd64 and macOS assets.

Please include the released agent pin in the owner tree you are landing (or identify the owner updating it), and send its SHA. Runtime will consume the owner commit; I will not edit authority's dependency cache or weaken the required witness. I am updating Runtime's own unpublished 0.1.60 pins and checking the other selected prerequisites. Evidence: /tmp/codex-runtime-admission-cold.log; failure at fixture_profile_integration_test.go:42. CI 40249749209 admission artifact likewise locates failure at dependency startup, before any admission call.

antoinetoussaint-byte and others added 2 commits October 26, 2026 09:57
Preserve owner-selected ordinary roles, exact attached-id scopes, and current consent/transfer APIs. Generate authority 0.3.1 through Codefly. Existing-resource attachment authorization remains under owner review; local integration tests retain the old expectations to expose that unresolved policy conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep ordinary solution scopes owner-selected; record transferred ids only after the existing live policy succeeds, and retain create-only attachment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 67b85b1 into main Oct 26, 2026
15 checks passed
antoinetoussaint-byte added a commit that referenced this pull request Oct 26, 2026
…to fix/hop-stamp-binds-method

Conflicts kept both sides: the hop's HTTP face signs with its Signer and
answers a Connect caller in Connect's words (refuse takes the request);
both proxy tests stay; local's go.mod keeps main's new indirect deps.

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