From fc37e342073aa7e688582b12489e0252d351b395 Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sun, 20 Sep 2026 05:22:41 +0500 Subject: [PATCH 1/2] fix(evidence): refuse record on unborn HEAD and write artifacts atomically An unborn-HEAD repository produced a signed artifact with an empty head_oid that verify can never accept (the schema requires a 40/64-hex commit). Record now fails closed with GDS_SESSION_EVIDENCE_BASELINE_UNAVAILABLE before signing, so no unverifiable artifact enters the hash chain. Artifact writes now go through a 0600 temporary file plus rename, so a crash cannot leave a truncated artifact at the final path. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- core/app/session_evidence.go | 29 ++++++++++++++++++++++++++- core/cli/evidence_test.go | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/core/app/session_evidence.go b/core/app/session_evidence.go index 708fc81..120f49b 100644 --- a/core/app/session_evidence.go +++ b/core/app/session_evidence.go @@ -89,6 +89,11 @@ func (services *Services) RecordSessionEvidence( if err != nil { return envelopeForError(command, info.WorktreeRoot, err) } + if status.Head.OID == "" { + return fail("GDS_SESSION_EVIDENCE_BASELINE_UNAVAILABLE", + "The repository has no commit to bind as evidence baseline; record after the first commit.", + map[string]any{"head_mode": status.Head.Mode}) + } topology, err := services.Git.InspectTopology(ctx, info.WorktreeRoot) if err != nil { return envelopeForError(command, info.WorktreeRoot, err) @@ -311,7 +316,29 @@ func writeSessionEvidenceArtifact(path string, artifact sessionevidence.Artifact if err != nil { return err } - return os.WriteFile(path, append(raw, '\n'), 0o600) + temporary, err := os.CreateTemp(filepath.Dir(path), ".evidence-*.tmp") + if err != nil { + return fmt.Errorf("create session evidence temporary file: %w", err) + } + if err := temporary.Chmod(0o600); err != nil { + temporary.Close() + os.Remove(temporary.Name()) + return fmt.Errorf("secure session evidence temporary file: %w", err) + } + if _, err := temporary.Write(append(raw, '\n')); err != nil { + temporary.Close() + os.Remove(temporary.Name()) + return fmt.Errorf("write session evidence artifact: %w", err) + } + if err := temporary.Close(); err != nil { + os.Remove(temporary.Name()) + return fmt.Errorf("close session evidence artifact: %w", err) + } + if err := os.Rename(temporary.Name(), path); err != nil { + os.Remove(temporary.Name()) + return fmt.Errorf("publish session evidence artifact: %w", err) + } + return nil } // latestSessionEvidenceDigest links each artifact to the newest previously diff --git a/core/cli/evidence_test.go b/core/cli/evidence_test.go index b482d9a..27fba2b 100644 --- a/core/cli/evidence_test.go +++ b/core/cli/evidence_test.go @@ -163,3 +163,41 @@ func TestEvidenceVerifyRejectsTamperedArtifact(t *testing.T) { t.Fatalf("tampered artifact verified: %#v", verifyEnvelope.Data) } } + +func TestEvidenceRecordRefusesUnbornHead(t *testing.T) { + disableGitFixtureMaintenance(t) + client := filepath.Join(t.TempDir(), "unborn") + if err := os.MkdirAll(filepath.Join(client, ".gds"), 0o755); err != nil { + t.Fatal(err) + } + runSessionGit(t, client, "init", "-q", "-b", "main") + anchor, err := os.ReadFile(filepath.Join( + repositoryRoot(t), "tests", "fixtures", "schemas", "v1", "valid-repository.yaml", + )) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(client, ".gds", "repository.yaml"), anchor, 0o644); err != nil { + t.Fatal(err) + } + keyPath, _ := writeSessionKeyPair(t) + evidenceRoot := filepath.Join(t.TempDir(), "session-evidence") + + exitCode, envelope, _ := executeJSON(t, recordArgs(client, keyPath, evidenceRoot)...) + if exitCode == 0 { + t.Fatalf("record succeeded on unborn HEAD: %#v", envelope.Data) + } + found := false + for _, finding := range envelope.Findings { + if finding.Code == "GDS_SESSION_EVIDENCE_BASELINE_UNAVAILABLE" { + found = true + } + } + if !found { + t.Fatalf("expected baseline-unavailable finding: %#v", envelope.Findings) + } + entries, err := filepath.Glob(filepath.Join(evidenceRoot, "*", "*.json")) + if err != nil || len(entries) != 0 { + t.Fatalf("unverifiable artifact written: %v %v", entries, err) + } +} From 701f11c175cd4d8aa77b47a90f701890d75814fb Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Sun, 20 Sep 2026 05:28:20 +0500 Subject: [PATCH 2/2] chore: restamp development lock after unborn-baseline guard --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index 2e254e7..2c83804 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -4,14 +4,14 @@ schema_version: 1 bundle: version: "0.9.7-dev" release_sequence: 0 - source_tree_digest: "sha256:a585302e6ef2f8fa86e88dcd6dd692ea4fc04ad03de874fe719b0c0dd2e891a4" - digest: "sha256:18d891dd71baa2f881ab5694e025dbc1d9e93b570bd4e1278779c76bbb17d084" + source_tree_digest: "sha256:17341c00e8252e7d0cd59eebc1fcecaf28301796285a2ae632da75c8932532d0" + digest: "sha256:2e8396d66b4365235b9a262a3f9fe66cd727d410ee121cb4e6a218c5e4a49719" projection: - input_digest: "sha256:beddf89f25e80a82d63914f92f8689f2d0d0873c581ca420496c4521367a7ebd" - output_digest: "sha256:1761fa50949eac3c2ff2d121dd69a66b3d2544c664877c52c5cea2ceb0d78121" + input_digest: "sha256:4b96ba6416faa4ce8f3ecf4642f142dfabb1a4fb65d8a34962bb00b94296700d" + output_digest: "sha256:3c98bb2cbe91041ea84b336ebc48f361409c7d796795affd6a0b4d27ff28e843" files: - path: ".gds/compiled-policy.json" digest: "sha256:9f498788bdc34e52a0ab793c536e0e6a7b360c2e1a20446cbf03ed51986cdc6f" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:c367d9e29a6fad68d8e38820b0bcf5b4e22f7c5c44fac48bf86111d2b2fb3790" + digest: "sha256:8fb50e6d5ea61d144d10c38f555a034646968d5537b811de4412f00cc5a958e3" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 865dbbc..277cf32 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.9.7-dev -# source-tree-digest: sha256:a585302e6ef2f8fa86e88dcd6dd692ea4fc04ad03de874fe719b0c0dd2e891a4 -# input-digest: sha256:beddf89f25e80a82d63914f92f8689f2d0d0873c581ca420496c4521367a7ebd +# source-tree-digest: sha256:17341c00e8252e7d0cd59eebc1fcecaf28301796285a2ae632da75c8932532d0 +# input-digest: sha256:4b96ba6416faa4ce8f3ecf4642f142dfabb1a4fb65d8a34962bb00b94296700d # output-digest: sha256:b9bf3d0c64c0fb371596e7d090e82e62aebbfde91929115fc15fb28644e4fd38 # edit-source: # - .gds/repository.yaml