Skip to content

feat: the authority core's decisions, and readers that honour resource owners - #18

Merged
antoinetoussaint-byte merged 3 commits into
mainfrom
feat/authority-core
Oct 26, 2026
Merged

antoinetoussaint-byte merged 3 commits into
mainfrom
feat/authority-core

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Collaborator

What we are trying to do

This is step 2 of the solution-principal work (spec: #11). The owner wants one generic authority core, so that the local authority and saas accounts become thin wrappers that share the same objects and decisions. That core is built test-first.

It is split by layer to stay under the ~2,000-line budget, in sequential PRs that do not stack:

  1. This PR: the pure layer, and readers that honour resource owners.
  2. Next: the core's service over a Store (the transactional Store/Tx the coordinator approved), the in-memory store, and table-driven tests encoding the conformance spec.
  3. Then: the local authority as a thin wrapper, the proto changes, the conformance target made default, and the old-model branches deleted.

This PR

libraries/authority-core/go, the pure layer. No store, no keys, no transport.

  • scope: the scope algebra moved from the local authority (state.go coverage, issuer/scope_limits.go), now owner-aware.
    • Coverage is keyed by (kind, owner).
    • An intersection keeps the narrower owner, and two different owners meet as nothing.
    • Also: Owned, HasOwner, and the parent limits.
  • domain: the shared objects:
    • Organization, Person, OrgRole, Module (with OwnerAware), Installation (with PrincipalID and AgentRole), RoleGrant, Delegation;
    • named refusals with a code (Denied, NotFound, Invalid, Precondition).
  • decide: each decision takes explicit inputs.
    • HeldRoles and RoleScopes: a role scope is owned by the installation's principal.
    • Issue names why an ask is refused: beyond for an action not held, widens for resources or an owner wider than held.
    • Child: confined versus widens.
    • OwnerAware: no owner-selected scope to an audience that does not declare owner-aware checking.
    • ChangeRole: the organization is checked before the caller's authority (r98's ordering note); then the caller must administer the organization or hold the installer role; then the installation must be active, the role declared, exactly one subject named, and that subject of the organization.
    • AuthorizeTransfer, ValidResourceID, ValidateModule, and Narrowed.

Readers now honour owners (r109 (1) and (3)). Before this PR, every reader flattened a scope with no ids to kind:action, so an owner-selected scope would have read as the whole kind.

  • Go hop (libraries/hop/go/hop.go): the stamp leaves owner-selected scopes out.
  • Go Claims: the owner-blind Allows never admits an owner-selected scope; it fails closed through core's owner-aware ScopeContained. AllowsOwned(kind, action, id, owner) is the module's check, and Scopes() keeps owners.
  • Python (authority_client/work_context.py): the projection keeps resource_owner_id, allows is owner-blind and fails closed, allows_owned is added, and scope_contained is owner-aware. codefly_hop._flatten leaves owner-selected scopes out.
  • TypeScript (work-context.ts): Scope.resourceOwnerId, an owner-blind allows, allowsOwned, an owner-aware scopeContained, and a stamp flatten that leaves owner rows out.
  • Tests (Go owner_test.go in workcontext and hop, python/tests/test_owner.py, typescript/tests/owner.test.mjs):
    • the old flattening fails them; this was mutation-checked in Go, Python and TypeScript;
    • each language feeds its verifier a signed chain whose hop drops or changes the owner and requires a refusal, while narrowing within the owner verifies.

