diff --git a/.agent-runtime/goals/first-v0-release.json b/.agent-runtime/goals/first-v0-release.json index da3695f..3e06e61 100644 --- a/.agent-runtime/goals/first-v0-release.json +++ b/.agent-runtime/goals/first-v0-release.json @@ -1,6 +1,6 @@ { "schema_version": "v1alpha1", - "revision": 10, + "revision": 16, "goal": { "id": "first-v0-release", "intent": "Publish the first verifiable v0.1.0 Go module/source release from an annotated signed exact-main tag", @@ -8,37 +8,86 @@ { "id": "contract", "acceptance": "One versioned fail-closed source of truth defines release identity, assets, bounds, workflow and action pins", - "status": "pending" + "status": "complete", + "evidence": [ + { + "type": "file", + "reference": "release/v1alpha1.json; schemas/release-contract-v1alpha1.schema.json; internal/releasecontract", + "result": "One versioned fail-closed source of truth defines release identity, assets, bounds, workflow and action pins; check-release-contract enforces it and CI runs it on every change." + } + ] }, { "id": "deterministic", "acceptance": "Exact-commit source archive, SPDX SBOM, notes, manifest and checksums are deterministic, closed and deeply tested", - "status": "pending" + "status": "complete", + "evidence": [ + { + "type": "command", + "reference": "check-release-contract --build twice into distinct leaves, then diff -rq", + "result": "Exact-commit source archive, SPDX SBOM, notes, manifest and checksums reproduce byte for byte; CI repeats the comparison on every pull request." + } + ] }, { "id": "security", "acceptance": "Tag-only minimal-permission publication verifies signed exact-main identity and keylessly attests every material asset", - "status": "pending" + "status": "complete", + "evidence": [ + { + "type": "link", + "reference": "https://github.com/NDDev-it-com/agent-runtime/releases/tag/v0.1.3", + "result": "Published from a tag-only minimal-permission workflow that verifies the signed exact-main identity and keylessly attests all five material assets." + } + ] }, { "id": "documentation", "acceptance": "Changelog, release docs, README, package and CLI version vocabulary match the honest v0.1.0 Go module/source boundary", - "status": "pending" + "status": "complete", + "evidence": [ + { + "type": "file", + "reference": "CHANGELOG.md; docs/releasing.md; README.md", + "result": "Changelog, release guide and README describe the Go module and source boundary and state plainly that v0.1.0 and v0.1.1 are tagged but unpublished." + } + ] }, { "id": "verification", "acceptance": "Focused/full/race/fuzz/vet/format/schema/security and exact-head CI/CodeQL/post-main evidence are green", - "status": "pending" + "status": "complete", + "evidence": [ + { + "type": "test", + "reference": "https://github.com/NDDev-it-com/agent-runtime/actions", + "result": "Focused, full, race, fuzz, vet, format, schema and security lanes plus exact-head CI and CodeQL were green on the published commit." + } + ] }, { "id": "publication", "acceptance": "Immutable GitHub release, asset digests, SBOM, manifest, attestations and Go module discovery are verified without ambiguity", - "status": "pending" + "status": "complete", + "evidence": [ + { + "type": "link", + "reference": "https://github.com/NDDev-it-com/agent-runtime/releases/tag/v0.1.2", + "result": "v0.1.2 was the first published release and v0.1.3 followed; both carry asset digests, SBOM, manifest and attestations, and the Go proxy resolves the module." + } + ] }, { "id": "closure", "acceptance": "Issue #9 closes only after terminal release evidence and clean synchronized main", - "status": "pending" + "status": "complete", + "evidence": [ + { + "type": "issue", + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9", + "result": "Closed after terminal release evidence on a clean synchronized main." + } + ] } ], "non_goals": [ @@ -46,253 +95,155 @@ "Observability/runtime semantic or repository governance check changes", "Organization, credentials, secrets, environments, siblings, GDS or unrelated repositories" ], - "state": "active", - "current_phase": "execute", + "state": "completed", + "current_phase": "closure", "receipts": { - "gap_plan": { - "phase": "gap_plan", - "summary": "Implement a local v1alpha1 release contract and Go builder/validator so Go v-tags, exact-main signed tag checks and repository-specific package semantics stay canonical. PR/main dry-run remains inside existing required CI checks; one tag-only publication workflow owns minimal write/OIDC permissions. Immutable release enablement and the signed tag remain post-merge gates.", + "closure": { + "phase": "closure", + "summary": "The first verifiable v0 release shipped, though not as v0.1.0. v0.1.0 and v0.1.1 were tagged and their publish jobs failed before building an asset, so both tags are permanently unpublished; v0.1.2 was the first published release and v0.1.3 followed. This journal was also migrated: it recorded each recovery cycle under an invented receipt key, which the Goal contract does not permit, so the module shipped inside its own release archive a journal its own CLI refused to load. Every cycle's summary and evidence is preserved, folded into the phase receipt each cycle already named in its own phase field. The pre-migration form is in this repository's Git history, and cmd/check-goal-journals now makes the omission that allowed it unrepeatable.", "evidence": [ { - "type": "issue", - "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9", - "result": "Executable acceptance defines five-asset closure, deterministic metadata, SPDX/manifest bindings, negative tests, publication and terminal verification." - } - ], - "recorded_at": "2026-08-13T03:49:29.568819Z" - }, - "orient": { - "phase": "orient", - "summary": "Accepted clean exact main 3b4087bc; audited module/public packages/reference CLI, Go 1.24 baseline and Go 1.25 scanner lane, AGPL/docs/changelog/security, absent tags/releases/consumers, workflow permissions/governance, inherited immutable tag rules, disabled immutable releases, sibling supply-chain conventions and current primary guidance. Chose honest v0.1.0 Go module/source-only release with exactly five assets and no platform binaries.", - "evidence": [ + "type": "link", + "reference": "https://github.com/NDDev-it-com/agent-runtime/releases/tag/v0.1.3", + "result": "Latest published release with five attested assets." + }, { "type": "issue", - "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9", - "result": "Canonical roadmap/release issue records done, remaining, exact acceptance, threat model, rollback, non-goals and omissions before tree implementation." + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280536696", + "result": "Recovery Attempt1 receipt, preserved verbatim as evidence on the verify phase by the migration." } ], - "recorded_at": "2026-08-13T03:49:29.409182Z" + "recorded_at": "2026-08-14T23:56:29.075454892Z", + "closure": { + "achieved_outcome": "A verifiable v0 Go module and source release is published from a signed exact-main tag, with deterministic assets and keyless attestations.", + "cleanup": "Recovery-cycle receipt keys folded into the eight contracted phases with every summary and evidence record retained; an evidence type of 'source', which was never in the contracted vocabulary, is recorded as a link.", + "remaining": [ + { + "kind": "debt", + "summary": "v0.1.0 and v0.1.1 remain tagged and unpublished. Their Go proxy entries are permanent, so the tags stay and must not be consumed; this is documented in the changelog, the README and the release guide." + } + ], + "next_work": [ + { + "type": "issue", + "reference": "self verified journals", + "result": "cmd/check-goal-journals holds every tracked journal to the Goal contract and the published schema, so a schema change can no longer invalidate a shipped artifact unnoticed." + } + ] + } }, - "recovery_cycle_verification_orient": { - "phase": "orient", - "summary": "New recovery-cycle Attempt1 preserved all earlier receipts and audited repository workflows, scripts, Make targets, docs and Goal journals. No owned module-tidy gate existed; the prior terminal command incorrectly compared intentional feature WIP with HEAD instead of evaluating go mod tidy -diff.", + "completeness_omission_audit": { + "phase": "completeness_omission_audit", + "summary": "[recovery_cycle_verification_omission] Asked what may have been forgotten: checked real tidy drift, clean modules amid expected and unrelated WIP, stdout/stderr separation, root error identity, nil execution boundaries, module-file immutability, CI/docs parity, build tags, fuzz targets, SPDX, schemas, action pins, dependency closure, dirty paths and tag/release absence. [recovery_cycle_darwin_alias_preverify_audit] Before the first gate, reviewed every new return, descriptor owner, rewalk, alias policy, cleanup path and verifier call site. Corrected partial-capture error returns so deferred cleanup retains ownership, preserved stable path-identity error vocabulary, and synchronized the public release procedure. Existing trust residual-debt, root-plus-cleanup, signer, module, SPDX, archive and publication boundaries remain unchanged. [recovery_cycle_darwin_alias_attempt2_omission] Asked what may have been forgotten: checked child creation/removal, parallel repositories, ancestor rename/inode swap/symlink insertion, owner/group/mode/type/unlink/link drift, alias target and identity drift, component and target budgets, root-plus-cleanup evidence, partial capture cleanup, fd close-once, platform build tags, workflow/docs parity, tags/releases and temporary proof residue.", "evidence": [ { - "type": "issue", - "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280458711", - "result": "API-byte-equal lineage records the side-effect-free verifier design and exact preserved base/diff state." + "type": "command", + "reference": "repository-wide rg, diff inventory, native gates and workflow/contract validation", + "result": "No omitted operational module command or verification bypass remains; cross-target local probe was blocked before execution by command-policy infrastructure and Linux remains an exact-head CI requirement." + }, + { + "type": "command", + "reference": "repository-wide signature/alias/cleanup search, complete changed-file inspection and diff check", + "result": "No alternate symlink resolver, path-based trust fallback, direct signature-verification fork or unowned captured descriptor remains before verification." + }, + { + "type": "command", + "reference": "negative/positive invariant tests, full fuzz/race/security gates and clean temporary-proof cleanup", + "result": "No silent alias expansion, stale equality check, test serialization, skipped platform lane, release side effect or remaining temporary bundle was found." } ], - "recorded_at": "2026-08-13T12:38:25Z" + "recorded_at": "2026-08-13T13:29:48Z" }, - "recovery_cycle_verification_execute": { + "execute": { "phase": "execute", - "summary": "Implemented one side-effect-free module verifier and thin command. CI and release documentation delegate to it; tests prove exact invocation, local toolchain enforcement, output/error preservation, worktree independence, real tidy drift rejection and module-file immutability.", + "summary": "[recovery_cycle_verification_execute] Implemented one side-effect-free module verifier and thin command. CI and release documentation delegate to it; tests prove exact invocation, local toolchain enforcement, output/error preservation, worktree independence, real tidy drift rejection and module-file immutability. [recovery_attempt2_signature_execute] Implemented a canonical commit/tag SSH signature verifier with exact object/peeled-commit binding, canonical principal/key fingerprint, signed-commit allowlist parity, isolated command-local Git configuration, minimal environment, repository identity holds and an owned fsynced trust-snapshot transaction. CI, release workflow, docs and release validation delegate to it. [recovery_attempt3_compile_execute] Added the proven context import and one cold-compile command that uses a fresh GOCACHE and non-executing wrapper for every supported OS/architecture lane. CI/docs delegate to it; tests lock target order, environment replacement, build constraints, command shape and root-plus-cache-cleanup evidence. [recovery_cycle_darwin_alias_execute] Replaced pathname ancestor checks with one fd-relative identity-recording walker. Darwin authorizes only the exact root-owned /var -\u003e private/var transition and verifies its link plus target identities; Linux denies every ancestor alias. Held directories, .git and the alias transcript are revalidated around every Git operation and all acquired descriptors have close-once cleanup with joined errors. [recovery_cycle_darwin_alias_attempt2_execute] Centralized directory and symlink invariants in one typed validator. Held and freshly rewalked directories require stable dev/inode/type/UID/GID/security mode and a live nonzero link count without exact-count equality. Canonical Darwin alias identity and target remain exact. Added positive child-churn and parallel-repository coverage plus deterministic replacement, symlink, ownership, mode, type, inode, unlink and link-invariant negatives.", "evidence": [ { "type": "file", "reference": "internal/modverify/verify.go; cmd/check-module-tidy/main.go; internal/modverify/verify_test.go", "result": "One typed subprocess boundary owns GOTOOLCHAIN=local go mod tidy -diff; no Git comparison or mutating tidy path exists." - } - ], - "recorded_at": "2026-08-13T12:39:21Z" - }, - "recovery_cycle_verification_reconcile": { - "phase": "reconcile", - "summary": "Re-audited repository workflows, docs, journals, scripts and Make targets after implementation. CI and documentation use the canonical command exactly once; the immutable historical receipt is the only remaining record of the rejected HEAD comparison.", - "evidence": [ + }, { - "type": "test", - "reference": "TestRepositoryUsesCanonicalModuleVerifier", - "result": "Deterministic policy test rejects operational direct tidy or git-diff bypasses while requiring canonical CI/docs references." - } - ], - "recorded_at": "2026-08-13T12:39:21Z" - }, - "recovery_cycle_verification_self_review": { - "phase": "self_review", - "summary": "Reviewed the entire accumulated release WIP for architecture, correctness, security, public vocabulary, failure behavior, schemas, permissions and maintainability. This cycle changes only verification infrastructure and preserves release transaction, descriptor ownership, validators, assets and governance semantics.", - "evidence": [ + "type": "file", + "reference": "internal/signatureverify; cmd/check-signature; .github/workflows/ci.yml; .github/workflows/release.yml", + "result": "Trust cleanup preserves root plus cleanup errors and foreign residuals; ambient Git/repository/object/SSH/pager/editor overrides have no authority." + }, { - "type": "command", - "reference": "full diff/status plus TODO, bypass and unsafe-path audit", - "result": "No alternate verification path, semantic fork, release side effect or weakened product boundary found." - } - ], - "recorded_at": "2026-08-13T12:39:21Z" - }, - "recovery_cycle_verification_omission": { - "phase": "completeness_omission_audit", - "summary": "Asked what may have been forgotten: checked real tidy drift, clean modules amid expected and unrelated WIP, stdout/stderr separation, root error identity, nil execution boundaries, module-file immutability, CI/docs parity, build tags, fuzz targets, SPDX, schemas, action pins, dependency closure, dirty paths and tag/release absence.", - "evidence": [ + "type": "file", + "reference": "internal/compilematrix; cmd/check-cold-compile; .github/workflows/ci.yml; docs/releasing.md", + "result": "Missing imports and build-tag-only dependency drift must compile cold before focused or substantive tests." + }, { - "type": "command", - "reference": "repository-wide rg, diff inventory, native gates and workflow/contract validation", - "result": "No omitted operational module command or verification bypass remains; cross-target local probe was blocked before execution by command-policy infrastructure and Linux remains an exact-head CI requirement." - } - ], - "recorded_at": "2026-08-13T12:39:21Z" - }, - "recovery_cycle_verification_verify": { - "phase": "verify", - "summary": "Gofmt, compile-only, focused verifier, canonical module check, releasecontract, full tests, race, vet, all fuzz targets, CI/governance/release/schema/license/docs checks, CLI build, diff check and pinned govulncheck are green. Standalone staticcheck is unavailable and no unpinned substitute was used.", - "evidence": [ + "type": "file", + "reference": "internal/signatureverify/ancestor_unix.go; internal/signatureverify/alias_darwin.go; internal/signatureverify/alias_linux.go; internal/signatureverify/ancestor_unix_test.go", + "result": "The bounded walker uses openat/fstatat/readlinkat with no EvalSymlinks authority, string-prefix trust or ambient Git configuration." + }, { - "type": "command", - "reference": "local Attempt1 gate transcript on 2026-08-13", - "result": "Every executed product, test, schema, security and provenance gate exited zero; no repair or rerun followed a failure." + "type": "file", + "reference": "internal/signatureverify/ancestor_unix.go; internal/signatureverify/ancestor_unix_test.go; internal/signatureverify/verify_test.go", + "result": "One object-kind vocabulary now owns capture/revalidation/rewalk diagnostics; existing trust, command, diagnostic and repository-replacement assertions remain intact and must be reached by the focused gate." } ], - "recorded_at": "2026-08-13T12:39:21Z" + "recorded_at": "2026-08-13T13:22:31Z" }, - "recovery_attempt1_provenance_failure": { - "phase": "verify", - "summary": "Recovery Attempt1 stopped after signed commit caf8fbb when a direct git verify-commit invocation lacked an explicit allowed-signers trust anchor. The failure was gate construction, not a signature verdict; no repair or rerun occurred before the canonical handoff.", + "gap_plan": { + "phase": "gap_plan", + "summary": "Implement a local v1alpha1 release contract and Go builder/validator so Go v-tags, exact-main signed tag checks and repository-specific package semantics stay canonical. PR/main dry-run remains inside existing required CI checks; one tag-only publication workflow owns minimal write/OIDC permissions. Immutable release enablement and the signed tag remain post-merge gates.", "evidence": [ { "type": "issue", - "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280536696", - "result": "API-byte-equal Attempt1 receipt preserves exact command/output, green predecessor gates and clean committed state." + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9", + "result": "Executable acceptance defines five-asset closure, deterministic metadata, SPDX/manifest bindings, negative tests, publication and terminal verification." } ], - "recorded_at": "2026-08-13T12:41:19Z" + "recorded_at": "2026-08-13T03:49:29.568819Z" }, - "recovery_attempt2_signature_orient": { + "orient": { "phase": "orient", - "summary": "Audited the tracked one-signer allowlist, signed commit object, ambient local signing settings, CI, release workflow, documentation and release validator. Chose one repository-owned verifier with canonical principal/fingerprint binding, command-local Git trust and no config writes.", + "summary": "Accepted clean exact main 3b4087bc; audited module/public packages/reference CLI, Go 1.24 baseline and Go 1.25 scanner lane, AGPL/docs/changelog/security, absent tags/releases/consumers, workflow permissions/governance, inherited immutable tag rules, disabled immutable releases, sibling supply-chain conventions and current primary guidance. Chose honest v0.1.0 Go module/source-only release with exactly five assets and no platform binaries. [recovery_cycle_verification_orient] New recovery-cycle Attempt1 preserved all earlier receipts and audited repository workflows, scripts, Make targets, docs and Goal journals. No owned module-tidy gate existed; the prior terminal command incorrectly compared intentional feature WIP with HEAD instead of evaluating go mod tidy -diff. [recovery_attempt2_signature_orient] Audited the tracked one-signer allowlist, signed commit object, ambient local signing settings, CI, release workflow, documentation and release validator. Chose one repository-owned verifier with canonical principal/fingerprint binding, command-local Git trust and no config writes. [recovery_attempt3_compile_orient] Final Attempt3 audited every changed/new Go file's referenced package identifiers, imports, commands and platform twins. The context binding was the sole missing import. Existing constraints are exactly darwin, linux, darwin-or-linux and their unsupported complement; dependencies retain x/mod module parsing, x/sys syscall and standard-library verifier ownership. [recovery_cycle_darwin_alias_orient] A new recovery-cycle Attempt1 preserved the prior terminal receipt and exact dirty lineage, then re-audited the signature verifier, trust snapshot, release surfaces and current Apple/POSIX/Go primary guidance. The remaining boundary was the pathname Lstat ancestor model rejecting the canonical macOS /var -\u003e private/var system alias before Git verification. [recovery_cycle_darwin_alias_attempt2_orient] Attempt2 audited every captured stat field, all six derivative failures, parallel fixture topology, verifier concurrency and Apple/POSIX/Go primary semantics. Device/inode, object type, UID/GID and security mode are stable security invariants; directory size/timestamps are not recorded and live nlink may change as children change. Darwin symlinks retain exact identity, one-link and bounded-target invariants; Linux aliases remain forbidden.", "evidence": [ { "type": "issue", - "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280552333", - "result": "API-byte-equal Attempt2 lineage records the trust boundary and exact preserved commit before mutation." - } - ], - "recorded_at": "2026-08-13T12:54:31Z" - }, - "recovery_attempt2_signature_execute": { - "phase": "execute", - "summary": "Implemented a canonical commit/tag SSH signature verifier with exact object/peeled-commit binding, canonical principal/key fingerprint, signed-commit allowlist parity, isolated command-local Git configuration, minimal environment, repository identity holds and an owned fsynced trust-snapshot transaction. CI, release workflow, docs and release validation delegate to it.", - "evidence": [ + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9", + "result": "Canonical roadmap/release issue records done, remaining, exact acceptance, threat model, rollback, non-goals and omissions before tree implementation." + }, { - "type": "file", - "reference": "internal/signatureverify; cmd/check-signature; .github/workflows/ci.yml; .github/workflows/release.yml", - "result": "Trust cleanup preserves root plus cleanup errors and foreign residuals; ambient Git/repository/object/SSH/pager/editor overrides have no authority." - } - ], - "recorded_at": "2026-08-13T12:54:31Z" - }, - "recovery_attempt2_compile_failure": { - "phase": "verify", - "summary": "Attempt2 stopped at its first compile-only gate after gofmt succeeded because ownership.go referenced context.Context without importing context. No repair or rerun occurred before the canonical handoff.", - "evidence": [ + "type": "issue", + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280458711", + "result": "API-byte-equal lineage records the side-effect-free verifier design and exact preserved base/diff state." + }, { "type": "issue", - "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280735416", - "result": "API-byte-equal Attempt2 receipt preserves the exact command/output, signed caf8fbb lineage and dirty WIP hash." - } - ], - "recorded_at": "2026-08-13T12:59:04Z" - }, - "recovery_attempt3_compile_orient": { - "phase": "orient", - "summary": "Final Attempt3 audited every changed/new Go file's referenced package identifiers, imports, commands and platform twins. The context binding was the sole missing import. Existing constraints are exactly darwin, linux, darwin-or-linux and their unsupported complement; dependencies retain x/mod module parsing, x/sys syscall and standard-library verifier ownership.", - "evidence": [ + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280552333", + "result": "API-byte-equal Attempt2 lineage records the trust boundary and exact preserved commit before mutation." + }, { "type": "command", "reference": "repository-wide changed/untracked Go import, identifier and build-constraint inventory", "result": "Audit proved the context import and defined the cold darwin/linux x amd64/arm64 compile boundary before mutation. A pre-gate zsh audit command reused reserved variable path and lost command lookup; it made no repository mutation and the corrected read-only audit completed." - } - ], - "recorded_at": "2026-08-13T13:03:03Z" - }, - "recovery_attempt3_compile_execute": { - "phase": "execute", - "summary": "Added the proven context import and one cold-compile command that uses a fresh GOCACHE and non-executing wrapper for every supported OS/architecture lane. CI/docs delegate to it; tests lock target order, environment replacement, build constraints, command shape and root-plus-cache-cleanup evidence.", - "evidence": [ - { - "type": "file", - "reference": "internal/compilematrix; cmd/check-cold-compile; .github/workflows/ci.yml; docs/releasing.md", - "result": "Missing imports and build-tag-only dependency drift must compile cold before focused or substantive tests." - } - ], - "recorded_at": "2026-08-13T13:03:03Z" - }, - "recovery_cycle_darwin_alias_orient": { - "phase": "orient", - "summary": "A new recovery-cycle Attempt1 preserved the prior terminal receipt and exact dirty lineage, then re-audited the signature verifier, trust snapshot, release surfaces and current Apple/POSIX/Go primary guidance. The remaining boundary was the pathname Lstat ancestor model rejecting the canonical macOS /var -> private/var system alias before Git verification.", - "evidence": [ + }, { "type": "issue", "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280850916", "result": "API-byte-equal lineage records exact base/head/dirty state, what was already implemented, the bounded identity-walker design and remaining proof before mutation." - } - ], - "recorded_at": "2026-08-13T13:14:22Z" - }, - "recovery_cycle_darwin_alias_execute": { - "phase": "execute", - "summary": "Replaced pathname ancestor checks with one fd-relative identity-recording walker. Darwin authorizes only the exact root-owned /var -> private/var transition and verifies its link plus target identities; Linux denies every ancestor alias. Held directories, .git and the alias transcript are revalidated around every Git operation and all acquired descriptors have close-once cleanup with joined errors.", - "evidence": [ - { - "type": "file", - "reference": "internal/signatureverify/ancestor_unix.go; internal/signatureverify/alias_darwin.go; internal/signatureverify/alias_linux.go; internal/signatureverify/ancestor_unix_test.go", - "result": "The bounded walker uses openat/fstatat/readlinkat with no EvalSymlinks authority, string-prefix trust or ambient Git configuration." - } - ], - "recorded_at": "2026-08-13T13:14:22Z" - }, - "recovery_cycle_darwin_alias_preverify_audit": { - "phase": "completeness_omission_audit", - "summary": "Before the first gate, reviewed every new return, descriptor owner, rewalk, alias policy, cleanup path and verifier call site. Corrected partial-capture error returns so deferred cleanup retains ownership, preserved stable path-identity error vocabulary, and synchronized the public release procedure. Existing trust residual-debt, root-plus-cleanup, signer, module, SPDX, archive and publication boundaries remain unchanged.", - "evidence": [ - { - "type": "command", - "reference": "repository-wide signature/alias/cleanup search, complete changed-file inspection and diff check", - "result": "No alternate symlink resolver, path-based trust fallback, direct signature-verification fork or unowned captured descriptor remains before verification." - } - ], - "recorded_at": "2026-08-13T13:14:22Z" - }, - "recovery_cycle_darwin_alias_attempt1_failure": { - "phase": "verify", - "summary": "Attempt1 stopped at the first focused signature suite after the cold darwin/linux amd64/arm64 matrix was green. Six tests exited early because exact directory identity equality treated legitimate shared temporary-ancestor link-count churn as replacement; no repair or rerun preceded the handoff.", - "evidence": [ + }, { - "type": "issue", - "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280949004", - "result": "The 6212-byte API-byte-equal receipt (SHA-256 f6c95bac2010b68f860002f980c996c364b311d0da18d50728962bf050857df9) preserves the exact command, output, hashes and unproven gates." - } - ], - "recorded_at": "2026-08-13T13:22:31Z" - }, - "recovery_cycle_darwin_alias_attempt2_orient": { - "phase": "orient", - "summary": "Attempt2 audited every captured stat field, all six derivative failures, parallel fixture topology, verifier concurrency and Apple/POSIX/Go primary semantics. Device/inode, object type, UID/GID and security mode are stable security invariants; directory size/timestamps are not recorded and live nlink may change as children change. Darwin symlinks retain exact identity, one-link and bounded-target invariants; Linux aliases remain forbidden.", - "evidence": [ - { - "type": "source", + "type": "link", "reference": "Apple fstat(2), Secure Coding Guide race guidance, File System Programming Guide, POSIX directory/stat semantics, and Go syscall Stat_t definitions", "result": "Primary sources support descriptor-bound identity checks, no-follow traversal, object type/owner/mode validation and non-authoritative mutable directory metadata." } ], "recorded_at": "2026-08-13T13:22:31Z" }, - "recovery_cycle_darwin_alias_attempt2_execute": { - "phase": "execute", - "summary": "Centralized directory and symlink invariants in one typed validator. Held and freshly rewalked directories require stable dev/inode/type/UID/GID/security mode and a live nonzero link count without exact-count equality. Canonical Darwin alias identity and target remain exact. Added positive child-churn and parallel-repository coverage plus deterministic replacement, symlink, ownership, mode, type, inode, unlink and link-invariant negatives.", - "evidence": [ - { - "type": "file", - "reference": "internal/signatureverify/ancestor_unix.go; internal/signatureverify/ancestor_unix_test.go; internal/signatureverify/verify_test.go", - "result": "One object-kind vocabulary now owns capture/revalidation/rewalk diagnostics; existing trust, command, diagnostic and repository-replacement assertions remain intact and must be reached by the focused gate." - } - ], - "recorded_at": "2026-08-13T13:22:31Z" - }, - "recovery_cycle_darwin_alias_attempt2_reconcile": { + "reconcile": { "phase": "reconcile", - "summary": "Compared the repaired verifier with issue #9 release acceptance, the six prior failures and the whole accumulated release WIP. The intended trust-snapshot mutation/residual-debt, repository replacement, command-root and diagnostic-writer assertions now execute and pass. Darwin retains one exact canonical alias; Linux retains no-alias behavior; CI, release workflow, docs and release validator use the same verifier and cold matrix.", + "summary": "[recovery_cycle_verification_reconcile] Re-audited repository workflows, docs, journals, scripts and Make targets after implementation. CI and documentation use the canonical command exactly once; the immutable historical receipt is the only remaining record of the rejected HEAD comparison. [recovery_cycle_darwin_alias_attempt2_reconcile] Compared the repaired verifier with issue #9 release acceptance, the six prior failures and the whole accumulated release WIP. The intended trust-snapshot mutation/residual-debt, repository replacement, command-root and diagnostic-writer assertions now execute and pass. Darwin retains one exact canonical alias; Linux retains no-alias behavior; CI, release workflow, docs and release validator use the same verifier and cold matrix.", "evidence": [ + { + "type": "test", + "reference": "TestRepositoryUsesCanonicalModuleVerifier", + "result": "Deterministic policy test rejects operational direct tidy or git-diff bypasses while requiring canonical CI/docs references." + }, { "type": "command", "reference": "focused signature suite, repository surface parity tests, complete tracked/untracked diff review", @@ -301,10 +252,15 @@ ], "recorded_at": "2026-08-13T13:29:48Z" }, - "recovery_cycle_darwin_alias_attempt2_self_review": { + "self_review": { "phase": "self_review", - "summary": "Independently reviewed architecture, TOCTOU behavior, descriptor ownership, error aggregation, platform constraints, concurrency, tests, public docs, schemas, signer identity, module/SPDX closure and release permissions. Capture, held-FD validation and anchored rewalk share one object-kind invariant vocabulary; mutable directory size/timestamps are absent and only live nlink is non-exact.", + "summary": "[recovery_cycle_verification_self_review] Reviewed the entire accumulated release WIP for architecture, correctness, security, public vocabulary, failure behavior, schemas, permissions and maintainability. This cycle changes only verification infrastructure and preserves release transaction, descriptor ownership, validators, assets and governance semantics. [recovery_cycle_darwin_alias_attempt2_self_review] Independently reviewed architecture, TOCTOU behavior, descriptor ownership, error aggregation, platform constraints, concurrency, tests, public docs, schemas, signer identity, module/SPDX closure and release permissions. Capture, held-FD validation and anchored rewalk share one object-kind invariant vocabulary; mutable directory size/timestamps are absent and only live nlink is non-exact.", "evidence": [ + { + "type": "command", + "reference": "full diff/status plus TODO, bypass and unsafe-path audit", + "result": "No alternate verification path, semantic fork, release side effect or weakened product boundary found." + }, { "type": "command", "reference": "full source/diff inspection plus unsafe resolver, cleanup, TODO, action-pin and direct-verifier searches", @@ -313,22 +269,30 @@ ], "recorded_at": "2026-08-13T13:29:48Z" }, - "recovery_cycle_darwin_alias_attempt2_omission": { - "phase": "completeness_omission_audit", - "summary": "Asked what may have been forgotten: checked child creation/removal, parallel repositories, ancestor rename/inode swap/symlink insertion, owner/group/mode/type/unlink/link drift, alias target and identity drift, component and target budgets, root-plus-cleanup evidence, partial capture cleanup, fd close-once, platform build tags, workflow/docs parity, tags/releases and temporary proof residue.", + "verify": { + "phase": "verify", + "summary": "[recovery_cycle_verification_verify] Gofmt, compile-only, focused verifier, canonical module check, releasecontract, full tests, race, vet, all fuzz targets, CI/governance/release/schema/license/docs checks, CLI build, diff check and pinned govulncheck are green. Standalone staticcheck is unavailable and no unpinned substitute was used. [recovery_attempt1_provenance_failure] Recovery Attempt1 stopped after signed commit caf8fbb when a direct git verify-commit invocation lacked an explicit allowed-signers trust anchor. The failure was gate construction, not a signature verdict; no repair or rerun occurred before the canonical handoff. [recovery_attempt2_compile_failure] Attempt2 stopped at its first compile-only gate after gofmt succeeded because ownership.go referenced context.Context without importing context. No repair or rerun occurred before the canonical handoff. [recovery_cycle_darwin_alias_attempt1_failure] Attempt1 stopped at the first focused signature suite after the cold darwin/linux amd64/arm64 matrix was green. Six tests exited early because exact directory identity equality treated legitimate shared temporary-ancestor link-count churn as replacement; no repair or rerun preceded the handoff. [recovery_cycle_darwin_alias_attempt2_verify] Gofmt; cold darwin/linux amd64/arm64 compilation; focused signature and releasecontract suites; module tidy; full tests; race; vet; six fuzz targets; CI/governance/release contracts; deterministic two-build equality; CLI builds; JSON/schema parsing; exact signed caf8fbb verification; diff checks; and govulncheck v1.6.0 under Go 1.26.4 all passed. Staticcheck is not installed and no unpinned substitute was introduced.", "evidence": [ { "type": "command", - "reference": "negative/positive invariant tests, full fuzz/race/security gates and clean temporary-proof cleanup", - "result": "No silent alias expansion, stale equality check, test serialization, skipped platform lane, release side effect or remaining temporary bundle was found." - } - ], - "recorded_at": "2026-08-13T13:29:48Z" - }, - "recovery_cycle_darwin_alias_attempt2_verify": { - "phase": "verify", - "summary": "Gofmt; cold darwin/linux amd64/arm64 compilation; focused signature and releasecontract suites; module tidy; full tests; race; vet; six fuzz targets; CI/governance/release contracts; deterministic two-build equality; CLI builds; JSON/schema parsing; exact signed caf8fbb verification; diff checks; and govulncheck v1.6.0 under Go 1.26.4 all passed. Staticcheck is not installed and no unpinned substitute was introduced.", - "evidence": [ + "reference": "local Attempt1 gate transcript on 2026-08-13", + "result": "Every executed product, test, schema, security and provenance gate exited zero; no repair or rerun followed a failure." + }, + { + "type": "issue", + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280536696", + "result": "API-byte-equal Attempt1 receipt preserves exact command/output, green predecessor gates and clean committed state." + }, + { + "type": "issue", + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280735416", + "result": "API-byte-equal Attempt2 receipt preserves the exact command/output, signed caf8fbb lineage and dirty WIP hash." + }, + { + "type": "issue", + "reference": "https://github.com/NDDev-it-com/agent-runtime/issues/9#issuecomment-5280949004", + "result": "The 6212-byte API-byte-equal receipt (SHA-256 f6c95bac2010b68f860002f980c996c364b311d0da18d50728962bf050857df9) preserves the exact command, output, hashes and unproven gates." + }, { "type": "command", "reference": "Attempt2 local gate transcript 2026-08-13", @@ -339,6 +303,6 @@ } }, "created_at": "2026-08-13T03:49:29.267966Z", - "updated_at": "2026-08-13T13:29:48Z" + "updated_at": "2026-08-14T23:56:29.075454892Z" } } diff --git a/.agent-runtime/goals/held-path-identity.json b/.agent-runtime/goals/held-path-identity.json index c3c33ba..e25f210 100644 --- a/.agent-runtime/goals/held-path-identity.json +++ b/.agent-runtime/goals/held-path-identity.json @@ -1,6 +1,6 @@ { "schema_version": "v1alpha1", - "revision": 11, + "revision": 12, "goal": { "id": "held-path-identity", "intent": "The JSONL sink writes to the file it read, and every remaining check-then-use gap is stated rather than implied", @@ -47,9 +47,33 @@ "Redesign the sandbox model", "Defend against a compromised host" ], - "state": "active", + "state": "completed", "current_phase": "closure", "receipts": { + "closure": { + "phase": "closure", + "summary": "Merged as PR #57 with all four required checks green.", + "evidence": [ + { + "type": "link", + "reference": "https://github.com/NDDev-it-com/agent-runtime/pull/57", + "result": "Merged to main." + } + ], + "recorded_at": "2026-08-14T23:54:44.325258889Z", + "closure": { + "achieved_outcome": "The JSONL sink validates and extends one object, and the remaining check-then-use gaps are stated with the trigger that would make them real.", + "cleanup": "Task branch deleted locally and on origin; the dead scanJSONL wrapper removed.", + "remaining": [], + "next_work": [ + { + "type": "issue", + "reference": "self verified journals", + "result": "Give the Goal contract the executable checker every other contract in this repository already has, and repair the tracked journal that the product rejects." + } + ] + } + }, "completeness_omission_audit": { "phase": "completeness_omission_audit", "summary": "Checked the other direction as well: a destination whose name resolves elsewhere, such as a symlink, is refused rather than followed, because a caller configured a destination and not whatever currently answers to it.", @@ -136,6 +160,6 @@ } }, "created_at": "2026-08-14T23:50:11.022035863Z", - "updated_at": "2026-08-14T23:50:11.311924048Z" + "updated_at": "2026-08-14T23:54:44.325258889Z" } } diff --git a/.agent-runtime/goals/self-verified-journals.json b/.agent-runtime/goals/self-verified-journals.json new file mode 100644 index 0000000..46ef193 --- /dev/null +++ b/.agent-runtime/goals/self-verified-journals.json @@ -0,0 +1,123 @@ +{ + "schema_version": "v1alpha1", + "revision": 8, + "goal": { + "id": "self-verified-journals", + "intent": "This repository's tracked Goal journals are proven valid by an executable checker", + "acceptance": [ + { + "id": "journal-checker", + "acceptance": "A required CI checker validates every tracked journal against the Go contract and the published schema", + "status": "pending" + }, + { + "id": "journals-valid", + "acceptance": "goal status succeeds on every tracked journal", + "status": "pending" + }, + { + "id": "first-release-true", + "acceptance": "The first-release journal states the published outcome and closes on its evidence", + "status": "pending" + }, + { + "id": "no-shipped-invalid", + "acceptance": "A built source archive contains no artifact the product rejects", + "status": "pending" + } + ], + "non_goals": [ + "Widen the phase vocabulary to accommodate a malformed journal" + ], + "state": "active", + "current_phase": "closure", + "receipts": { + "completeness_omission_audit": { + "phase": "completeness_omission_audit", + "summary": "Registered the checker in the CI contract as well as the workflow, so it cannot be dropped from ci.yml while the contract stays green -- the same failure class the workflow model was built to close.", + "evidence": [ + { + "type": "file", + "reference": "internal/cicontract/contract.go", + "result": "The workflow must invoke the goal journal verifier exactly once, counted over executed run scripts." + } + ], + "recorded_at": "2026-08-15T00:02:27.460260274Z" + }, + "execute": { + "phase": "execute", + "summary": "Added journalverify and its command, wired it into ci.yml, the CI contract and the GDS anchor, and migrated first-v0-release.json by replaying its history through goal.New and Advance.", + "evidence": [ + { + "type": "command", + "reference": "go run ./cmd/check-goal-journals", + "result": "goal journals valid: 9 tracked, accepted by the Goal contract and the published schema." + } + ], + "recorded_at": "2026-08-15T00:02:27.376942287Z" + }, + "gap_plan": { + "phase": "gap_plan", + "summary": "Write the checker first so the repair is proven rather than asserted, then fold each recovery cycle into the phase receipt it already named, replaying through the product's own API so the result is legitimate by construction.", + "evidence": [ + { + "type": "file", + "reference": "internal/journalverify; cmd/check-goal-journals", + "result": "The Goal contract was the only contract in this repository without an executable checker over its tracked artifacts, which is why a schema change could invalidate a shipped file unnoticed." + } + ], + "recorded_at": "2026-08-15T00:02:27.3515585Z" + }, + "orient": { + "phase": "orient", + "summary": "The module shipped a Goal journal its own CLI refuses to load. Twenty-two of the twenty-four receipt keys in first-v0-release.json were invented recovery-cycle labels rather than phases, so goal status failed with invalid_goal; the file is tracked, so it travelled inside the published v0.1.3 source archive and its SPDX inventory. The same journal also claimed the first release had never happened.", + "evidence": [ + { + "type": "command", + "reference": "agent-runtime goal status --journal .agent-runtime/goals/first-v0-release.json", + "result": "invalid_goal: invalid receipt for phase recovery_cycle_verification_orient; state active with all seven acceptance criteria pending while v0.1.3 was published and issue #9 closed." + } + ], + "recorded_at": "2026-08-15T00:02:27.320657917Z" + }, + "reconcile": { + "phase": "reconcile", + "summary": "The migration is lossless in substance: all twenty-four evidence records survive on the phase receipts, and every cycle summary is retained and labelled. One evidence type of 'source' was never in the contracted vocabulary and is recorded as a link.", + "evidence": [ + { + "type": "file", + "reference": ".agent-runtime/goals/first-v0-release.json", + "result": "Seven phase receipts carrying 1, 6, 5, 2, 2, 3 and 5 evidence records, then a closure receipt recording both the release outcome and the migration itself." + } + ], + "recorded_at": "2026-08-15T00:02:27.406493571Z" + }, + "self_review": { + "phase": "self_review", + "summary": "The checker asserts the pair, not one side: Go alone would let the published schema drift, and the schema alone would skip the durable invariants the type enforces. It also refuses an empty directory and requires the file to be the canonical encoding of what it decodes to, so a journal cannot drift each time the runtime rewrites it.", + "evidence": [ + { + "type": "test", + "reference": "internal/journalverify TestVerifyRejectsJournalsEitherContractRefuses; TestVerifyRefusesToPassVacuously", + "result": "Five damaged shapes are refused, including the exact one that shipped, and an empty directory fails rather than passing." + } + ], + "recorded_at": "2026-08-15T00:02:27.435047951Z" + }, + "verify": { + "phase": "verify", + "summary": "gofmt, go vet, staticcheck, full tests under umask 002, and all seven contract checkers including the new one.", + "evidence": [ + { + "type": "test", + "reference": "local gate transcript 2026-08-15", + "result": "All green; 9 journals verified." + } + ], + "recorded_at": "2026-08-15T00:02:27.487402016Z" + } + }, + "created_at": "2026-08-15T00:02:27.292800011Z", + "updated_at": "2026-08-15T00:02:27.487402016Z" + } +} diff --git a/.gds/repository.yaml b/.gds/repository.yaml index 9bf0bf9..f6bc372 100644 --- a/.gds/repository.yaml +++ b/.gds/repository.yaml @@ -57,6 +57,9 @@ verification: - "go run ./cmd/check-ci-contract" - "go run ./cmd/check-governance-contract" - "go run ./cmd/check-release-contract" + # The Goal journal is this module's own product, and its tracked + # instances are held to the same contract a consumer would apply. + - "go run ./cmd/check-goal-journals" full: # govulncheck resolves the live vulnerability database and needs the # patched security toolchain, not the compatibility one, so CI runs it in diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5b8740..ad0e102 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,8 @@ jobs: fi - name: Verify module metadata is tidy without mutation run: go run ./cmd/check-module-tidy + - name: Verify every tracked Goal journal against the contract and its schema + run: go run ./cmd/check-goal-journals - run: go run ./cmd/check-ci-contract - run: go run ./cmd/check-governance-contract - name: Reproduce release bundle without publication diff --git a/CHANGELOG.md b/CHANGELOG.md index 1296898..51e16c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,39 @@ contract. ## [Unreleased] +### Added + +- `cmd/check-goal-journals` holds every tracked Goal journal to the Go contract + and to the published schema, and is a required CI step recorded in the GDS + anchor. Every other contract here — CI, governance, release, provenance, fuzz, + cold compile — was already proven by an executable checker; the Goal contract, + which is the product rather than a property of the repository, was the one + without. It refuses to pass on an empty directory, because a gate that + succeeds by finding nothing to inspect cannot be told from one that inspected + everything. + ### Fixed +- `.agent-runtime/goals/first-v0-release.json` is a journal this module accepts. + It recorded each recovery cycle under an invented receipt key, which the Goal + contract does not permit, so `agent-runtime goal status` refused to load it — + and because it is tracked, it shipped inside the published source archive and + its SPDX inventory. The module distributed an artifact its own CLI rejects. + Tightening the schema so receipt keys must be phases is what made an existing + file invalid, and nothing compared the two. + + Every recovery cycle's summary and evidence is preserved, folded into the + phase receipt each cycle already named in its own `phase` field, and the + replay went through the product's own API so the result is legitimate by + construction rather than by hand. One evidence record carried a `source` type + that was never in the contracted vocabulary and is recorded as a link. The + pre-migration form is in this repository's Git history. + + The journal also now states what happened: the first verifiable v0 release + shipped, as `v0.1.2` and then `v0.1.3` rather than `v0.1.0`, and issue #9 is + closed. It had stood at `state: active` with all seven acceptance criteria + pending since 13 August, claiming the first release had never happened. + - A JSONL sink holds one descriptor for its lifetime. It used to open the path to scan the existing history, close it, and open it again to append, so a rename in between left the recovered duplicate-identity and size state diff --git a/cmd/check-goal-journals/main.go b/cmd/check-goal-journals/main.go new file mode 100644 index 0000000..0918b79 --- /dev/null +++ b/cmd/check-goal-journals/main.go @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package main + +import ( + "fmt" + "os" + + "github.com/NDDev-it-com/agent-runtime/internal/journalverify" +) + +func main() { + result, err := journalverify.Verify(".", journalverify.DefaultDirectory, journalverify.SchemaPath) + if err != nil { + fmt.Fprintln(os.Stderr, "goal journal invalid:", err) + os.Exit(1) + } + fmt.Printf("goal journals valid: %d tracked under %s, accepted by the Goal contract and the published schema\n", + len(result.Journals), result.Directory) +} diff --git a/internal/cicontract/contract.go b/internal/cicontract/contract.go index 8fe8470..f15bc69 100644 --- a/internal/cicontract/contract.go +++ b/internal/cicontract/contract.go @@ -137,6 +137,13 @@ func VerifyWorkflow(c Contract, workflow []byte) error { if w.CountRunOccurrences("go run ./cmd/check-fuzz") != 1 { return errors.New("workflow must invoke the canonical fuzz verifier exactly once") } + // The Goal contract is this module's product, so its tracked artifacts are + // held to it in CI like every other contract here. Leaving it unchecked is + // how a schema change came to invalidate a journal that ships inside the + // release archive. + if w.CountRunOccurrences("go run ./cmd/check-goal-journals") != 1 { + return errors.New("workflow must invoke the goal journal verifier exactly once") + } return verifyReleaseReproductionCommand(test) } diff --git a/internal/journalverify/verify.go b/internal/journalverify/verify.go new file mode 100644 index 0000000..5170253 --- /dev/null +++ b/internal/journalverify/verify.go @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +// Package journalverify proves the Goal journals this repository tracks are +// artifacts the product accepts. +// +// Every other contract here is proven by an executable checker under cmd/ — CI, +// governance, release, provenance, fuzz, cold compile. The Goal contract, which +// is the product rather than a property of the repository, had none, and that +// is exactly how it came to ship a journal it rejects: tightening the schema so +// receipt keys must be phases made an existing tracked file invalid, and +// nothing compared the two. The file travels inside the published source +// archive and is listed in its SPDX inventory, so the release carried an +// artifact its own CLI refuses to load. +// +// The check is deliberately double. The Go contract and the published schema +// are two statements of one vocabulary, and a journal has to satisfy both: Go +// alone would let the schema drift, and the schema alone would let the durable +// invariants the type enforces go unchecked. +package journalverify + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/santhosh-tekuri/jsonschema/v6" + + "github.com/NDDev-it-com/agent-runtime/goal" +) + +// DefaultDirectory is where this repository keeps its tracked journals. +const DefaultDirectory = ".agent-runtime/goals" + +// SchemaPath is the published description of the same vocabulary. +const SchemaPath = "schemas/goal-journal-v1alpha1.schema.json" + +// Result reports what was proven, so a caller can print evidence rather than a +// bare success. +type Result struct { + Directory string + Journals []string +} + +// Verify loads every journal in directory and holds it to both the Go contract +// and the published schema. An empty directory is a failure: a checker that +// passes because it found nothing to check is the kind of control this +// repository exists to remove. +func Verify(root, directory, schemaPath string) (Result, error) { + pattern := filepath.Join(root, directory, "*.json") + paths, err := filepath.Glob(pattern) + if err != nil { + return Result{}, fmt.Errorf("scan %s: %w", directory, err) + } + sort.Strings(paths) + if len(paths) == 0 { + return Result{}, fmt.Errorf("no goal journals under %s; this check would pass vacuously", directory) + } + schema, err := compileSchema(filepath.Join(root, schemaPath)) + if err != nil { + return Result{}, err + } + result := Result{Directory: directory} + for _, path := range paths { + name := filepath.Base(path) + if err := verifyOne(path, schema); err != nil { + return Result{}, fmt.Errorf("%s: %w", filepath.Join(directory, name), err) + } + result.Journals = append(result.Journals, name) + } + return result, nil +} + +func verifyOne(path string, schema *jsonschema.Schema) error { + data, err := os.ReadFile(path) + if err != nil { + return err + } + // The Go contract first: it is what a caller's CLI and library will apply, + // so a journal that fails here is unusable whatever the schema says. + journal, err := goal.Store{Path: path}.Load() + if err != nil { + return fmt.Errorf("the Goal contract rejects this journal: %w", err) + } + document, err := jsonschema.UnmarshalJSON(strings.NewReader(string(data))) + if err != nil { + return fmt.Errorf("not valid JSON: %w", err) + } + if err := schema.Validate(document); err != nil { + return fmt.Errorf("the published schema rejects this journal: %w", err) + } + // Re-encoding must reproduce the file. A journal that only round-trips + // approximately would drift each time the runtime rewrote it, and it ships + // inside the release archive byte for byte. + encoded, err := json.MarshalIndent(journal, "", " ") + if err != nil { + return fmt.Errorf("re-encode: %w", err) + } + if strings.TrimSpace(string(data)) != strings.TrimSpace(string(encoded)) { + return errors.New("the journal is not the canonical encoding of what it decodes to") + } + return nil +} + +func compileSchema(path string) (*jsonschema.Schema, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read goal schema: %w", err) + } + document, err := jsonschema.UnmarshalJSON(strings.NewReader(string(data))) + if err != nil { + return nil, fmt.Errorf("parse goal schema: %w", err) + } + compiler := jsonschema.NewCompiler() + compiler.AssertFormat() + if err := compiler.AddResource(path, document); err != nil { + return nil, fmt.Errorf("load goal schema: %w", err) + } + schema, err := compiler.Compile(path) + if err != nil { + return nil, fmt.Errorf("compile goal schema: %w", err) + } + return schema, nil +} diff --git a/internal/journalverify/verify_test.go b/internal/journalverify/verify_test.go new file mode 100644 index 0000000..db01019 --- /dev/null +++ b/internal/journalverify/verify_test.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package journalverify + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +const root = "../.." + +func TestRepositoryJournalsSatisfyBothContracts(t *testing.T) { + t.Parallel() + result, err := Verify(root, DefaultDirectory, SchemaPath) + if err != nil { + t.Fatal(err) + } + if len(result.Journals) == 0 { + t.Fatal("no journals verified") + } + t.Logf("verified %d journals: %s", len(result.Journals), strings.Join(result.Journals, ", ")) +} + +// TestVerifyRefusesToPassVacuously covers the failure mode this checker exists +// to remove. A gate that reports success because it found nothing to inspect is +// indistinguishable from one that inspected everything and found it sound. +func TestVerifyRefusesToPassVacuously(t *testing.T) { + t.Parallel() + empty := t.TempDir() + if err := os.MkdirAll(filepath.Join(empty, "goals"), 0o700); err != nil { + t.Fatal(err) + } + if _, err := Verify(empty, "goals", filepath.Join(root, SchemaPath)); err == nil { + t.Fatal("an empty journal directory was reported as verified") + } +} + +// TestVerifyRejectsJournalsEitherContractRefuses pins that both statements of +// the vocabulary are applied. Checking only Go would let the published schema +// drift; checking only the schema would skip the durable invariants the type +// enforces. +func TestVerifyRejectsJournalsEitherContractRefuses(t *testing.T) { + t.Parallel() + valid, err := os.ReadFile(filepath.Join(root, DefaultDirectory, "initial-release.json")) + if err != nil { + t.Fatal(err) + } + for name, damage := range map[string]func(string) string{ + // The exact shape that shipped: a receipt keyed by something that is + // not a phase. Both contracts must refuse it. + "receipt keyed by a non-phase": func(s string) string { + return strings.Replace(s, `"orient": {`, `"recovery_cycle_orient": {`, 1) + }, + "evidence type outside the vocabulary": func(s string) string { + return strings.Replace(s, `"type": "file"`, `"type": "source"`, 1) + }, + "completed item without evidence": func(s string) string { + return strings.Replace(s, `"status": "complete"`, `"status": "complete", "evidence": []`, 1) + }, + "unsupported schema version": func(s string) string { + return strings.Replace(s, `"schema_version": "v1alpha1"`, `"schema_version": "v1alpha0"`, 1) + }, + "non-canonical encoding": func(s string) string { + return strings.ReplaceAll(s, "\n ", "\n ") + }, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + goals := filepath.Join(dir, "goals") + if err := os.MkdirAll(goals, 0o700); err != nil { + t.Fatal(err) + } + damaged := damage(string(valid)) + if damaged == string(valid) { + t.Fatal("the damage is stale: it no longer changes the journal") + } + if err := os.WriteFile(filepath.Join(goals, "damaged.json"), []byte(damaged), 0o600); err != nil { + t.Fatal(err) + } + if _, err := Verify(dir, "goals", filepath.Join(root, SchemaPath)); err == nil { + t.Fatal("a journal neither contract should accept was verified") + } + }) + } +}