Packages
Deterministic protocol verification for cryptographically bounded proof-of-possession authority.
Current section
Files
Jump to
Current section
Files
bounded_authority_protocol
CHANGELOG.md
CHANGELOG.md
# Changelog
All notable changes to `bounded_authority_protocol` are documented here.
## [Unreleased]
## [0.1.0] — 2026-08-20
### Added
- **First public release (BAP-07, executed 2026-08-20 by owner decision).** The exact reviewed
candidate published to Hex as `bounded_authority_protocol` 0.1.0. Connected verification: the
private runtime's PostgreSQL 18 gate passed 996/996 against the pinned candidate, and the
consumer's gates passed 819 tests including the immutable authority-contract bundle's 15
consumer cases; the opt-in live-endpoint consumer gate was provisioned and run for this
closeout. Fresh correctness, security, and gate-integrity reviews closed with their findings
fixed in this landing; the cross-vendor peer (codex) returned no findings, and the third-family
GLM lens was a named sensitivity-policy skip. The publication sweep in this landing: the
private-strategy links left the README, every unpublished/deferred claim in the shipped docs
became the published truth, ADR 0008 gained the 2026-08-20 amendment lifting the deferral, the
consumer-seams design note left the package and hexdocs, the mutation batteries gained
baseline-green runs (an entry now fails if its target test cannot run green unmutated), the
bounds-aware facade gained widening/malformed-bounds rejection legs, and `.gitignore`/dependabot
gained the public-repo hygiene the security review named. The `v0.1.0` tag moved to the
published commit so hexdocs `source_ref` links resolve.
### Fixed
- **BAP-07 readiness reconciled across the repo docs.** The ROADMAP row's acceptance still named
the retired private-runtime PG 16/17/18 matrix (the private runtime's ADR 0010 made PostgreSQL 18
the sole supported major); the ROADMAP Next-action note, the release-candidate contract's Status,
the README Status, and SECURITY.md's supported-versions note each described BAP-07 as gated on the
private runtime's connected gates —
private BA-14 completed 2026-08-18, so BAP-07 is fully unblocked (the Hex-publication half stays
deferred by maintainer decision). Docs-only: zero code, wire, bound, or verdict change.
- **Cross-vendor review round 18 (codex blocking + claude should-fix/notes, all closed).**
Codex (blocking): the TS shape gate's nested expected-export members were opaque `"object"`
specs, so a malformed nested struct (chain missing `previousHash`, empty anchor) passed the
gate and the clause-3 hoist then derefed the missing field — a `TypeError` escape past
`trying()`; the nested members are now fully specified (chain/anchor/transition field shapes,
`archived.chunks` as a bytes sequence), red-leg + mutation proven (opaque specs → RED). Claude
(should-fix): the Python and Rust hoists walked `expected.transitions` per-element BEFORE the
`key_transitions` count ceiling, regressing the round-3 ceiling-first invariant the TS sibling
kept; the ceiling now runs at the top of the hoist in both (the dead later duplicate removed).
Claude (notes, reconciled): the entry-position shape gate's sequence walks before count
ceilings are a documented accepted margin (typeof-only per element); Python's body-level
`bytearray` tolerance was dead under the exact-bytes gate and is reconciled to bytes-only.
The timed-out fable peer's transcript was mined: it was converging on the ordering finding
(fixed) and an API-shape note that is benign (the SDKs' optional bounds default to maximum —
behavior-identical to the reference's public façade).
- **Round-12..14 per-clause pin debt paid on the touched surfaces (ADR 0017's Honest limit,
amended).** The 2026-08-18 cluster's legs double as the owed per-clause pins: the pre-digest
export gates (version shape/equality, key-count, key-id charset, key magnitude) are now pinned as
WORK legs in the Python battery (reject with zero sha256 calls — all five mutation-proven
load-bearing) with TS/Rust verdict matrices. Root cause of the "unpayable" debt: each gate is
verdict-subsumed by a later gate, so verdict legs were structurally impossible; the work form is
the red-capable pin. Still owed and disclosed: the standalone anchor-path `anchor_bytes` gates
(round 12) and allocation bounds (round 15) on untouched surfaces.
- **ADR 0018's named `assemble_compact` divergence closed: caller bounds threaded through
assembly in all three SDKs.** The reference takes limits at assemble (`runtime.ex:147-155` →
`CompactJws.assemble` — encoded-segment bounds, compact_bytes, and the kind re-parse against
`Bounds.coerce(limits)`); the SDKs hardcoded maximum. Each SDK now takes an optional bounds
parameter (Python `bounds: Bounds | None = None`, TypeScript `bounds?: Bounds`, Rust
`bounds: Option<&Bounds>` — an additive public-API change; absent bounds = maximum, backward
compatible) and threads it through the reference's gates. `signature_bytes` carries no
assemble-time gate — fixed-width at Bounds construction (the reference's check is subsumed).
Mutation-proven per SDK (reverting the threading reddens each battery's tightened-bounds legs).
- **ADR 0017 exception 2 closed: the expected-anchor identity ordering divergence (all three
SDKs).** The reference validates the expected struct (chain + both anchors' identity/binding
well-formedness + transitions) BEFORE hashing the archive chunks (`anchored_export_codec.ex:88-104`);
the SDKs ran those gates only post-digest. Verdict-invariant by subsumption — the fix restores the
clause-3 work ordering: malformed caller metadata now rejects without hashing the archive. The
Python battery proves it behaviorally (sha256 call-count == 0 on malformed-expected rejections,
mutation-proven); TS and Rust pin the ordering structurally (hoist block before the digest site,
mutation-proven) plus verdict-matrix legs.
- **ADR 0017 exception 1 closed: the SDK closed-Result escape family (Python + TypeScript).** A
mechanical family sweep (every façade × every parameter × wrong-typed values, then every struct
field the same way) proved the 2026-08-17 ledgered Python escapes were not two instances but a
total class: 31/34 Python parameter sites and every caller-supplied struct field raised
`AttributeError`/`TypeError` past the closed `Result`, and TypeScript — whose coercion the ADR
had classed as a "disclosed margin" — silently accepted `requestDigest(123/true/null/{})`
(digesting the coerced text) and threw `TypeError` past `trying()` on 109 parameter positions.
Both dynamic SDKs now gate every façade argument's shape before the body runs (Python:
annotation-driven `_closed_shape` over the declared dataclass shapes, `bool` ≠ `int`; TS:
`closedShape` with per-façade shape specs). Rust excludes the class by typing. The per-SDK
family-sweep batteries in `tests/test_permissiveness.{py,ts}` are the pre-fix red run and are
mutation-proven per façade. Exception 2 (expected-anchor identity post-digest) remains open.
### Added
- **Bounds-aware public compact assembly and current-major issuer posture.** Add the public
`BoundedAuthorityProtocol.V1.assemble_compact/3` facade over the existing bounded runtime
primitive; `/2` remains exactly `/3` with profile maxima. Native tests cover byte equality for
grant, proof, boundary-anchor, and key-transition compacts; tightened encoded-segment and final-
compact rejection; and continued rejection of `ba_dlg` / `ba+cap-delegated` in current v1.
[ADR 0020](docs/adr/0020-bounds-aware-assembly-and-issuer-reauthorization-posture.md) records that
a private authority may issue an independently valid, narrower current-v1 grant to a new holder,
while lineage remains runtime-only and portable holder-signed delegation remains successor-major.
- **The SDK contract ADRs (BAP-15 documentation slice).** Three new accepted ADRs record contracts
that had shipped as code + ROADMAP evidence amendments only: [ADR 0017](docs/adr/0017-inter-sdk-behavioral-contract.md)
(the inter-SDK behavioral contract the 17-round cross-vendor hardening arc converged on — closed
Result surface, type strictness, pre-hash validation, canonical-form byte equality +
signature-width gates, role-bounded frame reads), [ADR 0018](docs/adr/0018-sdk-bounds-contract.md)
(the caller-tightenable bounds contract through the expected structs, including the
nested-pins identity semantics and the named `assemble_compact` maximum-bounds divergence), and
[ADR 0019](docs/adr/0019-corpus-artifact-distribution.md) (ADR 0015 Decision 6's deferred
corpus-artifact question settled: per-SDK binding until the first SDK graduation, on the traced
two-SDK bump-amplitude evidence). Fact corrections in the same landing: ADR 0014's Node floor
corrected to >= 22 (raised at `d9df0bf` on Node 20 EOL) and its `@noble/curves` "optional
browser-build path" claim withdrawn (never present in `sdks/` history); ADR 0008's BAP-07
"publishes the exact candidate" consequence annotated with the maintainer's Hex-publication
deferral (the `v0.1.0` git tag at `c65d3be` is the internal pin). Zero wire-behavior change. The
ADR-0017 authoring review (cross-vendor) surfaced two verified SDK contract exceptions, disclosed
as ADR 0017's named exceptions and routed as SDK-code fixes: a Python closed-Result escape
(non-string `request_digest` operation / `ConsumptionEntry.chain_id` raises `AttributeError` past
the façade), and all three SDKs validating the expected-anchor identity fields post-digest where
the reference validates them pre-digest.
- **BAP-09 SDK conformance hardening (TypeScript + Python).** The TS + Python verifier SDKs now
enforce two checks the Elixir reference has and the SDKs were missing: (1) a decoded
**signature-width gate** in `parseCompact`/`parse_compact` (`len(signature) == signature_bytes`,
mirroring `runtime.ex:237`/`:259` — `scanCompact`/`scan_compact` intentionally stays shape-only,
mirroring `CompactJws.scan`); (2) **canonical-form equality** for boundary-anchor + key-transition
compacts — the protected header AND payload segments must equal the exact JCS re-encoding
(mirroring `boundary_anchor_codec.ex:95-96,118-119` + `key_transition_codec.ex:127-128,151-152`),
so a non-canonical encoding (e.g. reordered members) is rejected. New red-capable tests cover the
anchor + transition canonical paths (header + payload) and the signature-width gate. Verified: TS
92 unit + conformance 283/283; Python 62 unit + conformance 283/283. (The Rust/Go SDKs — BAP-15/16
— may carry the same gaps; a separate check is owed.)
- **All-SDK pre-hash validation hardening (BAP-15, cross-vendor rounds 11-15).** The archive
verify paths across Rust/TS/Python now validate the full caller-context shape BEFORE the
digest: the object versions (string, non-empty, UTF-8 bytes <= 512, well-formed, equal),
the key chain (exact count, key-id ASCII-unreserved class + width), key windows
(integral + magnitude + ordering), and identifier well-formedness — malformed metadata
no longer forces maximum-sized hashing, ill-formed strings fail closed in every SDK
(Python's UnicodeEncodeError escape closed; TS's silent U+FFFD replacement closed), and
frame reads are role-bounded per chain_row_bytes/anchor_bytes.
- **Result-contract fail-closure + ChainInput type strictness (BAP-15, cross-vendor rounds 16-17).**
Every path where a caller-supplied Python value could raise out of the closed Result API
now fails closed instead *(2026-08-17 delta: "every" was overclaimed — two further escape paths
were found by the ADR-0017 review and routed; see the SDK contract ADRs row below)*: ill-formed
or non-str identifier strings (`_utf8_bytes` gates
chain_ids, key_ids, versions, and every expected-side string the header construction
encodes), non-int chain integers (gated before the sequence arithmetic), non-bytes chunk
elements, and Boolean ChainInput integers (True == 1 no longer verifies — TS's strict
equality and Rust's typing already rejected them). The TS chunk-type sibling gate added
for family symmetry; the check_chain chain_id StringOrURI shape validation that only
Rust carried is now in all three SDKs.
- **Rust SDK bounds parity (BAP-15; closes the LAST named delta).** Caller-tightenable
bounds through the expected structs — the exact reference/sibling shape: five additive
`Option<Bounds>` fields (None = the profile maximum), the nested-bounds pins with identity
semantics (a present nested must coerce-equal the outer; an absent nested is valid only under
an effectively-untightened outer — identity overrides are NOT tightening), and the resolved
bounds threaded through every ceiling at encode AND verify + the standalone chain/anchor/
transition entries. 27 mutation-proven legs at closeout (permissiveness 39 → 67 — the original 12 plus
the five the four closeout lenses forced: the chain_rows count, the standalone
transition, the verify-pin family on a REAL corpus-signed archive, and the two
encode-pin isolations; the standalone anchor leg, the chunk-count MAGIC pin now reachable and landed, and a 7/7
Elixir oracle receipt (local re-runnable .forge artifact). `assemble_compact` stays at maximum
(the siblings too). (Scope driver: the 2026-08-14 session direction — the session's input,
not this entry's verdict.)
- **TypeScript + Python encode-path validation parity (BAP-15).** Both sibling SDKs gain the
same producer contract the Rust SDK just closed: a full `checkChain` re-check of the rows, and
gated parses + 7-field matches for the START anchor, the END anchor, and every transition
(their expected-side + key-path validations were already present — the deltas were the parses,
the matches, and the row re-check; the start anchor was framed raw too, plan-review F1).
15 legs per SDK at closeout (control + 8 tampers + the six pin/threading legs the
delta- and diff-reviews forced; the five pin legs each isolated under their named
mutation, the threading leg joint-by-construction per the settled diff-review record),
each proven red-capable; TS 107 unit + conformance 283/283; Python 77 + conformance 283/283.
mypy/ruff/purity/license clean. With this, all three shipped SDKs enforce the reference
producer's full contract; the Go SDK (BAP-16) picks it up at authoring.
- **Rust SDK encode-path validation parity (BAP-15).** `encode_anchored_export` now enforces the
Elixir reference producer's FULL validation contract (`anchored_export_codec.ex` encode):
expected-side consistency (chain_id binding of both anchors + all transitions; the
start/end sequence + hash bindings), a full `check_chain` re-check of the rows, gated parses
+ 7-field matches for BOTH anchors and every transition (the width/canonical gates now
reached at encode), and the key-path walk (running key, strictly-after transition times,
seen-list cycle guard, end anchor binding the final key with NON-STRICT `>=` chronology).
19 red-capable battery legs at closeout (permissiveness 19 → 38), each mutation-proven; every leg
verified against the Elixir reference oracle (20/20 fixtures — the 15 + control + the four
closeout-lens additions; the receipt is a local re-runnable .forge artifact). Honest
residuals, both NAMED: the TypeScript/Python SDKs carried the same producer permissiveness
(CLOSED by the sibling entries above); the Rust SDK is a documented maximum-bounds posture
at encode (the reference + siblings thread caller-tightened `expected.bounds` — carrying
that is a public-API change, its own slice; the sibling identity fix also WIDENS TS/Python
VERIFY-path acceptance for identity-override configurations, matching the reference; the
Rust chunk-count parity fix is a latent verdict-tightening flip unreachable at the frozen
maximum bounds — its pin is owed by the future caller-bounds slice).
- **Rust SDK conformance hardening (BAP-15).** The Rust verifier SDK now enforces the same two
checks the Elixir reference has (closing the "separate check owed" note above, mirroring
`18c6467`): (1) a decoded **signature-width gate** in all four `decode_*_parts` fns
(`REQ1-BOUNDS-fixed-widths`, mirroring `runtime.ex:237`/`:259`,
`boundary_anchor_codec.ex:88`, `key_transition_codec.ex:120`) — public verdict flips on
`decode_grant`/`decode_proof` (accepted a wrong-width signature at decode) and
`encode_anchored_export` (the start-anchor parse never width-checked its signature segment);
(2) **canonical-form equality** for boundary-anchor + key-transition compacts — the four
validators now assert `jcs_encode(value) == segment_bytes` for the protected header AND
payload (mirroring `boundary_anchor_codec.ex:95-96,118-119` +
`key_transition_codec.ex:127-128,151-152`), so a non-canonical (member-reordered) segment is
rejected across `assemble_compact`, `verify_historical_anchor`, `verify_key_transition`,
`encode_anchored_export`, and `verify_anchored_export`. Nine red-capable battery legs
(4 canonical + 2 decode-width + 1 export-encode width + 2 canonical-exclusion pins + the
four closeout-lens legs), each mutation-proven. Verified: cargo 338 unit + conformance 283/283 + permissiveness 38;
clippy/fmt clean. Honest residuals, all
routed: the encode path validates less than the reference producer — it frames the END
anchor AND every TRANSITION raw without parsing them (the reference parses both anchors +
all transitions through the width/canonical-gating codecs,
`anchored_export_codec.ex:40-52`), binds the start anchor by sequence only (the reference
matches all signed fields via `anchor_matches?`/`transition_matches?`), and does not
re-check rows against the chain at encode (`ConsumptionChain.check`). A non-canonical or
wrong-width end anchor or transition is therefore still accepted at Rust encode (probe-
proven; caught downstream — `verify_anchored_export` gates all of them); closing these
flips further verdict classes and is its own reviewed change. The Go SDK (BAP-16) picks
both classes up at authoring.
- **BAP-17 — offline-eligible grant claims (reserve + specify).** Reserve the `ba_offline`
grant-payload claim name in the [registries](docs/design/registries.md) (issuer-set offline
floor limits: maximum value with explicit currency, maximum offline use count, offline-window
expiry — a closed nested object; absence means online-only, per
[R-BAP-1](docs/design/offline-authorization-requirements.md)). [ADR 0016](docs/adr/0016-offline-eligible-grant-claims.md)
carries the activating-major mechanism to spec quality: the closed `{cnt, cur, max, win}` object,
the non-authorizing facts contract (an `offline_eligible` flag + `win` only — magnitudes are read
from the decoded grant, not redacted facts), malformed⇒`:invalid` (online-only is the *absent*
default), a wire-layer `max × cnt` ceiling (the cross-language SDKs compute exposure in fixed-width
integers), the `ba_dlg` attenuation composition, and the freshness scoping. **Activation is a
successor contract-major** — the closed v1 profile rejects `ba_offline` today (the `v0.1.0`-amend
alternative was considered and is the heavier path: the published governance change-class rule, the
three corpus-SHA-pinning SDKs, and intra-major fragmentation). This is a design-only slice: zero
`lib/`/`docs/protocol-v1.md`/`priv/conformance/` wire-behavior change (mirror BAP-11/BAP-14); an
R-BAP-2 legacy-rejection tripwire is added under `test/`. The offline runtime arc (private
`bounded_authority` BA-20..23) is successor-major-gated by this reservation.
- Close the **BAP-15 Rust verifier SDK** (Tasks 15–17 + closeout; the library Tasks 1–14 were already
landed and verified green). `sdks/rust/tests/permissiveness.rs` is the named per-language permissiveness
battery exercising all six ADR 0014 D6 closures through the public crate boundary (duplicate-reject,
source-order preservation, raw-lexeme ceiling, single-value, int/float tag distinction, base64url
pad-bits), each documented with its red-capable mutation. Closure #6 — the `(d)`-class per-node encode
bounds — is now enforced IN `jcs_encode`'s recursion (depth + total_nodes + a per-node `jcs_bytes`
early bail), so a hand-built value passed directly to the public primitive cannot force unbounded
recursion, traversal, or intermediate allocation. This narrows `jcs_encode`'s accept set
(verdict-preserving on every corpus case — all corpus inputs are decode-bounded within the same
ceilings; verified lib 331/331, conformance 283/283); the depth and total_nodes guards are proven
red-capable by live mutation. `sdks/rust/tools/purity_check.sh` + `license_check.sh` enforce the
lib-path purity invariant (no I/O/clock/RNG/network/env in `src/`; `#![forbid(unsafe_code)]` is the
compile-time `unsafe` half) and the runtime dependency-license allowlist (15 runtime deps, all
permissive; dev-deps excluded as non-consumer-facing) — both shellcheck-clean and red-capable. The
`rust-conformance` CI job (`.github/workflows/sdks.yml`) runs fmt + clippy + the full `cargo test`
(unit + permissiveness + 283-vector conformance + census) + purity + license on the MSRV 1.81
toolchain. The publish guard now scans `sdks/*/Cargo.toml` and blocks `cargo publish` /
`cargo release publish` / `cargo-release publish` / `crate-ci/cargo-release` (ADR 0015). New docs:
[`sdks/rust/README.md`](sdks/rust/README.md) and the deployment guide
(`docs/deployment/rust-sdk.md`) (AWS Lambda `provided.al2023`; PostgreSQL
`plrust` — ed25519-dalek-based verification is NOT plrust-trusted-mode-compatible as built). The three
signed anchored-export rows in [`docs/design/conformance-contract.md`](docs/design/conformance-contract.md)
are updated to their landed case ids. Zero wire byte, bound, or verdict change to the Elixir package.
- Close the BAP-15 prerequisite spec/corpus gaps (no-key half). `docs/protocol-v1.md` gains a
normative **JCS string and number serialization** subsection transcribing RFC 8785 §3.2.2.2–3
(control-range escapes, raw DEL `U+007F`, ECMAScript `Number::toString` float thresholds
`e < -6` / `e >= 21`, `-0`→`0`, shortest-round-trip digits — both ECMA §7.1.12.1 and TC39
§6.1.6.1.20 cited). The conformance corpus grows **259 → 280 cases**: 5 JCS float cases pinning
both threshold sides, a raw-DEL bare-string case, an astral-codepoint (U+10000) raw-emit case, a
float `cast_arguments` request-digest case, malformed-UTF-8-member-name and float-magnitude
`json.decode` rejects, three malformed-IPv6 `uri.normalize` rejects, a key-locator
protected-only (empty-segment) valid case, three `check_chain` rejects (canonical re-encode,
sequence-zero, genesis previous-hash forge), the byte-level cross-vendor findings for
`encode_consumption_entry` (seq-1 nonzero previous), `boundary_anchor_signing_input` (seq-0
nonzero chain_hash), `encode_anchored_export` (start-anchor sequence binding — a signed start
anchor carries a bogus sequence, isolating `start.sequence == first_sequence-1`), and
`verify_anchored_export` (empty chunk). Every invalid case is a one-defect **skip-would-accept**
construction (a verifier that drops its target check would accept it) — proven by defect-injecting
each target check in the independent runner and confirming the case flips. The two
`encode_/verify_anchored_export.maximum_plus_one` `n_a` reasons are corrected to name all three
bounds (`archive_bytes` + `archive_chunks` + `historical_key_transitions`) and the loader
representation constraint. The independent Node runner is strengthened to mirror the reference's
IPv6-structure (node:net `isIP`, replacing a crude charset check), consumption-entry and
boundary-anchor genesis bindings, `check_chain` positive-`first_sequence` and canonical-rehash
(hashing the canonical re-encode, not raw row bytes, matching `ConsumptionChain.parse_row/1`),
anchored-export start/end anchor bindings, and verify per-chunk emptiness — the invariants the new
cases exposed; every new case is dual-verified (`agreed=280 disagreed=0`). SDK
`CERTIFIED_INDEX_SHA` constants (Python hex + TypeScript base64url) rebind to the new `index.json`.
The encode_anchored_export binding case's authoring key is seed-generated and never enters the
import-boundary census (encode frames signatures, never verifies them), so
`public_key_fingerprints` stays 8 and the manifest three-partition is unchanged.
- Close the BAP-15 prerequisite corpus gap (signed half). The conformance corpus grows
**280 → 283 cases** with three signed `verify_anchored_export` cases exercising ADR 0004's
authenticated key-transition path: `#2` non-monotone chronology (two transitions with non-monotone
`effective_at` → `invalid_time`), `#2` fingerprint cycle (B→C→B → `invalid_key`), and `#3`
one-key/zero-transition valid (the equal start/end-time same-key case). Three new seeded Ed25519
keys join `public_key_fingerprints` (**8→11**) since the export verifier imports them; their seeds
never enter the corpus. The independent Node runner is strengthened to mirror the reference's
cross-transition invariants (`validate_expected_key_path`): strictly-increasing transition times,
fingerprint no-cycle, and the end-anchor chronologically at-or-after the last transition — gates
the per-element compact checks cannot express. Every new invalid case is a one-defect
**skip-would-accept** construction (a monotone / no-cycle variant accepts); every new case is
dual-verified (`agreed=283 disagreed=0`). SDK `CERTIFIED_INDEX_SHA` constants (Python hex +
TypeScript base64url) rebind to the new `index.json`; the vector manifest's canonical fingerprint
set grows 19→22 and the corpus partition 8→11.
- Record the SDK graduation and publish-topology decision: cross-language verifier SDKs are
authored under `sdks/` (per [ADR 0014](docs/adr/0014-cross-language-verifier-sdks.md)) but each
graduates to its own per-SDK repository (`bounded_authority_protocol_<lang>`) on first
publication; the decision boundary is publication irreversibility, not SDK count
([ADR 0015](docs/adr/0015-sdk-graduation-and-publish-topology.md)). Zero wire byte, bound, or
verdict change.
- Ship the ADR 0015 enforcement layer: a tracked pre-commit hook
(`scripts/hooks/pre-commit`, single pattern source `scripts/check_sdk_publish_infra.sh`, installed
via `scripts/install-hooks.sh`) and the `sdk-publish-guard` CI job
([`.github/workflows/sdk-publish-guard.yml`](.github/workflows/sdk-publish-guard.yml)) that together
reject SDK registry-publish infrastructure committed to the monorepo. Honesty limit: catches
committed publish infrastructure, not a literal ad-hoc publish run against a working tree; CI on
main is the non-bypassable backstop, the local hook is honor-system. Bypass:
`git commit --no-verify`.
- Add ROADMAP rows BAP-15 (Rust verifier SDK) and BAP-16 (Go verifier SDK) — typed reimplementations
of the frozen v1 profile from spec + corpus alone, authored under `sdks/` and graduating per
[ADR 0015](docs/adr/0015-sdk-graduation-and-publish-topology.md). Post-1.0 rows; no code shipped.
- Ship cross-language verifier SDKs (TypeScript `@bounded-authority/verifier` + Python
`bounded-authority-verifier`) under `sdks/` — typed reimplementations of the frozen v1 profile from
the spec + corpus alone, with no code-level derivation from the Elixir reference
([ADR 0014](docs/adr/0014-cross-language-verifier-sdks.md)). Each passes all 283 conformance vectors
(recomputed from scratch), asserts the corpus `index.json` SHA at startup, and proves every
permissiveness closure red-capable via a per-language mutation-gate. They are verifiers, not
authority runtimes (no key selection, replay reservation, or execution grant). NOT in the Hex
package `files:` (they are not Elixir). The `sdks-conformance` CI job
([`.github/workflows/sdks.yml`](.github/workflows/sdks.yml)) gates every `sdks/**` /
`priv/conformance/**` change. Zero wire byte, bound, or verdict change to the Elixir package.
- Draft the capability-authorization extension as a pre-submission package for the MCP
experimental-extension track (`docs/extensions/` — a draft `.mdx`, a draft
Extensions-Track SEP, and an Agent Payments Protocol (AP2) mandate-mapping note;
[ADR 0013](docs/adr/0013-capability-authorization-extension.md)), documenting the already-normative
v1 protocol. Identifier `io.bounded-authority/capability-authorization`. Partial conformance to the
MCP official-submission bar (SEP-2133); official submission is gated on external preconditions
(a reference implementation in an official MCP SDK, a working group + Extension Maintainers +
sponsor, SEP acceptance, and
IANA registration of the `ba_*`/`ba+*` names via BAP-12). The extension documents are repo-tracked,
excluded from the Hex package census (pre-submission drafts). Zero wire byte, bound, or verdict
change.
- Publish the governance policy (change classes, change-control triggers, errata no-verdict-flip
prohibition, deprecation windows, security-release handling) as a standalone normative project
document ([docs/governance.md](docs/governance.md); [ADR 0011](docs/adr/0011-published-governance.md))
— a companion republication of the charter § Governance, which remains the authoritative source.
[SECURITY.md](SECURITY.md) cross-references the verdict-change rule; the
[errata registry](docs/errata.md) header retargets to governance.md as the published policy home;
`SECURITY.md`, `docs/governance.md`, and `docs/design/standards-track.md` join
`.forge/critical-surfaces` (SECURITY.md a retroactive gap close since BAP-06). Zero wire byte,
bound, or verdict change.
- Resolve a contradiction in the deprecation/security policy: the twelve-month deprecation-window
minimum (`REQ1-EVO-deprecation-window-minimum`) is scoped to planned deprecations, and a security
contract-major (one remediating a verdict-changing vulnerability) is exempt — its accelerated
overlap window is published at announcement, proportional to severity, and a deployment-decided
sunset, never a silent change ([ADR 0012](docs/adr/0012-security-release-accelerated-deprecation-window.md);
charter § The evolution contract + § Governance; [governance.md](docs/governance.md) re-synced).
Deployment-policy refinement only — zero wire byte, bound, or verdict change.
### Changed
- **Documentation corrections (2026-08-18 alignment-audit items 1, 4, 7).** SECURITY.md's "most
recent package-bearing verified source" re-anchored from the BAP-06 head to the SDK
behavioral-closure cluster head `c281938`, with the superlative date-bounded ("as of
2026-08-18") so the claim expires visibly instead of silently: CI run 32118915019 and
supply-chain run 32118915034 passed at that exact revision, and the doc's own verification
recipe was re-run against the downloaded archive — checksum OK (ubuntu-built archive SHA-256
`c9b5b0cf…`), SLSA build-provenance attestation verified constrained to this repository, the
supply-chain workflow, `refs/heads/main`, that source digest, and GitHub-hosted runners; BAP-06
`4c64be3` joins the earlier-heads list. The TypeScript conformance runner's startup comment
miscited the corpus binding as "ADR 0014 D8" (enforcement posture) — corrected to D4, matching
the file's own correct citations. usage-rules rule 13's era-frozen "BAP-04 verification is
implemented" now states the v1 verification surface is implemented. Docs-only — no code, wire,
bound, or verdict change.
- **Documentation reconciliation — status prose frozen at the BAP-10 era (alignment-audit items
1–9).** `AGENTS.md` "Current state" rewritten from the tracker: names the closed set (BAP-00..06,
08..11, 13..15, 17), the three verifier SDKs under `sdks/` (ADR 0014/0015 graduation topology,
none published), ADRs 0001–0016, and the 283-case corpus + verifier CLI. README's status section
corrected the same way; the BAP-07 framing in README, SECURITY.md, and the release-candidate
contract now records the maintainer decision (Hex publication deferred — internal consumption via
the `v0.1.0` git tag at `c65d3be`, not a registry pin). SECURITY.md now cites the most recent
package-bearing verified head (BAP-06 `4c64be3`) with the BAP-05/BAP-04 history retained. The
TypeScript and Python SDK READMEs no longer point Install at registry URLs that do not resolve —
they carry the Rust README's reserved-identifier/not-yet-published framing, and "one of two" SDK
prose corrected to three; `sdks/README.md` floor corrected to Node >= 22 with the not-published
disclaimer. `docs/design/offline-authorization-requirements.md` status now points at the closed
BAP-17 row (ADR 0016). ROADMAP repairs: BAP-17 added to the complete list, the Rust façade count
corrected 15 → 17 (the exported public contract), and amendment #2's garbled closure/residual
splice repaired so the `assemble_compact` maximum-bounds residual stands as its own sentence,
consistent with amendment #3's restatement. Docs-only — no code, wire, bound, or verdict change.
## 0.1.0 release-candidate record — 2026-08-17 (published above as [0.1.0])
### Added
- Initialize the public Apache-2.0 repository and cold-start architecture authority.
- Define the deterministic verifier boundary, public/private dependency direction, protocol
charter, threat model, conformance contract, ADR, and implementation roadmap.
- Close BAP-00 after public-remote verification, cross-repository documentation reconciliation,
security-contract hardening, local-link/browser checks, independent reviews, and tamper-gate
verification.
- Add the unpublished `:bounded_authority_protocol` 0.1.0 Mix package with zero production
dependencies, no application callback, and no supervision tree.
- Enforce the pure-library boundary across source AST, compiled imports, application metadata,
dependency declarations, and the exact packed/unpacked Hex archive.
- Add full local quality, coverage, documentation, advisory, closed license, CycloneDX,
fresh-consumer, public CI, checksum, provenance-attestation, and SBOM-attestation gates.
- Freeze the normative v1 header, claim, selector, JSON, encoding, URI, domain-separator, bound,
and fixed-error tables with primary RFC and OTP provenance.
- Add bounded ordered JSON decoding with recursive duplicate rejection, strict canonical
base64url decoding, and a protected-header-only untrusted key locator.
- Add allow, deny, malformed, exact-boundary, deterministic malformed-input sweep, symmetric
numeric-magnitude, package-consumer, and purity-boundary proof.
- Enforce raw numeric-lexeme bytes and exact decimal magnitude before OTP conversion; validate the
companion Draft 2020-12 schemas with an independent validator and distinguish their structural
code-point limits from the normative UTF-8 byte contract.
- Align the tracked roadmap with Forge's authored-row contract while preserving BAP-00 through
BAP-07 identities and exact dependency labels.
- Document the exact public `BoundedAuthorityProtocol.V1.Json.decode/2` and
`BoundedAuthorityProtocol.V1.Base64Url.decode/2` surfaces, tightening-only positive-integer
limits, structural-schema boundary, and fixed value-free errors.
- Extend the packed external consumer to exercise both decoder success and rejection paths, and
add mutation-red proof that escaped string content cannot hide a following over-limit number.
- Reconcile the BAP-02 final trusted-main receipts and degraded peer-review record.
- Add deterministic standard compact-JWS grant and RFC 9449 holder-proof producers, exact external
signature assembly, and bounded grant/proof decoders.
- Add RFC 8785 canonical JSON, RFC 7638 public Ed25519 JWK thumbprints, bounded HTTPS URI
normalization, type-preserving request digests, and conjunctive selector evaluation.
- Add standalone raw-grant verification and combined raw-envelope verification with exact
issuer/audience/time/holder/request/nonce/operation bindings and redacted, non-authorizing
verified facts.
- Add public-only grant/holder-proof vectors, independent Node verification, exact public-key
census, meaningful byte-tamper and duplicate-member cases, portable timing/allocation bounds,
architecture census updates, and unpacked external-consumer API proof.
- Close BAP-03 at package-bearing head `f322e08bba665374599b9f53c362966b6b59710a`
after the supported CI matrix, complete quality/package boundary, independent Node verification,
exact archive checksum, SLSA provenance, and CycloneDX SBOM attestation passed. The single final
review admitted five findings; all five were fixed in one pass without review recursion.
- Add closed canonical consumption rows with domain-separated hashes and mandatory-boundary raw
chain verification for genesis and continued ranges.
- Add deterministic standard-JWS boundary anchors and authenticated historical-key transitions,
including derived RFC 7638 fingerprints and lower-inclusive/upper-exclusive validity windows.
- Add deterministic binary anchored-export framing and atomic raw-chunk verification of complete
digest, exact out-of-band object version, exact EOF, ordered key rollover, both signed
boundaries, and every canonical row.
- Add closed fixed-redacted non-authorizing chain, anchor, transition, and anchored-export facts;
exact tightening-only archive bounds; constant-time fixed-width comparisons; and expanded
source/BEAM architecture accounting.
- Add five Draft 2020-12 structural schemas and public-only same-key, rollover, shortened,
relinked, same-ID/equal-time, signed cross-chain, signed reverse-time, and signed invalid-genesis
evidence. The project-independent Node verifiers prove an exact two-way eleven-key census split
into exact per-verifier sets observed at their public-key import boundaries,
49 named decoded-byte, structural, boundary, coverage, chronology, and limit cases, two direct
valid chain cases, and seven signed semantic-edge cases; the isolated mutation gate proves 47
source-level invariants go red. Published fixture verdicts and complete redacted
chain/anchor/transition/export facts are exact-checked rather than treated as commentary.
- Add the maximum-count, maximum-width worst-of-20 chain/archive resource gate, with every sample
isolated in a fresh monitored process, plus ADR 0004, normative documentation, and packed
external-consumer coverage.
- Validate every nested expected field and historical-key shape before archive hashing or parsing;
reject duplicate outer conformance JSON members; and detect self-identifying Ed25519 PKCS#8 DER
regardless of its surrounding field name.
- Make Ed25519 public-key/signature and SHA-256 digest widths immutable bounds so tightening cannot
produce or admit a structurally nonconforming cryptographic value.
- Centralize strict StringOrURI validation across grant/proof and BAP-04 identifiers so malformed
percent escapes and non-URI bytes cannot enter any producer or verifier path.
- Preserve exact package-boundary accounting across supported Elixir compiler versions and pin
bitstring match sizes required by Elixir 1.20.
- Close BAP-04 at package-bearing head `c4d7716de6499f29524e60638207b1c36e9484b3`
after the supported CI matrix, complete quality/package boundary, independent Node verification,
47/47 mutation battery, maximum-shape resource gate, exact archive checksum, SLSA provenance,
and CycloneDX SBOM attestation passed.
- Author roadmap rows BAP-08 (capability-authorization extension proposal for the MCP
`modelcontextprotocol/ext-auth` extensions repository plus an AP2 mandate-mapping note;
depends on BAP-04 only) and BAP-09 (thin TypeScript and Python verifier SDKs consuming only
the published spec and vectors; depends on BAP-05). Each row's own ADR lands when its work
starts; neither changes any wire format, limit, or verification rule.
- Close the check_envelope selector-binding gap: add a non-trivial (`equals`) selector valid case
plus an `invalid_selector` case, teach the independent Node verifier to evaluate grant selectors,
and add a `selector-reject` mutation — so a verifier that ignores grant selectors now fails the
corpus. Uses two new deterministic conformance keypairs (census 6→8); a companion
`proof_signing_input` valid case carries the new holder key as a labeled field so the
cross-verifier census discovery scan finds it (corpus 212→215).
- Close the check_envelope authority-binding gap the same re-signing capability exposed: the
holder (`cnf.jkt`), grant (`ath`), request-argument (`ba_req`), and operation (`ba_op`) bindings
had no corpus case isolating them, so a verifier omitting any one of them still scored a perfect
corpus run — omitting the holder binding accepts any holder's proof against any grant. Add four
`invalid_claim` cases (each a one-defect variant of a shared valid base, with the named binding
as the sole rejecter) and four matching mutation-battery entries. The `ba_op` case needs a
hand-built proof payload: the request digest is computed over the server-derived operation, never
over the proof's own `ba_op` claim, so only a dishonest producer — which the façade cannot be —
emits a proof whose two operation fields disagree (corpus 215→219).
- Add an empty-path `invalid_selector` case so the independent runner's selector shape and width
validation is falsifiable: the official rejects an empty selector path at grant decode, and a
matcher treating `[]` as "the root" would accept what the official refuses (corpus 219→220).
- Close the independent runner's remaining permissiveness against the official decoder, so it can
no longer certify a grant the reference implementation refuses. Six divergences, each now carried
by a corpus case and a mutation entry: an extra member inside the closed `cnf` map; a
non-printable byte in an operation name (the official requires printable ASCII); a structurally
invalid selector on a NON-matching operation, and duplicate operation names (the official
validates every operation and enforces global name uniqueness, not just the requested one); a
lone surrogate in a selector path (the official requires valid UTF-8); and a `__proto__` member
in a selector value, which the tagged projection silently dropped to the prototype setter so that
two structurally different values compared equal — a collapse that reached the request digest as
well as selector matching. Selector values are now also held to the protocol JSON bounds. The
same operation validation is applied on `verify_grant`, which reaches the same official decode
path (corpus 220→247; `check_envelope/invalid_encoding` 6, `verify_grant/invalid_encoding` 5).
- Add `.gitleaks.toml`: the `jwt` and `generic-api-key` rules are allowlisted for the conformance
corpus and vector paths only, where all 283 findings are JWT-shaped high-entropy public test
material (109 `jwt`, 174 `generic-api-key`, the latter entirely key fingerprints). Every other
default rule still applies in those trees — a `ghp_…` token committed there is still caught —
and every rule applies everywhere else. Stated residual: a credential matching ONLY those two
rules, under those two machine-generated fixture directories, is not flagged.
- Complete the payload-field decode mirror, closing the last of the runner's permissiveness
against the official decoder. The runner previously validated only the grant/proof fields the
expected-context comparison happened to touch; it now mirrors decode_grant_fields /
decode_proof_fields field for field — issuer/grant-id/audience StringOrURI validity and length,
audience count bound and uniqueness, coherent times (iat<exp, nbf<exp), method token charset,
invocation UUID shape, and htu normalization — wherever it reads a grant or proof payload
(check_envelope, verify_grant, decode_grant, decode_proof). Nine cases on the DECODE surfaces
(no expected context to mask the validator, so each is the sole reject reason) with six mutation
entries prove them; corpus 236→247. One bound stays out of scope by construction: the aggregate
total_nodes/depth budget the official applies across the whole payload cannot appear inline (such
an input exceeds string_bytes) and these surfaces take no `.raw` sidecar, so it is exercised at
the json.decode surface instead. Two further field checks the first mirror pass missed,
found by the closeout review: the StringOrURI structural gate (the official validates
iss/jti/aud through URI.new, so it rejects a non-numeric port, an unterminated IPv6
literal, or a double `@`; the byte-only mirror accepted them — now matched to URI.new
across a 56-input boundary set, using node:net for IPv6 literals so the mirror is neither
looser nor stricter), and the optional proof nonce (present must be a 1..nonce_bytes
well-formed string). Each with a decode case and mutation entry; corpus 236→247.
- Close three further runner/official divergences the final cross-vendor pass found in selector
value validation, each now carried by a corpus case and a mutation entry: the magnitude bound
(the official caps |value| at 9007199254740991 and rejects 2^53; the runner checked only
finiteness), and a one-byte floor on object member keys that the official does not impose — the
runner rejected `{"":1}`, which `Json.decode` and `Jcs.encode` both accept, making it STRICTER
than the reference. Corpus 233→236 (a valid empty-object-key case pins the strictness fix).
- Adopt the standards track charter (ADR 0006, `docs/design/standards-track.md`,
`docs/design/registries.md`, `docs/errata.md`): evolution above the permanently closed wire
format via parallel contract-majors with published deprecation windows; the current profile
named as cryptographic suite `BAP1-Ed25519-SHA256` with an ML-DSA succession path and
cross-suite countersignature design for long-retention evidence; RFC 2119 requirement
identifiers with corpus traceability and IANA registration templates as release-gating roadmap
rows; delegation-with-attenuation decided (chained grants, `ba_dlg`/`ba_obo`/`ba+cap-delegated`
reserved, the conjunctive selector algebra as the attenuation language, no caveat DSL);
revocation and principal-binding deployment guidance homed; governance (change classes, errata
registry with the no-verdict-flip invariant, comment-window triggers) published. No wire byte,
bound, or verdict changes; new roadmap rows gate first publication on the unretrofittable items.
- Add the portable v1 conformance corpus (247 cases across 28 surfaces with a total
surface × class applicability matrix, `.raw` sidecars for oversize wire inputs) and the pure
`Conformance.Corpus`/`Runner`/`Report` core that loads, executes, and reports agreement.
- Harden the corpus against vacuous green: author the invalid vectors the crypto verifying
surfaces were missing — algorithm-confusion (`alg:"none"`), meaningful-byte signature/commitment/
anchor tampers (via a target-addressed tamper loader that binds a single-byte flip to the
compact / grant / proof / row / chunk bytes), and archive/chain/envelope binding failures — plus
exact-bound and maximum-plus-one pairs for every `json.decode` structural limit. Each new invalid
vector is a one-defect construction confirmed rejected by BOTH the official facade and the
independent Node runner; every remaining not-applicable applicability cell carries a falsifiable
inexpressibility reason. The independent Node runner gained the request-binding (method / URI /
invocation / operation / nonce) and object-name-byte checks the new vectors surfaced.
- Add the deterministic offline verifier CLI (escript `bounded_authority_conformance`,
`--corpus DIR` required, exits 0/1/2) with an exact-path purity carve-out (File/IO in `cli.ex`,
`System.halt` in `cli/main.ex` only) enforced by the architecture gate.
- Add the independent Node second-implementation runner
(`conformance/corpus_independent.mjs`, node:* only) that recomputes every corpus verdict from
scratch — making the corpus normative. Evolve the public-key census to three partitions
(bap03 + chain_archive + corpus = the canonical set); the corpus self-census is hard two-way.
- Add stream_data property gates (JCS determinism/idempotence, base64url round-trip/pad rejection,
URI normalization idempotence, facade closure totality) and a deterministic-PRNG fuzz gate.
- Add the source-isolated conformance mutation battery (proving the corpus integrity, CLI
carve-out, and runner-verdict gates actually catch their named failures); wire both mutation
batteries into `mix quality`. CI pins Node 20 for the quality job.
- Close BAP-05 with ADR 0005 (corpus formats, sidecar rule, published-artifacts definition,
applicability matrix + n_a criterion, census evolution, CLI contract, carve-out shape,
gate set). The corpus ships in the published package; the fresh-consumer check runs the packaged
escript against the packaged corpus, proving published-set sufficiency.
- Close the remaining independent-runner permissiveness residuals surfaced by a cross-vendor design
review (corpus grows 247 → 259 cases). The Node runner now mirrors the official on: the integer
magnitude bound for every integer claim (a proof `iat` of 2^53 is rejected); the full
`request_digest` gate on `cast_arguments` — operation validity, per-node bounds, `total_nodes`, and
`jcs_bytes` on the type-tagged projection, not an unbounded digest; and whole-payload container
depth on grant/proof payloads (a payload nested past depth 32 is rejected at parse). Fix the sibling
`jsonDecode` per-node-type depth error (it rejected a 32-deep scalar-inner nest the official
accepts — the too-strict direction that fails a conforming verifier). Twelve exact-bound /
maximum-plus-one cases and seven source mutations pin each, each verdict confirmed against the
official facade. ADR 0005 corrected: whole-payload depth and value-carried `cast_arguments`
node/byte bounds are inline-expressible and now tested; only compact-carried whole-payload
`total_nodes` and an inline 65-member `object_members` remain enforced-without-a-red-case.
- Close BAP-06: lock the 0.1.0 release-candidate public API surface (enumerated in the
[release-candidate contract](release-candidate-contract.md) and enforced by the
`@compiled_export_allowances` architecture-gate pin; [ADR 0008](adr/0008-release-candidate-contract.md)),
add the `release.candidate` reproducibility gate (two cache-isolated builds, byte-equal SHA-256,
wired into `mix quality`), and author the candidate-facing docs (release-candidate-contract.md,
SECURITY.md, CHANGELOG `[0.1.0]`, README). Zero wire byte, bound, or verdict change. Published
2026-08-20 — BAP-07 published the exact candidate after the connected gates passed.