Skip to content

Commit f2de0fa

Browse files
dipreeclaude
andcommitted
fix(attach): harden explicit-target mode for general CLI use
Review findings on the explicit-target attach, fixed for the CLI as a public command rather than platform plumbing: - Validate --commit-sha names a real commit in the repository (it was only shape-checked); silently recording an unknown SHA would produce provenance pointing at nothing. - Refresh remote state for the explicit checkpoint ID before treating it as new (ensureExplicitCheckpointFreshness): a retried attach from a fresh clone would otherwise rebuild the ID as an orphan that clobbers the original on push. Same-session hits stay idempotent no-ops; an ID that exists with other content is refused, never modified. Fetch failures degrade to 'not present' with a warning, preserving offline operation. - Keep explicit-target attaches out of trailer-linked session state: the SHA-bound review checkpoint is not the session's code checkpoint, so BaseCommit/LastCheckpointID are no longer overwritten (amend hooks and resume must never treat the review checkpoint as trailer-linked). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KY4W9TBQX9Y7JSSSHMW9J5ZC
1 parent a135352 commit f2de0fa

2 files changed

Lines changed: 187 additions & 17 deletions

File tree

cmd/entire/cli/attach.go

Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232

3333
"charm.land/huh/v2"
3434
"github.com/go-git/go-git/v6"
35+
"github.com/go-git/go-git/v6/plumbing"
3536
"github.com/go-git/go-git/v6/plumbing/object"
3637
"github.com/spf13/cobra"
3738
)
@@ -281,6 +282,19 @@ func runAttach(ctx context.Context, w, errW io.Writer, sessionID string, agentNa
281282
return err
282283
}
283284

285+
// The recorded commit binding must name a real commit: --commit-sha is
286+
// only shape-validated at the flag layer, and silently persisting a SHA
287+
// this repository has never seen would produce provenance pointing at
288+
// nothing.
289+
if opts.explicitTarget() {
290+
if _, chkErr := repo.CommitObject(plumbing.NewHash(opts.CommitSHA)); chkErr != nil {
291+
return fmt.Errorf(
292+
"--commit-sha %s does not name a commit in this repository: %w",
293+
opts.CommitSHA, chkErr,
294+
)
295+
}
296+
}
297+
284298
// If session already has a checkpoint, just offer to link it.
285299
// Explicit-target mode is exempt: it authors a separate new checkpoint
286300
// under the caller-supplied ID, so the session's own checkpoint (e.g.
@@ -391,17 +405,44 @@ func runAttach(ctx context.Context, w, errW io.Writer, sessionID string, agentNa
391405
}
392406
}
393407

