Skip to content

test: the solution-principal target, specified before it is built - #11

Merged
antoinetoussaint-byte merged 5 commits into
mainfrom
feat/solution-principal-tests
Oct 26, 2026
Merged

antoinetoussaint-byte merged 5 commits into
mainfrom
feat/solution-principal-tests

Conversation

@antoinetoussaint-byte

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

Copy link
Copy Markdown
Collaborator

What we are trying to do

The owner wants solution installation to be much simpler: a solution has an identity, a UUID like a person's, and its data is owned by that identity. Access to a solution means being granted one of that solution's roles. This PR is step 1: tests first. It pins the model as shared conformance cases plus library targets in Go, Python and TypeScript, before anything is implemented. Nothing here implements it.

It has been revised for spec reviews r75, r85 and r95, following the coordinator's decisions: maximum security, one mechanism per job.

The model

The authoritative text is the doc comment on SolutionPrincipalCase (libraries/conformance/go/solution_principal.go).

  1. Solution principal U. There is one per (organization, solution), with a stable UUID.
    • A reinstall reuses U; an uninstall disables U at once, and U's data stays owned by U.
    • The same solution installed in another organization has a different U.
  2. One identity. The installation acting for itself is U (owner_principal_id = principal_id; agent_principal_id is deleted).
    • It holds the role its install request named, and nobody inherits that role, the installing person included.
    • It acts through its operation binding, and it is confined like any person.
  3. Roles are levels on U's data, and role names carry no rank. What a role may do is the scopes it declares (the spec includes a role named editor that holds read only).
    • GrantRole and RevokeRole (by person or team, both idempotent) are the access, and ListRoleGrants answers who holds what.
    • Only an administrator of the installation's organization, or a holder of that installation's installer role, may grant, revoke and list.
    • GrantToTeam and team_ids are deleted.
  4. Confinement. A capability through the solution covers only U's data: owner-selected scopes {kind, actions, resource_owner_id: U}, taken from the solution's roles alone.
    • A person's own organization grants never flow in; they reach solution code only through an explicitly declared binding, under rule A.
    • A scope naming both an owner and ids admits a resource only when both match.
  5. Fail-closed modules.
    • Every audience receiving owner-selected scopes, solutions included, declares owner_aware; the authority refuses the rest.
    • Withdrawing owner_aware refuses the capabilities issued before.
    • Check with a method admits an owner-selected scope by kind and action, and the module enforces owner == the resource's owner: AllowsOwned in Go, allows_owned in Python, allowsOwned in TypeScript. The owner-blind allows never admits an owner-selected scope.
  6. Transfer is a decision. The store attests the resource and its current owner (from_owner_id). The authority allows the transfer only when all of these hold, each read live:
    • the current owner is the organization or a person of it;
    • the person holds admin on that resource from their own organization grants;
    • the person's current roles on U hold write on the kind.
      The response names U and authorized_by. The original creator stays the author, the previous owner keeps nothing through ownership, and organization grants (which name resources, not owners) are unaffected.
  7. Rule A for children is unchanged.
  8. Validity.
    • A new grant leaves existing capabilities valid.
    • A revoke, leaving a team or the organization, an uninstall, or a withdrawn owner_aware invalidates at once, and permanently: only fresh capabilities work afterwards. A revoke never touches anyone else's capabilities.
    • An uninstall clears the installation's role grants, so a reinstall needs re-grants.
    • A grant request naming another organization's installation is refused, naming the organization.

