Repository navigation
Release 2.0.0: CLI correctness fixes, AP2 format pin, single-source versioning, hardened release gates - #104
Merged
Merged
Conversation
Merge the four undated candidate sections into one [Unreleased] section with Added, Changed, Removed, Fixed and Security subsections, keeping every bullet's wording. The remote-binding removal moves from Security to Removed and is marked breaking, naming the CreateApiServerOptions.allowRemote option and the serve --allow-remote flag that 1.2.0 shipped, with a migration note. Released headings become dated, linked [X.Y.Z] - YYYY-MM-DD headings, and compare-link references are added at the bottom. The drift test now parses the bracketed, dated heading and requires [Unreleased] above it.
Add scripts/version.mjs with three commands. check fails, naming the file, when the lockfile, the RELEASE_VERSION literal, the conformance declaration for the release major.minor, OpenAPI info.version, the CHANGELOG headings and links, or an optional release tag disagree with package.json. sync edits those targets in place with one targeted replacement each and refuses to write if a replacement would change anything but the version field, because openapi.json does not survive a JSON.stringify round trip. notes prints a CHANGELOG section for the GitHub Release with relative links pinned to the tag. version:check runs first in verify:static, and sync is wired as the npm version lifecycle script. Running sync once moves OpenAPI info.version from 1.0.0 to the package release 1.2.0. CONTRIBUTING gains a Releasing section.
compareCaseAssessments reports validity as after.valid, while the targeted comparisons report afterValid. The CLI read only afterValid, so a pair of equally tampered revisions exited 0 with ok:true, contradicting the documented exit table and the four sibling comparisons, which exit 3. Derive the current-invalid flag from whichever field the comparison carries. Precedence is unchanged: noncomparable 3, regression 5, invalid current 3, otherwise 0. The new test fails on the previous build.
The API gated /v1/evaluations through a complete-case check, but the CLI
cast any JSON to an evaluation input. The engine then fabricated a
malformed-case decision dated 1970 that decide appended to the store and
both commands printed with exit 0, plus a raw TypeError line on stderr.
Move the gate into an internal src/evaluation-input.ts module shared by
both boundaries. The API rethrows its errors with the exact existing 422
codes and messages; the CLI applies it right after reading input, before
the engine runs or a store is opened or locked, and exits 3.
Tests that pinned {} or {pins:{}} as decide input now send the principal
scenario case so the stub-engine, stub-store and corrupt-store mappings
are still exercised. examples/README.md no longer sends users to decide
on a bundle file. Breaking for CLI callers only.
…cks as unavailable The CLI entrypoint installed no signal handling, so stopping serve --store with Ctrl+C orphaned the JSONL writer lock. Every later writer then failed with exit 5 and a message blaming a state conflict, while the API called the same condition 503. The entrypoint now passes an onServer hook that registers once-only SIGINT and SIGTERM handlers for a running server. Each closes the server, which closes the store and removes the lock, and sets exit 130 or 143; a second signal falls back to the default kill. Handlers exist only while a server does, so other commands keep default signal behavior. ALB_STORE_LOCKED now maps to exit 6 with a fixed, path-free hint about stale .lock files instead of exit 5. The end-to-end signal test is POSIX only because Windows cannot deliver a catchable SIGINT to a child; the handler logic is unit tested on every platform.
…vailability errors A non-loopback --host reached createApiServer after the store had been opened and locked, and its TypeError surfaced as exit 3 'Protocol artifact is invalid'. serve now validates the host with isLoopbackAddress and the port before opening a store, and a bad host exits 2 naming the accepted loopback IP literals. A busy, unbindable or forbidden port (EADDRINUSE, EADDRNOTAVAIL, EACCES) now exits 6 with ALB_SERVE_UNAVAILABLE instead of 70, while still closing the owned store. An unknown simulate scenario keeps ALB_SCENARIO_UNKNOWN and exit 3 but lists the valid scenarios instead of a generic message.
replay cast record.checkpoint straight to AppealCheckpoint, so a string,
number, array or wrongly typed object was reported as a history mismatch
with exit 5 and echoed back verbatim in completeness.checkpoint. replay
also accepted extra envelope keys, and appeal read record.event ?? input
without an exact-key check, so a misspelled decision key was dropped.
Add an exported isAppealCheckpoint guard that requires a plain object with
exactly a safe-integer sequence of at least 1 and a sha256 headDigest.
The CLI now accepts a bare events array or exactly {events, checkpoint?}
and rejects a malformed checkpoint with exit 3 and a fixed message that
never reflects the value; an event-keyed appeal envelope must be exactly
{event, decision?}. replayAppealEvents semantics are unchanged.
check-licenses.mjs returned early when node_modules was absent and then printed 'license check: 0 installed packages use approved licenses' with exit 0, so the release gate passed vacuously. The sibling advisory check already fails closed in the same case. Exit 1 with an actionable message when node_modules is missing, and also when the tree holds no package manifests at all. Approved licenses and the success wording are unchanged.
The runtime declares external_evidence_review_v1 as supported, but the conformance command and the published fixtureTests omitted its fixture file, and the parity test compared only the release and profiles, so nothing noticed. Append test/review-external.test.mjs to the conformance script and the declaration, publish every capability id with its status, and extend the parity test: the published capabilities must equal the runtime list, the fixture list must equal the files the npm script runs, and every listed file must exist. npm run conformance now runs 72 tests.
RELEASE_VERSION was stamped into every AP2 resolution, Evidence Pack and Pack verification report, and Pack verification required it to equal the running package. schemas/v1.2 pin the same field to the constant 1.2.0, so any package release would have invalidated every retained Pack and produced schema-violating artifacts. Setting RELEASE_VERSION to 9.9.9 broke three AP2 tests. Add AP2_DISPUTE_FORMAT_RELEASE, frozen at 1.2.0, and use it for the AP2 types, producers and the Pack shape check. Artifact bytes are unchanged. The HTML timeline now labels the value 'Pack format'. CLI --version and --help add protocolVersion and ap2PackFormatRelease and keep version as the protocol alias. Commit a golden Pack written by the released 1.2.0 code (tag v1.2.0, b2ca2c5) from that tag's own test helpers, with the Checkout Mandate iat/exp that main requires since fc27d70, and test that it verifies with its retained digest and holds only public JWK members. With RELEASE_VERSION at 9.9.9 every AP2 test now passes and only release-label tests fail. ADR 0003 records the version layers.
…ts CSP The metadata-only AP2 timeline is a reviewer-facing artifact, but its table had no header scopes, caption or keyboard-reachable scroll region, and its CSP lacked the base-uri and form-action locks the case report already ships. Add scope=col to every header, wrap the table in a focusable labelled scroll region with a caption, add the matching focus, scroll and print styles from the case report, and extend the CSP with base-uri 'none' and form-action 'none'. Escaping and raw-token omission are unchanged.
--help named the commands but not their actions, the simulate scenarios or the exit codes, and the action lists existed only as inline literals in the requireSubcommandInput calls, so help could not be derived from them. Hoist one frozen action list per subcommand family (casepack, policy, ap2-dispute, and the 23 operator actions). The dispatcher validates against those lists and CLI_COMMANDS publishes them as an optional actions field. Help also adds scenarios (all plus every synthetic scenario) and exitCodes derived from CLI_EXIT with a one-line meaning each. Existing help fields are unchanged; the change is additive.
…ough the privacy-notice forms review, serve, help and version had no user documentation, the full exit code table and per-command mapping existed nowhere, and the store lock and loopback Host/Origin rules were only summarized. Add docs/CLI.md covering input conventions, every input limit, each command's input, output and exit codes, the review input schema, extracted fields and verdicts, and serve's bind, Host, Origin, lock and signal behavior. Four doc lines claimed a 4 MiB cap the code does not enforce: operator batches and actions, casepack and case-report read up to 17 MiB with depth 48 and 250,000 nodes, and receipt canonical input is bounded to 16 MiB. Correct them in OPERATOR_WORKFLOWS and V1_2, and route the README command map and SECURITY's API boundary summary to the reference. A new test fails when a command, action, scenario or exit code is missing from the page or a documented limit stops matching the values the CLI reads with, now exported as CLI_INPUT_LIMITS from the CLI module only. Delete the legacy Markdown issue templates so only the forms that carry the synthetic-data warning remain; blank issues are already disabled.
…ype against the floor Windows is a declared target, but CI ran 4 of the test files there. The Windows job now builds and runs the whole suite before the installed package check; tests that cannot run on Windows already skip themselves. Add Node 26.11.1, which is in the setup-node manifest, to the static and coverage matrices beside 22.12.0 and 24.18.0. Drop the dist/cli.js exclusion from the coverage gate: with the CLI included the aggregate is 96.12% lines, 91.38% branches and 96.83% functions against 90/85/90. Set @types/node to ^22.20.5 so code cannot type-check against APIs that the Node 22.12 floor lacks, and make Dependabot ignore its semver-major updates. The routine update groups are unchanged.
…ked content The release workflow granted contents, id-token, attestations and artifact-metadata write at workflow level to the one job that runs npm ci and npm run verify, so every dev tool and test could request an OIDC token. It also configured an npm registry for a publish that never happens, and SECURITY.md promised a release secret scan that nothing performed. Default the workflow to contents: read with a per-tag concurrency group. Job build checks out the tag, runs the version check against the tag, verifies, packs, writes the SBOM and checksums, and hands the files on as a short-lived artifact. Job publish, the only one with write scopes, installs nothing and runs no repository code: it downloads the files, checks them with sha256sum -c, attests provenance and the SBOM, and runs gh release upload with an explicit --repo because it has no checkout. upload-artifact v7.0.1 and download-artifact v8.0.1 are pinned by SHA. The package check now reads every packed file and rejects a PEM private key block, reporting the path only. SECURITY.md lists the controls that actually run.
Cut the unreleased work since 1.2.0 and this branch as 2.0.0. It is a major release because the public CreateApiServerOptions.allowRemote option and the serve --allow-remote flag that 1.2.0 shipped are gone, and because CLI exit codes and accepted inputs changed (decide and preview reject incomplete cases, held locks and busy ports exit 6, a bad serve host exits 2). It also gives builds with the RFC 8785 separator correction a release label distinct from the regression window. Protocol, engine, native bytes and the AP2 Pack format release are unchanged. The conformance declaration moves to conformance/v2.0 before the bump so the npm version lifecycle hook finds it; npm version 2.0.0 then moved package.json and the lockfile and synced src/version.ts, the declaration and OpenAPI info.version. conformance/v1.2/README.md returns to its 1.2.0 text with a historical note, and conformance/v2.0/README.md carries the current fixture list including external review. The CHANGELOG dates [2.0.0] with an Upgrading from 1.2.0 section, opens an empty [Unreleased] and updates the compare links. SECURITY.md supports 2.0.x, and the adopter, evidence-operations, protocol and issue-template text no longer hard-code 1.2.0. Tests derive the expected release and the declaration path from package.json.
…t delegated expiry Add a second frozen Pack written by the released 1.2.0 code (tag v1.2.0, b2ca2c5) from that tag's unmodified makeFixture() and makePackInput() helpers. Its delegated Checkout Mandate payload has no exp. The 1.2.0 verifier reports it verified; since fc27d70 the resolution gate requires delegated expiry, so the current verifier reports it unresolved with AP2_PACK_RESOLUTION_UNRESOLVED while digestValid and anchorMatched stay true. The test pins that outcome, the AP2_EXPIRY_MISSING resolution issue and schema validity of the report. Rename the existing golden-Pack test to say its input carries delegated expiry, and extend the public-key-material scan to both fixtures.
The 2.0.0 upgrade notes, docs/V1_2.md and ADR 0003 said every Pack retained from 1.2.0 verifies unchanged. A Pack the released 1.2.0 code writes from its own test helpers has no exp on its delegated Checkout Mandate payload; since fc27d70 the resolution gate requires delegated expiry, so 2.0.0 verifies that Pack as unresolved with AP2_PACK_RESOLUTION_UNRESOLVED while its digest and anchor still check out. State what actually holds: the format label, Pack digest and anchor match are unchanged, verification re-derives the resolution under the current gates, and a 1.2.0 Pack without delegated exp is unresolved. Say the release label alone no longer invalidates retained Packs, and drop the pre-cut "Preserve 1.2.0 release pins ... historical AP2 packs" bullet from [2.0.0] Changed: the release label moves to 2.0.0 and the upgrade notes already list the unchanged native bytes, protocol and engine.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR releases 2.0.0. It cuts the 169 commits that have accumulated on
mainsincev1.2.0, plus the 18 commits below.It is a major release because 1.2.0 shipped the public
CreateApiServerOptions.allowRemoteoption and theserve --allow-remoteflag, and both are gone onmain. This PR also changes some CLI exit codes and accepted inputs on purpose. Protocol1.0.0, engine1.0.0, native artifact bytes, schemas and decision pins do not change.The PR does four things:
operator comparenow fails when the current case is invalid.decideandpreviewapply the API's complete-case gate. Stoppingservewith Ctrl+C no longer leaves a stale lock.servehost, port and scenario mistakes get proper usage and availability codes, andreplayandappealenvelopes are validated strictly.AP2_DISPUTE_FORMAT_RELEASE = "1.2.0". Two frozen Packs written by the released 1.2.0 code pin what that buys. The release label alone never invalidates a retained Pack, and a 1.2.0 Pack's digest and anchor still check out. Verification re-derives the resolution under the current gates, though, so a 1.2.0 Pack whose delegated Mandate payload has noexpnow verifies asunresolved: delegated payloads require expiry sincefc27d70. The upgrade notes say so.package.jsondrives a newscripts/version.mjs. Itscheckcommand fails CI on drift. Itssynccommand runs from the npmversionhook. Itsnotescommand prints the GitHub Release notes. The CHANGELOG now follows Keep a Changelog.node_modulesis missing.Review findings addressed
operator compareexits 0 withok: truewhen the current assessment is invalidserve --storeleaves a stale.lock, and later writes fail with exit 5 "state conflict"decide/previewskip the API complete-case gate and persist fabricatedmalformed-casedecisions with exit 0examples/README.mdrunsdecideon a bundle file that nothing producesserve --hostreports "Protocol artifact is invalid" (exit 3) after locking the storereplay/appealaccept unvalidated envelopes and echo arbitrary checkpoint valuesnode_modulesreview,serve,help,versionand the full exit-code table are undocumenteddecide {}serveon a busy port exits 70 instead of unavailable@types/nodetargets Node 26 while the floor is 22.12docs/V1_2.mdand ADR 0003 promise every Pack retained from 1.2.0 verifies unchanged; a Pack the 1.2.0 code writes from its own test helpers isunresolvedunder 2.0.0Commits
a9655aedocs(changelog): adopt Keep a Changelog with dated, linked releases9158024build(version): single-source the release version and gate drift in CI47cf3e6fix(cli): exit 3 from operator compare when the current case is invalid3eb18d5fix(cli): require a complete evaluation case before decide or preview607ec56fix(cli): release the store lock on SIGINT/SIGTERM and report held locks as unavailable0a61412fix(cli): report serve host, port and scenario mistakes as usage or availability errors87ca43bfix(cli): validate appeal and replay envelopes and replay checkpointse949b85fix(scripts): fail the license gate closed without a dependency treed2a16eefix(conformance): run and publish the external evidence review fixturesb81432ffix(ap2-dispute): pin the AP2 Pack format release apart from the package88b8118fix(ap2-dispute): make the evidence timeline accessible and tighten its CSP6e5fc77feat(cli): list subcommand actions, scenarios and exit codes in --helped659ffdocs: add a CLI reference, correct input limits, and route issues through the privacy-notice forms298d2c1ci: run the full suite on Windows, test Node 26, gate CLI coverage, type against the floor69502caci(release): verify without write tokens, attest separately, scan packed contentf5ec9d2chore(release): 2.0.0377c376test(ap2-dispute): pin the unresolved outcome for a 1.2.0 Pack without delegated expirye10709fdocs: correct the retained 1.2.0 Pack compatibility claimEach commit builds and passes the full suite on its own.
Versioning and release plan
These surfaces all read 2.0.0, and
node scripts/version.mjs check --tag v2.0.0passes:package.json, and both version fields inpackage-lock.json;RELEASE_VERSIONinsrc/version.ts;conformance/v2.0/capabilities.jsonrelease;openapi/openapi.jsoninfo.version;## [2.0.0] - 2026-10-09heading and its compare links.These stay unchanged by design:
PROTOCOL_VERSION1.0.0,ENGINE_VERSION1.0.0, andAP2_DISPUTE_FORMAT_RELEASE1.2.0(seedocs/adr/0003-version-layers.md).--versionnow prints:{"name":"MandateBound","version":"1.0.0","protocolVersion":"1.0.0","releaseVersion":"2.0.0","engineVersion":"1.0.0","ap2PackFormatRelease":"1.2.0"}Tag to create after merge:
v2.0.0, lightweight likev1.0.0andv1.2.0, created bygh release create.gh pr merge --merge) so all 18 commits stay onmain. Do this only after every CI leg is green, becausemainhas no branch protection.[2.0.0]date first. The check accepts any valid date.release.yml, thengh run watchfollows it:buildruns read-only. It verifies the tag and every version surface, runsnpm run verify, then packs the tgz and writes the SPDX SBOM andSHA256SUMS.publishholds the write scopes. It runssha256sum -c, attests provenance and the SBOM, then runsgh release upload --repo.gh release view v2.0.0 --json assetsshows three assets.sha256sum -c SHA256SUMSandgh attestation verify mandatebound-2.0.0.tgz -R EauDoon/mandatebound.Nothing is published to npm. The package is not on the registry, and merging deploys nothing.
You can also run an optional smoke test before the real release:
gh workflow run release.yml -f release_tag=v1.2.0. It must fail inbuildat the tag-diff or version check, andpublishmust be skipped.Verification
Run on Windows 11 with Node 24.18.0 and npm 12.0.2, on the final branch head:
npm ci --ignore-scriptsnpm run verifynpm run conformancenpm run buildthennode --test(the new Windows CI step)node scripts/version.mjs check --tag v2.0.0node scripts/version.mjs notes 2.0.0[2.0.0]section, with relative links pinned toblob/v2.0.0RELEASE_VERSIONset to 9.9.9, then reverted)v1.2.0rebuilt in scratch, Pack from its unmodified helpers)verified; this head reportsunresolvedwithAP2_PACK_RESOLUTION_UNRESOLVED,digestValidandanchorMatchedtrue. Adding onlyexpto the delegated Checkout Mandate makes itverifiedat this headpy -I privacy_screen.py <repo> origin/main --extra-file <this body>ci.yml,release.ymlanddependabot.ymlload; release job permissions are as designede10709fnpm run verifyran these steps:@types/node22.20.5;dist/cli.jsincluded: 96.12% lines, 91.38% branches and 96.83% functions, against thresholds of 90, 85 and 90;examples/case-study.mjs;The end-to-end SIGINT/SIGTERM lock test runs only on the Linux CI legs. Windows cannot deliver a catchable SIGINT to a child process. The handler logic has a cross-platform unit test, and an in-process scratch run on Windows confirmed the lock is removed and the exit code is 130.
Deviations from the plan
git archiveinto a scratch directory outside the repo, rather than withgit worktree. It was then built atv1.2.0(b2ca2c5). The Pack was produced by that released code from the tag's ownmakeFixture/makePackInput, with one change: the Checkout Mandate carriesiat/exp. Sincefc27d70(after 1.2.0),mainrequires expiry on delegated payloads. Without that change, the current verifier reports the pure-tag Pack asunresolved, although its digest and anchor still verify. The test comment records this. Commit 17 adds that pure-tag Pack as a second frozen fixture and pins itsunresolvedoutcome, and commit 18 corrects the upgrade notes,docs/V1_2.mdand ADR 0003, which had promised that every retained 1.2.0 Pack verifies unchanged.src/store.tswas needed. The handler is exported from the CLI module ascloseServerOnSignals, with an injectable emitter and exit setter, so it can be unit-tested on every platform. The entrypoint also keeps a signal received during startup from being overwritten by the command's success code.--portis now also validated before the store opens. An invalid--hostchanges from exit 3 to exit 2, and the upgrade notes record this as breaking.Appeal input is invalid.. A non-object, non-array replay input now reportsReplay input is invalid., where it used to report the generic shape message.CLI_INPUT_LIMITSis exported fromdist/cli.jsonly, so the docs drift test reads the real limits; it is not part of any package entry point.SECURITY.mdnow points to the exactserveboundary rules.node --testafter the guarded build, notnpm test, which would build a second time. A CHANGELOG bullet records the CI changes.conformance/v1.2/README.mdreturns to its 1.2.0 text with a historical note, because commit 9 had extended it while it was still the current declaration.conformance/v2.0/README.mdcarries the current list.serve --host,operator compare,replayandappealbehavior changes, anddocs/ADOPTER_WORKFLOW.mdlists the CI Node versions.scripts/version.mjs.checkalso rejects impossible dates and requires the[Unreleased]link to compare fromv<version>.notesrewrites relative links to the tag's blob URLs, so they resolve on the release page.@types/nodeto^22.20.5and ignores its semver-major updates. build(deps-dev): bump @types/node from 26.6.2 to 26.6.4 in the npm-routine-updates group across 1 directory #101 conflicts inpackage-lock.jsonand should be closed.