fail closed on unreadable checkpoint policy · Entire

fail closed on unreadable checkpoint policy

9762bcb→main·

pfleidi·2w ago·11 files·+292 added/-32 removed

Treat malformed local checkpoint policy refs as read errors for checkpoint-data writers and strategy checkpoint work.

Agent hooks surface read-specific disabled-checkpoint messages, while Git hooks skip checkpoint work without blocking Git.

Sessions

8dfabaa3d786View transcript

?\ Enforce Checkpoint Policies in CLICodex·GPT-5.5·3 steps

Changes

11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

package cli

import (
    "context"
    "testing"

"github.com/entireio/cli/cmd/entire/cli/checkpoint"
    "github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
    "github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/go-git/go-git/v6/plumbing/filemode"
    "github.com/go-git/go-git/v6/plumbing/object"
    "github.com/stretchr/testify/require"
)

func writeMalformedCheckpointPolicyForCLITest(t *testing.T, repo *git.Repository) {
    t.Helper()
    blobHash, err := checkpoint.CreateBlobFromContent(repo, []byte(`{"checkpoint_version":`))
    require.NoError(t, err)

treeHash, err := checkpoint.BuildTreeFromEntries(context.Background(), repo, map[string]object.TreeEntry{
    checkpointpolicy.PolicyFileName: {Name: checkpointpolicy.PolicyFileName, Mode: filemode.Regular, Hash: blobHash},
})
    require.NoError(t, err)
    commitHash, err := checkpoint.CreateCommit(context.Background(), repo, treeHash, plumbing.ZeroHash, "malformed checkpoint policy", "Test", "test@example.com")
    require.NoError(t, err)
    require.NoError(t, checkpointpolicy.SetRef(repo, checkpointpolicy.RefName, commitHash))
}

Acmd/entire/cli/checkpoint_policy_malformed_test.go+27

3 unmodified lines

4
5
6
7
7
8
9
11
10
11
12
13
14
15
16
17
18
20
19
20
21
22
23
24
25

1 unmodified line

27
28
29
28
30
31
32
33
34
35
36
37
38
39
35
40
41
42
38
39
40
41
43
44
43
45
46
47
48
3 unmodified lines

52
53
54
55
56
57
58

3 unmodified lines

"context"
    "errors"
    "fmt"
    "log/slog"
    "strings"

"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
    "github.com/entireio/cli/cmd/entire/cli/logging"
    "github.com/entireio/cli/cmd/entire/cli/versioncheck"
    "github.com/entireio/cli/cmd/entire/cli/versioninfo"
    "github.com/go-git/go-git/v6"
)

var errUnsupportedCheckpointPolicy = errors.New("checkpoint policy cannot be satisfied by this Entire CLI")
var errUnreadableCheckpointPolicy = errors.New("checkpoint policy could not be read")

func checkpointVersionForNewCheckpoint(ctx context.Context, repo *git.Repository) (string, error) {
    policy := localCheckpointPolicyForNewCheckpoint(ctx, repo)
    policy, err := checkpointPolicyForCheckpointData(ctx, repo)
    if err != nil {
        return "", err
    }
    if !checkpointpolicy.CanSatisfyPolicy(policy) {
        return "", unsupportedCheckpointPolicyError(policy)
    }

}

func ensureCheckpointPolicyAllowsCheckpointData(ctx context.Context, repo *git.Repository) error {
    policy := localCheckpointPolicyForNewCheckpoint(ctx, repo)
    policy, err := checkpointPolicyForCheckpointData(ctx, repo)
    if err != nil {
        return err
    }
    if checkpointpolicy.CanSatisfyPolicy(policy) {
        return nil
    }
    return unsupportedCheckpointPolicyError(policy)
}