The cases (18, each making its own grants)

  1. install creates the principal and its roles; a reinstall keeps its UUID; another organization's is another principal
  2. the install request's role is the agent's, and no person's. The agent is confined; refused with widens:
    • the whole kind, another installation's principal or its ids, another organization's principal;
    • a person, the installation, or the organization as owner;
    • an id alone
  3. without a role on the solution a person is refused, naming the reason (holds no role)
  4. a viewer reads exactly U's data: the root's own-kind scope, the child, ListResources {all, owner U}, and the narrowed {id u-doc, owner U}. The same eight asks are refused with widens.
  5. a route requiring the kind admits the owner-selected scope, and the checked context names U; a route requiring write is refused with requires. The module's owner check is pinned in the library targets.
  6. confinement: store-x through A is refused with confined, for read by the Reader and for admin by the Administrator. A person holding an organization grant on A's own kind still gets exactly the owner-U scope there.
  7. every receiving audience declares owner_aware. Refused with owner-aware:
    • the legacy store, through a person and through the agent;
    • a module's hop (the store carrying owner scopes on to the legacy store);
    • solution C (delivered without it)
  8. withdrawing owner_aware refuses earlier capabilities, and re-declaring it revives none
  9. a resource created through the solution is owned by U, the person its author
  10. a role writes when its scopes hold write, whatever it is called: author writes; editor (read only) is refused with widens
  11. who may grant: grants are idempotent (two grants give one row), and the installer grants, revokes and lists. Fourteen refusals, each named:
    • a viewer granting itself installer, revoking, or listing;
    • another organization's administrator granting, revoking or listing on A;
    • A's installer granting or revoking on B;
    • acme's administrator (and A's installer) granting, revoking or listing on globex's installation with acme as the organization (organization);
    • another organization's id;
    • an undeclared role;
    • both or neither of person and team;
    • a person, or a team, of another organization
  12. another installation's and another organization's members never reach U's data
  13. a child never widens the owner, the actions or the ids it narrowed to
  14. transfer. A role named editor (read only) is refused with write on; the same capability is allowed once a write-holding role is granted, which proves the roles are read live. Allowed from the organization and from a person of it. Fourteen refusals:
    • from another solution principal; from another organization; from another organization's person; from an unknown owner; from no owner;
    • of a resource the person does not administer;
    • to a principal the capability is not through;
    • by a reader; by the installer role alone; by the agent;
    • from a non-serving actor; with a capability addressed elsewhere;
    • the * id; a 129-byte id
  15. revoking a person's role stops their access at once and theirs alone; ListRoleGrants reflects it; old capabilities stay dead after a re-grant
  16. a team's role is its members' access; revoking it or leaving the team stops them at once; a direct grant survives the team's revoke
  17. a person leaving the organization loses U's data at once
  18. uninstall disables U:
    • while disabled: the old capability, the agent's old capability, a child of a pre-uninstall parent, a transfer to U, the agent, a person's new issue, and GrantRole are all refused;
    • after reinstall and re-grant: the old person, agent and parent capabilities stay dead

Library targets:

  • Go: the hop keeps owner-selected scopes out of the whole-kind stamp, and its claims admit the owner and refuse another; AllowsOwned covers owner, owner plus ids, whole-kind with owner, and plain scopes.
  • Go: Claims.Scopes() keeps each scope's resource owner (TestScopesKeepTheirResourceOwner).
  • Python (tests/test_owner_target.py) builds its claims through authenticate() from a signed context, so the decode path must keep the owner. TypeScript (tests/owner-target.test.mjs) mirrors the claims target the same way.

Run vs target: one host passes both

The target supersedes Run's conflicting cases. Each now asserts the earlier model while the published contract is the earlier one, and the target's model once the contract carries Installation.principal_id. Today's local authority still passes Run unchanged. Run cases changed:

Run case Under the target
conformance.go an installation acts for itself its owner is principal_id
conformance.go the permission matrix a row at a solution audience asks within the owner the person holds there
conformance.go callee staleness the viewer's direct roles are revoked and restored exactly, instead of a team exposure; the fixture viewer must hold them only directly
conformance.go applyManifest solution deliveries declare owner_aware
solution_roles.go [0] [1] [2] [4] owner-selected asks and scopes; the delivery declares owner_aware
solution_roles.go [3] refused with holds no role
installation_chain.go the agent root's owner is U; every hop is owner-selected
solution_tools.go the asks at the solution audience are owner-selected
installation_lifecycle.go exposure a team's GrantRole, read through ListRoleGrants

The installing person no longer holds the installer role. No suite code relies on that; a target host's fixture grants installer explicitly where its matrix expects it. composition.proto:115 ("held by whoever installs it") is contract text the target changes. "holds nothing" stays only for module audiences (no organization grant), which is a different refusal.

How it fails today

Everything is opt-in with AUTHORITY_TARGET=solution-principal, as the coordinator decided:

  • The conformance target fails at its contract check. It names 16 elements the contract lacks, including AuthorityManifestV1.owner_aware, AuthorizeTransferRequest.from_owner_id and ResourceGrant.resource_owner_id outside the oneof. It names 4 elements the contract still carries: GrantToTeam, GrantToTeamRequest, team_ids, agent_principal_id.
  • The Go targets fail because core refuses WorkScopeV1 field 4 as an unknown field.
  • The Python target fails in authenticate() ("payload is noncanonical or carries a field this reader does not know").
  • The TypeScript target fails with "WorkScopeV1 has no resource_owner_id".

The local driver's leave-team and leave-organization hooks return a labelled "not yet" error, to be filled in step 2.

From #7: reused and deleted (step 2)

  • Reused:
    • the revision check under the state lock;
    • the id rule;
    • the installation lookups, and provenance read from a verified capability;
    • the person-only and own-organization-grant checks;
    • the sole-serving-module check, for the transfer caller;
    • the delivery helpers, and the consumer version ranges.
  • Deleted: AttachResource, DetachResource, installation_owned_scopes, owned-id lists, the 32-id cap, the claim races, and org-wide revision bumps.

