Skip to content

feat(moduleauthority): serve core's three live-authority sources to a callee's verifier - #44

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
feat/served-verifier-sources
Oct 14, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
feat/served-verifier-sources

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

The client half of codefly-dev/module-saas-starter#1107, which merged as 9ab36e7e.

A composed module that receives a Work Context is its callee, and a callee verifies with sdk-go/workcontext's PinnedVerifier — core's verifier with its configuration settled. That verifier needs three sources only the issuer holds: a SealSource, a RevisionSource and a GrantSource. Nothing served them, so a module could do no more than core's forwarding-hop Inspector — encoding, signature, issuer, audience, window, shape, nothing live — and a capability whose installation was revoked, whose build was replaced, whose operation binding was withdrawn or whose principal's epoch had moved stayed accepted there until it expired.

The host now serves them on ModuleCapabilitiesService/ResolveWorkContextAuthority. This is the client:

sources, err := client.WorkContextSources(moduleauthority.WorkContextSourceSettings{
    Capability: presented,
})
verifier, err := workcontext.NewVerifier(workcontext.VerifierSettings{
    Issuer: issuer, Audience: audience,
    Seals: sources, Revisions: sources, Grants: sources,
    Replay: replayStore, // yours, durable and shared across replicas
}, keySet)

A source is not a verifier

AGENTS.md's standing rule — "a callee-side verifier does not come back here" — is about the decision, and the decision stays in sdk-go/workcontext. This holds no key, reads no signature and decides nothing about a capability: it is a client of the issuer's live state, which is this SDK's whole job. The rule gains that clause rather than an exception.

The one claim a source reads off a capability (its owner, for the revision question) only narrows what it asks. The host checks the same claim against the same capability, so a misreading refuses rather than misanswers.

The capability is the only required setting and it is the whole authorization: the host authenticates it, requires it to be addressed to the calling module and to its tenant, and answers only about the identifiers it names — so there is no tenant, audience or owner for a caller to supply or get wrong.

Answers are translated, never interpreted. Core's own sentinels carry the three absences (ErrNoSeal, ErrNoApprovedBuild, ErrNoBinding). A state this client does not understand, an answer on another arm, and an answer about another principal, installation or binding are each ErrUnreadableAuthorityAnswer — a refusal, never a permissive default. A host refusal arrives with its connect code, which is how a caller tells CodeFailedPrecondition ("the issuer decided, mint again") from CodeUnavailable ("the lookup failed, retry"). Positive answers are cached for at most AuthorityCacheBound (2s), a ceiling rather than a default because a cached answer delays a revocation by exactly its bound: a larger one is refused rather than clamped, and refusals are never cached.

What the ref move carries, and what it is not

gen/ moves from module-saas-starter v0.0.94 to 9ab36e7e, which is where the RPC first exists. SOURCE.txt records the commit — a ref this repository's gen-drift job resolves — so the SDK does not wait on a deploy tag to carry a contract its consumers need.

The move carries the host's other accounts contract changes since v0.0.94 (datasource, installations, api_keys, module_registration, work_contexts). Those are the host's, not this SDK's. One breaking removal, also the host's: SessionId and its GetSessionId() are gone from StartTaskWorkContextRequest, StartChildSessionWorkContextRequest and StartInstallationTaskRequest, because the host stopped accepting a caller-named session — every session a capability carries is now the one its caller authenticated with, or a fresh one the issuer assigns and returns. A consumer that set one was naming a session it did not own; it drops the assignment. Those are the only exported symbols the regeneration removed: nothing else lost a type, a field or an RPC.

The SDK's release version does not move: the saas-starter module version is still 0.1.0.

One broker-contract change, found by the conformance suite

conformance/hostgateway is there to catch the broker contract moving under a ref change, and it did: the gateway now relays accounts' FailedPrecondition on the module Work Context mint as HTTP 412, where v0.0.94 relayed it as a 502. A workload the host admits that is installed in no organization it may mint for therefore used to read as "the host is broken" and now reads as "the host works and there is nothing to mint for you yet".

412 is now ErrNoHostStateToMintFor on the work-context and operation-context mints: the module is proven, nothing is denied, and a mint succeeds once it is installed. It is deliberately distinct from ErrPermissionDenied and from a bare 502. The source mint's own 412 keeps its own meaning (ErrDelegationMissing).

Tests

