Skip to content

Releases: Aftergraph/works-execution

v0.3.5 — per-action identity completion (close k-064 backlog)

Choose a tag to compare

@JonasAbde JonasAbde released this 03 Sep 08:32

v0.3.5 — per-action identity completion (close the entire k-064 backlog)

Tag: v0.3.5 → main 9d4f92c (squash-merged PR #37)
Range since v0.3.4: 526f507..9d4f92c (1 PR; + the 3ff0db2 coverage restore from main)
Status: merged to main, gates green, NOT yet deployed to :18191 — deploy + live smoke on request.

Why this PR matters: v0.3.4 shipped three open findings as pinned tests (k-064 B, C, D) with flip-on-fix instructions. v0.3.5 fixes all three; each pin flips in-wave from "gap" to regression — a future regression in any of them is caught immediately.

What landed

Slice File Fix Pin (now regression)
k-065 services/api/lease_owner_authz.go + leases.go gateLeaseOwner in leaseItemHandler: bearer token's worker_id must equal lease's WorkerID. 404-before-403 (no id-oracle on the verb surface). Dev mode passes. Denial provably leaves lease ACTIVE. TestAdversary34_NonGrantLeaseVerbsUnbound — foreign revoke now 403 lease_not_owner; l.Status == LeaseActive after the denial
k-066 services/runner/registry.go + services/api/enroll.go validWorkerID delegates to exported runner.RunnerIDPattern (^wrkr_[a-z0-9_-]{1,64}$). The two charsets are now ONE law. runner.RunnerIDPatternSource exported for error messages. TestAdversary34_EnrollmentCharsetLegacyPass — rewritten to drive the REAL /v1/workers/enroll endpoint (fixture mint bypasses enrollment by design; a34EnrollSecret fixture makes the HTTP path testable)
k-067 services/api/leases.go grantLease trims body.WorkerID before any gate or write. Padded lookalike ("wrkr_a ") is the same identity as canonical, so the dev-mode control-token gate sees the real runner. TestAdversary34_DevModeLookalikeWorkerIDEscapesGate + the pPaddedTokA matrix rows (both postures now answer identically)

Plus the 3ff0db2 ancestor (in main) that restores 1204 lines of adversary_v034_test.go dropped by v0.3.4's squash-merge. Lesson pinned in the commit message: after squash-merge, verify the test FILES on main, not just the fix symbols (the 14 Adversary34 pins are the only way the user can see the laws holding; if they're gone, the laws are invisible).

Composition-adversary ledger (k-064) — CLOSED

Finding Status Pin
A k-061 broke cmd/works-runner-id -register in prod (CLI gained -token) closed v0.3.4 regression
B Enrollment charset ⊃ registry charset closed v0.3.5 regression (k-066)
C Dev-mode lookalike worker id forgery closed v0.3.5 regression (k-067)
D Non-grant lease verbs not owner-bound closed v0.3.5 regression (k-065)
E AUTH.md lied about works subtree / readyz / omitted laws closed v0.3.4 regression
F not_runner_owner inlined literal closed v0.3.4 n/a (cosmetic)

k-065 — owner-bind the four non-grant lease verbs

gateLeaseOwner sits in leaseItemHandler AFTER the method check and BEFORE the verb dispatch. Every action on an existing lease (heartbeat/complete/release/revoke) is now owner-bound. The grant verb's owner-bind is in grantLease (k-060) — distinct entry point, distinct gate, same interlock. Dev mode (no claims) passes through; e2e posture is unaffected.

Denial order is 404-before-403 — a not-found lease returns 404 (not_found) and never 403, so the verb surface cannot be used to confirm whether a lease id exists. k-064 (D) already proved lease ids appear on no read surface; k-065 removes even the denial-side channel.

The pin test now requires (a) 403 lease_not_owner on a foreign revoke, (b) l.Status == LeaseActive after the denial (proven no state mutation), and (c) the existing mitigation sweep (no read surface leaks the id).

k-066 — enroll and registry share one charset

validWorkerID no longer accepts a broad [A-Za-z0-9_.-]{1,128} — it delegates to runner.RunnerIDPattern.MatchString. The pattern is the same ^wrkr_[a-z0-9_-]{1,64}$ that runner.BuildIdentity enforces; the two layers are now a single invariant.