Tests (head 433f8ef)

go vet is clean in every touched Go module. With the targets skipped, these pass:

  • go test -race -count=1 ./... in modules/authority/services/local/code (the full main suite), libraries/hop/go and libraries/authority-client/go;
  • Python: 137 passed, 1 skipped;
  • TypeScript: 38 passed, 1 skipped.

With AUTHORITY_TARGET=solution-principal, every target fails as described above.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 5 commits October 25, 2026 20:51
An installed solution becomes a principal of its organisation with a stable
UUID; its roles are permission levels on the data that principal owns;
granting a role is the access; scopes select resources by owner, not by id
lists; an existing resource joins only by a transfer the person administers
through their own organisation grants.

RunSolutionPrincipalTarget is the shared conformance target (not yet in Run).
It first names every contract element the published descriptors lack, then
specifies nine cases. The local authority runs it only under
AUTHORITY_TARGET=solution-principal, where it fails today, naming the missing
contract.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coordinator decisions on the target:
- the installation acting for itself is the solution principal: its owner id
  is U, not <installation>/agent;
- the install request's role is the agent's role (editor's level, not
  installer's admin), and no person or team inherits it;
- RevokeRole, by person or team, stops access at once (an old capability fails
  Check) and re-granting restores it; GrantRole by team is a team's access;
- a scope naming an owner and ids admits only when both match, and a child
  never drops the ids it narrowed to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coordinator decisions: confinement to the principal's data; one identity
(agent_principal_id deleted, the agent's owner id is principal_id, and Run's
installation case reads installationPrincipal so one host passes both);
fail-closed modules (hop and Claims targets, and no owner-selected scopes to
an audience that does not declare owner_aware); who may grant (an organisation
administrator or the solution's installer) with every malformed grant
refused; a second organisation; old capabilities dead after re-grant and
reinstall; transfer with a store-attested from_owner_id, only from a person
or the organisation, needing the person's own admin and editor on the
principal; the installer as an explicit grant.

Also: GrantToTeam, GrantToTeamRequest, team_ids required absent;
ListRoleGrants; ResourceGrant.resource_owner_id qualifies a match outside the
oneof, asserted on a narrowed capability; team- and organisation-leaving
hooks; a person, installation or organisation id refused as owner; a
dedicated person with admin on one resource; independent cases; named
revocation reasons; the agent through its operation binding; disabled-
principal refusals; own-kind scopes owner-selected; a revoke leaves unrelated
capabilities alive; transfer id rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coordinator decisions:
- the agent is confined: eight widening asks refused "widens";
- grant authority is per organization and per installation: another
  organization's administrator and A's installer acting on B are refused on
  GrantRole, RevokeRole and ListRoleGrants;
- transfer admin is bound to the resource (an unadministered id is refused);
- every audience receiving owner-selected scopes declares owner_aware,
  solutions included (A and B declare it; C does not and is refused);
- the target supersedes Run's conflicting cases, which now assert the target
  model whenever the published contract is the target's, so one host passes
  both (solution_roles [0]-[4], installation_chain, solution_tools, the
  permission matrix, the callee staleness probe, installation lifecycle
  exposure, applyManifest; the installing person holds no role).

Majors: transfer's previous owner allowlisted (the organization or its
person); write on the kind, not a role name ("editor" declares read only);
Check admits an owner-selected scope by kind and action and the module
enforces the owner (a route case); Python and TypeScript owner-aware targets.

Minors: grant idempotency, AllowsOwned on plain scopes and whole-kind owner
asks, a hop positive, revival after reinstall for the agent and a parent,
transfer reads roles live, own-kind confinement locally, one spelling
("organization"), owner_aware withdrawal refuses earlier capabilities, grant
edge cases (disabled installation, another organization's team, the
installer listing, direct plus team grant).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
N1: GrantRole, RevokeRole and ListRoleGrants naming the caller's own
organization with another organization's installation are refused
"organization" (for the administrator and the installer).
N2: the Python owner target authenticates a signed context, so the decode
path that projects scopes must keep the owner.

Coordinator decisions in the model and cases: an uninstall clears role
grants; a new grant leaves existing capabilities valid; invalidation is
permanent (re-declaring owner_aware revives nothing); a module's hop into a
non-owner-aware audience is refused; Go Claims.Scopes() keeps each scope's
resource owner (TestScopesKeepTheirResourceOwner); Run's staleness branch
revokes and restores the viewer's direct roles exactly and requires the
fixture viewer to hold them only directly.

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