Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f3e271d
feat(workcontext)!: mint once per execution and seal the credential t…
antoinetoussaint-byte Oct 2, 2026
0afe59c
refactor(workcontext)!: delete this module's Work Context implementat…
antoinetoussaint-byte Oct 2, 2026
e84ae76
docs(receipts): name the verifier the interceptor defers to, which is…
antoinetoussaint-byte Oct 2, 2026
40bd5ec
fix(workcontext): name a foreign encoding with core's own check, not …
antoinetoussaint-byte Oct 2, 2026
75e99cc
docs(workcontext): say what the conformance kit actually drives
antoinetoussaint-byte Oct 2, 2026
45501b7
ci(workcontext): sweep every ref CI builds for a second implementatio…
antoinetoussaint-byte Oct 2, 2026
defe3cc
fix(workcontext)!: refuse plaintext and redirects on every mint clien…
antoinetoussaint-byte Oct 2, 2026
c035e5e
fix(workcontext)!: core's Inspect owns the seal rule, the client owns…
antoinetoussaint-byte Oct 2, 2026
923e909
fix(workcontext): freshness before any cached credential, and the dyn…
antoinetoussaint-byte Oct 3, 2026
130c215
refactor(workcontext)!: re-pin to core 9f68df85 — ErrUnsealed is dele…
antoinetoussaint-byte Oct 3, 2026
24a833d
fix(workcontext)!: only an outage serves the held credential — a refu…
antoinetoussaint-byte Oct 3, 2026
b9b8584
fix(workcontext)!: the wrapper hides the raw stream and holds trailer…
antoinetoussaint-byte Oct 3, 2026
8440c8e
refactor(workcontext)!: re-pin to core's settled surface at cd443989 …
antoinetoussaint-byte Oct 3, 2026
42b2a17
fix(workcontext)!: the stream wrapper owns the context, the mint requ…
antoinetoussaint-byte Oct 3, 2026
d943934
fix(workcontext)!: the gate reads both modules, latching is the enume…
antoinetoussaint-byte Oct 3, 2026
520a31b
refactor(workcontext): re-pin to core 67ee7220 — the lifetime bound i…
antoinetoussaint-byte Oct 3, 2026
892a854
docs(workcontext): the ceiling's comment says what the code now does
antoinetoussaint-byte Oct 3, 2026
67271e6
refactor(workcontext)!: re-pin to core 4cb260d3 — the execution is pe…
antoinetoussaint-byte Oct 3, 2026
efaccef
fix(workcontext)!: the ref sweep looked at almost nothing, and the gr…
antoinetoussaint-byte Oct 3, 2026
8630806
refactor(workcontext): re-pin to core 4051e438 — Recheck is this veri…
antoinetoussaint-byte Oct 3, 2026
1d6947d
fix(workcontext)!: a function call is not a type, a frozen struct is …
antoinetoussaint-byte Oct 3, 2026
04e93c0
fix(workcontext)!: a gate that could not look reported success, and a…
antoinetoussaint-byte Oct 3, 2026
3b697f0
test: a reproducer that depended on the machine's git configuration
antoinetoussaint-byte Oct 3, 2026
2f2d721
feat(workcontext)!: deny by default, decide with go/types, and retrac…
antoinetoussaint-byte Oct 3, 2026
07a93ec
test: the sweep fixtures name no branch outside this repository
antoinetoussaint-byte Oct 3, 2026
d8cb0af
fix(workcontext)!: one import policy for every ref, an interface is a…
antoinetoussaint-byte Oct 3, 2026
998f218
fix(workcontext)!: the latch sites audited, one outstanding token rea…
antoinetoussaint-byte Oct 3, 2026
2aaa755
fix(workcontext)!: the retract interval covered nothing, and the boun…
antoinetoussaint-byte Oct 4, 2026
cdc33fb
fix(workcontext)!: the gates must read every shipped file, and every …
antoinetoussaint-byte Oct 4, 2026
7ea600f
fix(workcontext)!: the method is the contract, not the shape — and a …
antoinetoussaint-byte Oct 4, 2026
151e103
fix(workcontext)!: the validation is mandatory, and an unreachable br…
antoinetoussaint-byte Oct 4, 2026
e99df34
fix(workcontext)!: the batched audit fails closed per ref, and there …
antoinetoussaint-byte Oct 4, 2026
d7876d5
refactor(workcontext)!: re-pin to core db9197f7 — the execution is pe…
antoinetoussaint-byte Oct 4, 2026
b1b10cb
fix(ci): the required check answered differently depending on which e…
antoinetoussaint-byte Oct 4, 2026
ae32f9b
refactor(workcontext)!: pin core v0.9.0 — the tag, not a sha that lea…
antoinetoussaint-byte Oct 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions .claude/skills/dual-module-change/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: dual-module-change
description: Use when a change touches workcontext/ — the leaf module — or both it and the root SDK. Covers the dependency budget that is the module's whole reason to exist, the two go.mod/go.sum pairs that must be tidied independently, the two different coverage gates, and how a consumer resolves a version of a Go submodule. Reach for it when a root-module command reports "ok" but workcontext was never compiled, when `go mod tidy` at the root leaves workcontext dirty, or before adding any import to workcontext.
description: Use when a change touches workcontext/ — the leaf module — or both it and the root SDK. Covers the dependency budget and the one rule that overrides it (the Work Context has one implementation, core's), the two go.mod/go.sum pairs that must be tidied independently, the two different coverage gates, and how a consumer resolves a version of a Go submodule. Reach for it when a root-module command reports "ok" but workcontext was never compiled, when `go mod tidy` at the root leaves workcontext dirty, or before adding any import to workcontext.
---

# Changing the two modules
Expand All @@ -14,21 +14,37 @@ That is the contract. Everything below follows from it.

## The dependency budget

`workcontext/go.mod` currently has three direct requirements — `core` (for the
`codefly/base/v0` proto types), `testify`, `grpc` — and five indirect ones. The
root SDK's indirect list is roughly fifty.
`workcontext/go.mod` has four direct requirements — `core` (the
`codefly/base/v0` proto types **and** `core/workcontext`), `protobuf`,
`testify`, `grpc`. The root SDK's indirect list is roughly fifty.

**Adding an import to `workcontext` is a design change.** Before you do:
The fourth one arrived by decision, not by accident, and the decision is worth
knowing before you argue from the budget: `core/workcontext` pulls
`protovalidate` → `cel-go` → `antlr`, which this module used to avoid, and it
was taken anyway because the Work Context has exactly one implementation and it
is core's. A smaller `go.sum` is not worth a second implementation of a signed
credential. See `workcontext/README.md`, "One implementation, and the gate that
keeps it that way".

- Can the type come from `codefly/base/v0`, which the module already has?
**Adding a further import to `workcontext` is still a design change.** Before
you do:

- Can the type come from `codefly/base/v0` or `core/workcontext`, which the
module already has?
- Does it belong in `workcontext/grpctransport/` instead? That subpackage exists
so a verify-only consumer never compiles grpc. Anything transport-shaped goes
there, not beside the verifier.
so a consumer that makes no gRPC call never compiles grpc. Anything
transport-shaped goes there.
- Does it belong in the root SDK, with `workcontext` left alone?

If none of those work, say in the PR body what the consumer's `go.sum` now
costs. Growth is a decision someone makes, not a side effect of an import line.

**One import is refused outright**: nothing in this module may import
`crypto/ed25519` or `crypto/ecdsa`, and only `mint.go` may import
`encoding/json` (the mint endpoint's HTTP bodies).
`TestNoSecondWorkContextImplementation` fails the build on either, which is the
gate rather than this paragraph.

The root module must never import `workcontext` to reach shared code. That would
make the leaf module a dependency of the thing it was split out of, and a
consumer taking the leaf would be back to the full tail.
Expand Down
131 changes: 124 additions & 7 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
name: codefly go SDK

on:
# compat/** carries the release lines consumers pin when main holds an
# unreleased breaking change. A branch whose commits are pinned by a consumer
# is a published artifact, so it is built like one.
# ANY branch a consumer can pin is a published artifact and is built like
# one. This used to name compat/** as "the release lines consumers pin",
# which was a rule about a naming convention: a consumer pins a commit, and
# go.mod records a pseudo-version with no idea what the branch was called.
# The one-implementation sweep covers every published ref for the same
# reason, and a dependabot branch carrying the deleted implementation is what
# showed the convention was doing no work.
# So the triggers name no convention either. They said `compat/**` for three
# revisions after the comment above stopped saying it, which meant a branch a
# consumer can pin — a dependabot branch, say — was never built and never
# swept by its own push, while the comment claimed every published ref was
# held to the rule.
push:
branches: [ main, 'compat/**' ]
branches: [ '**' ]
pull_request:
branches: [ main, 'compat/**' ]

concurrency:
group: sdk-go-${{ github.workflow }}-${{ github.ref }}
Expand All @@ -26,7 +34,15 @@ jobs:
module: [ ".", "workcontext" ]
steps:
- name: Checkout
# fetch-depth 0 so the TAGS are present.
# TestEveryPublishedTagCarryingTheImplementationIsRetracted sweeps every
# published tag and refuses to pass having seen none — nineteen of them
# publish the deleted implementation and a tag can only be retracted,
# never fixed. A shallow checkout would have made that gate green by
# looking at nothing.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
Expand All @@ -42,6 +58,77 @@ jobs:
go mod tidy
git diff --exit-code -- go.mod go.sum

# The sweep lives HERE, in the workflow that builds every ref this repository
# publishes, rather than in one of its own that ran on main alone. go.yml's
# own `on:` is the statement of which refs are built, so a gate that has to
# hold on all of them belongs beside the build rather than beside a list of
# refs that drifts from it.
#
# The job's name is load-bearing: the active ruleset on main requires the
# status check "no second Work Context implementation" by that exact context,
# so renaming this job would leave a required check that never reports.
one-implementation:
name: no second Work Context implementation
runs-on: ubuntu-latest
# Ten, not five. This job sweeps the working tree and every published ref,
# reading imports with go/parser — one `git cat-file` per Go file per ref.
# Measured locally between 22s and 131s depending only on the OS file
# cache, and a CI runner is slower than that. A gate that times out is a
# gate somebody deletes, and the honest answer to "the gates cost more now"
# is to say what they cost.
timeout-minutes: 10
steps:
- name: Checkout
# fetch-depth 0 so every published ref is reachable: the second step
# reads them out of the object database.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Set up Go
# The sweep reads imports with go/parser now (scripts/importsof),
# because a regex over lines cannot read Go — `import
# "\x63rypto/ed25519"` compiles and is crypto/ed25519 to the compiler.
# So this job needs a toolchain, which it did not before.
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Build the import reader once
# Built once and passed to every invocation, rather than rebuilt for
# each of ~25 refs.
run: |
go build -o "$RUNNER_TEMP/importsof" ./scripts/importsof
echo "IMPORTSOF_BIN=$RUNNER_TEMP/importsof" >> "$GITHUB_ENV"
- name: Sweep every tracked Go file for a second implementation
run: ./scripts/check-one-implementation.sh
- name: Sweep every OTHER ref this repository publishes
# The logic lives in scripts/sweep-published-refs.sh so it can be
# TESTED. Inline, it was wrong and nothing could notice: it listed refs
# with `git for-each-ref 'refs/remotes/origin/*'`, and `*` does not
# cross a slash, so every namespaced branch was silently dropped. This
# step reported "sweeping 1 other published ref(s)" and "ok
# origin/badges" and went GREEN while
# origin/dependabot/go_modules/gomod-06d3dc2861 published
# workcontext/work_context.go importing crypto/ed25519.
#
# The script lists by prefix and asserts SWEPT AGAINST WHAT THE REMOTE
# PUBLISHES, because a gate that sweeps a subset and reports success is
# worse than no gate — the green is the evidence somebody acts on.
# SWEEP_BASE is passed EXPLICITLY, from the repository's own default
# branch, so the sweep does not have to guess which ref this change
# merges into. Guessing is what made this check answer differently on a
# push build than on a pull_request build: GITHUB_BASE_REF is empty on
# a push, the base fell through to the pushed branch, and the default
# branch — which carries the implementation until this merges — was
# swept and failed.
env:
SWEEP_BASE: ${{ github.event.repository.default_branch }}
SWEEP_HEAD: ${{ github.head_ref || github.ref_name }}
run: |
git fetch --no-tags --prune --quiet origin \
'+refs/heads/*:refs/remotes/origin/*'
./scripts/sweep-published-refs.sh

lint:
name: lint
runs-on: ubuntu-latest
Expand All @@ -51,7 +138,15 @@ jobs:
module: [ ".", "workcontext" ]
steps:
- name: Checkout
# fetch-depth 0 so the TAGS are present.
# TestEveryPublishedTagCarryingTheImplementationIsRetracted sweeps every
# published tag and refuses to pass having seen none — nineteen of them
# publish the deleted implementation and a tag can only be retracted,
# never fixed. A shallow checkout would have made that gate green by
# looking at nothing.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
Expand All @@ -70,7 +165,10 @@ jobs:
coverage:
name: coverage
runs-on: ubuntu-latest
timeout-minutes: 5
# Ten: the root suite now includes the published-tag audit, which
# sweeps 83 tags through the import reader. Measured 22s-131s on a warm
# versus cold file cache.
timeout-minutes: 10
permissions:
contents: write
# The receipts store is written in the caller's transaction, admits on a
Expand All @@ -95,7 +193,15 @@ jobs:
CODEFLY_TEST_POSTGRES_DSN: postgres://codefly:codefly@localhost:5432/receipts?sslmode=disable
steps:
- name: Checkout
# fetch-depth 0 so the TAGS are present.
# TestEveryPublishedTagCarryingTheImplementationIsRetracted sweeps every
# published tag and refuses to pass having seen none — nineteen of them
# publish the deleted implementation and a tag can only be retracted,
# never fixed. A shallow checkout would have made that gate green by
# looking at nothing.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
Expand Down Expand Up @@ -123,7 +229,10 @@ jobs:
race:
name: race
runs-on: ubuntu-latest
timeout-minutes: 5
# Ten: the root suite now includes the published-tag audit, which
# sweeps 83 tags through the import reader. Measured 22s-131s on a warm
# versus cold file cache.
timeout-minutes: 10
strategy:
matrix:
module: [ ".", "workcontext" ]
Expand All @@ -149,7 +258,15 @@ jobs:
CODEFLY_TEST_POSTGRES_DSN: postgres://codefly:codefly@localhost:5432/receipts?sslmode=disable
steps:
- name: Checkout
# fetch-depth 0 so the TAGS are present.
# TestEveryPublishedTagCarryingTheImplementationIsRetracted sweeps every
# published tag and refuses to pass having seen none — nineteen of them
# publish the deleted implementation and a tag can only be retracted,
# never fixed. A shallow checkout would have made that gate green by
# looking at nothing.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
Expand Down
4 changes: 3 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ linters:
exclusions:
generated: lax
rules:
# ST1005 forbids a capitalized error string, which it cannot tell from a
# proper noun: every sentinel in these files begins with "Codefly".
- linters:
- staticcheck
path: (^|/)(runtime_environment_file|work_context)\.go$
path: (^|/)(runtime_environment_file|mint|stream)\.go$
text: "ST1005:"
paths:
- .*\.pb\.go
Expand Down
8 changes: 8 additions & 0 deletions .testcoverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ threshold:
exclude:
paths:
- \.pb\.go$
# scripts/ is TOOLING, not the library a consumer imports: importsof reads a
# Go file's imports for the ref sweep and runs as a built binary, so the
# test that drives it (TestTheImportReaderAnswersWithWhatGoSees) execs it
# and no coverage counter is attributed. Counting it would lower the gate on
# the library in exchange for a number about a 40-line main, and raising
# the gate by excluding real code is the thing this line must not become —
# it names scripts/ exactly, not a pattern that grows.
- ^scripts/
45 changes: 39 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,20 @@ package line.
| --- | --- |
| `codefly.go` | `Init`, the immutable env snapshot, `Inject*`, process-level accessors |
| `for.go` | the `For(ctx)` query: endpoints, configuration, secrets, workspace values |
| `authority.go` | authority-bearing values, read once at boot; a drift is refused, never reloaded |
| `runtime_value.go` | `RuntimeValue`, for values the runtime injects directly |
| `runtime_environment_file.go` | loading a runtime-written env file |
| `fixture.go` | the selected fixture, and resolving its principals by role |
| `tls.go` | workload leaf certificates, reloaded on rotation |
| `receipts/` | effect receipts: the store, the digest, the replay/conflict interceptor |
| `receipts/grpctransport/`, `receipts/connecttransport/` | the two transport adapters, split so neither drags the other's dependency in |
| `workcontext/` | **separate leaf module**: Work Context signing and verification |
| `workcontext/grpctransport/` | the gRPC carrier, so verify-only consumers never compile grpc |
| `workcontext/` | **separate leaf module**: the mint-once client, the carriers, the cache partition, the stream guard, and typed access to core's one implementation. Its own `AGENTS.md` holds what the two gates refuse |
| `workcontext/grpctransport/` | the gRPC carrier, so a consumer that makes no gRPC call never compiles grpc |

`workcontext` exists to keep a verify-only consumer's `go.sum` small. Adding a
dependency to it is a design change, not a detail — see the skill below.
`workcontext` mints nothing and verifies nothing: `core/workcontext` is the only
implementation of the capability, and this module re-exports core's verifier by
type alias. Adding a dependency to it is still a design change, not a detail —
see the skill below.

## Rules that bite

Expand All @@ -113,12 +116,42 @@ dependency to it is a design change, not a detail — see the skill below.
- **An empty injected value is not a value.** A composition templating an unset
variable ships the name with an empty string; `RuntimeValue` reports `false`
so it cannot shadow the configuration a caller falls back to. Keep that.
- **The Work Context has exactly one implementation and it is not here.**
`core/workcontext` signs, verifies, and answers the *structural* question
through `Inspect`. Nothing here may sign, check a signature, encode a
capability **or decide what a capability is** — a local seal rule is a second
implementation even when it signs nothing, and ours disagreed with core's own
fixtures about which sentinel three refusals earn. **Four gates hold that, and
all four read both modules**: a **deny-by-default import allowlist per
module** and a **`go/types`-decided codec rule** (both via
`x/tools/go/packages`), the syntactic AST gate, and
`scripts/check-one-implementation.sh` over every published ref, which reads
imports with `go/parser` because a regex cannot — `import "\x63rypto/ed25519"`
compiles. The allowlist replaced a denylist of names that was behind by one
entry every round for ten rounds, the last being `crypto/mldsa` signing a
`WorkContextV1` that `encoding/json/v2` had encoded in the deleted format,
with both gates green. `testdata` is walked and `import "C"` is refused.
**Published TAGS cannot be fixed, only retracted**: nineteen of them carry the
deleted implementation and `go.mod` retracts them.
**`workcontext/AGENTS.md` is what each one refuses and why** — read it before
touching that module, the gate, or the sweep.
- **An authority-bearing value is read once.** A principal, binding or audience
comes from `ReadAuthority` at boot. `WorkspaceValue` answers from that pin for
a pinned name, so a drift is an error rather than a reload — the process has
already minted a credential sealed to the old value.
- **A receipt is written inside the transaction that commits its effect.** A
receipt written after the commit leaves a window where the effect exists and
the receipt does not, and a recovery landing there reads "no receipt" for an
effect that already happened. That is why `receipts.Record` takes a `Tx`.
- **`compat/**` branches are published artifacts.** Consumers pin them when
`main` holds an unreleased breaking change, so CI builds them like `main`.
- **Every published ref is held to these rules, not exempted from them.** A
consumer can pin any branch, so the sweep covers every one rather than a
`compat/*` name glob: measured, 22 of 25 remote branches still carried the
deleted implementation, so retiring the three NAMED compat refs would have
turned the gate green with 19 copies published. A line that cannot meet a
rule is **retired** by the owner, never granted a period during which the
rule is false here. A bullet forbidding the back-port of a breaking deletion
used to live here; it is what kept the implementation alive on refs CI
builds, so it is deleted.

## Procedures

Expand Down
Loading
Loading