Repository navigation
fix(workcontext)!: core's verifier is not re-exported; a verifier's keys come from an admitted endpoint, on the host's literal transport rule - #53
Conversation
fa4460d to
3fcd4a6
Compare
|
lazybox has finished working on this. claim detail |
|
E / core-solutions#362 attempted the actual published Core #710 commit 49093a1573bb33c25cbd45dfeba0811876dd0cb1 ( |
|
E localized the endpoint compatibility fix: Core already exposes |
|
To workstream E: please do not prepare the narrow Coordination from the Lane 3 coordinator (handbook#196): the The owner's merge order is core v0.12.0 → sdk-go#57 → consumers. A narrow What that means for this PR. One thing worth flagging to whoever lands #57, since it is adjacent to this PR's |
…eys come from an admitted endpoint, on the host's literal transport rule Third pass on the 2026-10-04 security posture audit findings this repository owns (#52), answering the xhigh BLOCK at 3fcd4a6. Four of the six previous findings conformed; this closes the rest and the three new ones. BREAKING: workcontext.Verifier is no longer exported. Core's verifier is a struct whose every field has a usable zero value, so re-exporting it was a second way in that skipped every check this package requires — the shape GuardStreams already exists to avoid here, where the correct wiring took two calls and only one was reachable from the type system. NewVerifier is the only constructor and PinnedVerifier the only type here with a Verify method; a component that is itself the issuer imports core/workcontext directly, as core.go already says about the verify-only Authenticator. TestEveryVerificationPathIsPinned holds it. The endpoint, the fetch and the verifier are now one operation. KeySetEndpoint and KeySet are opaque with one constructor each, AcquireKeySet is the only thing that produces a KeySet, and NewVerifier takes nothing else — so "this location was admitted" and "these are the keys in use" are one fact rather than two. The decoding stays the caller's and the keys cross as plain bytes: reading a key set means naming a signature primitive, which the import allowlist refuses, so fillVerificationKeys infers the element type from the field it fills. The mesh assertion is compared LITERALLY. It was trimmed first, so that a value delivered by file would not be refused over its trailing newline; but TrimSpace trims every Unicode space, so values that are not the assertion asserted it. A delivery's shape is the delivery's to fix, and the refusal names what the value must be. Operators delivering it by file must deliver exactly "true". A failed read of the assertion is no longer absence. A setting nothing carries is an answer; a delivery that cannot be read, or an authority-bearing value that has drifted from the one pinned at boot, is not, and is refused. TransportSource therefore requires WorkspaceValueIfSet, a new three-valued accessor on the root module's Query, rather than WorkspaceValue, which reports "not configured" and "could not be read" with one error. No refusal echoes a configured value. Not the URL, not the parser's own message, and not the assertion — WorkspaceValueIfSet reads the secret namespace too, so that value may be a credential and a configuration error reaches boot-time logging. scripts/security-posture.sh is the re-runnable check for SP-WC-01 and SP-WC-05. The catalogue's commands counted lines, which answers 0 for a passing run without -v, 1 whatever the subtests did, and 0 for a test that no longer exists. A check that cannot fail reports green forever. This one reads the exit status and separately requires every named test to have run. scripts/importsof parses whole files. parser.ImportsOnly stops at the first non-import declaration, so a file whose body does not parse swept clean — against the sweep's own documented claim — and an import placed after a declaration was never read at all. Docs: workcontext/README.md, workcontext/doc.go, workcontext/AGENTS.md, AGENTS.md. .golangci.yaml extends its ST1005 proper-noun exclusion to verifier.go. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d161ea2 to
3f4671a
Compare
Summary
Closes the two findings of the 2026-10-04 security posture audit this repository
owns (#52): SA-F-WC-ISSUER and SA-F-WC-TTL. This repository is public, so
each is described by the invariant it restores and by the required behaviour; the
scenarios stay in the private register under the same ID.
Third pass, answering the xhigh BLOCK at
3fcd4a6b.Breaking:
workcontext.Verifieris no longer exported. See R53-01 below.Rebased onto
main; the branch is one commit again. sdk-go#57 ("adopt corev0.12.0 endpoint resolution") merged as
9b9d219, and this PR is now3f4671aon top of it.
91a2f91— thefor.goendpoint-location fix that was pushed tothis branch mid-pass — is dropped, not carried: #57 owns that change, per the
Lane 3 coordinator (handbook#196), and
main's version is what this branchcarries.
Two things differed from the rehearsal and were recomputed rather than replayed:
mainresolves corev0.13.0, not the v0.12.0 in fix!: adopt core v0.12.0 endpoint resolution #57's title. Every gatebelow ran against v0.13.0.
scripts/security-posture.shis the check thatmatters here — it is what would catch a core bump dropping the derivation clamp
or changing the verifier surface, and it passes, so the audit invariants hold
against the version this repo actually resolves.
for.goneeded two patches, not one. A single patch spanning this PR's twoearlier passes silently includes
91a2f91'sExternal()line and collides withmain's rewritten resolver. Split per pass, both verified to contain noExternal/Visibilityline, both apply clean.So
mainownsresolveLocalNetworkInstanceentirely —SelectEndpointForReferenceand
selected.External()are untouched here — and the onlyfor.golines this PRadds are
(*Query).IsLocalRunand(*Query).WorkspaceValueIfSet, besideWorkspaceValue.codefly_test.gokeeps both sides: #57'sprepareDeclaredEndpointWorkspaceand this PR's two accessor tests.AGENTS.mdcarries both #57's core-pin bullet and this PR's verifier bullet, at 195 lines of
its 200-line budget.
endpoint_location_test.goand the rootREADME.mdcomefrom
mainuntouched.Nothing about the audit findings changed in the rebase.
Review findings
KeySethas a single constructor that fetches from the admitted endpoint, andNewVerifiertakes nothing else.errors.Isand out of the message.scripts/importsofparses whole files; three malformed shapes are committed as sweep probes.scripts/security-posture.shis the check, and it fails when the invariant is violated — proved five ways below. The counts are corrected.R53-01 — the verifier, the endpoint and the keys are one operation
Two things were open: the exported verifier remained usable without the helper,
and the admitted endpoint did not establish where
verifier.Keyscame from.Core's verifier is no longer re-exported.
type verifier = corework.Verifieris unexported in
core.go;PinnedVerifieris the only type here with aVerifymethod and
NewVerifieris its only constructor. Every field of core's verifierhas a usable zero value, so exporting it was a second way in that skipped every
check — the shape
GuardStreamsalready exists to avoid in this module, wherethe correct wiring took two calls and only one was reachable from the type
system. A component that genuinely is the issuer imports
core/workcontextdirectly, which
core.goalready says aboutErrNoSealand the verify-onlyAuthenticator.The keys come from the admitted endpoint.
KeySetEndpointandKeySetareopaque with one constructor each,
AcquireKeySetis the only thing that producesa
KeySet, andNewVerifiertakes nothing else:The decoding stays the caller's and the keys cross as plain
[]byte, becausereading a key set means naming a signature primitive —
crypto/ed25519, base64decode and
encoding/jsonoutsidemint.goare each refused by the importallowlist that ten rounds of review built, and re-opening it here would undo the
thing the four gates exist for.
fillVerificationKeysis generic so the elementtype is inferred from the field it fills, which names nothing.
The fetch stays on the admitted address: a redirect is refused rather than
followed, the body is bounded, a non-200 is
ErrKeySetUnavailablerather than aconfiguration fault, and the HTTP client is this module's — a caller supplies
none, on the mint client's terms.
workcontext/grpctransport/two_service_test.go, which is the consumer-shapedtest here, now builds its verifiers through that whole path against a real
loopback endpoint rather than by constructing one.
What this does not close, stated rather than claimed: a consumer can still
import
core/workcontextand build core's verifier itself. That is deliberate —an issuer must — and it is now visible in a grep rather than available one import
shorter. The consumer-tree half of the SP-WC-05 command below is what reads it.
R53-07 — the assertion is compared literally
trueasserts the mesh. Unset, empty andfalsekeep plaintext refused. Everyother value stops the process, a value carrying whitespace included.
The previous revision trimmed first, with the host's reasoning: a value delivered
by file carries the newline the file ends with. The reviewer is right that this is
the wrong trade.
TrimSpacetrims every Unicode space, so" true"and sixother shapes asserted the mesh while not being the assertion. A delivery's shape
is the delivery's to fix, and the refusal names what the value must be.
Seven whitespace variants and nine other misspellings are committed refusals.
This is stricter than the host's own copy (
meshtransport, module-saas-starter#1008),which still trims; the same finding applies there, and that is the host's to
make. Noted under Operator steps because a composition delivering the assertion
by file now has to deliver it exactly.
R53-08 — no refusal echoes a configured value
The assertion is read from the public namespace and then the secret one, so its
value may be a credential, and a configuration error reaches boot-time logging.
The refusal now names the setting and the permitted values; the value itself and
the source's error text stay in the error chain for
errors.Isand out of themessage. The same treatment covers the decoder's error and the transport's, both
of which name what they were given.
R53-09 — a failed read is not absence
WorkspaceValuereports "not configured" and "the delivery could not be read"with one error, because its callers want a value or nothing. A posture question
cannot live with that. So
TransportSourcerequiresWorkspaceValueIfSet(name, key) (string, bool, error), added to the rootmodule's
*Query, which returns:truewhen it is set;("", false, nil)when nothing carries it — absence is an answer;file, and
ErrAuthorityValueChanged— a drifted authority-bearing value.A failed read refuses every admission decision, including an https one, so a
declaration that cannot be read never leaves the process admitting. Covered by a
root-module test driving the real carriers, including the
ReadAuthoritydriftcase the review used.
R53-10 — the sweep parses whole files
parser.ImportsOnlystops at the first non-import declaration, so a file whosebody does not parse read as a file with imports and no problem — against the
sweep's own documented claim that "a file that does not parse is a failure, not a
file with no imports" — and an import placed after a declaration was never read
at all. It is a full syntax parse now (still not a type-check: resolving a file at
a two-year-old tag would need that tag's dependencies). The three shapes are
committed in
TestTheImportScannerReadsOneLineOnce, and restoringImportsOnlyturns all three red.
SA-F-WC-TTL — unchanged and still partly this repo's
TestDerivedContextNeverOutlivesParentis unchanged and still passes against theresolved core
v0.9.0, mutation-checked below. The verification half needs anabsolute-deadline claim that
codefly.base.v0.WorkContextV1does not carry andWorkActorV1has no per-hop timestamp for; that remainscodefly-dev/core's,and nothing is worked around here.
The catalogue checks, which now fail when the invariant does
The old commands counted lines:
go test … | grep -c '^--- PASS'answers 0for a passing run without
-v, 1 whatever the subtests did, and 0 for atest that no longer exists. A check that cannot fail reports green forever, which
is worse than no check — the rule stops being audited and nothing says so.
scripts/security-posture.shreplaces them. It reads the exit status, andseparately requires every named test to have run, so a renamed, deleted or
filtered-out test fails rather than passing silently.
SP-WC-05 has a second half, over each consumer tree, which is what reads the
residue named above:
Proved by violating it, five ways — each run after the violation, each exit 1:
security-posture.shderiveno longer clamps (behind a filesystemreplace)TestEveryVerificationPathIsPinnedThe counts, corrected to what the commands actually print. The review is
right and the previous body was wrong:
-v, catalogue's anchored^--- PASS-v, counting indented subtests./scripts/security-posture.shok, exit 0Test plan
go test ./...(ok, 46.6s) and(cd workcontext && go test ./...)(ok, 7.9s) — against core v0.13.0go test -race ./...(ok, 18.0s) and(cd workcontext && go test -race ./...)(ok, 16.7s)golangci-lint runin each module — 0 issues, 0 issues..golangci.yamlextends its existing ST1005 proper-noun exclusion (Codefly…sentinels) toverifier.go, besidemint.goandstream.go.go mod tidy+git diff --exit-code -- go.mod go.sumclean in each moduleworkcontext94.3% (gate ≥ 75% each).verifier.gois 95–100% per function;AcquireKeySet95.2% andNewVerifier95.5%, the uncovered statements beinghttp.NewRequestWithContextfailing on a URL that already parsed.scripts/check-one-implementation.sh→ok working tree (78 Go files)scripts/security-posture.sh→ok SP-WC-01,ok SP-WC-05scripts/allowed-imports.txtunchanged.verifier.goaddscontext,errors,io,net/httpandtimeto thefmt,net/url,slices,stringsit already had — every one already on the leaf module's list, because the mint client needs them. No crypto, no codec, no base64.Mutation checks
Each is run against the whole leaf module suite, not a selected test, so a
mutation nothing committed catches shows as green. All twenty-four are red.
The three new findings and the provenance fix:
TestTheAssertionRefusalDoesNotEchoTheValueTestEveryVerificationPathIsPinnedVerifymethod/a_key_set_that_was_never_acquired/a_redirect,_which_leaves_the_admitted_address/a_decoder_error,_whose_text_is_the_caller'sThe transport contract and the pin, re-run against the rewritten file: M1 the
assertion not consulted · M2 the address rule not consulted · M3 the suffix taken
as "whatever follows svc" · M4 DNS-1123 dropped · M6 loopback admitted when
deployed · M7 the parse error not read · M8
HostforHostname· M9 thekey-length refusal removed · M10 the key length written as a different number
(red on the length case and every happy path) · M12 the raw URL echoed · M13
the remedy dropped · M14 the assertion read only for plaintext · M15 an empty pin
accepted · M16 whitespace trimmed instead of refused · M20 a missing source not
named.
core/workcontext/mint.go, behind a filesystemreplace(an unmutated copypasses first;
go.modunchanged in the diff): C1 the clamp removed · C2 anexpired parent admitted · C3 a widening hop admitted.
And
scripts/importsof: restoringparser.ImportsOnlyturns the three malformedshapes red.
Docs
workcontext/README.md— "Settle the verifier: admit the location, fetch thekeys, pin the rest", "The transport contract" (admission table, literal
comparison, failed-read rule, loopback), and "Re-auditing this".
workcontext/doc.go— core's verifier is not re-exported and what replaces it.workcontext/AGENTS.md— why the alias is unexported, why the endpoint/fetch/verifier are one operation, the transport rule's five clauses, and why a check
that cannot fail is worse than no check.
AGENTS.md— the bullet under Rules that bite (186 lines, under the 200 cap)..golangci.yaml— the ST1005 exclusion list.Operator steps
One, following from R53-07 rather than from a choice made here: a composition
delivering
internal-transport/mesh-protectedby file must deliver exactlytrue, with no trailing newline. The value is now compared byte for byte, so apadded value stops the process with a named error instead of being read as the
assertion. A composition delivering it inline is unaffected.
Do not tag. Consumers pin by commit hash.
Not verified
cloud state was touched.
AcquireKeySetis exercisedagainst
httptestservers on loopback — redirect, status, oversize, deadendpoint, decoder failure — and not against a deployed key-set host over https.
agrees with the address rendered beside it is checked here only between the two
values a consumer passes in.
validator the first review recommended: core exports none. The number is held
to
ed25519.PublicKeySizeby a test, which is thesealCarriesAnExecutionarrangement this module already uses.
NewVerifierandAcquireKeySethave no consumer yet. The end-to-end pathis exercised by this repository's own tests, including the two-service gRPC
test, and nowhere else.
meshtransportstill trims the assertion. R53-07 applies to itas written; it is not touched from here.
absolute-deadline claim SP-WC-01's verification half needs, and a
key-configuration validator.
Postgres-backed receipt tests skip locally; CI's
coverageandracejobs runthem.
🤖 Generated with Claude Code