moduleauthority/verifier_sources_test.go, against the package's fake host (which now serves the new procedure): every question carries the capability and names the capability's own owner; each absence state becomes core's sentinel; every unreadable answer — no state on each of the five, an answer about another installation or principal, an answer on another arm, an empty answer — is refused; a host refusal travels with its connect code for all three classes; a capability that is not structurally one is refused before any call; a cache bound above the ceiling is refused; the cache serves one answer inside its bound, asks again past it, treats a different question as different, and never caches a refusal.

The host's half of the contract — which capability a module may ask about, and what the live answers are — is held upstream by a test that drives this SDK's verifier dependency against the real handler.

Verified / not verified

Run and green: go build ./..., go vet ./..., go test ./... (including internal/apiboundary and internal/agentcontext), gofmt -l . | grep -v '^gen/' empty, the byte-for-byte regeneration check (diff -r against a fresh generate from the recorded ref — silent), and conformance/hostgateway/run.sh against a checkout of 9ab36e7e: 9 tests ran and passed.

Not verified: go test -race ./.... It cannot build on this machine — the Go toolchain's race-instrumented stdlib is incomplete there (package math/cmplx is not in std), which is unrelated to this change. CI runs it.

Nothing here was run against a cluster, and no consumer was compiled against this branch, so the SessionId removal's downstream impact is stated from the diff rather than measured.

🤖 Generated with Claude Code

… callee's verifier

A composed module that receives a Work Context is its callee, and a callee
verifies with sdk-go/workcontext's PinnedVerifier — core's verifier, with its
configuration settled. That verifier needs three sources only the issuer holds:
a SealSource, a RevisionSource and a GrantSource. Nothing served them, so a
module could do no more than core's forwarding-hop Inspector, which checks the
encoding, signature, issuer, audience, window and shape and nothing live. A
capability whose installation was revoked, whose build was replaced, whose
operation binding was withdrawn or whose principal's epoch had moved stayed
accepted there until it expired.

module-saas-starter#1107 (merged as 9ab36e7e) serves them on
ModuleCapabilitiesService/ResolveWorkContextAuthority. This is the client:
`WorkContextSources` implements all three over that RPC, so a consumer hands
one value to NewVerifier as Seals, Revisions and Grants and brings its own
replay store.

FIX, at the place that owns the behaviour. A SOURCE IS NOT A VERIFIER, and
AGENTS.md now says so beside the rule it qualifies: this module holds no key,
reads no signature and decides nothing about a capability — it is a client of
the issuer's live state, which is this SDK's job, and the decision stays in
sdk-go/workcontext. The one claim a source reads off a capability (its owner)
only NARROWS what it asks; the host checks the same claim against the same
capability, so a misreading refuses rather than misanswers.

Answers are translated and never interpreted. Core's own sentinels carry the
three absences (ErrNoSeal, ErrNoApprovedBuild, ErrNoBinding); a state this
client does not understand, an answer on another arm, and an answer about
another principal, installation or binding are each
ErrUnreadableAuthorityAnswer, which is a refusal and never a permissive
default. Positive answers are cached for at most AuthorityCacheBound, a
CEILING rather than a default because a cached answer delays a revocation by
exactly its bound; a larger one is refused rather than clamped, and refusals
are never cached.

gen/ moves from module-saas-starter v0.0.94 to 9ab36e7e, which is where the RPC
first exists; SOURCE.txt records the commit, which this repository's gen-drift
job resolves, so the SDK does not wait on a deploy tag. The move carries the
host's other contract changes since v0.0.94 and one breaking removal that is
the host's, not this SDK's: SessionId is gone from three Work Context mint
requests, because the host stopped accepting a caller-named session. Those are
the only exported symbols the regeneration removed.

conformance/hostgateway found one broker-contract change at the new ref, which
is what that suite is for: the gateway now relays accounts' FailedPrecondition
on the work-context mint as HTTP 412 rather than 502. 412 is now
ErrNoHostStateToMintFor — the module is proven and the host holds nothing to
mint for it, most usually a workload installed in no organization — which is
neither a denial nor a host failure.

Verified: go build, go vet, go test, gofmt, the apiboundary and agentcontext
gates, the byte-for-byte regeneration check against the recorded ref, and
conformance/hostgateway against a checkout of 9ab36e7e (9 tests ran and
passed). NOT verified: `go test -race`, which cannot build on this machine —
the toolchain's race-instrumented stdlib is incomplete ("package math/cmplx is
not in std"), unrelated to this change; CI runs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 14fe024 into main Oct 14, 2026
3 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the feat/served-verifier-sources branch October 25, 2026 17:04
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