func localCheckpointPolicyForNewCheckpoint(ctx context.Context, repo *git.Repository) checkpointpolicy.Policy {
func checkpointPolicyForCheckpointData(ctx context.Context, repo *git.Repository) (checkpointpolicy.Policy, error) {
    state, err := checkpointpolicy.ReadLocal(ctx, repo)
    if err != nil {
        logging.Warn(ctx, "checkpoint policy read failed; using default checkpoint policy",
            slog.String("error", err.Error()),
        )
        return checkpointpolicy.DefaultPolicy()
        return checkpointpolicy.Policy{}, unreadableCheckpointPolicyError(err)
    }
    return state.Policy
    return state.Policy, nil
}

func unsupportedCheckpointPolicyError(policy checkpointpolicy.Policy) error {
3 unmodified lines

))
    return fmt.Errorf("%w:\n%s", errUnsupportedCheckpointPolicy, message)
}

func unreadableCheckpointPolicyError(err error) error {
    return fmt.Errorf("%w: %w", errUnreadableCheckpointPolicy, err)
}
}

Mcmd/entire/cli/checkpoint_policy_write.go+16/-10

208 unmodified lines

209
210
211
212
212
213
214
215
216
217
218
219
220
221
222
223
224
13 unmodified lines

238
239
240
241
242
243
244
245
246
247
5 unmodified lines

253
254
255
256
257
258
259
260
261
262
263
264
265
266

208 unmodified lines

}
    defer repo.Close()

policy := localCheckpointPolicyForNewCheckpoint(ctx, repo)
    policy, err := checkpointPolicyForCheckpointData(ctx, repo)
    if err != nil {
        logging.Warn(ctx, "checkpoint policy read failed for agent hook",
            slog.String("error", err.Error()))
        if eventType == agent.SessionStart {
            return true, writeUnsupportedPolicySessionStartWarning(errW, ag, sessionStartPolicyReadErrorWarning(err))
        }
        fmt.Fprint(errW, agentCheckpointCaptureDisabledReadErrorMessage(err))
        return false, NewSilentError(err)
    }
    if checkpointpolicy.CanSatisfyPolicy(policy) {
        return false, nil
    }
13 unmodified lines

return message + "\n\n" + details
}

func sessionStartPolicyReadErrorWarning(err error) string {
    return fmt.Sprintf("Entire CLI is enabled, but this repository's checkpoint policy could not be read. No Entire checkpoints will be created for this session until the policy can be read.\n\n[entire] Details:\n[entire]   %%v", err)
}

func agentCheckpointCaptureDisabledMessage(policy checkpointpolicy.Policy) string {
    var b strings.Builder
    b.WriteString("[entire] Checkpoint capture is disabled for this repository.\n")
5 unmodified lines

return b.String()
}

func agentCheckpointCaptureDisabledReadErrorMessage(err error) string {
    var b strings.Builder
    b.WriteString("[entire] Checkpoint capture is disabled for this repository.\n")
    b.WriteString("[entire] No Entire checkpoints will be created until the checkpoint policy can be read.\n")
    fmt.Fprintf(&b, "[entire] Details:\n[entire]   %%v\n", err)
    return b.String()
}

func writeUnsupportedPolicySessionStartWarning(errW io.Writer, ag agent.Agent, message string) error {
    if writer, ok := agent.AsHookResponseWriter(ag); ok {
        if err := writer.WriteHookResponse(message); err != nil {

}

func TestExecuteAgentHookSessionStartSkipsCaptureWhenPolicyUnreadable(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)
    enableEntire(t, repoRoot)

repo, err := git.PlainOpen(repoRoot)
    require.NoError(t, err)
    t.Cleanup(func() { _ = repo.Close() })
    writeMalformedCheckpointPolicyForCLITest(t, repo)

sessionID := "policy-unreadable-session-start"
    payload, err := json.Marshal(map[string]string{
        "session_id":      sessionID,
        "transcript_path": filepath.Join(repoRoot, "transcript.jsonl"),
    })
    require.NoError(t, err)

cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&bytes.Buffer{})
    cmd.SetContext(context.Background())

require.NoError(t, executeAgentHook(cmd, agent.AgentNameClaudeCode, claudecode.HookNameSessionStart, false))

Mcmd/entire/cli/hook_registry.go+22/-1

199 unmodified lines

200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
25 unmodified lines

260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297

199 unmodified lines

require.True(t, os.IsNotExist(statErr), "session-start must not claim the session when checkpoint policy is unsupported")
}

