diff --git a/docs/specs/proofkit-spec-proof-core/overview.md b/docs/specs/proofkit-spec-proof-core/overview.md index f17f028..1b77346 100644 --- a/docs/specs/proofkit-spec-proof-core/overview.md +++ b/docs/specs/proofkit-spec-proof-core/overview.md @@ -92,8 +92,9 @@ execution receipts, and merge policy. and emits only bounded candidate requirements, bindings, or owner guidance. - `REQ-PROOFKIT-SPEC-018`: an authored command-family catalog covers every public CLI command exactly once, deterministically generates the private - runtime navigation projection, and adds opt-in family help without changing - root help, per-command help, or leaf dispatch. + runtime navigation projection, and adds opt-in family help while preserving + existing help invocation forms, process channels, no-input behavior, and leaf + dispatch; descriptor and help truth remains owned by `REQ-PROOFKIT-QUALITY-004`. ## Non-Claims diff --git a/docs/specs/proofkit-spec-proof-core/requirements.v1.json b/docs/specs/proofkit-spec-proof-core/requirements.v1.json index 578cff2..a69407f 100644 --- a/docs/specs/proofkit-spec-proof-core/requirements.v1.json +++ b/docs/specs/proofkit-spec-proof-core/requirements.v1.json @@ -467,7 +467,7 @@ { "requirementId": "REQ-PROOFKIT-SPEC-018", "ownerId": "proofkit.spec-proof-core", - "invariant": "Command family navigation projects the complete public CLI command inventory from one authored family catalog into a deterministic private runtime projection, preserves existing root and per-command help and leaf dispatch, admits only opt-in family help forms, rejects missing, duplicated, oversized, unsorted, or reserved-operand-colliding families, and does not infer command selection, execute commands through family membership, or create a second command inventory owner.", + "invariant": "Command family navigation projects the complete public CLI command inventory from one authored family catalog into a deterministic private runtime projection, preserves root and per-command help invocation forms, stdout and stderr channel discipline, no-input behavior, and leaf dispatch, admits only opt-in family help forms, rejects missing, duplicated, oversized, unsorted, or reserved-operand-colliding families, and does not infer command selection, execute commands through family membership, or create a second command inventory owner.", "claimLevel": "blocking", "riskClass": "medium", "proofBindingRefs": [ @@ -477,7 +477,7 @@ "NC-PROOFKIT-SPEC-018" ], "nonClaims": [ - "This requirement does not claim command recommendation, command execution through family membership, consumer policy, native witness execution, merge approval, release approval, rollout approval, or production readiness." + "This requirement does not claim byte-for-byte help text compatibility across owner-approved CLI contract corrections, command recommendation, command execution through family membership, consumer policy, native witness execution, merge approval, release approval, rollout approval, or production readiness." ], "lifecycle": { "state": "active", diff --git a/docs/specs/proofkit-supply-chain-quality/overview.md b/docs/specs/proofkit-supply-chain-quality/overview.md index 5a938dc..df5d39f 100644 --- a/docs/specs/proofkit-supply-chain-quality/overview.md +++ b/docs/specs/proofkit-supply-chain-quality/overview.md @@ -81,6 +81,11 @@ vulnerability absence, or consumer rollout safety by itself. stderr discipline, generated-source hash, owner-source parity, and exact explicit-input counts despite an unlisted poison file in the consumer working directory. +- `REQ-PROOFKIT-QUALITY-020`: package artifact execution starts from admitted + candidate-owned output roots, rejects ambient provider or unowned release + state before mutation, stays confined to the repository across symlinks, + binds non-empty generated content to stable source and execution-context + snapshots, and emits a schema-versioned execution record. ## Non-Claims diff --git a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json index b136d95..b5772f0 100644 --- a/docs/specs/proofkit-supply-chain-quality/requirements.v1.json +++ b/docs/specs/proofkit-supply-chain-quality/requirements.v1.json @@ -255,6 +255,19 @@ "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, "deferral": null, "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-020", + "ownerId": "proofkit.supply-chain-quality", + "invariant": "Package artifact execution rejects ambient provider evidence and unowned release state before mutating candidate outputs, removes only candidate-owned package, wheel, and release outputs through a repository-confined filesystem root without following symlinks outside that root, invalidates prior execution evidence, executes the canonical package pipeline against stable source, environment, and toolchain snapshots, requires a non-empty content-bound artifact snapshot, and emits a schema-versioned execution record.", + "claimLevel": "blocking", + "riskClass": "high", + "proofBindingRefs": ["proofkit/requirement-bindings.json"], + "nonClaimRefs": ["NC-PROOFKIT-QUALITY-020"], + "nonClaims": ["This requirement does not claim producer authentication, hermetic or reproducible builds, registry publication, provider evidence freshness, release approval, rollout approval, production readiness, or exact artifact closure outside downstream package and release validators."], + "lifecycle": {"state": "active", "replacementRequirementIds": [], "evidenceRefs": []}, + "deferral": null, + "updatePolicy": {"reviewOwnerId": "proofkit.supply-chain-quality", "requiresImpactDeclaration": true, "requiresProofBindingReview": true} } ] } diff --git a/internal/app/cli_contract_test.go b/internal/app/cli_contract_test.go index fa04315..ce5806d 100644 --- a/internal/app/cli_contract_test.go +++ b/internal/app/cli_contract_test.go @@ -680,9 +680,46 @@ func TestDescriptorFlagConstraintsMatchCommandParsers(t *testing.T) { if _, err := parseConformanceProfileArgs([]string{"--input", "-", "--list", "--format", "markdown"}); err == nil { t.Fatal("conformance markdown without --profile was accepted") } - usage := commandUsageLine(commandDescriptorByName["adoption-contract-envelope"]) - if !strings.Contains(usage, " --mode ") || strings.Contains(usage, "[--mode ]") { - t.Fatalf("required --mode rendered as optional: %s", usage) +} + +func TestDescriptorFlagConstraintsAreRenderedTruthfully(t *testing.T) { + expectedConstrainedUsage := map[string]string{ + "adoption-contract-envelope": "agentic-proofkit adoption-contract-envelope --input [--agent-envelope] [--checked-scope ] [--guidance-mode ] [--materialization-manifest] --mode [--pilot ] [--touched-rule-id ]", + "conformance-profile": "agentic-proofkit conformance-profile --input [--format ] [--input-pointer ] (--list | --profile | --verify)", + "json-report-cli-adapter-source": "agentic-proofkit json-report-cli-adapter-source [--format ] --language ", + "requirement-browser-server": "agentic-proofkit requirement-browser-server --input [--empty-local-environment-policy] [--host 127.0.0.1|::1] [--input-pointer ] [--local-environment-class ] [--open] [--port ] [--scope ] [--serve] --view ", + "requirement-proof-resolver": "agentic-proofkit requirement-proof-resolver --input [--input-pointer ] (--empty-local-environment-policy | --local-environment-class )", + "stack-preset": "agentic-proofkit stack-preset --preset ", + "typescript-public-api-surfaces": "agentic-proofkit typescript-public-api-surfaces --input [--input-pointer ] --repo-root ", + } + constrainedCount := 0 + for _, descriptor := range commandDescriptors { + usageLine := commandUsageLine(descriptor) + for _, flag := range descriptor.requiredFlags { + if !strings.Contains(usageLine, " "+flag) || strings.Contains(usageLine, "["+flag) { + t.Fatalf("%s required flag %s is not rendered as required: %s", descriptor.name, flag, usageLine) + } + } + if len(descriptor.requiredFlags) > 0 || len(descriptor.exactlyOneOfFlagGroups) > 0 { + constrainedCount++ + expected, admitted := expectedConstrainedUsage[descriptor.name] + if !admitted { + t.Fatalf("%s has constraints but no independent help oracle", descriptor.name) + } + if usageLine != expected { + t.Fatalf("%s constrained usage = %q, want %q", descriptor.name, usageLine, expected) + } + } + commandHelp := commandUsage(descriptor) + for _, requirement := range descriptor.flagValueRequirements { + expected := fmt.Sprintf(" %s %s requires: %s", requirement.Flag, requirement.Value, strings.Join(requirement.RequiredFlags, ", ")) + if !strings.Contains(commandHelp, expected) { + t.Fatalf("%s value constraint %q is missing from command help", descriptor.name, expected) + } + } + } + if constrainedCount != len(expectedConstrainedUsage) { + t.Fatalf("constrained descriptor count = %d, independent help oracle count = %d", constrainedCount, len(expectedConstrainedUsage)) } } diff --git a/internal/app/command_family_catalog_test.go b/internal/app/command_family_catalog_test.go index 8c8bb78..107b949 100644 --- a/internal/app/command_family_catalog_test.go +++ b/internal/app/command_family_catalog_test.go @@ -2,17 +2,10 @@ package app import ( "bytes" - "crypto/sha256" - "encoding/hex" "strings" "testing" ) -const ( - rootHelpSHA256 = "d8f71779e3feab2faf4ac19d81881583d857155f2d75e923dc5bcfa61de8b6bb" - perCommandHelpSetSHA256 = "9bd92c609b1f96ee4964c1c7ae3bf2a5150a2e4a9145b179f19ace11a6b15b8d" -) - func TestCommandFamilyCatalogMatchesDescriptors(t *testing.T) { seen := map[string]string{} for _, family := range generatedCommandFamilyCatalog().Families { @@ -70,20 +63,28 @@ func TestCommandFamilyHelpFormsAreOptInAndLeafDispatchIsUnchanged(t *testing.T) } } -func TestExistingHelpBytesRemainOwnerApproved(t *testing.T) { - rootDigest := sha256.Sum256([]byte(usage())) - if got := hex.EncodeToString(rootDigest[:]); got != rootHelpSHA256 { - t.Fatalf("root help hash = %s, want %s", got, rootHelpSHA256) +func TestExistingHelpEntrypointsRemainCompatible(t *testing.T) { + rootHelp := usage() + if !strings.Contains(rootHelp, "agentic-proofkit help [|-h|--help]") { + t.Fatal("root help omits the admitted positional command target") } - hash := sha256.New() - for _, descriptor := range commandDescriptors { - _, _ = hash.Write([]byte(descriptor.name)) - _, _ = hash.Write([]byte{0}) - _, _ = hash.Write([]byte(commandUsage(descriptor))) - _, _ = hash.Write([]byte{0}) + rootForms := [][]string{nil, {"help"}, {"--help"}, {"-h"}, {"help", "--help"}, {"help", "-h"}} + for _, args := range rootForms { + if got := runTextCommand(t, args); got != rootHelp { + t.Fatalf("root help entrypoint %v diverged from the root projection", args) + } } - if got := hex.EncodeToString(hash.Sum(nil)); got != perCommandHelpSetSHA256 { - t.Fatalf("per-command help set hash = %s, want %s", got, perCommandHelpSetSHA256) + for _, descriptor := range commandDescriptors { + expected := commandUsage(descriptor) + aliases := [][]string{{"help", descriptor.name}} + if descriptor.name != "help" { + aliases = append(aliases, []string{descriptor.name, "--help"}, []string{descriptor.name, "-h"}) + } + for _, args := range aliases { + if got := runTextCommand(t, args); got != expected { + t.Fatalf("per-command help %v changed across an existing alias", args) + } + } } } @@ -111,7 +112,7 @@ func runTextCommand(t *testing.T, args []string) string { t.Helper() var stdout bytes.Buffer var stderr bytes.Buffer - status := Run(t.Context(), args, strings.NewReader(""), &stdout, &stderr) + status := Run(t.Context(), args, panicReader{}, &stdout, &stderr) if status != 0 || stderr.Len() != 0 { t.Fatalf("Run(%v) status=%d stdout=%q stderr=%q", args, status, stdout.String(), stderr.String()) } diff --git a/internal/app/command_help.go b/internal/app/command_help.go index 2227a24..7df734a 100644 --- a/internal/app/command_help.go +++ b/internal/app/command_help.go @@ -62,6 +62,9 @@ func commandUsage(descriptor commandDescriptor) string { } func commandUsageLine(descriptor commandDescriptor) string { + if descriptor.name == "help" { + return "agentic-proofkit help [|-h|--help]" + } segments := []string{"agentic-proofkit", descriptor.name} if descriptor.input == commandInputRequired { segments = append(segments, "--input ") diff --git a/internal/tools/packageartifact/main.go b/internal/tools/packageartifact/main.go index b2ee7ea..4d77612 100644 --- a/internal/tools/packageartifact/main.go +++ b/internal/tools/packageartifact/main.go @@ -65,11 +65,10 @@ func runWithDependencies(root string, runner Runner, dependencies orchestrationD if err := packageartifactrecord.Invalidate(root); err != nil { return err } - sourceRevision, sourceDigest, err := packageartifactrecord.SourceSnapshot(root) - if err != nil { + if err := packageartifactrecord.PrepareCandidateArtifactOutputs(root); err != nil { return err } - artifactBaseline, err := packageartifactrecord.ArtifactEvidenceBaseline(root) + sourceRevision, sourceDigest, err := packageartifactrecord.SourceSnapshot(root) if err != nil { return err } @@ -103,8 +102,6 @@ func runWithDependencies(root string, runner Runner, dependencies orchestrationD artifactEvidence, err := packageartifactrecord.ArtifactEvidenceSnapshot(root) if err != nil { evidenceErr = errors.Join(evidenceErr, err) - } else if artifactEvidence.FreshnessDigest == artifactBaseline.FreshnessDigest { - evidenceErr = errors.Join(evidenceErr, fmt.Errorf("package artifact command produced no fresh artifact state")) } if runErr == nil && exitCode != 0 { runErr = fmt.Errorf("package artifact command returned exit code %d without an execution error", exitCode) @@ -115,21 +112,19 @@ func runWithDependencies(root string, runner Runner, dependencies orchestrationD } artifactDigest := artifactEvidence.SnapshotDigest record := packageartifactrecord.Record{ - Argv: packageartifactrecord.CanonicalCommandArgv(), - ArtifactFreshnessBaselineDigest: artifactBaseline.FreshnessDigest, - ArtifactFreshnessDigest: artifactEvidence.FreshnessDigest, - ArtifactSnapshotDigest: artifactDigest, - CommandID: packageartifactrecord.CommandID, - EnvironmentDigest: environmentDigest, - ExecutionArgv: packageartifactrecord.CanonicalExecutionArgv(), - ExitCode: exitCode, - FinishedAt: finishedAt.Format(time.RFC3339Nano), - SchemaVersion: packageartifactrecord.SchemaVersion, - SourceRevision: sourceRevision, - SourceSnapshotDigest: sourceDigest, - StartedAt: startedAt.Format(time.RFC3339Nano), - Status: status, - ToolchainDigest: toolchainDigest, + Argv: packageartifactrecord.CanonicalCommandArgv(), + ArtifactSnapshotDigest: artifactDigest, + CommandID: packageartifactrecord.CommandID, + EnvironmentDigest: environmentDigest, + ExecutionArgv: packageartifactrecord.CanonicalExecutionArgv(), + ExitCode: exitCode, + FinishedAt: finishedAt.Format(time.RFC3339Nano), + SchemaVersion: packageartifactrecord.SchemaVersion, + SourceRevision: sourceRevision, + SourceSnapshotDigest: sourceDigest, + StartedAt: startedAt.Format(time.RFC3339Nano), + Status: status, + ToolchainDigest: toolchainDigest, } if err := packageartifactrecord.Write(root, record); err != nil { return errors.Join(runErr, evidenceErr, err) diff --git a/internal/tools/packageartifact/main_test.go b/internal/tools/packageartifact/main_test.go index a08ede9..0068387 100644 --- a/internal/tools/packageartifact/main_test.go +++ b/internal/tools/packageartifact/main_test.go @@ -59,14 +59,18 @@ func TestRunWithDependenciesRecordsCanonicalAndExecutionArgv(t *testing.T) { } } -func TestRunWithDependenciesRejectsTrueRunnerWithoutArtifactMutation(t *testing.T) { +func TestRunWithDependenciesRejectsTimestampOnlyMutationOfPreexistingArtifact(t *testing.T) { root := packageArtifactFixture(t) writeArtifactFixture(t, root, "artifact-v1") + artifactPath := filepath.Join(root, "artifacts", "package", "package.tgz") err := runWithDependencies(root, runnerFunc(func(string, []string) (int, error) { + if touchErr := os.Chtimes(artifactPath, time.Now(), time.Now()); !os.IsNotExist(touchErr) { + t.Fatalf("preexisting artifact remained available for timestamp-only mutation: %v", touchErr) + } return 0, nil }), stableDependencies()) - if err == nil || !strings.Contains(err.Error(), "produced no fresh artifact state") { + if err == nil || !strings.Contains(err.Error(), "produced no artifacts") { t.Fatalf("runWithDependencies() error = %v", err) } record, readErr := packageartifactrecord.Read(root) @@ -77,7 +81,58 @@ func TestRunWithDependenciesRejectsTrueRunnerWithoutArtifactMutation(t *testing. t.Fatalf("record result = %s/%d, want failed/0", record.Status, record.ExitCode) } if validateErr := packageartifactrecord.ValidateCurrent(root, record); validateErr == nil { - t.Fatal("ValidateCurrent() accepted evidence from a no-op true runner") + t.Fatal("ValidateCurrent() accepted evidence from a timestamp-only runner") + } +} + +func TestRunWithDependenciesInvalidatesPriorRecordButRetainsCandidateOutputsOnProviderEvidence(t *testing.T) { + root := packageArtifactFixture(t) + writeArtifactFixture(t, root, "candidate") + writeFileFixture(t, root, "artifacts/registry/npm-registry.json", "provider") + if err := packageartifactrecord.Write(root, packageartifactrecord.Record{Status: "passed"}); err != nil { + t.Fatal(err) + } + runnerCalled := false + + err := runWithDependencies(root, runnerFunc(func(string, []string) (int, error) { + runnerCalled = true + return 0, nil + }), stableDependencies()) + if err == nil || !strings.Contains(err.Error(), "rejects ambient provider evidence") { + t.Fatalf("runWithDependencies() error = %v", err) + } + if runnerCalled { + t.Fatal("runner executed after ambient provider evidence rejection") + } + if _, statErr := os.Stat(filepath.Join(root, filepath.FromSlash(packageartifactrecord.RecordPath))); !os.IsNotExist(statErr) { + t.Fatalf("prior execution record survived rejected package run: %v", statErr) + } + if content, readErr := os.ReadFile(filepath.Join(root, "artifacts/package/package.tgz")); readErr != nil || string(content) != "candidate" { + t.Fatalf("candidate output changed before provider-evidence rejection: content=%q err=%v", content, readErr) + } +} + +func TestRunWithDependenciesAcceptsCleanRegenerationWithIdenticalBytes(t *testing.T) { + root := packageArtifactFixture(t) + writeArtifactFixture(t, root, "artifact-v1") + artifactPath := filepath.Join(root, "artifacts", "package", "package.tgz") + + err := runWithDependencies(root, runnerFunc(func(root string, _ []string) (int, error) { + if _, statErr := os.Stat(artifactPath); !os.IsNotExist(statErr) { + t.Fatalf("preexisting artifact was not removed before regeneration: %v", statErr) + } + writeArtifactFixture(t, root, "artifact-v1") + return 0, nil + }), stableDependencies()) + if err != nil { + t.Fatalf("runWithDependencies() error = %v", err) + } + record, readErr := packageartifactrecord.Read(root) + if readErr != nil { + t.Fatal(readErr) + } + if record.Status != "passed" || record.ExitCode != 0 { + t.Fatalf("record result = %s/%d, want passed/0", record.Status, record.ExitCode) } } diff --git a/internal/tools/packageartifactrecord/record.go b/internal/tools/packageartifactrecord/record.go index 97dd30f..7acdb78 100644 --- a/internal/tools/packageartifactrecord/record.go +++ b/internal/tools/packageartifactrecord/record.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "encoding/hex" "encoding/json" + "errors" "fmt" "io/fs" "os" @@ -21,10 +22,11 @@ import ( ) const ( - CommandID = "proofkit.package-artifact" - RecordPath = "artifacts/proofkit/package-artifact-execution.json" - SchemaVersion = 1 - maxRecordBytes = 1 << 20 + CommandID = "proofkit.package-artifact" + RecordPath = "artifacts/proofkit/package-artifact-execution.json" + SchemaVersion = 2 + maxRecordBytes = 1 << 20 + maxReleaseManifestBytes = 8 << 20 ) var ( @@ -58,6 +60,28 @@ var artifactEvidenceRoots = []string{ "artifacts/release", } +var candidateArtifactResetRoots = []string{ + "artifacts/package", + "artifacts/pypi", +} + +var candidateReleaseOutputPaths = []string{ + "artifacts/release/checksums.sha256", + "artifacts/release/metadata-checksums.sha256", + "artifacts/release/release-manifest.json", + "artifacts/release/release-notes.md", + "artifacts/release/sbom-subjects.sha256", + "artifacts/release/sbom.cdx.json", +} + +var providerEvidencePaths = []string{ + "artifacts/attestations", + "artifacts/pypi-registry", + "artifacts/registry", + "artifacts/release/github-release.json", + "artifacts/release/retained-evidence-checksums.sha256", +} + func CanonicalCommandArgv() []string { return append([]string(nil), commandArgv...) } @@ -67,27 +91,24 @@ func CanonicalExecutionArgv() []string { } type Record struct { - Argv []string `json:"argv"` - ArtifactFreshnessBaselineDigest string `json:"artifactFreshnessBaselineDigest"` - ArtifactFreshnessDigest string `json:"artifactFreshnessDigest"` - ArtifactSnapshotDigest string `json:"artifactSnapshotDigest"` - CommandID string `json:"commandId"` - EnvironmentDigest string `json:"environmentDigest"` - ExecutionArgv []string `json:"executionArgv"` - ExitCode int `json:"exitCode"` - FinishedAt string `json:"finishedAt"` - SchemaVersion int `json:"schemaVersion"` - SourceRevision string `json:"sourceRevision"` - SourceSnapshotDigest string `json:"sourceSnapshotDigest"` - StartedAt string `json:"startedAt"` - Status string `json:"status"` - ToolchainDigest string `json:"toolchainDigest"` + Argv []string `json:"argv"` + ArtifactSnapshotDigest string `json:"artifactSnapshotDigest"` + CommandID string `json:"commandId"` + EnvironmentDigest string `json:"environmentDigest"` + ExecutionArgv []string `json:"executionArgv"` + ExitCode int `json:"exitCode"` + FinishedAt string `json:"finishedAt"` + SchemaVersion int `json:"schemaVersion"` + SourceRevision string `json:"sourceRevision"` + SourceSnapshotDigest string `json:"sourceSnapshotDigest"` + StartedAt string `json:"startedAt"` + Status string `json:"status"` + ToolchainDigest string `json:"toolchainDigest"` } type ArtifactEvidence struct { - FileCount int - FreshnessDigest string - SnapshotDigest string + FileCount int + SnapshotDigest string } func Read(root string) (Record, error) { @@ -104,7 +125,7 @@ func Read(root string) (Record, error) { if !ok { return Record{}, fmt.Errorf("package artifact execution record must be an object") } - if err := admit.KnownKeys(object, []string{"argv", "artifactFreshnessBaselineDigest", "artifactFreshnessDigest", "artifactSnapshotDigest", "commandId", "environmentDigest", "executionArgv", "exitCode", "finishedAt", "schemaVersion", "sourceRevision", "sourceSnapshotDigest", "startedAt", "status", "toolchainDigest"}, "package artifact execution record"); err != nil { + if err := admit.KnownKeys(object, []string{"argv", "artifactSnapshotDigest", "commandId", "environmentDigest", "executionArgv", "exitCode", "finishedAt", "schemaVersion", "sourceRevision", "sourceSnapshotDigest", "startedAt", "status", "toolchainDigest"}, "package artifact execution record"); err != nil { return Record{}, err } content, err := json.Marshal(raw) @@ -120,21 +141,19 @@ func Read(root string) (Record, error) { func Write(root string, record Record) error { value := map[string]any{ - "argv": stringsToAny(record.Argv), - "artifactFreshnessBaselineDigest": record.ArtifactFreshnessBaselineDigest, - "artifactFreshnessDigest": record.ArtifactFreshnessDigest, - "artifactSnapshotDigest": record.ArtifactSnapshotDigest, - "commandId": record.CommandID, - "environmentDigest": record.EnvironmentDigest, - "executionArgv": stringsToAny(record.ExecutionArgv), - "exitCode": record.ExitCode, - "finishedAt": record.FinishedAt, - "schemaVersion": record.SchemaVersion, - "sourceRevision": record.SourceRevision, - "sourceSnapshotDigest": record.SourceSnapshotDigest, - "startedAt": record.StartedAt, - "status": record.Status, - "toolchainDigest": record.ToolchainDigest, + "argv": stringsToAny(record.Argv), + "artifactSnapshotDigest": record.ArtifactSnapshotDigest, + "commandId": record.CommandID, + "environmentDigest": record.EnvironmentDigest, + "executionArgv": stringsToAny(record.ExecutionArgv), + "exitCode": record.ExitCode, + "finishedAt": record.FinishedAt, + "schemaVersion": record.SchemaVersion, + "sourceRevision": record.SourceRevision, + "sourceSnapshotDigest": record.SourceSnapshotDigest, + "startedAt": record.StartedAt, + "status": record.Status, + "toolchainDigest": record.ToolchainDigest, } content, err := stablejson.Marshal(value) if err != nil { @@ -172,6 +191,170 @@ func Invalidate(root string) error { return nil } +func PrepareCandidateArtifactOutputs(root string) error { + rootFS, err := os.OpenRoot(root) + if err != nil { + return fmt.Errorf("open artifact repository root: %w", err) + } + defer rootFS.Close() + + for _, relativePath := range providerEvidencePaths { + _, exists, err := artifactPathState(rootFS, relativePath) + if err != nil { + return err + } + if exists { + return fmt.Errorf("candidate package artifact execution rejects ambient provider evidence at %s", relativePath) + } + } + if err := admitCandidateReleaseManifest(rootFS); err != nil { + return err + } + if err := admitCandidateReleaseOutputs(rootFS); err != nil { + return err + } + + for _, relativeRoot := range candidateArtifactResetRoots { + path, exists, err := artifactPathState(rootFS, relativeRoot) + if err != nil { + return err + } + if exists { + if err := rootFS.RemoveAll(path); err != nil { + return fmt.Errorf("reset candidate artifact root %s: %w", relativeRoot, err) + } + } + } + for _, relativePath := range candidateReleaseOutputPaths { + path, exists, err := artifactPathState(rootFS, relativePath) + if err != nil { + return err + } + if exists { + if err := rootFS.Remove(path); err != nil { + return fmt.Errorf("reset candidate release output %s: %w", relativePath, err) + } + } + } + releaseRoot := "artifacts/release" + releasePath, exists, err := artifactPathState(rootFS, releaseRoot) + if err != nil { + return err + } + if !exists { + return nil + } + return rootFS.Remove(releasePath) +} + +func admitCandidateReleaseManifest(rootFS *os.Root) error { + const relativePath = "artifacts/release/release-manifest.json" + path, exists, err := artifactPathState(rootFS, relativePath) + if err != nil || !exists { + return err + } + file, err := rootFS.Open(path) + if err != nil { + return err + } + defer file.Close() + raw, err := admission.DecodeJSON(file, maxReleaseManifestBytes) + if err != nil { + return fmt.Errorf("candidate package artifact execution cannot classify existing release manifest: %w", err) + } + object, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("candidate package artifact execution cannot classify existing release manifest") + } + artifactKind, kindOK := object["artifactKind"].(string) + schemaVersion, versionOK := object["schemaVersion"].(json.Number) + if !kindOK || artifactKind != "proofkit.release-manifest.v1" || !versionOK || schemaVersion.String() != "1" { + return fmt.Errorf("candidate package artifact execution cannot classify existing release manifest") + } + channels, ok := object["channels"].([]any) + if !ok || len(channels) == 0 { + return fmt.Errorf("candidate package artifact execution cannot classify existing release manifest") + } + for _, value := range channels { + channel, ok := value.(map[string]any) + if !ok { + return fmt.Errorf("candidate package artifact execution cannot classify existing release manifest") + } + status, ok := channel["status"].(string) + if !ok || (status != "candidate" && status != "planned" && status != "not_applicable") { + return fmt.Errorf("candidate package artifact execution rejects provider-derived release manifest at %s", relativePath) + } + if _, present := channel["publicationMode"]; present { + return fmt.Errorf("candidate package artifact execution rejects provider-derived release manifest at %s", relativePath) + } + if _, present := channel["trustedPublisher"]; present { + return fmt.Errorf("candidate package artifact execution rejects provider-derived release manifest at %s", relativePath) + } + } + return nil +} + +func admitCandidateReleaseOutputs(rootFS *os.Root) error { + releaseRoot, exists, err := artifactPathState(rootFS, "artifacts/release") + if err != nil { + return err + } + if !exists { + return nil + } + directory, err := rootFS.Open(releaseRoot) + if err != nil { + return err + } + entries, readErr := directory.ReadDir(-1) + closeErr := directory.Close() + if readErr != nil { + return readErr + } + if closeErr != nil { + return closeErr + } + allowed := map[string]struct{}{} + for _, relativePath := range candidateReleaseOutputPaths { + allowed[filepath.Base(filepath.FromSlash(relativePath))] = struct{}{} + } + for _, entry := range entries { + if _, ok := allowed[entry.Name()]; !ok { + return fmt.Errorf("candidate package artifact execution rejects unowned release state at artifacts/release") + } + if entry.Type()&os.ModeSymlink != 0 || !entry.Type().IsRegular() { + return fmt.Errorf("candidate release output artifacts/release/%s must be a regular file", entry.Name()) + } + } + return nil +} + +func artifactPathState(rootFS *os.Root, relativePath string) (string, bool, error) { + cleanPath := filepath.Clean(filepath.FromSlash(relativePath)) + if cleanPath == "." || !filepath.IsLocal(cleanPath) { + return "", false, fmt.Errorf("artifact path must stay repository-relative: %s", relativePath) + } + parts := strings.Split(cleanPath, string(filepath.Separator)) + current := "" + for index, part := range parts { + current = filepath.Join(current, part) + info, err := rootFS.Lstat(current) + if os.IsNotExist(err) { + return filepath.Join(append([]string{current}, parts[index+1:]...)...), false, nil + } + if err != nil { + return "", false, err + } + if info.Mode()&os.ModeSymlink != 0 { + return "", false, fmt.Errorf("artifact path %s traverses a symlink; symlinks are not admitted", relativePath) + } + if index < len(parts)-1 && !info.IsDir() { + return "", false, fmt.Errorf("artifact path %s traverses a non-directory", relativePath) + } + } + return current, true, nil +} + func ValidateCurrent(root string, record Record) error { if record.SchemaVersion != SchemaVersion || record.CommandID != CommandID || !equalStrings(record.Argv, commandArgv) || !equalStrings(record.ExecutionArgv, executionArgv) { return fmt.Errorf("package artifact execution record identity is invalid") @@ -189,15 +372,10 @@ func ValidateCurrent(root string, record Record) error { } if !isSHA256(record.SourceSnapshotDigest) || !isSHA256(record.ArtifactSnapshotDigest) || - !isSHA256(record.ArtifactFreshnessBaselineDigest) || - !isSHA256(record.ArtifactFreshnessDigest) || !isSHA256(record.EnvironmentDigest) || !isSHA256(record.ToolchainDigest) { return fmt.Errorf("package artifact execution record snapshot digests must be lowercase sha256") } - if record.ArtifactFreshnessBaselineDigest == record.ArtifactFreshnessDigest { - return fmt.Errorf("package artifact execution record does not prove fresh artifacts") - } revision, sourceDigest, err := SourceSnapshot(root) if err != nil { return err @@ -212,9 +390,6 @@ func ValidateCurrent(root string, record Record) error { if artifactEvidence.SnapshotDigest != record.ArtifactSnapshotDigest { return fmt.Errorf("package artifact execution record artifact snapshot is stale: recorded %s current %s", record.ArtifactSnapshotDigest, artifactEvidence.SnapshotDigest) } - if artifactEvidence.FreshnessDigest != record.ArtifactFreshnessDigest { - return fmt.Errorf("package artifact execution record artifact freshness is stale: recorded %s current %s", record.ArtifactFreshnessDigest, artifactEvidence.FreshnessDigest) - } return nil } @@ -262,55 +437,36 @@ func ArtifactSnapshot(root string) (string, error) { return evidence.SnapshotDigest, nil } -func ArtifactEvidenceBaseline(root string) (ArtifactEvidence, error) { - return artifactEvidenceSnapshot(root, false) -} - func ArtifactEvidenceSnapshot(root string) (ArtifactEvidence, error) { - return artifactEvidenceSnapshot(root, true) -} - -func artifactEvidenceSnapshot(root string, requireFiles bool) (ArtifactEvidence, error) { + rootFS, err := os.OpenRoot(root) + if err != nil { + return ArtifactEvidence{}, fmt.Errorf("open artifact snapshot root: %w", err) + } + defer rootFS.Close() paths := []string{} for _, relativeRoot := range artifactEvidenceRoots { - artifactRoot := filepath.Join(root, filepath.FromSlash(relativeRoot)) - err := filepath.WalkDir(artifactRoot, func(path string, entry fs.DirEntry, walkErr error) error { + artifactRoot := filepath.ToSlash(filepath.Clean(filepath.FromSlash(relativeRoot))) + err := fs.WalkDir(rootFS.FS(), artifactRoot, func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if !entry.IsDir() { - relative, err := filepath.Rel(root, path) - if err != nil { - return err - } - paths = append(paths, filepath.ToSlash(relative)) + paths = append(paths, path) } return nil }) - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return ArtifactEvidence{}, err } } if len(paths) == 0 { - if !requireFiles { - return emptyArtifactEvidence(), nil - } return ArtifactEvidence{}, fmt.Errorf("package artifact execution produced no artifacts") } - snapshotDigest, err := digestPaths(root, paths) + snapshotDigest, err := digestPathsAtRoot(rootFS, paths) if err != nil { return ArtifactEvidence{}, err } - freshnessDigest, err := digestPathFreshness(root, paths) - if err != nil { - return ArtifactEvidence{}, err - } - return ArtifactEvidence{FileCount: len(paths), FreshnessDigest: freshnessDigest, SnapshotDigest: snapshotDigest}, nil -} - -func emptyArtifactEvidence() ArtifactEvidence { - emptyDigest := hex.EncodeToString(sha256.New().Sum(nil)) - return ArtifactEvidence{FreshnessDigest: emptyDigest, SnapshotDigest: emptyDigest} + return ArtifactEvidence{FileCount: len(paths), SnapshotDigest: snapshotDigest}, nil } func EnvironmentDigest(environment []string) string { @@ -385,57 +541,61 @@ func gitOutput(root string, args ...string) (string, error) { } func digestPaths(root string, paths []string) (string, error) { + rootFS, err := os.OpenRoot(root) + if err != nil { + return "", fmt.Errorf("open snapshot root: %w", err) + } + defer rootFS.Close() + return digestPathsAtRoot(rootFS, paths) +} + +func digestPathsAtRoot(rootFS *os.Root, paths []string) (string, error) { orderedPaths := append([]string(nil), paths...) sort.Strings(orderedPaths) hash := sha256.New() for _, path := range orderedPaths { - normalizedPath, fullPath, err := normalizedSnapshotPath(root, path) + normalizedPath, err := normalizedSnapshotPath(path) if err != nil { return "", err } - info, content, err := readRegularFile(fullPath) + _, exists, err := artifactPathState(rootFS, normalizedPath) if err != nil { - return "", fmt.Errorf("snapshot %s: %w", normalizedPath, err) + return "", err } - writeDigestField(hash, []byte(normalizedPath)) - writeDigestField(hash, []byte("regular")) - writeDigestField(hash, []byte(normalizedFileMode(info))) - writeDigestField(hash, content) - } - return hex.EncodeToString(hash.Sum(nil)), nil -} - -func digestPathFreshness(root string, paths []string) (string, error) { - orderedPaths := append([]string(nil), paths...) - sort.Strings(orderedPaths) - hash := sha256.New() - for _, path := range orderedPaths { - normalizedPath, fullPath, err := normalizedSnapshotPath(root, path) + if !exists { + return "", fmt.Errorf("snapshot %s: file does not exist", normalizedPath) + } + info, err := rootFS.Lstat(filepath.FromSlash(normalizedPath)) if err != nil { - return "", err + return "", fmt.Errorf("snapshot %s: %w", normalizedPath, err) + } + if info.Mode()&os.ModeSymlink != 0 { + return "", fmt.Errorf("snapshot %s: symlinks are not admitted", normalizedPath) } - info, _, err := readRegularFile(fullPath) + if !info.Mode().IsRegular() { + return "", fmt.Errorf("snapshot %s: non-regular files are not admitted: mode %s", normalizedPath, info.Mode()) + } + content, err := rootFS.ReadFile(filepath.FromSlash(normalizedPath)) if err != nil { - return "", fmt.Errorf("snapshot freshness %s: %w", normalizedPath, err) + return "", fmt.Errorf("snapshot %s: %w", normalizedPath, err) } writeDigestField(hash, []byte(normalizedPath)) writeDigestField(hash, []byte("regular")) writeDigestField(hash, []byte(normalizedFileMode(info))) - writeDigestField(hash, []byte(fmt.Sprintf("%d", info.Size()))) - writeDigestField(hash, []byte(fmt.Sprintf("%d", info.ModTime().UnixNano()))) + writeDigestField(hash, content) } return hex.EncodeToString(hash.Sum(nil)), nil } -func normalizedSnapshotPath(root string, path string) (string, string, error) { +func normalizedSnapshotPath(path string) (string, error) { normalizedPath := filepath.ToSlash(filepath.Clean(filepath.FromSlash(path))) if normalizedPath == "." || filepath.IsAbs(filepath.FromSlash(path)) || normalizedPath == ".." || strings.HasPrefix(normalizedPath, "../") { - return "", "", fmt.Errorf("snapshot path %q must be a normalized relative path", path) + return "", fmt.Errorf("snapshot path %q must be a normalized relative path", path) } if path != normalizedPath { - return "", "", fmt.Errorf("snapshot path %q is not normalized as %q", path, normalizedPath) + return "", fmt.Errorf("snapshot path %q is not normalized as %q", path, normalizedPath) } - return normalizedPath, filepath.Join(root, filepath.FromSlash(normalizedPath)), nil + return normalizedPath, nil } func readRegularFile(path string) (fs.FileInfo, []byte, error) { diff --git a/internal/tools/packageartifactrecord/record_test.go b/internal/tools/packageartifactrecord/record_test.go index 459485f..dac6ebb 100644 --- a/internal/tools/packageartifactrecord/record_test.go +++ b/internal/tools/packageartifactrecord/record_test.go @@ -17,10 +17,6 @@ func TestValidateCurrentBindsSourceAndArtifactSnapshots(t *testing.T) { writeFixture(t, root, ".gitignore", "artifacts/\n") runGit(t, root, "add", "source.txt", ".gitignore") runGit(t, root, "commit", "-m", "fixture") - baseline, err := ArtifactEvidenceBaseline(root) - if err != nil { - t.Fatal(err) - } writeFixture(t, root, "artifacts/package/package.tgz", "artifact-v1") revision, sourceDigest, err := SourceSnapshot(root) @@ -32,25 +28,28 @@ func TestValidateCurrentBindsSourceAndArtifactSnapshots(t *testing.T) { t.Fatal(err) } record := Record{ - Argv: CanonicalCommandArgv(), - ArtifactFreshnessBaselineDigest: baseline.FreshnessDigest, - ArtifactFreshnessDigest: artifactEvidence.FreshnessDigest, - ArtifactSnapshotDigest: artifactEvidence.SnapshotDigest, - CommandID: CommandID, - EnvironmentDigest: strings.Repeat("a", 64), - ExecutionArgv: CanonicalExecutionArgv(), - ExitCode: 0, - FinishedAt: "2026-07-11T10:00:01Z", - SchemaVersion: SchemaVersion, - SourceRevision: revision, - SourceSnapshotDigest: sourceDigest, - StartedAt: "2026-07-11T10:00:00Z", - Status: "passed", - ToolchainDigest: strings.Repeat("b", 64), + Argv: CanonicalCommandArgv(), + ArtifactSnapshotDigest: artifactEvidence.SnapshotDigest, + CommandID: CommandID, + EnvironmentDigest: strings.Repeat("a", 64), + ExecutionArgv: CanonicalExecutionArgv(), + ExitCode: 0, + FinishedAt: "2026-07-11T10:00:01Z", + SchemaVersion: SchemaVersion, + SourceRevision: revision, + SourceSnapshotDigest: sourceDigest, + StartedAt: "2026-07-11T10:00:00Z", + Status: "passed", + ToolchainDigest: strings.Repeat("b", 64), } if err := ValidateCurrent(root, record); err != nil { t.Fatalf("ValidateCurrent() valid record error = %v", err) } + legacyRecord := record + legacyRecord.SchemaVersion = 1 + if err := ValidateCurrent(root, legacyRecord); err == nil { + t.Fatal("ValidateCurrent() accepted legacy pre-clean-materialization record") + } writeFixture(t, root, "source.txt", "source-v2") if err := ValidateCurrent(root, record); err == nil || !strings.Contains(err.Error(), "source snapshot is stale") { @@ -143,6 +142,93 @@ func TestArtifactSnapshotIgnoresFilesOutsideOwnedOutputRoots(t *testing.T) { } } +func TestPrepareCandidateArtifactOutputsRemovesOnlyCandidateOwnedOutputs(t *testing.T) { + root := t.TempDir() + for _, path := range []string{ + "artifacts/package/package.tgz", + "artifacts/pypi/package.whl", + } { + writeFixture(t, root, path, "generated") + } + writeFixture(t, root, "artifacts/release/release-manifest.json", `{"artifactKind":"proofkit.release-manifest.v1","channels":[{"status":"candidate"},{"status":"planned"}],"schemaVersion":1}`) + retainedPath := "artifacts/proofkit/retained.json" + writeFixture(t, root, retainedPath, "retained") + + if err := PrepareCandidateArtifactOutputs(root); err != nil { + t.Fatal(err) + } + for _, path := range []string{"artifacts/package", "artifacts/pypi", "artifacts/release"} { + if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(path))); !os.IsNotExist(err) { + t.Fatalf("owned output root %s still exists: %v", path, err) + } + } + if content, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(retainedPath))); err != nil || string(content) != "retained" { + t.Fatalf("non-owned proof artifact changed: content=%q err=%v", content, err) + } +} + +func TestPrepareCandidateArtifactOutputsRejectsProviderEvidenceBeforeMutation(t *testing.T) { + root := t.TempDir() + writeFixture(t, root, "artifacts/package/package.tgz", "candidate") + writeFixture(t, root, "artifacts/registry/npm-registry.json", "provider") + + err := PrepareCandidateArtifactOutputs(root) + if err == nil || !strings.Contains(err.Error(), "rejects ambient provider evidence") { + t.Fatalf("PrepareCandidateArtifactOutputs() error = %v", err) + } + if content, readErr := os.ReadFile(filepath.Join(root, "artifacts/package/package.tgz")); readErr != nil || string(content) != "candidate" { + t.Fatalf("candidate output changed before provider-evidence rejection: content=%q err=%v", content, readErr) + } +} + +func TestPrepareCandidateArtifactOutputsRejectsUnownedReleaseStateBeforeMutation(t *testing.T) { + root := t.TempDir() + writeFixture(t, root, "artifacts/package/package.tgz", "candidate") + writeFixture(t, root, "artifacts/release/unowned.json", "unowned") + + err := PrepareCandidateArtifactOutputs(root) + if err == nil || !strings.Contains(err.Error(), "rejects unowned release state") { + t.Fatalf("PrepareCandidateArtifactOutputs() error = %v", err) + } + if content, readErr := os.ReadFile(filepath.Join(root, "artifacts/package/package.tgz")); readErr != nil || string(content) != "candidate" { + t.Fatalf("candidate output changed before unowned-state rejection: content=%q err=%v", content, readErr) + } +} + +func TestPrepareCandidateArtifactOutputsRejectsProviderDerivedReleaseManifestBeforeMutation(t *testing.T) { + root := t.TempDir() + writeFixture(t, root, "artifacts/package/package.tgz", "candidate") + writeFixture(t, root, "artifacts/release/release-manifest.json", `{"artifactKind":"proofkit.release-manifest.v1","channels":[{"publicationMode":"published_by_workflow","status":"published"}],"schemaVersion":1}`) + + err := PrepareCandidateArtifactOutputs(root) + if err == nil || !strings.Contains(err.Error(), "rejects provider-derived release manifest") { + t.Fatalf("PrepareCandidateArtifactOutputs() error = %v", err) + } + if content, readErr := os.ReadFile(filepath.Join(root, "artifacts/package/package.tgz")); readErr != nil || string(content) != "candidate" { + t.Fatalf("candidate output changed before provider-manifest rejection: content=%q err=%v", content, readErr) + } +} + +func TestPrepareCandidateArtifactOutputsDoesNotFollowArtifactSymlink(t *testing.T) { + root := t.TempDir() + external := t.TempDir() + externalPath := filepath.Join(external, "sentinel.txt") + if err := os.WriteFile(externalPath, []byte("retained"), 0o600); err != nil { + t.Fatal(err) + } + if err := os.Symlink(external, filepath.Join(root, "artifacts")); err != nil { + t.Fatal(err) + } + + err := PrepareCandidateArtifactOutputs(root) + if err == nil || !strings.Contains(err.Error(), "traverses a symlink") { + t.Fatalf("PrepareCandidateArtifactOutputs() error = %v", err) + } + if content, readErr := os.ReadFile(externalPath); readErr != nil || string(content) != "retained" { + t.Fatalf("external sentinel changed through artifact symlink: content=%q err=%v", content, readErr) + } +} + func TestSnapshotDigestBindsNormalizedMode(t *testing.T) { root := t.TempDir() writeFixture(t, root, "source.txt", "source") @@ -174,6 +260,23 @@ func TestSnapshotDigestRejectsNonRegularFile(t *testing.T) { } } +func TestSnapshotDigestRejectsIntermediateSymlinkEscape(t *testing.T) { + root := t.TempDir() + external := t.TempDir() + writeFixture(t, external, "package.tgz", "external") + if err := os.MkdirAll(filepath.Join(root, "artifacts"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink(external, filepath.Join(root, "artifacts", "package")); err != nil { + t.Fatal(err) + } + + _, err := digestPaths(root, []string{"artifacts/package/package.tgz"}) + if err == nil || !strings.Contains(err.Error(), "traverses a symlink") { + t.Fatalf("digestPaths() intermediate symlink error = %v", err) + } +} + func runGit(t *testing.T, root string, args ...string) { t.Helper() command := exec.Command("git", args...) diff --git a/internal/tools/releasecloseoutinput/main_test.go b/internal/tools/releasecloseoutinput/main_test.go index 1de89d9..2de86af 100644 --- a/internal/tools/releasecloseoutinput/main_test.go +++ b/internal/tools/releasecloseoutinput/main_test.go @@ -879,21 +879,19 @@ func writePackageArtifactExecutionFixture(t *testing.T, root string) packagearti t.Fatal(err) } record := packageartifactrecord.Record{ - Argv: packageartifactrecord.CanonicalCommandArgv(), - ArtifactFreshnessBaselineDigest: strings.Repeat("0", 64), - ArtifactFreshnessDigest: artifactEvidence.FreshnessDigest, - ArtifactSnapshotDigest: artifactEvidence.SnapshotDigest, - CommandID: packageartifactrecord.CommandID, - EnvironmentDigest: strings.Repeat("1", 64), - ExecutionArgv: packageartifactrecord.CanonicalExecutionArgv(), - ExitCode: 0, - FinishedAt: "2026-07-01T10:00:01Z", - SchemaVersion: packageartifactrecord.SchemaVersion, - SourceRevision: revision, - SourceSnapshotDigest: sourceDigest, - StartedAt: "2026-07-01T10:00:00Z", - Status: "passed", - ToolchainDigest: strings.Repeat("2", 64), + Argv: packageartifactrecord.CanonicalCommandArgv(), + ArtifactSnapshotDigest: artifactEvidence.SnapshotDigest, + CommandID: packageartifactrecord.CommandID, + EnvironmentDigest: strings.Repeat("1", 64), + ExecutionArgv: packageartifactrecord.CanonicalExecutionArgv(), + ExitCode: 0, + FinishedAt: "2026-07-01T10:00:01Z", + SchemaVersion: packageartifactrecord.SchemaVersion, + SourceRevision: revision, + SourceSnapshotDigest: sourceDigest, + StartedAt: "2026-07-01T10:00:00Z", + Status: "passed", + ToolchainDigest: strings.Repeat("2", 64), } if err := packageartifactrecord.Write(root, record); err != nil { t.Fatal(err) diff --git a/proofkit/requirement-bindings.json b/proofkit/requirement-bindings.json index b82a6e6..0e9f524 100644 --- a/proofkit/requirement-bindings.json +++ b/proofkit/requirement-bindings.json @@ -562,6 +562,16 @@ "This requirement does not claim registry publication, every command's runtime behavior, execution of non-native platform binaries, provider-side install freshness, consumer dependency admission, release approval, rollout approval, or production readiness." ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-020", + "ownerId": "proofkit.supply-chain-quality", + "specPath": "docs/specs/proofkit-supply-chain-quality/requirements.v1.json", + "claimLevel": "blocking", + "proofState": "witness_backed", + "nonClaims": [ + "This requirement does not claim producer authentication, hermetic or reproducible builds, registry publication, provider evidence freshness, release approval, rollout approval, production readiness, or exact artifact closure outside downstream package and release validators." + ] + }, { "requirementId": "REQ-PROOFKIT-SPEC-017", "ownerId": "proofkit.spec-proof-core", @@ -579,7 +589,7 @@ "claimLevel": "blocking", "proofState": "witness_backed", "nonClaims": [ - "This requirement does not claim command recommendation, command execution through family membership, consumer policy, native witness execution, merge approval, release approval, rollout approval, or production readiness." + "This requirement does not claim byte-for-byte help text compatibility across owner-approved CLI contract corrections, command recommendation, command execution through family membership, consumer policy, native witness execution, merge approval, release approval, rollout approval, or production readiness." ] } ], @@ -1575,6 +1585,36 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-020", + "scenarioId": "proofkit.supply-chain-quality.package-artifact-clean-materialization", + "witnessId": "proofkit.package-artifact.clean-materialization-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageartifact/main_test.go", + "commandIds": [ + "proofkit.go-test", + "proofkit.package-artifact" + ], + "environmentClasses": [ + "local-go", + "local-go-python" + ] + }, + { + "requirementId": "REQ-PROOFKIT-QUALITY-020", + "scenarioId": "proofkit.supply-chain-quality.package-artifact-output-ownership-and-confinement", + "witnessId": "proofkit.package-artifact.output-ownership-and-confinement-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/tools/packageartifactrecord/record_test.go", + "commandIds": [ + "proofkit.go-test", + "proofkit.package-artifact" + ], + "environmentClasses": [ + "local-go", + "local-go-python" + ] + }, { "requirementId": "REQ-PROOFKIT-QUALITY-015", "scenarioId": "proofkit.supply-chain-quality.release-closeout-completion-criteria", @@ -1918,6 +1958,19 @@ "local-go" ] }, + { + "requirementId": "REQ-PROOFKIT-SPEC-018", + "scenarioId": "proofkit.spec-proof-core.command-family-help-compatibility", + "witnessId": "proofkit.command-family-navigation.help-compatibility-falsifier", + "witnessKind": "contract", + "witnessPath": "internal/app/command_family_catalog_test.go", + "commandIds": [ + "proofkit.go-test" + ], + "environmentClasses": [ + "local-go" + ] + }, { "requirementId": "REQ-PROOFKIT-SPEC-018", "scenarioId": "proofkit.spec-proof-core.command-family-navigation-parity",