diff --git a/AGENTS.md b/AGENTS.md index 1b8e744..29fad52 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,16 +72,8 @@ Run each of those a second time from `workcontext/` — it is a separate module with its own `go.mod` and `go.sum`, so nothing at the root covers it. Lint finds the root `.golangci.yaml` by walking up, so it needs no config of its own. -Coverage thresholds differ by module and are not interchangeable: - -| Module | Gate | Where | -| --- | --- | --- | -| root | total ≥ 75%, per-file and per-package 0 | `.testcoverage.yaml` | -| `workcontext` | module total ≥ 75%, via `awk` in CI | `go.yml`, `coverage` job | - -The `workcontext` gate is on the **module total** across both its packages, so -`grpctransport` sitting low is carried by the verifier. Read the total, not a -package line. +Coverage gates differ by module: the root's is `.testcoverage.yaml` (total ≥ 75%); +the `workcontext` module's is described in `workcontext/AGENTS.md`. ## Where things live diff --git a/endpoint_deployed_test.go b/endpoint_deployed_test.go index 33208ad..118cef9 100644 --- a/endpoint_deployed_test.go +++ b/endpoint_deployed_test.go @@ -231,3 +231,25 @@ func TestLocalProcessWithoutAWorkspaceRefusesAMalformedCarrier(t *testing.T) { } } } + +// A module run from its own checkout inside a larger composition finds its +// repository's workspace on disk, which declares the module under its own name, +// not the name the composition gave it. That workspace is not the +// composition, so the process resolves from the carrier the composition's CLI +// injected, as a workspace-free process does, rather than refusing every +// endpoint because its repository does not know the composition's name. +func TestAComposedModuleRunFromItsOwnCheckoutResolvesItsCarrier(t *testing.T) { + prepareProcessWithoutWorkspace(t, "local") + checkout := t.TempDir() + require.NoError(t, os.WriteFile(checkout+"/workspace.codefly.yaml", + []byte("name: module-own-dev\nlayout: modules\nmodules:\n - name: module-own-name\n"), 0o600)) + t.Chdir(checkout) + workspace, err := resources.FindWorkspaceUp(context.Background()) + require.NoError(t, err) + require.NotNil(t, workspace, "the test needs the module repository's own workspace on disk") + access := carrierFor("rest", "rest", "http://localhost:24601") + injectEndpointForTest(t, "runtime", access) + instance, err := queryCarriedService().API("rest").ResolveNetworkInstance() + require.NoError(t, err) + require.Equal(t, uint16(24601), instance.Port) +} diff --git a/for.go b/for.go index 2d5b37b..3d61c87 100644 --- a/for.go +++ b/for.go @@ -126,7 +126,7 @@ func (q *Query) ResolveNetworkInstance() (*resources.NetworkInstance, error) { if err != nil { return nil, err } - if workspace == nil { + if workspace == nil || !composes(workspace, q.module) { return q.resolveFromAuthoritativeCarrier() } // The declaration decides eligibility before any carrier can supply an @@ -155,6 +155,25 @@ func (q *Query) ResolveNetworkInstance() (*resources.NetworkInstance, error) { return local, nil } +// composes reports whether the workspace found on disk is the composition +// this process runs in: it declares the module the query names. A module run +// from its own checkout inside a larger composition finds its repository's +// workspace, which knows the module by its own name, not by the name the +// composition gave it; reading eligibility from it would refuse every +// endpoint. That process is answered as a workspace-free one is — from the +// carriers the composition's CLI judged and injected at run time. +func composes(workspace *resources.Workspace, module string) bool { + if workspace.Name == module { + return true + } + for _, declared := range workspace.ModulesNames() { + if declared == module { + return true + } + } + return false +} + // resolveFromAuthoritativeCarrier answers a workspace-free process from its carriers // alone. The carrier is keyed by the query's canonical identity — the // declaration is not on disk to supply any part of it — and nothing is judged diff --git a/workcontext/AGENTS.md b/workcontext/AGENTS.md index b358b62..29b2081 100644 --- a/workcontext/AGENTS.md +++ b/workcontext/AGENTS.md @@ -1,422 +1,7 @@ -# Working in `workcontext/` +# Working in workcontext -A separate leaf module (`github.com/codefly-dev/sdk-go/workcontext`, Go 1.27, -its own `go.mod`/`go.sum` and its own coverage gate). It mints nothing and -verifies nothing: `codefly-dev/core/workcontext` is the only implementation of -the capability and this module re-exports core's verifier by type alias, in -`core.go`. Adding a dependency here is a design change — see -`.claude/skills/dual-module-change`. - -Both modules pin core's **v0.13.0 release tag** (`f2423c9c`). Core owns the -endpoint vocabulary: visibility is `private`/`internal`/`public`, and -`location: external` describes an endpoint outside the system. The root SDK -uses core's location predicate for local resolution; public visibility alone -does not prevent it. Keep that resolution in the root SDK, outside this leaf. - -Depth that used to live in the root `AGENTS.md` and does not fit its 200-line -budget. The root file carries the rule; this carries what the gates refuse. - -## What the two gates refuse, and why each rule exists - -**There are four gates and they read BOTH modules.** Two are type-checked with -`x/tools/go/packages`: `TestEveryImportIsOnItsModulesAllowlist` is a -**deny-by-default import allowlist per module**, and -`TestNoCodecTouchesACapabilityByType` decides the codec rule with `go/types`. -`TestNoSecondWorkContextImplementation` is the syntactic gate that remains. -`scripts/check-one-implementation.sh` sweeps every published ref, reading -imports with `go/parser`. - -**Deny by default is the spine, and it replaces an argument that lost ten -times.** Every revision banned a LIST OF NAMES and every review found the next -name — `hkdf`, then `sha512`, then `protoiface`, and finally `crypto/mldsa` -signing a `WorkContextV1` that `encoding/json/v2` had encoded as snake_case -JSON, which is the exact format this repository deleted, with both gates green -and lint clean. The list was not behind by one entry; it was behind by a design. -Each module now declares the imports it HAS, measured from the tree, and -anything else is a finding — so `protodelim`, the gRPC codec registry, -`prototext`, `jsontext` and `hpke` are refused by one rule, and so is the one -nobody has thought of. `import "C"` is refused by name, because a MAC built -through CommonCrypto names no Go package at all. - -**go/types is necessary and not sufficient**, which is why both halves exist. A -syntactic resolver answers with a SPELLING where the question is a KIND: six -compiled probes decoded a real capability through `func into[M proto.Message]`, -an embedded interface, an alias of `proto.Message`, a `reflect.Call`, a closure -taking `proto.Unmarshal`, and a descriptor-built message. `types.IsInterface` -and object identity answer all of them, and the indirection allowlist is keyed -on `*types.Func` — so a second method named `Handle` on a new type is a -different object. **The irreducible residue** is a hand-written implementation -that imports nothing: a base64url decoder is twenty lines and a wire walk is -fifty. That is closed by review, and saying so is part of the rule. - -The syntactic gate used to walk this module only, which was a hole with nothing -behind it: the deleted implementation lived at the repository root, and the -sweep bans *imports* rather than reading code. A reviewer built the consequence -— a root file with a **hand-written base64url decoder**, so no banned import at -all, and `proto.Unmarshal` into `basev0.WorkContextV1` — and it compiled, linted -clean and swept green. - -The two modules are held to different rules, deliberately. **In here the -capability is the subject**, so every codec use is allowlisted by name. **Outside -it the subject is everything else** — receipts, configuration, runtime documents -— so enumerating the legitimate types would be noise that rots; the rule there -is the decisive one on its own: **no codec may be applied to a Work Context -message**, recognised as a type under core's module whose name begins with -`Work`. A prefix, so a message core adds later is covered the day it exists. - -Type names resolve to the imported **package PATH**, never to the local -qualifier, because the qualifier is the author's choice. - -- **By capability, never by name.** A signature, MAC or JOSE/JWT primitive, - anything under `x/crypto`, `protojson`/`protowire`/`anypb`. The bare `crypto` - is included: `crypto.Signer` signs Ed25519 with no `ed25519` import anywhere. - The predicate used to be "imports a primitive AND mentions WorkContext", - which a second implementation defeats by putting the signer in one file and - the wrapper in another. -- **An allowlisted import is held to FILES and SYMBOLS.** `crypto/tls`, - `crypto/x509`, `mime`, `crypto/sha256` and `encoding/base64` each have an - honest use in exactly one file. Whole-file is allowed-for-everything — which - is how `x509.ParsePKCS8PrivateKey`, a signer getting a key without naming - `ed25519`, would be a permitted use of an allowed import. -- **base64 may ENCODE and never DECODE.** `cache_partition.go` builds a cache - key. base64 decoding plus `proto.Unmarshal` is the entire second parser this - module deleted, and the symbol rule alone cannot see it: `base64.StdEncoding` - is the permitted symbol and `DecodeString` is a method on the value it names. -- **A codec is held to (file, codec, type).** Not to a file — a whole-file - exemption is an exemption for every type in it — and not to an operation - name, because `Marshal` is an operation name shared by every codec, so - `mint.go`'s `encoding/json` row also permitted `proto.Unmarshal` on anything - *named* `mintResponse`. Types resolve package-qualified (`basev0.WorkScopeV1`, - not `WorkScopeV1` from anywhere), in the scope of the use, before the use, and - an ambiguous identifier is refused rather than guessed. - `encoding/asn1` is limited to `mint.go` decoding SAN GeneralNames into - `[]asn1.RawValue`; it cannot encode or decode any capability. This standard - library import adds no module dependency and preserves the signed URI bytes. -- **No function-local type, and no second local name for core's types outside - `core.go`** — alias or defined type, through a pointer or a slice, and a local - name is CHASED to what it names. Either one hands a type-name allowlist - whatever name it asks for: `type WorkScopeV1 = basev0.WorkContextV1` makes a - capability marshal as an allowed scope, and a declaration inside a function - body is invisible to every rule reading a file's declarations. One `*` walked - past both halves for three rounds — `type carrier = *basev0.WorkContextV1` - resolved to `"carrier"`, neither a capability nor unresolvable, so the root - module's one rule permitted a `proto.Unmarshal` into it. -- **A codec-allowed struct's field set is FROZEN, embedded fields included.** - The codec rule names the top-level type, so a new field is a new thing encoded - through a row written for the fields listed. An embedded field has no `Names`, - so a loop over `field.Names` skipped every one — and `encoding/json` promotes - an embedded struct's exported fields into the enclosing object. -- **No `encoding/json` outside `mint.go` by exact path, and no json-tagged - struct elsewhere.** The deleted implementation signed a hand-written JSON - payload. The path is exact because matching the base name let any new - `mint.go` inherit the allowance — an allowlist undone by `touch`. -- **No declaration named `workcontext*`** (case-insensitively, every - declaration kind) unless it aliases into core's module. -- **The sweep reads imports with GO'S OWN PARSER** (`scripts/importsof`), not a - regex. Its awk extractor was rewritten three times for this class and five - more shapes walked past it, the decisive one being `import - "\x63rypto/ed25519"` — which compiles, IS `crypto/ed25519` to the compiler, - and is not that string to anything matching text. No regex closes that. A - file that does not parse is a failure, not a file with no imports. -- **ONE import policy, `scripts/allowed-imports.txt`,** read by the Go gate and - by the sweep. There were two and they disagreed in both directions, which was - a blocker twice: the sweep is the only thing that runs over a published ref - and is the whole of the required check, so a branch carrying `crypto/mldsa` - and `encoding/json/v2` swept clean while the allowlist in `go test` refused - it. A line may name the FILES that may have an import; `legacy` lines apply - to published refs only, because deny-by-default describes today's tree and - sweeping two years of tags with it flagged 33 clean versions. -- **The gates must READ every shipped file, asserted against `git ls-files`.** - `packages.Load` builds one configuration, so a build-constrained file, a cgo - file under `CGO_ENABLED=0`, a `_`-prefixed directory, a nested module or a - vendor tree is invisible to every semantic rule. The sweep still reads them - (go/parser ignores build tags), so an import ban holds and nothing else does - — and assembly, object code and C are refused outright, because no rule here - can read them. -- **A gate that could not look must not report success.** Four fail-opens, three - inside the assertion added the round before: a swallowed `git ls-remote` - failure skipped the comparison entirely, the comparison was between COUNTS - rather than ref sets, an entry `git cat-file` could not read swept clean, and - the fix for that `exit`ed a command substitution's subshell — printing `FAIL` - and then `ok`. Every enumeration's exit status is read. - -**What no import ban catches:** a hand-rolled HMAC over `crypto/sha256` builds a -MAC out of parts that are not MACs. `crypto/cipher` and the block ciphers are -banned in both gates now; the hash cannot be, because two digests here need it. -So in this module the symbol rule bounds that residue to one file, and in the -root module it is closed by review. "Any MAC is a finding" was a list of names; -this is what the list reaches. - -`TestTheGateCatchesItsOwnBypasses`, -`TestTheRepositorySweepCatchesItsOwnBypasses`, -`TestTheImportScannerReadsOneLineOnce` and -`TestTheSweepRefusesWhenItCouldNotLook` hold both gates to these claims: every -case is a bypass reported against a previous revision, and every one worked. `TestTheSDKParsePathsAgreeWithCore` drives every core conformance -fixture through this module's own parse paths and requires core's sentinel — -read a refusal's sentinel off the fixture, never off a constant here, because -three fixtures changed sentinel under us and the tests that did that needed no -edit. - -## Configuration is this module's to refuse; verification is not - -**Core's verifier is NOT re-exported, and that is the one deliberate difference -between `core.go` and a plain re-export.** `type verifier = corework.Verifier` is -unexported; `PinnedVerifier` is the only type here with a `Verify` method and -`NewVerifier` is its only constructor. Exported, core's verifier was a second way -in: a struct whose every field has a usable zero value, constructible beside the -configuration that is supposed to be required — the shape `GuardStreams` already -exists to avoid in this module, where the correct wiring took two calls and only -one was reachable from the type system. `TestEveryVerificationPathIsPinned` holds -it: nothing exported here may name core's `Verifier`, and the only exported -`Verify`/`Recheck` are `PinnedVerifier`'s. - -A component that genuinely IS the issuer imports `core/workcontext` directly, as -`core.go` already says about `ErrNoSeal` and the verify-only `Authenticator`. -That is not a gap; at that point it is a participant in the capability rather -than a client of it. - -**The endpoint, the fetch and the verifier are 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. They were -two, and a checked endpoint sitting beside independently supplied keys is a -check that establishes nothing. - -**The decoding is the CALLER'S and the keys cross as plain bytes.** Reading a key -set means naming a signature primitive, which is the one thing this module may -never do — `crypto/ed25519`, base64 DECODE and `encoding/json` outside `mint.go` -are each refused by the allowlist, and that allowlist is the thing ten rounds of -review built. `fillVerificationKeys` is generic so the element type is INFERRED -from the field it fills: a conversion through a type parameter whose core type is -`[]byte` names nothing. - -**Every field of core's verifier has a usable zero value**, so an incomplete -configuration is expressible and the invariant requires it to be refused BY NAME. -Each missing value is named separately, the four sources included, where core -names all four in one message. Key material of the wrong length is refused here -too — core refuses it per request, and a configuration fault belongs in a refusal -before anything is served. - -**`verificationKeySize` is a number here and a call in core**, which is the -`sealCarriesAnExecution` arrangement again and for the same reason. The number is -stated once and a `_test.go` — where the import is legitimate — holds it to -`ed25519.PublicKeySize`. If core grows a key-configuration validator this becomes -a call and the number goes. - -## The transport rule is the host's, reimplemented, and this is the SDK's one copy - -In-cell transport security is the mesh's, so a composition asserts it with -`internal-transport/mesh-protected` and a service accepts plaintext to an -in-cluster Service address exactly under that assertion. Both halves are -required: the assertion covers only what a mesh can cover, and a name alone is -not evidence a mesh wraps the wire. - -- **`ClusterServiceHost` accepts `..svc` or that followed by - `cluster.local` and nothing else**, with both leading labels held to DNS-1123. - The suffix is matched LITERALLY, because a name carrying an `svc` label in - another domain is an ordinary public name and an arbitrary suffix is not - evidence of a cluster domain. -- **The assertion is compared LITERALLY.** `true` asserts; unset, empty and - `false` keep plaintext refused; every other value stops the process, a value - carrying whitespace included. An earlier revision trimmed first so a - file-delivered value would not be refused over its trailing newline — but - `TrimSpace` trims every Unicode space, so values that are not the assertion - asserted it. The delivery's shape is the delivery's to fix. -- **A lookup failure is not 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. `TransportSource` requires - `WorkspaceValueIfSet` for exactly that: `WorkspaceValue` reports "not - configured" and "could not be read" with one error, and a failed read must not - decide a posture question. `(*Query).WorkspaceValueIfSet` is the root module's - three-valued accessor, added for this. -- **Loopback is the LOCAL shape only.** A deployed runtime has one admission rule - and loopback is not an exception to it even with the assertion set, since a - mesh carries no hop that never leaves the pod. -- **Both facts come from `TransportSource`, not from parameters.** A boolean - parameter is a per-service opt-in no address has to qualify for, which is the - shape the host deleted. `codefly.For(ctx)` satisfies the interface, and - `(*Query).IsLocalRun` exists so a leaf can require the runtime through an - interface rather than importing the root module. -- **No refusal echoes a configured value** — not the URL, not the parser's own - message, and not the assertion, which is read from the secret namespace too. - Every plaintext refusal ends with `MeshTransportRemedy`. -- **The fetch stays on the admitted address**: a redirect is refused rather than - followed, the body is bounded, and the HTTP client is this module's. - -## The posture checks are commands whose EXIT STATUS is the answer - -`scripts/security-posture.sh` is how SP-WC-01 and SP-WC-05 are re-audited here. -It exists because the catalogue's own commands could not fail: they counted lines -(`go test … | grep -c '^--- PASS'`), 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, which is worse than no check** — -the invariant stops being audited and nothing says so. So the script reads the -exit status AND requires every named test to have run, and a renamed or deleted -test is a failure rather than a silent pass. - -## A derived credential never outlives its parent, and the test for that lives here - -Core's `derive` clamps a derived expiry to the parent's and refuses a parent -already past its own expiry; `TestDerivedContextNeverOutlivesParent` drives it -over a matrix of parent lifetimes and requested windows, plus a chain, a grant -capability and a widening hop. **The point of testing it from a module that -derives nothing** is that the test binds the version of core THIS module resolves -to, so the rule is required of whatever this module depends on rather than of -core in general. - -What the test cannot assert, and says where it asserts the rest: a verifier holds -the capability and not its parent, and `WorkContextV1` carries no -absolute-deadline claim and no per-hop timestamp — so the verification half needs -a claim, which is core's. A renewal in this module is not a derivation at all: -`MintClient` presents the projected token for a fresh mint and never the -credential it holds. - -## A credential is sealed or it is not a credential, and core says what that means - -The seal and each actor hop's epoch are schema-required, so a missing one is a -`protovalidate` refusal inside core's decode. The **execution** — -`image_digest` and `build_incarnation` — is a PAIR OR NEITHER: it lives in -`SealSource.ApprovedBuild` keyed on the principal, every hop attests its own, -and a principal that bears none (a person at a terminal) seals none rather than -inventing a value. An operation binding carries its id, revision and -incarnation or none of them; and core refuses an unknown field or a -non-canonical encoding, so nothing here may ever marshal a capability. - -**Zero is an answer, not a gap.** `GetBuildIncarnation()` returns 0 both for -"bears no execution" and for a value core says is never legitimate, since an -incarnation starts at 1. `mint.go`'s echo cross-check asks about PRESENCE -before comparing, because a host echoing `"0"` otherwise matched a capability -sealing no execution — a host asserting an execution the issuer does not hold, -passing the check that exists to catch that. `Attach` refuses an unsealed -capability with `corework.Decode`'s answer — false for a while precisely -because we answered it ourselves and never read the actor chain. - -**One sentinel for every structural seal defect, `ErrInvalid`.** `ErrUnsealed` -is deleted: once the schema requires the seal, no branch can produce it, and a -sentinel no branch produces invites a handler that never runs. - -**`Credential.Seal()` returns core's own wire message**, cloned. That is why -core making `ImageDigest` a required seal field cost this module no code: a -hand-filled struct would have needed a new field everywhere it was built. - -## Metadata leaves a stream by three routes, not one - -The embedded field; gRPC's own flush when the handler RETURNS; and the -package-level `grpc.SetHeader`/`SendHeader`/`SetTrailer`, which resolve a -`grpc.ServerTransportStream` out of the context and write to the transport. -Each was a live bypass found only after the previous one was closed, and the -last two were measured over bufconn with the guard revoked for the whole call -and never asked. - -So `GuardedServerStream` keeps the stream private and forwards every interface -method by hand, HOLDS headers and trailers until `Finish` re-checks and releases -them, and owns the transport stream in the context it hands out. -`GuardStreams` — not `Guard`, and not a bare interceptor — is the recipe, -because it does the -wrapping, the context and the `Finish`, and because its `Intercept` REFUSES -EVERY STREAM until `Validate(server.GetServiceInfo(), …)` has passed. The -interceptor constructor is unexported for that reason: exporting it beside the -validation meant the correct wiring took two calls and only one was reachable -from the type system, so a consumer could install the enforcement and skip the -check — which the README recipe itself did for several revisions. Whether a method is guarded is a -property of the METHOD and the first answer binds; a per-request `(nil, nil)` is -the optional-carrier shape this module deleted. - -A refusal reaches the client as a gRPC status: core's sentinels become -`codes.Unauthenticated`, an unreachable re-check source becomes -`codes.Unavailable`, and the Go error chain is kept alongside the code so -`errors.Is` still works in the server that produced it. **`ErrInvalid` is -`Unauthenticated` and this package's own misuse is `Internal`** — two sentinels, -because they were one: core answers an EXPIRED capability with `ErrInvalid`, so -mapping every `ErrInvalid` to `Internal` told a client not to mint again about -the most ordinary mid-stream refusal there is. `errStreamMisuse` wraps -`ErrInvalid` and is checked first. - -## The mint client holds a credential it cannot verify - -It is the party the credential is minted FOR, not a receiver, so it reads the -window through `corework.Decode` and checks no signature. Three consequences -that are easy to get backwards: - -- **The lifetime bound is CORE'S**, in `decodeClaims`, so `Inspect` carries it - — and `Inspect` is this client's read path. Core's first two attempts bounded - `Authority.Start` and then `Verify` alone, neither of which a holder can see, - so a thirty-day capability every `Verify` refuses was reported to its holder - as thirty days of validity. `MaxCredentialLifetime` survives as a DEPLOYMENT - POLICY below core's ceiling, defaulting to `corework.MaxTTLCeiling` so there - is no second number; exactly-at-ceiling is accepted and this module pins that - with its own test, because the default IS that constant. -- **The sentinel says what RECOVERY is possible.** `ErrMintRefused` is latched - and terminal — a process seeing one must stop serving — so anything transient - classified that way permanently stops a process holding a good credential: an - interrupted response read, a projected-token `EMFILE`, a momentarily empty - projection, a 408. All of those are `ErrMintUnavailable`. TLS verification - and peer-admission failures are also retryable: the handshake refuses them - before any projected token leaves, and rotation or admission can recover. - Local misuse is `ErrInvalid`, never `ErrMintRefused`, or `Refused()` and - `errors.Is` disagree. -- **The shared request is detached from whoever started it**, bounded by - `RequestTimeout`. On the caller's context, a cancelled leader counted as no - failure, so twenty callers with deadlines shorter than a degraded host's - latency produced twenty mint requests — each of which the host may complete - and audit while this process discards it. - -## The mint transport admits the peer before disclosing the projection - -`MintOptions` requires three sources: `TrustAnchor`, `ClientCertificate` and -`AdmittedPeers`. Construction checks configuration without calling any source. -The SDK owns its HTTP client and transport; a caller can supply neither. The -private TLS dialer reads the current anchor for normal chain and hostname -verification. `GetClientCertificate` reads the workload's current X.509-SVID; -the endpoint must request it. `VerifyConnection` reads admitted peers after -chain verification. After the handshake, the dialer re-reads roots and peers, -re-verifies the certificate chains and hostname, and repeats admission before -returning the connection to HTTP. - -Before reading admission, check the parsed leaf's X.509-SVID server purpose: -IsCA must be false; KeyUsage must include digitalSignature and exclude -keyCertSign and cRLSign. If the EKU extension is present, it must explicitly -include serverAuth, even if empty or carrying anyExtendedKeyUsage. Ordinary -Go verification alone does not enforce these restrictions. An absent EKU is -allowed. Both admission passes use the same leaf-purpose check. - -The leaf must contain exactly one URI SAN, read from the signed extension's -original bytes: serializing x509's parsed URL loses an empty fragment delimiter. -Both leaf and admitted SPIFFE IDs are validated before comparison; only scheme -and trust-domain case are ignored. The trust domain uses ASCII letters, digits, -dots, hyphens and underscores, with no empty labels; paths compare byte-for-byte. -Empty segments, trailing slashes, dot segments, userinfo, ports, queries, -fragments and percent-escapes are refused rather than normalized. - -Every mint, renewal and refresh dials fresh: TLS 1.3 minimum, no connection or -TLS session reuse, no compression, proxy or redirect. HTTP/1.1 prevents -multiplexing across admission decisions. Withdrawal during the handshake is -caught by the post-handshake check. Each source value's residual freshness -interval starts when it is sampled and ends at the transport's first HTTP -write. Rechecking shortens this to the re-read-to-write window, including later -source waits: the second peer callback may block after the second anchor was -sampled. Independent sources are not a coordinated snapshot; stronger -consistency requires a shared snapshot/version check. Withdrawal in this -interval cannot retract an in-flight request. - -Every source wait is bounded by the dial and request contexts, including -post-handshake reads. Cancellation closes the raw connection while a callback -or private-key operation is blocked. Like `projectedToken`, all three transport -sources and the TLS operations they trigger share one outstanding worker slot -per client across retries, held through both handshake phases. Arbitrary -callbacks and signers cannot be interrupted; cancellation bounds the wait and -closes the socket, but only the worker's return releases its slot. Later -attempts check the slot before dialing and refuse without a connection attempt -until then; late answers are discarded and the worker exits. Recovery reads -fresh data. TLS alone performs signing. - -A missing source is `ErrInvalid` at construction. A malformed admitted entry -is `ErrInvalid` at the handshake, even alongside a valid entry. Unreadable or -nil anchors, unreadable or empty certificates, empty or unreadable peer sets, -malformed admitted IDs and TLS failures return `ErrMintUnavailable`; malformed -admitted IDs also retain `ErrInvalid`. Invalid leaf purposes and invalid, -ambiguous or unadmitted leaf identities additionally wrap -`ErrMintPeerNotAdmitted`. These errors never latch. -`mint_transport_test.go` requires no HTTP disclosure on refusal, withdrawal and -rotation inside the handshake, bounded source reads and connection closure on -cancellation, and recovery after release or corrected projections. +This module's coverage gate is the **module total** across both its packages, +≥ 75%, computed with `awk` in CI (`.github/workflows/go.yml`, `coverage` job) — +not the root's `.testcoverage.yaml`, and the two are not interchangeable. +`grpctransport` sitting low is carried by the verifier: read the total, not a +package line.