Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7ace7a4
Kill the connector at every ledger state, and recover
jorgemanrubia Sep 17, 2026
7665f7a
Run the recovery harness against the real agents, opted into
jorgemanrubia Sep 17, 2026
46f41e4
Scale the prompt budget check by a measured tokenizer ratio
jorgemanrubia Sep 17, 2026
3719fff
Follow the rebase: bind the dispatch with its context, reconcile a re…
jorgemanrubia Sep 17, 2026
25bc026
Assert on the worker the ledger recorded, and join the follow-up befo…
jorgemanrubia Sep 17, 2026
c3a543e
Check the feed's checkpoint after the walk closed the loss
jorgemanrubia Sep 17, 2026
668e1cf
Put the import's tombstone entry first, so a crash at the first entry…
jorgemanrubia Sep 17, 2026
0cd2603
Watch every checkpoint the walk's run holds, not only its last
jorgemanrubia Sep 17, 2026
eef8b35
Satisfy the linter: the ledger's own context, and no needless conversion
jorgemanrubia Sep 17, 2026
0a986f1
Close the adversarial review: a real state directory, an ordered over…
jorgemanrubia Sep 17, 2026
5d60ed9
Keep the real-agent run off every Basecamp but a closed port, and exp…
jorgemanrubia Sep 17, 2026
359d86b
Floor the checkpoint watcher on what stands long enough to be seen
jorgemanrubia Sep 17, 2026
5f4e697
Order the overflow check by event, not by page: one page may serve both
jorgemanrubia Sep 17, 2026
e817578
Hold the harness to the one-owner rule: a surviving tree keeps its at…
jorgemanrubia Sep 17, 2026
625f151
Group the harness's imports
jorgemanrubia Sep 17, 2026
512b0ae
Close the second adversarial review: no line kill races the dispatche…
jorgemanrubia Sep 17, 2026
ac80714
Answer Copilot: a portable zombie check, a comparator that cannot ove…
jorgemanrubia Sep 17, 2026
547e3dc
Close the third adversarial review; rebase onto the outbox's start an…
jorgemanrubia Sep 17, 2026
67c6c0c
Take the task token from the connector's socket, as the bridge does, …
jorgemanrubia Sep 17, 2026
99293fc
Dial the token socket on the turn's context
jorgemanrubia Sep 17, 2026
f5e6ba1
Follow the prompt's URL cap: the worst case is the longest URL it rep…
jorgemanrubia Sep 17, 2026
ac289c6
Give the opt-in real run a stored credential, since the bridge hands …
jorgemanrubia Sep 17, 2026
7c48a0e
Group the real run's imports
jorgemanrubia Sep 17, 2026
25535bf
Let a loaded box take its time: a run fails on what the ledger says, …
jorgemanrubia Sep 17, 2026
f56ab34
Close the fourth adversarial review: watch the session directory, hol…
jorgemanrubia Sep 17, 2026
2f77c5e
Make the credential check fail closed: what it could not read, and wh…
jorgemanrubia Sep 17, 2026
bf4129f
Require the connector to say what it decided, where the ledger cannot…
jorgemanrubia Sep 17, 2026
03856d5
Check every task token the connector minted, whoever its worker was
jorgemanrubia Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions internal/connector/recovery_claude_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//go:build unix

package connector

import (
"bufio"
"context"
"encoding/json"
"os"
"slices"
"time"

"github.com/basecamp/basecamp-cli/internal/connector/driver"
"github.com/basecamp/basecamp-cli/internal/connector/driver/claude"
)

// The Claude Code spawn driver's row: `claude -p` speaking stream-json.
func init() {
registerHarnessDriver(harnessDriver{
Name: claude.Name,
New: func(agent string) driver.Driver {
return claude.New(claude.Options{Binary: agent, CloseGrace: 5 * time.Second, Lookup: func(string) (string, bool) { return "", false }})
},
Agent: fakeClaude,
})
if os.Getenv(harnessRealEnv) != "" {
// The real Claude Code on PATH, for TestRecoveryAgainstRealAgents.
registerHarnessDriver(harnessDriver{
Name: claude.Name + "-real",
Real: true,
New: func(string) driver.Driver { return claude.New(claude.Options{}) },
})
}
}

// fakeClaude is `claude -p --input-format stream-json --output-format
// stream-json`: one process per session, a user message per prompt, the init
// message before the first result, and a result per turn.
func fakeClaude(w *fakeWorker) int {
args := os.Args[1:]
flag := func(name string) string {
i := slices.Index(args, name)
if i < 0 || i+1 >= len(args) {
return ""
}
return args[i+1]
}
// The server declaration is read before the init message: the driver
// removes the file once the agent reports its servers started.
var config struct {
MCPServers map[string]struct {
Command string `json:"command"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
} `json:"mcpServers"`
}
data, err := os.ReadFile(flag("--mcp-config"))
if err != nil {
return 10
}
if err := json.Unmarshal(data, &config); err != nil {
return 11
}
names := make([]string, 0, len(config.MCPServers))
for name, s := range config.MCPServers {
names = append(names, name)
if name == MCPServerName {
if err := w.Bind(context.Background(), driver.MCPServer{Name: name, Command: s.Command, Args: s.Args, Env: s.Env}); err != nil {
return 12
}
}
}

sessionID := flag("--session-id")
if sessionID == "" {
sessionID = flag("--resume")
}
mode := flag("--permission-mode")
badMode := w.BadMode()
if badMode {
mode = "bypassPermissions"
}

out := bufio.NewWriter(os.Stdout)
emit := func(v any) {
data, _ := json.Marshal(v)
_, _ = out.Write(append(data, '\n'))
_ = out.Flush()
}
in := bufio.NewScanner(os.Stdin)
in.Buffer(make([]byte, 64<<10), 16<<20)
inited := false
for in.Scan() {
var msg struct {
Type string `json:"type"`
Message struct {
Content string `json:"content"`
} `json:"message"`
}
if json.Unmarshal(in.Bytes(), &msg) != nil {
continue
}
switch msg.Type {
case "control_request":
emit(map[string]any{"type": "result", "subtype": "error_during_execution", "is_error": true, "session_id": sessionID})
continue
case "user":
default:
continue
}
if !inited {
inited = true
servers := make([]map[string]string, 0, len(names))
for _, name := range names {
servers = append(servers, map[string]string{"name": name, "status": "connected"})
}
emit(map[string]any{"type": "system", "subtype": "init", "session_id": sessionID, "permissionMode": mode, "mcp_servers": servers})
if badMode {
// It reported the wrong mode and waits to be ended. Whether a
// real agent would already have acted is exactly what the
// connector cannot know; this one acting would only race the
// driver's kill.
w.log(0, 0, "bad-mode")
time.Sleep(2 * time.Minute)
return 9
}
}
if err := w.Turn(context.Background(), msg.Message.Content); err != nil {
emit(map[string]any{"type": "result", "subtype": "error_during_execution", "is_error": true, "session_id": sessionID})
continue
}
emit(map[string]any{"type": "result", "subtype": "success", "stop_reason": "end_turn", "is_error": false, "session_id": sessionID,
"usage": map[string]any{"input_tokens": 1, "output_tokens": 1}})
}
return 0
}
Loading