394-
// Explicit-target idempotency: a retried attach with the same server-minted
395-
// checkpoint ID and session is a no-op success, not an error — the caller
396-
// (platform provenance publication) retries the whole script.
408+
// Explicit-target IDs are usually new — but "usually" is not "always": a
409+
// retried attach from a fresh clone would not have the earlier attempt's
410+
// data locally, and treating the ID as brand-new would rebuild it as an
411+
// orphan that clobbers the original on push. Refresh remote state for the
412+
// ID first, then either no-op (same session already recorded — the caller
413+
// retries whole scripts), refuse (ID exists with other content — never
414+
// modify an existing checkpoint), or proceed (genuinely new).
397415
if opts.explicitTarget() {
398-
exists, readErr := checkpointHasSessionMetadata(ctx, repo, refs, checkpointID, sessionID)
399-
if readErr != nil {
400-
return fmt.Errorf("failed to check checkpoint %s for session %s: %w", checkpointID.String(), sessionID, readErr)
416+
freshRepo, present, freshErr := ensureExplicitCheckpointFreshness(ctx, logCtx, repo, refs, checkpointID)
417+
if freshRepo != nil && freshRepo != repo {
418+
oldRepo := repo
419+
repo = freshRepo
420+
if closeErr := oldRepo.Close(); closeErr != nil {
421+
logging.Warn(logCtx, "failed to close stale repository handle after explicit checkpoint refresh",
422+
slog.String("error", closeErr.Error()))
423+
}
424+
// The store handle wraps the stale repo; reopen against the fresh one.
425+
store, err = openAttachStore(ctx, repo, refs)
426+
if err != nil {
427+
return err
428+
}
401429
}
402-
if exists {
403-
fmt.Fprintf(w, "Session %s already attached to checkpoint %s\n", sessionID, checkpointID.String())
404-
return nil
430+
if freshErr != nil {
431+
return freshErr
432+
}
433+
if present {
434+
exists, readErr := checkpointHasSessionMetadata(ctx, repo, refs, checkpointID, sessionID)
435+
if readErr != nil {
436+
return fmt.Errorf("failed to check checkpoint %s for session %s: %w", checkpointID.String(), sessionID, readErr)
437+
}
438+
if exists {
439+
fmt.Fprintf(w, "Session %s already attached to checkpoint %s\n", sessionID, checkpointID.String())
440+
return nil
441+
}
442+
return fmt.Errorf(
443+
"checkpoint %s already exists without session %s; refusing to modify an existing checkpoint",
444+
checkpointID.String(), sessionID,
445+
)
405446
}
406447
}
407448

@@ -626,6 +667,43 @@ func ensureCheckpointAvailable(ctx, logCtx context.Context, repo *git.Repository
626667
return repo, missingCheckpointError(logCtx, checkpointID, primaryIsRefs)
627668
}
628669

670+
// ensureExplicitCheckpointFreshness surfaces any remote copy of an
671+
// explicit-target checkpoint ID locally and reports whether the checkpoint
672+
// exists. Unlike ensureCheckpointAvailable, a checkpoint that is still missing
673+
// after the refresh is NOT an error — an explicit-target ID is normally brand
674+
// new; the refresh exists so a retry from a fresh clone sees the earlier
675+
// attempt instead of rebuilding the ID as an orphan that would clobber it on
676+
// push. Fetch failures degrade to "not present" with a warning, matching
677+
// ensureCheckpointAvailable's tolerance for offline operation.
678+
func ensureExplicitCheckpointFreshness(ctx, logCtx context.Context, repo *git.Repository, refs cpkg.PersistentRefs, checkpointID id.CheckpointID) (*git.Repository, bool, error) {
679+
cfg, err := settings.LoadCheckpointsConfig(ctx)
680+
if err != nil {
681+
return repo, false, fmt.Errorf("resolve checkpoints config: %w", err)
682+
}
683+
primaryIsRefs := cpkg.PrimaryIsRefs(cfg)
684+
685+
present, readErr := checkpointPresentLocally(ctx, repo, refs, checkpointID, primaryIsRefs)
686+
if readErr != nil {
687+
return repo, false, fmt.Errorf("failed to read checkpoint %s: %w", checkpointID, readErr)
688+
}
689+
if present {
690+
return repo, true, nil
691+
}
692+
693+
freshRepo, fetchErr := refreshCheckpoint(ctx, checkpointID, primaryIsRefs)
694+
if fetchErr != nil {
695+
logging.Warn(logCtx, "failed to refresh explicit checkpoint before attach; treating as new",
696+
slog.String("checkpoint_id", checkpointID.String()),
697+
slog.String("error", fetchErr.Error()))
698+
return repo, false, nil
699+
}
700+
present, readErr = checkpointPresentLocally(ctx, freshRepo, refs, checkpointID, primaryIsRefs)
701+
if readErr != nil {
702+
return freshRepo, false, fmt.Errorf("failed to read checkpoint %s after refresh: %w", checkpointID, readErr)
703+
}
704+
return freshRepo, present, nil
705+
}
706+
629707
// refreshCheckpoint fetches the checkpoint referenced by HEAD from the remote and
630708
// returns a freshly-opened repo so go-git sees the newly-fetched refs/packfiles.
631709
// The fetch is backend-aware: git-refs fetches just this checkpoint's ref, while
@@ -753,21 +831,26 @@ func saveAttachSessionState(ctx context.Context, repo *git.Repository, existingS
753831
}
754832
}
755833

756-
// Populate BaseCommit from HEAD if not already set, so the session becomes
757-
// active and future commits in the same session receive Entire-Checkpoint trailers.
758-
if state.BaseCommit == "" {
759-
if head, headErr := repo.Head(); headErr == nil {
760-
headHash := head.Hash().String()
761-
state.BaseCommit = headHash
762-
state.AttributionBaseCommit = headHash
834+
// Trailer-linked attaches establish the session's code-checkpoint linkage:
835+
// BaseCommit lets later commits/amends recognize the session, and
836+
// LastCheckpointID is the trailer the amend hook restores. An explicit-target
837+
// review checkpoint is instead bound through commit_sha metadata and must not
838+
// replace the session's own checkpoint/base or appear in resume/session output.
839+
if !opts.explicitTarget() {
840+
if state.BaseCommit == "" {
841+
if head, headErr := repo.Head(); headErr == nil {
842+
headHash := head.Hash().String()
843+
state.BaseCommit = headHash
844+
state.AttributionBaseCommit = headHash
845+
}
763846
}
847+
state.LastCheckpointID = checkpointID
764848
}
765849

766850
state.CLIVersion = versioninfo.Version
767851
state.AttachedManually = true
768852
state.AgentType = agentType
769853
state.TranscriptPath = transcriptPath
770-
state.LastCheckpointID = checkpointID
771854
// Only transition to Ended if the session is not already active — avoid
772855
// breaking an ongoing session whose BaseCommit has just been restored above.
773856
if !state.Phase.IsActive() {

cmd/entire/cli/attach_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,27 @@ func TestAttach_ReviewExplicitTargetCreatesShaBoundCheckpoint(t *testing.T) {
12591259
t.Errorf("session metadata Kind = %q, want agent_review", metadata.Kind)
12601260
}
12611261

1262+
// The commit-SHA-bound review checkpoint is not the session's code
1263+
// checkpoint. Preserve the session-stop checkpoint and leave its empty base
1264+
// untouched so amend hooks and resume/session output never treat targetID as
1265+
// trailer-linked session state.
1266+
attachedState, err := stateStore.Load(context.Background(), sessionID)
1267+
if err != nil {
1268+
t.Fatalf("load attached session state: %v", err)
1269+
}
1270+
if attachedState == nil {
1271+
t.Fatal("attached session state is missing")
1272+
}
1273+
if attachedState.LastCheckpointID != ownCheckpointID {
1274+
t.Errorf("LastCheckpointID = %s, want original session checkpoint %s", attachedState.LastCheckpointID, ownCheckpointID)
1275+
}
1276+
if attachedState.BaseCommit != "" {
1277+
t.Errorf("BaseCommit = %q, want empty; explicit-target attach must not bind session state to HEAD", attachedState.BaseCommit)
1278+
}
1279+
if attachedState.AttributionBaseCommit != "" {
1280+
t.Errorf("AttributionBaseCommit = %q, want empty; explicit-target attach must not bind attribution state to HEAD", attachedState.AttributionBaseCommit)
1281+
}
1282+
12621283
// HEAD must be untouched: same commit, no trailer appended.
12631284
newHeadRef, err := repo.Head()
12641285
if err != nil {
@@ -1292,6 +1313,72 @@ func TestAttach_ReviewExplicitTargetCreatesShaBoundCheckpoint(t *testing.T) {
12921313
}
12931314
}
12941315

1316+
// A well-formed --commit-sha that names no commit in the repository must be
1317+
// rejected: silently recording it would produce provenance pointing at nothing.
1318+
func TestAttach_ExplicitTargetRejectsUnknownCommit(t *testing.T) {
1319+
setupAttachTestRepo(t)
1320+
1321+
sessionID := "review-session-unknown-commit"
1322+
setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"please review"},"uuid":"u1"}
1323+
`)
1324+
1325+
var out bytes.Buffer
1326+
err := runAttach(context.Background(), &out, &out, sessionID, agent.AgentNameClaudeCode, attachOptions{
1327+
Force: true,
1328+
Review: true,
1329+
CheckpointID: id.CheckpointID("aabbccddeeff"),
1330+
CommitSHA: strings.Repeat("deadbeef", 5),
1331+
})
1332+
if err == nil || !strings.Contains(err.Error(), "does not name a commit") {
1333+
t.Fatalf("expected unknown-commit rejection, got: %v", err)
1334+
}
1335+
}
1336+
1337+
// An explicit-target ID that already exists with OTHER content must be
1338+
// refused, never modified: the ID is the caller's identity contract, and
1339+
// appending to or rebuilding an existing checkpoint would corrupt it.
1340+
func TestAttach_ExplicitTargetRefusesExistingCheckpointWithoutSession(t *testing.T) {
1341+
setupAttachTestRepo(t)
1342+
1343+
repoRoot := mustGetwd(t)
1344+
repo, err := git.PlainOpen(repoRoot)
1345+
if err != nil {
1346+
t.Fatal(err)
1347+
}
1348+
headSHA, err := repo.Head()
1349+
if err != nil {
1350+
t.Fatal(err)
1351+
}
1352+
1353+
targetID := id.CheckpointID("aabbccddeeff")
1354+
firstSession := "review-session-original"
1355+
setupClaudeTranscript(t, firstSession, `{"type":"user","message":{"role":"user","content":"first"},"uuid":"u1"}
1356+
`)
1357+
var out bytes.Buffer
1358+
if err := runAttach(context.Background(), &out, &out, firstSession, agent.AgentNameClaudeCode, attachOptions{
1359+
Force: true,
1360+
Review: true,
1361+
CheckpointID: targetID,
1362+
CommitSHA: headSHA.Hash().String(),
1363+
}); err != nil {
1364+
t.Fatalf("first explicit-target attach failed: %v", err)
1365+
}
1366+
1367+
secondSession := "review-session-collision"
1368+
setupClaudeTranscript(t, secondSession, `{"type":"user","message":{"role":"user","content":"second"},"uuid":"u1"}
1369+
`)
1370+
out.Reset()
1371+
err = runAttach(context.Background(), &out, &out, secondSession, agent.AgentNameClaudeCode, attachOptions{
1372+
Force: true,
1373+
Review: true,
1374+
CheckpointID: targetID,
1375+
CommitSHA: headSHA.Hash().String(),
1376+
})
1377+
if err == nil || !strings.Contains(err.Error(), "refusing to modify an existing checkpoint") {
1378+
t.Fatalf("expected collision refusal, got: %v", err)
1379+
}
1380+
}
1381+
12951382
// The explicit-target flags are a unit: both must be supplied, only with
12961383
// --review, and the commit SHA must be a full 40-hex value.
12971384
func TestAttach_ExplicitTargetFlagValidation(t *testing.T) {

0 commit comments

Comments
 (0)