func TestExecuteAgentHookSessionStartSkipsCaptureWhenPolicyUnreadable(t *testing.T) {
    setupStopTestRepo(t)
    repoRoot := mustGetwd(t)
    enableEntire(t, repoRoot)

repo, err := git.PlainOpen(repoRoot)
    require.NoError(t, err)
    t.Cleanup(func() { _ = repo.Close() })
    writeMalformedCheckpointPolicyForCLITest(t, repo)

transcriptPath := filepath.Join(repoRoot, "transcript.jsonl")
    require.NoError(t, os.WriteFile(transcriptPath, []byte(`{"type":"user","message":{"content":"hi"}}`+"\n"), 0o600))
    payload, err := json.Marshal(map[string]string{
        "session_id":      "policy-unreadable-turn-start",
        "transcript_path": transcriptPath,
        "prompt":          "hello",
    })
    require.NoError(t, err)

var stderr bytes.Buffer
    cmd := &cobra.Command{}
    cmd.SetIn(bytes.NewReader(payload))
    cmd.SetErr(&stderr)
    cmd.SetContext(context.Background())

Mcmd/entire/cli/hook_registry_test.go+61

73 unmodified lines

74
75
76
77
77
78
79
80
81
82
83
84
85
86
87
88
89
90

73 unmodified lines

}
    defer repo.Close()

policy := localCheckpointPolicyForNewCheckpoint(g.ctx, repo)
    state, err := checkpointpolicy.ReadLocal(g.ctx, repo)
    if err != nil {
        logging.Warn(g.ctx, "checkpoint policy read failed; skipping git hook checkpoint work",
            slog.String("error", err.Error()))
        if interactive.CanPromptInteractively() {
            fmt.Fprintf(os.Stderr, "[entire] Could not read checkpoint policy; skipping Entire checkpoint work: %%v\n", err)
        }
        return true
    }

policy := state.Policy
    if checkpointpolicy.CanSatisfyPolicy(policy) {
        return false
    }
}

Mcmd/entire/cli/hooks_git_cmd.go+11/-1

306 unmodified lines

307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352

306 unmodified lines

t.Fatalf("commit message changed under unsupported policy:\ngot:\n%%s\nwant:\n%%s", got, message)
    }
}

func TestHooksGitCommitMsgSkipsWhenPolicyUnreadable(t *testing.T) {
    repoDir := t.TempDir()
    testutil.InitRepo(t, repoDir)
    testutil.WriteFile(t, repoDir, "f.txt", "x")
    testutil.GitAdd(t, repoDir, "f.txt")
    testutil.GitCommit(t, repoDir, "init")
    t.Chdir(repoDir)
    paths.ClearWorktreeRootCache()
    session.ClearGitCommonDirCache()
    gitHooksDisabled = false

enableEntire(t, repoDir)

repo, err := git.PlainOpen(repoDir)
    if err != nil {
        t.Fatal(err)
    }
    t.Cleanup(func() { _ = repo.Close() })
    writeMalformedCheckpointPolicyForCLITest(t, repo)

msgFile := filepath.Join(repoDir, "COMMIT_EDITMSG")
    message := []byte("Entire-Checkpoint: abc123def456\n")
    if err := os.WriteFile(msgFile, message, 0o600); err != nil {
        t.Fatal(err)
    }

cmd := newHooksGitCmd()
    cmd.SetArgs([]string{"commit-msg", msgFile})
    cmd.SetContext(context.Background())

if err := cmd.Execute(); err != nil {
        t.Fatalf("commit-msg should skip checkpoint work when policy is unreadable: %%v", err)
    }

got, err := os.ReadFile(msgFile)
    if err != nil {
        t.Fatal(err)
    }
    if string(got) != string(message) {
        t.Fatalf("commit message changed under unreadable policy:\ngot:\n%%s\nwant:\n%%s", got, message)
    }
}
}

}

Mcmd/entire/cli/hooks_git_cmd_test.go+43

68 unmodified lines

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

68 unmodified lines

testutil.GitAdd(t, repoDir, "f.txt")
    testutil.GitCommit(t, repoDir, "init")
    t.Chdir(repoDir)

repository, err := git.PlainOpen(repoDir)
    require.NoError(t, err)

