Skip to content

feat(run): derive what a composition would have to be granted - #1076

Closed
antoinetoussaint-byte wants to merge 3 commits into
mainfrom
feat/derive-the-install-proposal
Closed

antoinetoussaint-byte wants to merge 3 commits into
mainfrom
feat/derive-the-install-proposal

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Groundwork for the auto-install idea in #1074: the half that is safe to automate.

Why only half

InstallSolution already produces one agent principal, one solution scope node, one standing role grant and one organization installation, and the installer surface is already token, inspect, apply, verify. So "show what needs approving, then approve it" is the shape that exists.

What is missing is the input to inspect: something that says what a given composition is actually asking for.

This derives that, and stops there. MODULE_INSTALLATION.md is explicit about why:

Policy publication is an explicit trust-root operation; an ordinary module release declaration cannot write this projection.

A module that could derive its own envelope could grant itself authority by declaring it. So this derives the request and never the approval. It grants nothing, signs nothing, and reaches no network.

What it does

For each service in the composition it reports the workspace-configuration groups it reads and the endpoints it calls, and separates what leaves the composition from what is inside it. A dependency the operator is installing in the same act is composed, not granted — listing it buries the ones that genuinely cross a boundary. For record-store that means saas/accounts/authority and saas/auth-gateway/rest surface, while record-store/store/tcp correctly does not.

Derive is pure, so what an operator approves in a deployed environment is what a local run derived from the same manifests, rather than a differently ordered reading of it. There is a test for that specifically.

The rendered text states twice that nothing is granted by deriving it, because a list of requirements reads like a list of things already arranged.

What is deliberately not here

Approving it. In a local environment the fixture already declares every input an approval would need — pinned users with role: super_admin, pinned organizations ("a module principal grant names its tenant by organization id"), and ReadCompositionGrants merging each principal's grants — so a dev auto-install would be reading an approval someone committed, not inventing one. That is the obvious next step and it decides authority, so it belongs to whoever owns the trust root rather than to this PR.

Context

