Repository navigation
feat(moduleauthority): serve core's three live-authority sources to a callee's verifier - #44
Merged
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sPinnedVerifier— core's verifier with its configuration settled. That verifier needs three sources only the issuer holds: aSealSource, aRevisionSourceand aGrantSource. Nothing served them, so a module could do no more than core's forwarding-hopInspector— 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: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 insdk-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 eachErrUnreadableAuthorityAnswer— a refusal, never a permissive default. A host refusal arrives with its connect code, which is how a caller tellsCodeFailedPrecondition("the issuer decided, mint again") fromCodeUnavailable("the lookup failed, retry"). Positive answers are cached for at mostAuthorityCacheBound(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 to9ab36e7e, which is where the RPC first exists.SOURCE.txtrecords the commit — a ref this repository'sgen-driftjob 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:SessionIdand itsGetSessionId()are gone fromStartTaskWorkContextRequest,StartChildSessionWorkContextRequestandStartInstallationTaskRequest, 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/hostgatewayis there to catch the broker contract moving under a ref change, and it did: the gateway now relays accounts'FailedPreconditionon 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
ErrNoHostStateToMintForon 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 fromErrPermissionDeniedand 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 ./...(includinginternal/apiboundaryandinternal/agentcontext),gofmt -l . | grep -v '^gen/'empty, the byte-for-byte regeneration check (diff -ragainst a fresh generate from the recorded ref — silent), andconformance/hostgateway/run.shagainst a checkout of9ab36e7e: 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
SessionIdremoval's downstream impact is stated from the diff rather than measured.🤖 Generated with Claude Code