claudeDir := t.TempDir()
    require.NoError(t, os.WriteFile(filepath.Join(claudeDir, "s.jsonl"),
        []byte(`{"type":"user","uuid":"u1","message":{"role":"user","content":"hi"}}`+"\n"), 0o644))

cmd := newImportCmd()
    var out bytes.Buffer
    cmd.SetOut(&out)
    cmd.SetErr(&out)
    cmd.SetArgs([]string{"claude-code", "--path", claudeDir, "--dry-run"})

err = cmd.Execute()
    require.ErrorContains(t, err, "checkpoint policy could not be read")
    require.ErrorContains(t, err, "parse policy.json")
    require.NotContains(t, out.String(), "Would import")
}

}

Mcmd/entire/cli/import_cmd_test.go+29

13 unmodified lines

14
15
16
17
17
18
19
20
20
21
22
23
23
24
25
25
26
27
28
5 unmodified lines

34
35
36
37
38
39
37
38
39
40
41
42
43
31 unmodified lines

75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

13 unmodified lines

"github.com/go-git/go-git/v6"
)

func readLocalCheckpointPolicy(ctx context.Context, repo *git.Repository) (checkpointpolicy.Policy, bool) {
func readLocalCheckpointPolicy(ctx context.Context, repo *git.Repository) (checkpointpolicy.Policy, error) {
    state, err := checkpointpolicy.ReadLocal(ctx, repo)
    if err != nil {
        logging.Warn(ctx, "checkpoint policy read failed; allowing checkpoint work",
            
            )
        return checkpointpolicy.Policy{}, false
        return checkpointpolicy.Policy{}, err
    }
    return state.Policy, true
    return state.Policy, nil
}

func checkpointPolicyAllowsGitHook(ctx context.Context) bool {
5 unmodified lines

}
    defer repo.Close()

policy, ok := readLocalCheckpointPolicy(ctx, repo)
    if !ok {
        return true
    policy, err := readLocalCheckpointPolicy(ctx, repo)
    if err != nil {
        warnOrLogCheckpointPolicyReadFailure(ctx, err)
        return false
    }
    if checkpointpolicy.CanSatisfyPolicy(policy) {
        return true
    }
31 unmodified lines

}
}

func warnOrLogCheckpointPolicyReadFailure(ctx context.Context, err error) {
    if interactive.CanPromptInteractively() {
        fmt.Fprintf(stderrWriter, "[entire] Could not read checkpoint policy; skipping Entire checkpoint work: %%v\n", err)
        return
    }
    logging.Warn(ctx, "checkpoint policy read failed; skipping checkpoint work",
            
        )
}

func warnOrLogCheckpointPolicySyncFailure(ctx context.Context, err error) {
    if interactive.CanPromptInteractively() {
        fmt.Fprintf(stderrWriter, "[entire] Could not refresh checkpoint policy: %%v\n", err)
}

Mcmd/entire/cli/strategy/checkpoint_policy.go+18/-7

17 unmodified lines

18
19
20
21
22
23
24
25
28 unmodified lines

54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
191 unmodified lines

283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301

17 unmodified lines

"github.com/entireio/cli/redact"
    "github.com/go-git/go-git/v6"
    "github.com/go-git/go-git/v6/plumbing"
    "github.com/go-git/go-git/v6/plumbing/filemode"
    "github.com/go-git/go-git/v6/plumbing/object"
    "github.com/stretchr/testify/require"
)

28 unmodified lines

require.Nil(t, result)
}

func TestCondenseSessionRejectsUnreadablePolicy(t *testing.T) {
    workDir := setupGitRepo(t)
    t.Chdir(workDir)
    paths.ClearWorktreeRootCache()

repo, err := git.PlainOpen(workDir)
    require.NoError(t, err)
    t.Cleanup(func() {
        _ = repo.Close()
    })

strategy := NewManualCommitStrategy()
    sessionID := "policy-unreadable-condense"
    setupSessionWithCheckpoint(t, strategy, repo, workDir, sessionID)
    state, err := strategy.loadSessionState(context.Background(), sessionID)
    require.NoError(t, err)
    require.NotNil(t, state)

writeMalformedCheckpointPolicy(t, repo)

result, err := strategy.CondenseSession(
        context.Background(),
        repo,
        testTrailerCheckpointID,
        state,
        nil,
    )
    require.ErrorContains(t, err, "checkpoint policy could not be read")
    require.ErrorContains(t, err, "parse policy.json")
    require.Nil(t, result)
}