Found while making a module run standalone (#1074, #1075). The motivating case: record-store/records declares five workspace-configuration groups and could not start without four of them, because it had only ever been run composed with a host — which is the class of thing this derivation makes visible before it is a surprise.

🤖 Generated with Claude Code

antoinetoussaint-byte and others added 3 commits October 19, 2026 14:50
A service that verifies an inbound Work Context loads its key set from an
endpoint, and `workcontext.AdmitEndpoint` — the one transport rule for every
key set — already admits "plaintext http to a loopback host, on a local run
only". Nothing served one, so a module whose identity comes from the host
could not boot on its own: `codefly run service` resolved the graph, started
its dependencies, and then the service failed runtime-init with "no
configuration found for work-context". A developer with only that module's
repository could start no service at all.

The native run now mints an Ed25519 key for itself, serves the public half as
a JWKS over that admitted loopback hop, and hands the work-context group to
any service that declares the dependency.

The group stays AUTHORED and is never render-owned, exactly as
FederationConfiguration is: a composition with a host declares it, pointing at
the host's key set, and this only answers for the composition that has none.
It is handed last so it settles over what an authored group leaves empty
rather than displacing one.

Nothing is relaxed by this. The service boots on its unchanged FromJWKS path,
the KeySet invariant holds because the keys still come from an admitted
endpoint with admission and fetch as one operation, and the run issues under
its own issuer — so a capability minted here is structurally unacceptable to a
deployed service, which pins the host's. Each run's issuer names the run, so
two runs on one machine never accept each other's capabilities. The listener
lives exactly as long as the run.

Tests cover the shape a verifier admits (OKP/Ed25519, raw-URL base64, a 32-byte
key), that the published half verifies what the signer signs, that each run
issues under its own issuer, and that a run with no identity serves nothing.

Refs #1074

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tions

The first attempt appended the work-context group in renderOwnedGroups, and
it never reached the service: `codefly run service record-store/records`
failed exactly as before with "no configuration found for work-context".

A declared workspace-configuration dependency is resolved from the manager's
OWN loaded set — configurations.Manager.GetWorkspaceDependenciesConfigurations
reads manager.worspaceConfigurations, which only its Loader populates.
renderOwnedGroups feeds the per-service configurations handed to the runtime,
which that lookup never consults; RenderOwnedWorkspaceConfiguration says as
much, that render-owned groups are "never dependencies of the configuration
manager". federation is an extra handed to the host, work-context is a
declared dependency, and following the federation precedent put it in the
wrong half.

So the run's key set now layers onto the loader where WithLoader is bound, and
the flow owns the authority the launcher borrows.

Two constraints the decorator respects. Configurations() returns the same
instances on every call, because the manager resolves secret references in
those objects in place and reads them back, so fresh copies would discard what
it resolved. And the local reader carries two optional capabilities --
CompositionRootWorkspaceConfigurationNames and ambiguous-group reporting --
which are forwarded; swallowing either would leave the manager unable to tell
which groups come from the composition root.

An authored group still wins: if the loaded set already names work-context,
the decorator adds nothing.

Verified against obin-ai/module-record-store: the failure moves from
"no configuration found for work-context" to "no configuration found for
platform", the next group that service declares. work-context resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An installation is one solution's bound identity in an org, and
InstallSolution produces one agent principal, one solution scope node, one
standing role grant and one organization installation. Its installer surface
is already four paths — token, inspect, apply, verify — so the shape of
"show what needs approving, then approve it" exists. What is missing is the
first part: something that says what a given composition is asking for.

This derives that from the manifests. For each service it reports the
workspace-configuration groups it reads and the endpoints it calls, and
separates what leaves the composition from what is inside it — a dependency
the operator is installing in the same act is composed, not granted, and
listing it buries the ones that genuinely cross a boundary.

It derives the REQUEST and never the approval. MODULE_INSTALLATION.md is
explicit that the envelope is not derivable from a module's own declaration:
"Policy publication is an explicit trust-root operation; an ordinary module
release declaration cannot write this projection." A module that could write
its own envelope could grant itself authority by declaring it. So this grants
nothing, signs nothing and reaches no network, and the rendered text says so
twice, because a list of requirements reads like a list of things already
arranged.

Derive is pure, so what an operator approves in a deployed environment is what
a local run derived from the same manifests rather than a differently ordered
reading of it. A test asserts that stability directly.

This is the half that is safe to automate. Approving against a fixture's
declared approver in a local environment is the obvious next step and is not
here: it decides authority, and that decision belongs to whoever owns the
trust root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte antoinetoussaint-byte added the working Claimed by a lazybox agent; cleared when the session ends. label Oct 19, 2026
@antoinetoussaint-byte

antoinetoussaint-byte commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

A lazybox agent is working on this. claude (Opus 5) started at 2026-11-04 03:12 UTC, last renewed the claim at 2026-11-04 04:47 UTC, and the claim lapses at 2026-11-04 05:47 UTC unless it is renewed again. A claim is an assertion with an expiry, not proof that a process is alive.

claim detail
Lazybox-Claim-Device: effacd542b611010656e
Lazybox-Claim-Session: cf807214c3
Lazybox-Claim-Workspace: github-codefly-dev-cli-1076
Lazybox-Claim-Agent: claude
Lazybox-Claim-Model: Opus 5
Lazybox-Claim-Started: 2026-11-04T03:12:46Z
Lazybox-Claim-Heartbeat: 2026-11-04T04:47:18Z
Lazybox-Claim-Expires: 2026-11-04T05:47:18Z

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

Closed: superseded by the authority interface design. The CLI no longer holds authority knowledge such as serving key sets or deriving grants. Authority lives in codefly-dev/authority, and the CLI only delivers through codefly.dev/composition (#1128).

🤖 Generated with Claude Code

@antoinetoussaint-byte
antoinetoussaint-byte deleted the feat/derive-the-install-proposal branch October 23, 2026 19:44
@antoinetoussaint-byte antoinetoussaint-byte added working Claimed by a lazybox agent; cleared when the session ends. and removed working Claimed by a lazybox agent; cleared when the session ends. labels Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working Claimed by a lazybox agent; cleared when the session ends.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant