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
cmd/entire/cli
Acheckpoint_policy_malformed_test.go+27
Mcheckpoint_policy_write.go+16/-10
Mhook_registry.go+22/-1
Mhook_registry_test.go+61
Mhooks_git_cmd.go+11/-1
Mhooks_git_cmd_test.go+43
Mimport_cmd_test.go+29
strategy
Mcheckpoint_policy.go+18/-7
Mcheckpoint_policy_test.go+47
Mmanual_commit_condensation.go+8/-7
Mmanual_commit_hooks.go+10/-6
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