Why this closes B: the k-058 legacy-pass class is "no RAB on file" ⇒ claim sails through. With k-066, no token can exist for an id the registry rejects, so the legacy-pass path is unreachable for any authenticated identity. The pin test now drives the real /v1/workers/enroll endpoint (the fixture mints via Auth.Mint, bypassing enrollment by design — every other test uses it for clean identity fixtures; this one specifically must not). Defense in depth: even a hand-minted token cannot register the id (the registry's own Validate refuses it).

k-067 — trim the worker id at grant

grantLease calls strings.TrimSpace(body.WorkerID) immediately after the missing_field guard and BEFORE the k-060 owner gate. A whitespace-only id is rejected with 400 invalid_worker_id. Otherwise, padded and canonical are the same string for the rest of the request.

The ordering law is preserved: identity question (owner bind) still precedes capability question (control-token-required), but with a normalized body. The TestAdversary34_ClaimGateOrderOwnerFirst weird-rows pin splits into two classes — padded (now 403 control_token_required because they trim to the canonical runner and hit the capability gate) and other (still 403 worker_id_mismatch because they mismatch the verified identity exactly).

Gates (this head, /tmp/wt-v035 + main post-merge)

  • go build ./... — green
  • go vet ./... — green
  • go test ./... -count=1 — green, 9 packages with output
  • go test -tags=e2e ./e2e/... -count=1 — green
  • go test ./services/api/ -run Adversary34 — all 14 pins green
  • ./bin/works-standards audit — exit 0
  • gofmt -l clean on every file touched in this PR (3 pre-existing repo flags on base untouched)
  • Id-collision check: wrkr_prod_1, wrkr_v034smoke, wrkr_v033smoke all match the new pattern (direct regex check)

v0.3.4 → v0.3.5 migration (operators)

  • API only — no env, no schema, no wire-format change.
  • Behavior change (prod, auth ON): heartbeat / complete / release / revoke now require the bearer's worker_id to equal the lease's WorkerID. Any internal service holding a lease issued under a different identity will get 403 lease_not_owner — check the lease id and the bearer before deploy.
  • Behavior change (prod, enrollment): worker IDs that don't match ^wrkr_[a-z0-9_-]{1,64}$ (e.g. uppercase WRKR_…, or longer than 64 chars after wrkr_) will be refused at /v1/workers/enroll with 400 invalid_worker_id. Production worker id wrkr_prod_1 matches.
  • Behavior change (dev, padding): wrkr_a and wrkr_a are now the same identity at grant; the dev-mode lookalike path is closed.

Live smoke (post-deploy, on :18191)

smoke_v035.sh extends smoke_v034's 9 clauses with:

  • foreign revoke → 403 lease_not_owner, victim lease still ACTIVE
  • enroll(WRKR_A) → 400 invalid_worker_id
  • padded claim → control-token gate (403 control_token_required), not 201

What's NOT in this PR

  • The internal/worker Go client's enroll path uses its own identity generation (runner.MintRunnerID()), which already matches the new pattern — no client change required.
  • services/publisher pre-existing race (k-impl-021) — out of scope; non-race is the project gate.

v0.3.3 — enforcement wave (k-054 re-land + k-057/058/059)

Choose a tag to compare

@JonasAbde JonasAbde released this 03 Sep 04:39

v0.3.3 — enforcement wave: the laws now run on production paths

Tag: v0.3.3 → main dadfb6a (squash-merged PR #35)
Range since v0.3.2: 606a28e..dadfb6a (1 PR; k-054 re-land + 3 new slices)
Status: shipped, merged, deployed, runtime-verified

Also corrects v0.3.2: PR #34 was merged from an integration tip that predated the k-054 adversary results, so main received slices k-051/52/53 only. v0.3.3 re-lands the full k-054 set (deep-copy fix, namespaced linkage, adversary tests) — see docs/RELEASE-v0.3.2.md correction note. Verified by git grep on origin/main, not diffstats.

What landed

v0.3.1 shipped kernel laws. v0.3.2 wired them into service internals. v0.3.3 makes them enforce where production actually runs — plus the re-land.

Slice Area Commit What it enforces
k-054 (re-land) services/api (in #35) cloneRAB deep-copies Extra via kernel JSON round-trip; RuntimeABI linkage namespaced under rab_runtime_meta; 3 adversary tests (A/B regressions, C now closed)
k-057 internal/worker 5b67942 ADR-0022 on the REAL exec path: secret refs resolved once per item before the docker/command branch
k-058 services/api 38f0ce9 rab/1.0 control-token law gates lease grant: control-capable RAB ⇒ claim without X-RAB-Control-Token => 403 BEFORE any state transition
k-059 services/api 245398a bearer auth on mutating POST /abi (closes k-054 finding C); reads stay public; anonymous downgrade => 401

k-057 — secrets on the production worker path

The internal/worker daemon executes ReadyItem.Env literally. A secret:// value there previously reached the child env unresolved. Now resolveItemEnv replaces every ref value with its resolved secret at execution time (mapping from the packages/secrets kernel; SECRET_<PROVIDER>_<NAME>), and a resolution failure means the node NEVER EXECUTES — the error names the ref only, never a value. Zero-ref items are byte-identical to the old behavior (pinned by test). Exec-level test proves resolution reaches cmd.Env via printenv; sentinel leak-sweep over every failure path.

k-058 — the control-token law gates claims

A runner whose advertised RAB RequiresControlToken() can no longer grant itself a lease without presenting X-RAB-Control-Token. Denied claims return 403 with the lease state UNCHANGED (the test asserts the pre-transition ordering deterministically). Interlock shipped: the claiming worker_id resolved against the runner registry — the worker_id == runner_id convention already load-bearing for BYOC pool enforcement; no RAB on file ⇒ legacy pass (pre-k-053 runners unaffected). Token VALUE verification against an issuing authority is explicitly out of scope and documented at the check site — this enforces the ADVERTISEMENT law, which is what rab/1.0 is.

k-059 — no more anonymous capability downgrade

POST /v1/runners/{id}/abi is behind requireBearer. The k-054 pin flipped to TestAdversary_UnauthenticatedRABDowngradeBlocked: anonymous downgrade now 401s and the victim's RAB is unchanged; an authenticated cross-runner rewrite still succeeds — pinned as the residual per-action-authz gap (auth.go's own 'NOT a substitute for per-action authz' boundary; next slice if it becomes load-bearing).

Live runtime-verify (prod :18191, post-deploy 06:38)

register:201
anon-abi-POST:401                     ← k-059 closed
auth-abi-POST:200  (control+required)
claim WITHOUT control token:  HTTP 403 {"error":"control_token_required", ...}   ← k-058 law live
claim WITH    control token:  HTTP 201 lease granted                              ← gate precedes state change
meta-linkage: True | caps: ['observe','control']                                  ← k-054 B fix live

Smoke work cancelled after the test; zero errors/panics in journal since restart.

Gates (stacked integration worktree, per k-050/k-054 doctrine)

  • go build / go vet / full go test ./... — green on the STACK, not just per-branch
  • e2e suite green; Adversary+ClaimGate+ABI suites green together; internal/worker green
  • gofmt clean on all touched files
  • exact-head CI works-execution=success, post-merge main verified by direct grep of each fix on origin/main

What's NOT in this PR

  • Per-action authz (which token may rewrite WHICH runner) — open by design, pinned by test
  • Token value verification (issuing-authority check) — out of scope per k-058 docstring
  • services/publisher pre-existing race (k-impl-021; non-race is the project gate)

v0.3.2 — integration wave + composition-adversary gate

Choose a tag to compare

@JonasAbde JonasAbde released this 03 Sep 04:11

v0.3.2 — integration wave: secrets, obslaw, abi wired into runtime

Tag: v0.3.2 → main e217869 (squash-merged PR #34)
Range since v0.3.1: 0c6e2c2..e217869 (1 PR, 4 slices, 1 composition-adversary gate)
Status: shipped, merged, deployed, runtime-verified

What landed

Three pure-law kernels from v0.3.1 (packages/{secrets,obslaw,abi}) are now wired into the services that have a real runtime use for them. Each slice landed on its own branch with disjoint file ownership, then stacked for composition verification. The composition-adversary sweep (k-054) ran in a fresh context against the stacked tree and pinned three real, reproducible seams — two fixed in this PR, one pinned as a separate auth-surface question.

Slice Branch Commit Files New tests
k-051 secret-ref runtime v032/secret-runtime e7e3504 services/runner/{secrets,secrets_test}.go (new), real.go (+34/-3) 9 funcs / 19 cases
k-052 obslaw evidence+audit v032/obs-evidence b2d0fa1 services/evidence/obslaw.go, services/audit/obslaw.go (new), bundle.go +16, cloud_events.go +9 15 (9+6)
k-053 rab/1.0 registry v032/abi-registry ae996bc services/api/runner_abi.go (new, 320 lines), runner_abi_test.go, runner_register.go +8, api.go +13 ~60 cases
k-054 composition-adversary v032/adversary d17b537 services/api/adversary_test.go (new, 312 lines) 3 (1 pin + 2 regressions)

k-051 — secret refs at execution time (ADR-0022)

SecretResolver.ResolveEnv(ctx, scope, env) resolves any secret://… value in Step.Env immediately before exec.CommandContext. Values land only in cmd.Env; never in Result/StepResult, never in any error string, never cached. NewEnvSecretResolver is an adapter over packages/secrets.EnvResolver (nil lookup → os.LookupEnv, fresh kernel struct per call). Nil resolver is byte-for-byte legacy pass-through (pinned by test).

Deviation: cmd/works-worker wiring is honestly documented as a non-trivial follow-up: there is no one-line enrollment/Run call site; the worker today uses internal/worker/worker.go and runner is not on that path. Stray secret://… strings in the worker are inert (never resolved, cached, or echoed into evidence/audit); surfacing the wire is the integrator's call.

k-052 — obslaw boundary in evidence and audit (ADR-0024)

Evidence side. (*Bundle).ObsLawRecord() projects a bundle into the law (Kind=evidence, Signed=true, Trimmable=false); CitesHash is empty by design — the kernel law requires 64-hex, but the wire-side bundle_id is evb_ + sha256[:32hex] and the full digest is not retained on the struct (intentional). Documented in the source. AttestBundle(b, key) layers a law-level Attested{Record,Signature} via obslaw.Verifier in addition to (never replacing) the existing Signatures. A fail-fast law check is wired into the Produce hot path — untriggerable today by construction, but it is the real choke point that catches future drift.

Audit side. LawRecord(e) projects a CloudEvent as Kind=event, Signed=false. CheckEvent is wired into SQLiteEmitter.Emit as a fail-fast assertion. Tests prove the teeth: a hand-crafted Kind=event + Signed=true record is rejected at the kernel with ErrEventCannotBeSigned.

Zero schema edits. Zero behavior change for existing consumers.

k-053 — rab/1.0 in the runner registry (ADR-0012/0014)

POST /v1/runners/{id}/abi publishes a validated rab/1.0 advertisement, GET reads it, POST …/negotiate returns the law-level (granted_caps, control_token_required, err). Routes are mounted in api.go AFTER the existing /v1/runners/{id} prefix handler so the more-specific patterns win. The control-token law (caps contains control ⇒ control_token_required must be true) is enforced at POST with the kernel's own error message and is visible on the wire in negotiate responses so callers can gate privileged operations. Identity-first order: a RAB requires a registered identity (404 if not). Overwrite is idempotent upsert; no DELETE.

k-054 — composition-adversary gate

Fresh-context sweep against the stacked tree. Three real seams found:

A — cloneRAB shallow copy of Extra (now fixed). The original implementation did out.Extra[k] = v, which is a shared reference for any nested map/slice — the N-1 kernel shape accepts arbitrary JSON, so any nested object aliased the stored advertisement in both directions. Reader mutating a record handed out by getABI/getRuntimeABI/listABI silently rewrote what every future GET served. Fix: cloneRAB now re-canonicalises Extra through the kernel via json.Marshal + json.Unmarshal into a fresh map[string]any — a complete deep clone of every JSON value the kernel accepts. TestAdversary_RABCopyOutPromiseBrokenBySharedNestedExtra was a PIN test; flipped to a regression assertion in this commit.

B — RuntimeABI.MarshalJSON flattened-with-overlay (now fixed). Linkage keys runner_id and registered_at were placed at the top level of the response, silently overwriting any N-1 advertised field whose name happened to collide. Fix: linkage moved under the namespaced rab_runtime_meta object so the advertised document round-trips bit-for-bit while bookkeeping stays discoverable. TestAdversary_RABFlattenDestroysCollidingAdvertisedField was a PIN; flipped to a regression assertion.

C — unauthenticated RAB downgrade (pinned, not fixed). k-053's POST /abi inherits the pre-existing zero-auth runner registry surface (k-002's enrollment model). Any client can POST a smaller RAB for any runner id and the victim's own negotiate(control) immediately returns an empty grant. Pinned as TestAdversary_UnauthenticatedRABDowngradeOfForeignRunner — the fix is a separate auth-surface decision and not in this slice's domain.

Surfaces verified clean (no test, "just works" reported): nil-resolver legacy pass-through on the only runner exec path; k-051 unwired from works-worker but stray secret:// is inert; Produce rejects empty HMACKey without panic; law hook cannot leak on error paths; Attested correctly lives outside the bundle JSON; evt_/evb_ ID prefixes are disjoint; registry RWMutex is consistent; POST /abi before register is a clean 404.

Gates (octopus-merge in /tmp/wt-v032)

  • go build ./... ✓
  • go vet ./... ✓
  • gofmt -l empty on every file we touched in this PR ✓
  • go test ./... -count=1 (non-race) — full suite green, 0 FAIL
  • go test -tags=e2e ./e2e/... green
  • go test ./services/api/ -count=1 -run 'Adversary' — 3/3 pass

Live runtime-verify (post-deploy 06:08)

  1. Enroll + Register: Worker enrolled, identity minted (wrkr_98f0...), registered with trust_class: standard.
  2. RAB POST (k-053): Accepted rab/1.0 with N-1 fields (spec, x_meta). Response includes the advertised document intact.
  3. RAB GET (k-054 fix verified): The response returns the full advertised document including the nested spec object and x_meta. Server linkage (runner_id, registered_at) is namespaced under rab_runtime_meta so user fields round-trip safely.
  4. Negotiate (k-053 law): Requested ["observe", "control"]. Returned {"caps": ["observe"], "control_token_required": false}. Correctly granted only observe (since the RAB didn't advertise control) and correctly reported false for the token requirement. Fail-closed on control.
  5. Brain surface: Still enabled (Brain surface enabled in logs).

Roll-back

Each slice is a single-purpose PR-of-one-branch. Reverting this PR is git revert; the three pure-law kernels from v0.3.1 are untouched and remain usable.

What's NOT in this PR

  • No new schema migration.
  • No wire-format change for any pre-existing endpoint.
  • No fix for the k-002 auth surface (separate decision).
  • No cmd/works-worker wiring for k-051 (documented deviation; integrator's call).

v0.3.1 — pure-law kernels and a registry-vs-repo auditor

Choose a tag to compare

@JonasAbde JonasAbde released this 03 Sep 02:09

v0.3.1 — pure-law kernels and a registry-vs-repo auditor

Tag: v0.3.1 → main 099a315 (squash-merged PR #33)
Range since v0.3.0: 0ed07ca..099a315 (1 PR, 4 disjoint slices, 1 octopus)
Status: shipped, merged, not yet deployed (pure-domain; runtime-verify optional)

What landed

Four slices, all packages/* or internal/* — zero overlap with services/,
zero schema migration, zero wire-format change. The packages are kernel
laws (law-once-decided, then enforced wherever they are wired in later
integration PRs). The audit tool surfaces drift between the standards
registry and the actual files in docs/standards/mappings/.

Slice Package ADR Tests Commit
k-046 packages/secrets ADR-0022 (value-never-serializes) 60 1047377
k-047 packages/obslaw ADR-0024 (obs vs evidence type-law) 30 24f5c2c
k-048 packages/abi ADR-0012/0014 (rab/1.0) 60 4501b90
k-049 internal/standards/audit + cmd/works-standards audit (tooling) 9 funcs 7aa3159

What each slice is for

k-046 — secret.ref/1.0 (ADR-0022)

The law: payloads and audit records may only ever carry a secret://
REF; the kernel is the sole component that resolves ref→value, at
execution time, inside the worker's env. The resolved value is
radioactive — it must never be serialised (not to audit, not to the
store, not to logs, not to error messages).

  • Typed Ref (compile-time refuses *Ref ↔ string confusion).
  • EnvResolver mapping (documented): secret://provider/name under
    scope "" → SECRET_<PROVIDER>_<NAME> (dashes → underscores;
    colons normalised because POSIX env names cannot contain colons).
  • Grant.Redeem(ctx, Resolver, workID): returns the value straight to
    the caller — never caches, never logs, never puts it in the returned
    error.
  • Leak test sets a sentinel value in env, forces every failure path,
    sweeps every error.Error() string for the sentinel. Zero leaks.

k-047 — observability-vs-evidence type-law (ADR-0024)

The law: events (logs/metrics) are operationally disposable — trimmable,
samplable, not signed. Evidence is a CLAIM about what happened — signed,
never trimmable. A signed event is a category error: observability
pretending to attest.

  • Constructors make illegal states unrepresentable: NewEvidence refuses
    signed=false; events cannot produce signed=true.
  • Record is the frozen schema shape (no Signature field — adding one
    would violate additionalProperties:false). Signatures live in
    Attested beside the record.
  • Verifier: HMAC-SHA256 over canonical JSON of the record MINUS the
    signature (sign the shape, not the bytes). Constant-time compare.
  • TrimPolicy(kind) exports the law as a tiny function so runbooks/UIs
    can ask.
  • Tamper tests: flip one bool, verify fails. Value-copy audit: Sign
    must not mutate the input record.

k-048 — rab/1.0 runtime ABI (ADR-0012/0014)

The lower half of the CPI/RAB split. Where CPN is "the kernel's view
of a whole computer" (Handshake/Provision/Exec/Snapshot/Teardown),
RAB is the capability advertisement a runtime (browser RT, code RT,
desktop RT) publishes into the registry — WHAT it can do, not WHO owns
it.

  • Closed universe of 5 caps: screenshot, input, record, observe, control. A 6th is a law violation.
  • The control-token law the schema cannot express gets Go teeth:
    caps contains control ⇒ control_token_required = true,
    fail-closed. The schema is a partial guard; the Go law closes the gap.
  • N-1 unknown-field tolerance via Extra map[string]any round-trip
    (forward-compat per proto.charter/1.0 ADR-0021; unknown FIELDS
    tolerated, unknown ABI VERSION rejected fail-closed).
  • Negotiate(requested) preserves requested order, returns
    (caps, controlTokenRequired, err) so callers can gate control ops
    with one call.

k-049 — standards coverage-audit tool

works-standards audit [--repo-root=PATH] [--fail-on-warn]

Cross-checks docs/standards/registry.json claims against filesystem
reality. Five check kinds, each a small pure function:

  • missing-mapping-file (High) — registry row claims mapping X but
    os.Stat(X) fails.
  • orphan-mapping (High) — file under docs/standards/mappings/
    referenced by NO registry row.
  • empty-status (Medium) — row status is empty/whitespace.
  • duplicate-id (Medium) — two rows share an id.
  • stale-generated-at (Medium) — generated_at > 30d old (injectable
    now for deterministic tests).

Real-repo run at PR-open time:

$ ./bin/works-standards audit
orphan-mapping docs/standards/mappings/ai.md ...
orphan-mapping docs/standards/mappings/ci.md ...
orphan-mapping docs/standards/mappings/identity.md ...
orphan-mapping docs/standards/mappings/innovation.md ...
orphan-mapping docs/standards/mappings/observability.md ...
orphan-mapping docs/standards/mappings/performance.md ...
orphan-mapping docs/standards/mappings/platform-build.md ...
orphan-mapping docs/standards/mappings/platform.md ...
orphan-mapping docs/standards/mappings/policy.md ...
orphan-mapping docs/standards/mappings/supply-chain.md ...
10 findings found
exit 1

The 10 orphans are real drift: the mapping files were authored, but no
registry row links to them. Fixing this is a docs-wave concern, NOT a
k-049 concern — the tool's job is to make the drift loud, not to hide
it. Whoever owns the registry rows is the owner of the follow-up.

Why a kernel package, not a runtime check?

Because every layer that touches observability or evidence needs to ask
the same question in the same way, and services/audit/,
services/evidence/, and the work journal each have their own code
paths. The package centralises the invariant so an integrator wires it
consistently. Future integration PRs (e.g. secrets resolver in
cmd/worker/, obslaw in the evidence bundle builder, abi in the
runtime registry) are the next step.

Gates (octopus-merge in this worktree)

  • go build ./... ✓
  • go vet ./... ✓
  • go test ./... -count=1 ✓ (133 new subtests added; all green)
  • make build && ./bin/works-standards audit runs clean on real repo
    (exits 1 with 10 High findings, exactly as designed)

Roll-back

Each package is a fresh packages/<name>/* and the audit tool is
internal/standards/audit*.go + a subcommand line in
cmd/works-standards/main.go. Reverting the octopus PR is a single
git revert — none of the four slices shares files with anything in
services/ or with the v0.3.0 brain wave.

What this release does NOT do

  • No wire-format change. No API surface change. No schema migration.
  • No runtime integration of the new packages. The kernels are
    available; whoever wires them does so behind a follow-up slice.
  • No registry content change. The 10 orphan-mapping findings are
    preserved on purpose — fixing them is a docs owner concern, not a
    release concern.

v0.3.0 — Company Brain: knowledge mounted, not prompted

Choose a tag to compare

@JonasAbde JonasAbde released this 03 Sep 01:32
0ed07ca

v0.3.0 — Company Brain: knowledge mounted, not prompted

Date: 2026-09-03 · Base: main @ 20a4ad4 (v0.2.0 line) · Wave: v03/brain — k-041..k-045

v0.2 made the kernel governed — every surface pinned by a frozen,
hash-attested contract. v0.3 teaches the kernel the Company Brain:
five collections under /org/<id>/ (missions, decisions, capabilities,
evidence, notes) where knowledge is mounted, not prompted. An
agent that wants to cite a decision no longer pastes a chunk of context
into a prompt — it asks the kernel for a mount of that path, and the
kernel returns a read view, scoped to the caller, with the authority law
already applied (you can read an authoritative object; you can never
be one without a human stamp).

That is why the brain namespace was the last big unmaterialized frozen
contract. ADR-0023 was already a brain.ns/1.0 schema in
contracts/manifest.json (sha256 0f5ed6…35f4); what v0.3 does is wire
the store, the REST surface, and the release-ring law that lets the
brain surface roll out under the same one-step-only discipline as the
rest of the kernel. The contract is law; v0.3 makes it live.

What this wave added

Four sibling slices land in this release. All four are gate-checked
together — the brain surface goes live when store + API land
together, never half-on.

k-041 — brain kernel laws (k-impl, v03/brain-kernel)

The namespace regex, the append-only revision law, the central
authoritative ⇒ human_stamped promotion invariant, the
ephemeral-never-authoritative law, tombstone-as-append, and the mount
scope + org-boundary rules. This is where ADR-0023 becomes code, not
just a schema.
Without k-041 the rest of the wave is unmounted; with
k-041 alone, nothing is exposed.

  • Namespace regex (frozen in brain.ns/1.0):
    ^/org/[a-f0-9-]+/(missions|decisions|capabilities|evidence|notes)/[A-Za-z0-9_/-]+$
    — five collections only, lowercase hex org id, an object may live in
    one collection. No cross-collection aliases; no implicit
    normalisation. A path that does not match is a 400 at the API and a
    schema-rejection at the store.
  • Append-only revisions: every write to a (path, revision) pair
    is a new row, never a mutation. The PK is (path, revision) so
    revision collisions are detected at the constraint, not in
    application logic. There is no UPDATE path on brain_objects;
    the only mutating verbs are promote and tombstone, and both
    append a new revision.
  • The central law — authoritative ⇒ human_stamped: if and only
    if authoritative == true then promotion == "human_stamped".
    This is the allOf/if/then clause in the schema. The store
    enforces it again on insert; the API enforces it on the wire; the
    test
    tests/contracts/contracts_test.go::TestAdversarialAgentKnowledgeCannotBecomeAuthoritative
    enforces it as regression law. Three layers, one rule.
  • Ephemeral can never become authoritative: class == "ephemeral"
    ⇒ authoritative == false is not a soft warning, it is a
    structural property. Promoting an ephemeral object is rejected
    with the same error code as any other authoritative-without-human
    attempt.
  • Tombstone-as-append: deleting an object is not a row removal,
    it is a new revision with tombstone == true and empty body.
    Reads default to "latest non-tombstone revision", so a tombstone
    hides the object without erasing its lineage. A consumer that
    walks the full revision log still sees the whole history.
  • Mount scope + org-boundary: a mount is a READ view of a path
    rooted at a single org. The mount's org_id must equal the
    object's org; a mount from org 0a1b cannot reach into org
    deadbeef regardless of token scopes. Cross-org reads are
    rejected at the store, not the API.

k-042 — brain schema v11 persistence (k-impl, v03/brain-conformance)

Two new tables behind the existing schema_version ledger:

table PK notes
brain_objects (path, revision) append-only, every write is a new revision, tombstone revisions hide without erasing
brain_mounts (subject, path) read views; subject is the token identity, path is the org-rooted brain path; revoked_at nullable

Restart migrates v10 → v11 idempotently. Both tables are
CREATE TABLE IF NOT EXISTS; the existing bumpSchemaVersion(11)
guard (services/work/store/store.go) checks
SELECT COALESCE(MAX(version), 0) FROM schema_version and only
inserts 11 if current < 11. No backfill, no destructive ALTER.

How to check (operator-side, after the v0.3 binary is live and a
restart has happened):

sqlite3 /var/lib/works/works.db 'select max(version) from schema_version'
# expect: 11

Honest note: measured baseline on this host (2026-09-03) — the live
store at /var/lib/works/works.db reports max(version) = 10 with
the v0.2 ledger rows 5, 6, 7, 9, 10. The 11 will only appear after
the v0.3 binary has been started at least once. If you read 10, the
binary on disk is still v0.2.

k-043 — /v1/brain REST surface (k-impl, v03/brain-cli)

The HTTP surface that the kernel exposes for brain objects and brain
mounts. Bearer auth, 64 KiB bodies (KB-class per ADR-0026 §9; the
link surface uses the same cap, kept consistent for v1), JSON
in/out. Every route is documented in contracts/, and the wire
table below is exactly the route table — do not invent flags.

Endpoint Method Auth Body Purpose
/v1/brain/objects POST Bearer (worker) {path, class, body, evidence_ref} append a new revision to a path (writes require a real wrk_<32hex> work id; see runbook)
/v1/brain/objects GET Bearer (worker, read scope) — ?path=<exact> returns the latest non-tombstone revision; ?prefix=<dir> lists latest-non-tombstone revisions under that org-rooted prefix
/v1/brain/objects/promote POST Bearer (worker + write scope) {path, revision, human_id} append a new revision with authoritative: true, promotion: human_stamped; rejected if class == "ephemeral"
/v1/brain/objects/tombstone POST Bearer (worker + write scope) {path, revision} append a new revision with tombstone: true; the object is hidden from default reads but its revision log is preserved
/v1/brain/mounts POST Bearer (worker) {path, scope, reason} create a read view of a path for the caller's subject; org-boundary checked
/v1/brain/mounts GET Bearer (worker) — ?subject=<self> (or omit for self) lists the caller's active mounts
/v1/brain/mounts/revoke POST Bearer (worker) {path, subject} revoke a mount the caller owns; idempotent

Auth model (v1): bearer-auth with read scopes on the GET
surface, write scopes on the promote / tombstone / write verbs.
A policy.token carrying a narrower scope than the route requires
is rejected (401). The policy-token scope-subset enforcement on
mounts themselves
(the ADR-0023 §6 double-enforcement rule) is not
in this wave
— v1 mounts are bearer-auth with read scopes only.
See Known deferred limits below.

Fail-closed 503-unwired design. When the brain surface is not
fully wired — the running binary predates k-042, so the concrete
store does not yet satisfy the BrainBackend interface — every
/v1/brain/* route answers 503 brain_unavailable. There is no
env opt-in and no secret: the wiring is a type assertion in
cmd/works-api/main.go. This is loud by design: an operator
pulling on a brain route before the wave is fully landed sees a 503
with the exact reason, never a 200 with an empty list. The boot log
line is Brain surface enabled (/v1/brain/) when live (mirrors the
link-surface log line). This is what the runbook greps for.

The brain surface goes live when store + API land together.
k-041, k-042, and k-043 are three pieces of the same lock: the
routes mount whenever the API side (k-043) is in the binary, but
they only serve once the store side (k-042) is too — the type
assertion is the interlock. The integrator's deploy (rebuild +
restart after all three merges) flips the surface live as one
atomic action.

k-044 — release.rings law (k-impl, v03/release-rings)

The release-ring contract from the frozen release.rings/1.0 schema
(9677b52…01a9 in the manifest) is now operational law for every
kernel-surface rollout, not just the link surface. The brain wave
is the first surface shipped under it.

  • Advance one-step-only. rings is an enum: [internal, alpha, beta, stable] array with uniqueItems: true; the advance law
    is that a surface moves at most one ring per release promotion.
    No skipping internal → beta, no skipping alpha → stable. The
    schema's no_ring_skips: true is the regression test; the
    release tool refuses to advance two rings.
  • 48h beta soak. beta_soak_hours: const 48 is in the
    schema. A surface in beta cannot be promoted to stable until
    it has been continuously live in beta for at least 48 hours.
    The release tool records the beta_at timestamp and refuses
    promotion otherwise. This is the same soak that the link
    surface is currently running.
  • Kill-switch required off-internal. Every ring below stable
    MUST have at least one entry in kill_switch (a list of env-var
    names or runtime flags that can revert the surface without a
    redeploy). The schema accepts an empty list for stable. The
    brain surface ships with WORKS_BRAIN_KILL_SWITCH (default: off
    in prod) wired in from day one.
  • Revert-with-reason + RevertLog. Any revert to an earlier ring
    must carry a written reason (operator note, ticket ref, or
    auto: <detector> for the automated health-check reverts) and is
    appended to release_revert_log (durable, append-only). The
    RevertLog is what makes the rings auditable after the fact; it
    is the surface of evidence that the law was followed.
  • **Stable needs freeze evid...
Read more

v0.2.0 — governed kernel: contracts, missions, evidence, link

Choose a tag to compare

@JonasAbde JonasAbde released this 02 Sep 21:08
20a4ad4

v0.2.0 — Governed kernel: contracts, missions, evidence, link

Date: 2026-09-02 · Base: main @ 05cdacb · Range: 0eca06b (freeze-slice-0, PR #9) → fb5bbfb (kanban closure for the v0.2 wave, PR #27)

v0.1 built the execution machinery: durable Work objects, lease-based
scheduling, disposable workers, webhook ingestion, result publishing. v0.2
makes that machinery governed: every surface the kernel exposes is pinned by
a frozen, hash-attested contract, and regression tests over those contracts
are law — drift changes a hash, never silently changes semantics.

What each merged slice added

Contract Freeze Slice 0 — contracts(freeze) (PR #9, 0eca06b)

Twenty frozen contracts materialized as 21 draft-07 JSON schemas under
contracts/schemas/, wrapped in contracts/manifest.json (21 entries;
work.schema/handoff inlined) with a freeze-attestation SHA-256 in
contracts/manifest.sha256
(2d2f1d27474a908a19aafb9c152be5e27c80987400f21cdfca94080b8bf14a86).
tests/contracts/ adds 21 contract tests (conformance, baseline,
adversarial, compatibility) that are now regression law: a schema change
without a re-generated manifest hash fails CI by design. Compatibility rule
frozen per proto.charter/1.0: N-1 read tolerance; breaking changes require a
major version bump.

k-mission-01 — kernel(mission) (PR #10, ab87f8d)

Mission contract + budget ledger (ADR-0008/0009). Mission Works carry a
MissionContract (budget ceiling, verification criteria, purpose bindings,
kill switch) and a BudgetLedger (reserved at lease time, consumed
continuously, clock paused while WAITING_HUMAN, stopped on hard stop; late
provider bills recorded but never push user-visible consumption past the
ceiling). The state machine gains the forward mission states
WAITING_HUMAN, SUSPENDED, BUDGET_EXHAUSTED — reachable only by mission
works, never by CI works.

k-mission-02 — kernel(mission) persistence (PR #11, 5041452)

Handoff/checkpoint persistence (ADR-0010, handoff.schema/1.0). A handoff is
the frozen 5-layer checkpoint payload (state snapshot, narrative, decision
log, priority queue, warnings) written only on kernel-recognized
suspend/wait/fail transitions. Suspend/resume is fail-closed: an invalid
handoff is never persisted, resume without a checkpoint is refused, and a
checkpoint whose re-derived hash mismatches its persisted hash
(ErrCorruptHandoff) is never silently resumed from. Schema v8: works.mission_json
column + work_handoffs table.

k-evid-01 — evidence(quittance) (PR #12, a83a8a9)

FailureAttribution + Quittance (ADR-0011/0024) in services/evidence: the
settlement-grade mission receipt layer. Kernel-negation law: a work that
failed verification can never carry a price hint
(ErrQuittanceFailedPriced) and a quittance cannot exist without an evidence
bundle (ErrQuittanceNoEvidence) — failed ⇒ no price.

k-hal-01 — hal(cpi) (PR #13, 4f05fc0)

The CPN provider boundary at cpi/1.0 (ADR-0012/0018): a single frozen
handshake/exec/snapshot contract every compute provider speaks. Plus the
conformance harness (packages/providers): interface compliance alone proves
nothing — every future provider must pass the executable ConformanceSuite
before it can be registered.

k-pulse-01 — hal(pulse) (PR #14, 4994501)

PULSE-Node CPN adapter (packages/providers/pulse_provider.go, ADR-0013/0026),
consent-gated and request-only: it announces only capabilities backed by an
active ConsentGrant, every wire call carries the grant reference and the
daemon re-validates (double enforcement), and with no grants it emits zero
outbound bytes — every call fails locally before any network dial. v1
transport is loopback HTTP on 127.0.0.1:7777 with a localhost pairing token;
mTLS is the v2 upgrade path.

k-now-01 — shell(now) (PR #15, d9c77de)

NOW-shell contract surface (ADR-0025, shell.contracts/1.0,
packages/shell): typed validation law for every shell surface plus the NOW
projection — the live mission view of what needs human attention first and
what the budget clock is doing. NOW is a read surface (T1); privileged
actions (approve/deny/kill/take/hand_back) belong to the COMMAND surface at
T3 and can never be exposed by pulse or local_only surfaces.

k-cap-01 — capability(cap) (PR #16, b1491fd)

Capability-evaluation law (packages/capability, policy.token/1.0): a
shell contract declaration executes only if the frozen contract allows it AND
the caller's policy token carries a matching scope. Grants carry token_id,
work_id, org, and can only ever narrow, never widen, delegation.

k-billing-01 — billing(settle) (PR #18, d7bc4e4)

Settlement law over quittance + budget (packages/billing, ADR-0011/0024 +
0009): settlement consumes the budget ledger against a quittance under four
laws — clock law (paused clocks never meter), clamp law (late bills never
push past the ceiling), late-bill evidence law, fail-closed (no quittance, no
settlement). Settlement never mutates the ledger.

WORKS Conversation V1 Task 1 — journaling (PR #19, e89d2e9)

Every canonical state transition is journaled to the durable per-work event
journal (schema v9 work_events, globally monotonic sequences), with a REST
journal listing for the conversation mirror; SSE consumers resume with
sequence > cursor.

k-link-01 — feat(link) (PR #20, 4f9bc69)

The WORKS-Link server surface (ADR-0026/0027, services/api/link_handler.go,
packages/link): the only routes a PULSE device may present to the kernel —
POST /link/v1/pair, POST /link/v1/mounts, GET /link/v1/missions,
POST /link/v1/revoke. /link/v1/commands exists in the frozen
link.wire/1.0 enum but is deliberately not mounted: the request-only law
(PULSE is never a controller) makes every privileged command structurally
unmountable. Auth: SAS pairing (6-char human-displayed code) mints a
short-lived HMAC device token; the service re-reads durable device state on
every call. Schema v10: link_devices + link_mounts. Enabled opt-in via
WORKS_LINK_PAIRING_SECRET (min 32 bytes); without it every link route stays
mounted and answers 503 fail-closed — never a silent default. See
runbooks/link-surface.md.

Upgrade notes (v0.1 → v0.2)

  • No breaking wire changes. All new surfaces are additive and the frozen
    compatibility rule is N-1 read tolerance per proto.charter/1.0; existing
    API clients keep working unchanged.
  • Schema migration v7 → v10, automatic and idempotent on restart. The
    production store (/var/lib/works/works.db) is currently at schema v7.
    Restarting works-api/works-worker with the new binary applies, in order:
    v8 (works.mission_json + work_handoffs, PRAGMA-checked column add),
    v9 (work_events journal), v10 (link_devices + link_mounts). All
    migrations are CREATE TABLE IF NOT EXISTS / introspection-guarded ALTER TABLE — safe to run repeatedly, no data backfill. Verify after restart:
    sqlite3 /var/lib/works/works.db 'select max(version) from schema_version'
    → 10.
  • WORKS_LINK_PAIRING_SECRET is opt-in. Generate one (openssl rand -hex 32, ≥ 32 bytes), add it to /etc/works/works.env, and restart. If it
    is absent or short, the link surface is mounted but every route answers
    503 — that is the intended fail-closed posture, not an error to fix under
    pressure. Setup walk-through: runbooks/link-surface.md.
  • Contract tests are regression law. If you change anything under
    contracts/schemas/, re-run python3 contracts/gen_freeze.py — the
    manifest hash must move visibly. CI runs the 21 tests in tests/contracts/.

Governance invariants (unchanged by v0.2, now contract-pinned)

  • State machine authority — only the kernel moves a Work through its
    transitions; the transition table is frozen in work.schema/1.0 and locked
    by baseline contract tests.
  • Evidence-on-disk for SUCCEEDED — a succeeded work has evidence
    records; a quittance cannot exist without an evidence bundle.
  • Lease TTL — leases expire and are re-grantable; expired leases are
    reclaimable via ListExpiredLeases.
  • Idempotent webhooks — webhook ingestion is replay-safe.
  • Frozen contract tests — the 21 tests in tests/contracts/ fail on
    drift; the manifest hash is the freeze attestation.
  • Pool enforcement at grant — BYOC pool membership is checked when a
    lease is granted, not later.
  • Cache exit≠0 refusal — the content cache never stores results from a
    failed attempt.

ADR boundary (honest note)

ADRs 0005–0007 live in this repo (docs/adr/). ADRs 0008–0027 referenced by
the v0.2 slices live in the AVC workspace docs, not in this repository —
they are cited from the frozen schemas and package docs here, but their text
is not vendored in-repo.

Verification at release

  • go build ./... — clean
  • go test ./tests/contracts/ -count=1 — 21/21 PASS
  • Full suite go test ./... -count=1 — all packages ok (per
    contracts/FREEZE_EVIDENCE.md: 30/30 packages, exit 0)
  • v0.2.0 wave slices (PRs #22–#27) — exact-head CI + post-merge main CI
    both SUCCESS; e2e green; works-kanban validate green; standards
    registry parses.

v0.2.0 wave (the last 5 PRs in this release)

Slice Title PR Merge SHA
k-039 v0.2.0 release notes + WORKS-Link runbook + README kernel section #22 46a7a10
k-035 Revoke cascades mission suspension with handoff (ADR-0020/0026) #23 8ec59b3
k-036 Wire conformance of /link/v1 vs frozen link.wire/1.0 + pairing/1.0 #24 8d0ee8d
k-037 works missions CLI — NOW-ordered mission projection #25 03ba423
k-038 WebUI work-detail journal timeline (escaped, last 50 events) #26 e405f9e
kanban k-impl-035..039 closed on the board #27 fb5bbfb

Five workers, disjoint file-ownership, all exact-head CI SUCC...

Read more