carlos / platform Public

release: a dangling proof policy symlink is an error, not D3 off (H2)

Absence is now decided by Lstat: only nothing at the path is off. A symlink whose target has gone, a directory or an unreadable file at the path is an error, which the gate reads as on and waiting for every named-unit route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 7e032fedef45ae296d7a8485a7baa2dcfab154d1 parent 1358ba3
2 files changed, +31 −6
  • internal/release/proofpolicy.go +11 −6
  • internal/release/proofpolicy_test.go +20 −0
diff --git a/internal/release/proofpolicy.go b/internal/release/proofpolicy.go
index b1c25d317..c18a36454 100644
--- a/internal/release/proofpolicy.go
+++ b/internal/release/proofpolicy.go
@@ -7,10 +7,12 @@
// 0644 and holds no secrets: app names, the prover name, public keys and a
// duration. The agent and root both read it at the start of every pass.
//
-// Absent: D3 is off. Anything else that does not parse, including an unknown
-// key, is an error, and the gate reads an error as "on and waiting for every
-// named-unit route": the box cannot tell which apps were listed, and a typo
-// must not open the gate.
+// Absent: D3 is off. Absent means nothing at all is at the path (Lstat says
+// it does not exist). Anything else that cannot be read or does not parse,
+// including a dangling symlink, a directory, an unknown key or data after the
+// policy object, is an error, and the gate reads an error as "on and waiting
+// for every named-unit route": the box cannot tell which apps were listed,
+// and a typo or a broken link must not open the gate.
package release
import (
@@ -57,10 +59,13 @@ type RequireEntry struct {
func (e RequireEntry) PublicKey() ed25519.PublicKey { return e.pub }
func LoadProofPolicy(path string) (*ProofPolicy, error) {
- b, err := os.ReadFile(path)
- if errors.Is(err, os.ErrNotExist) {
+ // Lstat, not the read's error, decides absence: a symlink whose target
+ // has gone reads as ENOENT, and reading that as "D3 off" would let an
+ // unproven build in because of a broken config.
+ if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
return nil, nil
}
+ b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("proof policy %s: %w", path, err)
}
diff --git a/internal/release/proofpolicy_test.go b/internal/release/proofpolicy_test.go
index 176860466..187001447 100644
--- a/internal/release/proofpolicy_test.go
+++ b/internal/release/proofpolicy_test.go
@@ -82,3 +82,23 @@ func TestProofPolicyTrailingWhitespaceParses(t *testing.T) {
t.Fatal(err)
}
}
+
+// Absent means nothing is at the path. A dangling symlink or a directory
+// there is a broken config, and a broken config is closed, not off.
+func TestProofPolicyBrokenPathIsAnError(t *testing.T) {
+ dir := t.TempDir()
+ dangling := filepath.Join(dir, "dangling.json")
+ if err := os.Symlink(filepath.Join(dir, "gone.json"), dangling); err != nil {
+ t.Fatal(err)
+ }
+ if p, err := LoadProofPolicy(dangling); err == nil {
+ t.Fatalf("dangling symlink = %+v, nil; want an error (closed), not off", p)
+ }
+ asDir := filepath.Join(dir, "dir.json")
+ if err := os.Mkdir(asDir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if p, err := LoadProofPolicy(asDir); err == nil {
+ t.Fatalf("directory = %+v, nil; want an error (closed), not off", p)
+ }
+}