func TestCondenseAndMarkFullyCondensedSkipsUnsupportedPolicy(t *testing.T) {
    workDir := setupGitRepo(t)
    t.Chdir(workDir)

require.NoError(t, err)
}

func writeMalformedCheckpointPolicy(t *testing.T, repo *git.Repository) {
        t.Helper()
    blobHash, err := cpkg.CreateBlobFromContent(repo, []byte(`{"checkpoint_version":`))
    require.NoError(t, err)

treeHash, err := cpkg.BuildTreeFromEntries(context.Background(), repo, map[string]object.TreeEntry{
    checkpointpolicy.PolicyFileName: {Name: checkpointpolicy.PolicyFileName, Mode: filemode.Regular, Hash: blobHash},
})
    require.NoError(t, err)
    commitHash, err := cpkg.CreateCommit(context.Background(), repo, treeHash, plumbing.ZeroHash, "malformed checkpoint policy", "Test", "test@example.com")
    require.NoError(t, err)
    require.NoError(t, checkpointpolicy.SetRef(repo, checkpointpolicy.RefName, commitHash))
}

}

Mcmd/entire/cli/strategy/checkpoint_policy_test.go+47

147 unmodified lines

148
149
150
151
152
153
154
155
156
157
151
152
153
154
155
156
157
158
159
160
161
162

147 unmodified lines

}
    logCtx := logging.WithComponent(ctx, "checkpoint")
    condenseStart := time.Now()
    checkpointVersion := checkpointpolicy.DefaultCheckpointVersion()
    if policy, ok := readLocalCheckpointPolicy(logCtx, repo); ok {
        if !checkpointpolicy.CanSatisfyPolicy(policy) {
            warnIfCheckpointPolicyNeedsUpgrade(logCtx, policy)
            return nil, errors.New("checkpoint policy cannot be satisfied by this Entire CLI")
        }
        checkpointVersion = checkpointpolicy.CheckpointVersion(policy)
    }
    policy, err := readLocalCheckpointPolicy(logCtx, repo)
    if err != nil {
        return nil, fmt.Errorf("checkpoint policy could not be read: %%w", err)
    }
    if !checkpointpolicy.CanSatisfyPolicy(policy) {
        warnIfCheckpointPolicyNeedsUpgrade(logCtx, policy)
        return nil, errors.New("checkpoint policy cannot be satisfied by this Entire CLI")
    }
    checkpointVersion := checkpointpolicy.CheckpointVersion(policy)

shadowBranchName := getShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID)
    ref, hasShadowBranch := resolveShadowRef(repo, shadowBranchName, o.shadowRef)
}
}

Mcmd/entire/cli/strategy/manual_commit_condensation.go+8/-7

2806 unmodified lines

2807
2808
2809
2810
2811
2812
2813
2814
2815
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822

2806 unmodified lines

return 1 // Count as error - all checkpoints will be skipped
}

def defer repo.Close()
if policy, ok := readLocalCheckpointPolicy(logCtx, repo); ok {
    if !checkpointpolicy.CanSatisfyPolicy(policy) {
        warnIfCheckpointPolicyNeedsUpgrade(logCtx, policy)
        state.TurnCheckpointIDs = nil
        return 1
    }
    policy, err := readLocalCheckpointPolicy(logCtx, repo)
    if err != nil {
        warnOrLogCheckpointPolicyReadFailure(logCtx, err)
        state.TurnCheckpointIDs = nil
        return 1
    }
    if !checkpointpolicy.CanSatisfyPolicy(policy) {
        warnIfCheckpointPolicyNeedsUpgrade(logCtx, policy)
        state.TurnCheckpointIDs = nil
        return 1
    }

prompts := readPromptsFromShadowBranch(ctx, repo, state)
}

Mcmd/entire/cli/strategy/manual_commit_hooks.go+10/-6