Core is pinned at 8af4b787 (core#845, WorkScopeV1.resource_owner_id) in every Go module. The local proto mirrors field 4 (the descriptor-parity test requires it). The service and client bindings were regenerated with codefly generate proto and codefly generate client in Go, Python and TypeScript, using a CLI built from main that includes cli#1169. Python generation therefore works again: the Python bindings are current.

Nothing mints owner-selected scopes yet

The local authority is unchanged here and issues no owner-selected scope. When the wrapper PR turns owner minting on, the core refuses to mint one for an audience that does not declare owner_aware (decide.OwnerAware; conformance target case 7). That covers r109 (2).

Fleet consumers that must move before owner minting is turned on anywhere. Each reads scopes owner-blind today:

  • module-annotations authority.go:307
  • module-evaluation identity.go:337 and :528
  • module-record-store operations.go:188
  • cli executionrecorder

Each needs core 8af4b787+, or this client's AllowsOwned, and must declare owner_aware only once it compares owners.

Reuse and classification

The algebra, limits, validation and role rules are moved from the local authority's state.go and issuer.go, not rewritten. The local copies are deleted in the wrapper PR. This is a fix at the owner.

Tests

  • go vet is clean and go test -race -count=1 ./... passes in all nine Go modules plus libraries/authority-core/go (scope and decide).
  • The local main suite, including registry_test, is green.
  • Python: 188 passed. TypeScript: 46 passed.
  • Mutation checks:
    • the Go hop flatten, the Python scope_contained owner rule, the Python hop flatten and the TypeScript flatten each fail their tests when reverted;
    • in the next layer's service tests (not in this PR): person-wide versus per-installation revoke, confinement, owner-aware withdrawal, dropping the owner in Owned, and an epoch bump on grant.
  • The conformance solution-principal target stays opt-in (AUTHORITY_TARGET=solution-principal) until the wrapper PR.

Generator outputs to note

codefly generate client (CLI main) now writes the library's own version, 0.0.0, into pyproject.toml and package.json (cli#1164), and adds per-package TypeScript namespace exports (src/packages/). These are generator output, not hand edits.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 3 commits October 25, 2026 22:43
…e owners

libraries/authority-core/go is the authority's one generic core. This first
layer is pure: scope (the owner-aware scope algebra and parent limits, moved
from the local authority), domain (the objects both hosts share) and decide
(issuance classification, confinement, the owner-aware check, the role-grant
rules with the organization checked before the caller, AuthorizeTransfer,
module validation). The service over a Store, and the in-memory store, follow
in the next PR.

Core is pinned at 8af4b787 (WorkScopeV1.resource_owner_id), mirrored in the
local proto and regenerated (codefly generate proto / generate client, Go,
Python, TypeScript). Every reader now refuses to read an owner-selected scope
as the whole kind: the Go hop stamp, Claims.Allows (owner-blind, fails
closed) with AllowsOwned and Scopes() keeping owners, Python work_context
(projection, allows/allows_owned, scope_contained) and codefly_hop, and the
TypeScript reader. Each language verifies a signed chain whose hop drops or
changes the owner as a refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion per resource, owners refused locally, resolvable modules

- f1: decide.RoleScopes refuses an installation with no principal and
  scope.Owned refuses an empty owner; an empty owner selected every owner.
- f2: scope.Attenuates decides per (kind, owner, action, resource) instead of
  comparing rendered rows, and Intersect no longer lets an exact row hide an
  AnyAction row. Both are tested against a reference point-set model over
  200k random cases; Intersect's union of repeated rows and Same's owner
  comparison are documented and tested.
- f3: the local issuer refuses an owner-selected ask or delegation by name
  until it issues through the owner-aware core.
- f4/f5: authority-core requires resources and delivery at main's
  pseudo-version and hop requires the authority client at 2737858, with no
  replace in any library.
- f6: withdrawing owner-aware checking is a narrowing.
- f7: decide.ForeignInstallation is the one answer for another organization's
  installation and for none. f8: HeldRoles counts only the installation's
  grants. f9: a revoke skips the subject-membership check. f10: Child says
  confined in the same words whatever the person's own grants hold; the unused
  ceiling is gone. f11: ValidateModule validates method policies and names
  ErrNoInstallerRole. f12: a malformed resource id is Invalid; the local
  service declares 0.0.1, so the generated packages carry 0.0.1 again.
- The owner-selected reader rows are one Core-signed entry of the shared
  claims.json, run by the Go, Python and TypeScript fixture loops.
- CI: #16's go.yml, verbatim, until #16 lands.

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