Harden worker federation and operator UI
This commit is contained in:
+30
-18
@@ -142,7 +142,7 @@ func ClaudeActivity(path string) ([]ToolCall, error) {
|
||||
// 2. Shell commands: Codex's actual tool surface is a single freeform
|
||||
// `custom_tool_call` named "exec" whose `input` is a JS snippet calling
|
||||
// `tools.exec_command({cmd:"...", ...})` — not a flat arguments object.
|
||||
// codexExecCommand best-effort-extracts the first embedded cmd string.
|
||||
// codexExecCommands extracts every embedded cmd string in source order.
|
||||
// Success is read from the paired `custom_tool_call_output`'s text
|
||||
// blocks: a failed script's output observably starts with "Script
|
||||
// error:" on this machine's real transcripts (both a JS syntax error and
|
||||
@@ -173,7 +173,7 @@ func CodexActivity(path string) ([]ToolCall, error) {
|
||||
Payload payload `json:"payload"`
|
||||
}
|
||||
|
||||
pending := map[string]ToolCall{}
|
||||
pending := map[string][]ToolCall{}
|
||||
var calls []ToolCall
|
||||
s := bufio.NewScanner(f)
|
||||
s.Buffer(make([]byte, 1<<20), 10<<20)
|
||||
@@ -188,13 +188,20 @@ func CodexActivity(path string) ([]ToolCall, error) {
|
||||
calls = append(calls, ToolCall{Name: "apply_patch", Kind: "file", Key: path, Success: e.Payload.Success})
|
||||
}
|
||||
case e.Type == "response_item" && e.Payload.Type == "custom_tool_call":
|
||||
kind, key := "", ""
|
||||
if cmd := codexExecCommand(e.Payload.Input); cmd != "" {
|
||||
kind, key = "command", cmd
|
||||
var pendingCalls []ToolCall
|
||||
if e.Payload.Name == "exec" {
|
||||
for _, cmd := range codexExecCommands(e.Payload.Input) {
|
||||
pendingCalls = append(pendingCalls, ToolCall{Name: e.Payload.Name, Kind: "command", Key: cmd, IsTest: isTestCommand("command", cmd)})
|
||||
}
|
||||
}
|
||||
pending[e.Payload.CallID] = ToolCall{Name: e.Payload.Name, Kind: kind, Key: key, IsTest: isTestCommand(kind, key)}
|
||||
// Retain a resolved call without an extractable command as activity:
|
||||
// it is useful for ordering, but deliberately carries no key.
|
||||
if len(pendingCalls) == 0 {
|
||||
pendingCalls = []ToolCall{{Name: e.Payload.Name}}
|
||||
}
|
||||
pending[e.Payload.CallID] = pendingCalls
|
||||
case e.Type == "response_item" && e.Payload.Type == "custom_tool_call_output":
|
||||
if tc, ok := pending[e.Payload.CallID]; ok {
|
||||
if pendingCalls, ok := pending[e.Payload.CallID]; ok {
|
||||
failed := false
|
||||
for _, o := range e.Payload.Output {
|
||||
if strings.HasPrefix(strings.TrimSpace(o.Text), "Script error:") {
|
||||
@@ -202,8 +209,10 @@ func CodexActivity(path string) ([]ToolCall, error) {
|
||||
break
|
||||
}
|
||||
}
|
||||
tc.Success = !failed
|
||||
calls = append(calls, tc)
|
||||
for _, tc := range pendingCalls {
|
||||
tc.Success = !failed
|
||||
calls = append(calls, tc)
|
||||
}
|
||||
delete(pending, e.Payload.CallID)
|
||||
}
|
||||
}
|
||||
@@ -211,18 +220,21 @@ func CodexActivity(path string) ([]ToolCall, error) {
|
||||
return calls, s.Err()
|
||||
}
|
||||
|
||||
// codexExecCmdRe extracts the first `cmd:"..."` argument out of an "exec"
|
||||
// custom-tool-call's JS-scripted input. Only the first embedded command in a
|
||||
// multi-call script is captured — a documented limitation, not an oversight.
|
||||
// codexExecCmdRe extracts `cmd:"..."` arguments out of an "exec"
|
||||
// custom-tool-call's JS-scripted input. A single script can invoke several
|
||||
// commands; their source order is the observable execution order.
|
||||
var codexExecCmdRe = regexp.MustCompile(`cmd\s*:\s*"((?:[^"\\]|\\.)*)"`)
|
||||
|
||||
func codexExecCommand(input string) string {
|
||||
m := codexExecCmdRe.FindStringSubmatch(input)
|
||||
if m == nil {
|
||||
return ""
|
||||
func codexExecCommands(input string) []string {
|
||||
matches := codexExecCmdRe.FindAllStringSubmatch(input, -1)
|
||||
commands := make([]string, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
cmd := strings.TrimSpace(strings.NewReplacer(`\"`, `"`, `\n`, "\n", `\t`, "\t", `\\`, `\`).Replace(m[1]))
|
||||
if cmd != "" {
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
}
|
||||
cmd := strings.NewReplacer(`\"`, `"`, `\n`, "\n", `\t`, "\t", `\\`, `\`).Replace(m[1])
|
||||
return strings.TrimSpace(cmd)
|
||||
return commands
|
||||
}
|
||||
|
||||
// OpenCodeActivity has no verified source. OpenCodeUsage already only reads
|
||||
|
||||
@@ -178,6 +178,26 @@ func TestCodexActivityMarksScriptErrorAsFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexActivityPreservesAllCommandsInAScript(t *testing.T) {
|
||||
path := writeJSONL(t, []string{
|
||||
codexExecCallLine(t, "c1", `const test = await tools.exec_command({cmd:"go test ./..."}); const commit = await tools.exec_command({cmd:"git commit -am done"}); text(test.output); text(commit.output)`),
|
||||
codexExecOutputLine(t, "c1", "Script completed"),
|
||||
})
|
||||
calls, err := CodexActivity(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(calls) != 2 {
|
||||
t.Fatalf("calls=%+v, want both commands", calls)
|
||||
}
|
||||
if calls[0].Key != "go test ./..." || calls[1].Key != "git commit -am done" || !calls[0].Success || !calls[1].Success {
|
||||
t.Fatalf("calls=%+v, want successful commands in source order", calls)
|
||||
}
|
||||
if !DetectMilestone(calls) {
|
||||
t.Fatalf("want the later successful git commit to be a milestone")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectThrashConsecutiveTestFailures(t *testing.T) {
|
||||
calls := []ToolCall{
|
||||
{Name: "Bash", Kind: "command", Key: "go test ./...", Success: false, IsTest: true},
|
||||
|
||||
@@ -42,10 +42,6 @@ type PromptLeaser interface {
|
||||
LeasePrompt(context.Context, string, string, string) (Session, error)
|
||||
}
|
||||
|
||||
type WorktreeCreator interface {
|
||||
CreateWorktree(context.Context, string, string, string) (string, error)
|
||||
}
|
||||
|
||||
// TurnBoundary is optional so older herdr deployments remain usable. A true
|
||||
// result means the current harness turn has ended and handoff is safe.
|
||||
type TurnBoundary interface {
|
||||
@@ -99,17 +95,6 @@ const HandoffFile = ".orchestra-handoff.json"
|
||||
// seals the resulting canonical JSON.
|
||||
const HandoffReportFile = ".orchestra-handoff-report.md"
|
||||
|
||||
func (a CLIAdapter) CreateWorktree(ctx context.Context, repo, root, taskID string) (string, error) {
|
||||
path, err := a.Client.Worktree(ctx, repo, filepath.Join(root, taskID), "orchestra/"+taskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("adapter: herdr returned empty worktree path")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (a CLIAdapter) Lease(ctx context.Context, task, worktree string) (Session, error) {
|
||||
return a.LeasePrompt(ctx, task, worktree, defaultTaskPrompt(task))
|
||||
}
|
||||
|
||||
@@ -193,14 +193,16 @@ const (
|
||||
// after 2+ minutes of polling, with no error surfaced anywhere. A
|
||||
// legitimate attach has been observed taking "well over a minute", so
|
||||
// this window is deliberately longer than bootRetryWindow.
|
||||
agentAttachWindow = 90 * time.Second
|
||||
agentAttachPoll = 2 * time.Second
|
||||
|
||||
claudeTrustObserveWindow = 15 * time.Second
|
||||
claudeTrustClearWindow = 15 * time.Second
|
||||
claudeTrustPoll = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
agentAttachWindow = 90 * time.Second
|
||||
agentAttachPoll = 2 * time.Second
|
||||
)
|
||||
|
||||
type paneStatus struct {
|
||||
Agent string `json:"agent"`
|
||||
AgentStatus string `json:"agent_status"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -82,6 +83,97 @@ func TestStartAgentPassesEmptyHarnessArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartAgentAttachesTwoSameHarnessSessionsWithDistinctNames(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
starts := make(chan Request, 2)
|
||||
go func() {
|
||||
for i := 0; i < 4; i++ {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var req Request
|
||||
if json.NewDecoder(bufio.NewReader(conn)).Decode(&req) == nil {
|
||||
switch req.Method {
|
||||
case "agent.start":
|
||||
starts <- req
|
||||
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{}`)})
|
||||
case "pane.get":
|
||||
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: json.RawMessage(`{"type":"pane_info","pane":{"agent":"opencode","agent_status":"idle"}}`)})
|
||||
}
|
||||
}
|
||||
_ = conn.Close()
|
||||
}
|
||||
}()
|
||||
c := &Client{Path: ln.Addr().String(), panes: map[string]string{"/one": "w1:p1", "/two": "w2:p1"}, dial: func() (net.Conn, error) {
|
||||
return net.Dial("tcp", ln.Addr().String())
|
||||
}}
|
||||
first, err := c.StartAgent(context.Background(), "", "/one", "", "opencode", "first-task")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := c.StartAgent(context.Background(), "", "/two", "", "opencode", "second-task")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.AgentName == second.AgentName || first.AgentName == "" || second.AgentName == "" {
|
||||
t.Fatalf("agent names must be distinct and persisted: %+v / %+v", first, second)
|
||||
}
|
||||
for _, want := range []string{first.AgentName, second.AgentName} {
|
||||
req := <-starts
|
||||
params, _ := json.Marshal(req.Params)
|
||||
var got struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = json.Unmarshal(params, &got)
|
||||
if got.Name != want {
|
||||
t.Fatalf("agent.start name = %q, want %q", got.Name, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartAgentRejectsSuccessWithoutAttachment(t *testing.T) {
|
||||
oldWindow, oldPoll := agentAttachWindow, agentAttachPoll
|
||||
agentAttachWindow, agentAttachPoll = 25*time.Millisecond, time.Millisecond
|
||||
t.Cleanup(func() { agentAttachWindow, agentAttachPoll = oldWindow, oldPoll })
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
var req Request
|
||||
if json.NewDecoder(bufio.NewReader(conn)).Decode(&req) != nil {
|
||||
return
|
||||
}
|
||||
result := json.RawMessage(`{"type":"pane_info","pane":{"agent_status":"unknown"}}`)
|
||||
if req.Method == "agent.start" {
|
||||
result = json.RawMessage(`{}`)
|
||||
}
|
||||
_ = json.NewEncoder(conn).Encode(Response{ID: req.ID, Result: result})
|
||||
}()
|
||||
}
|
||||
}()
|
||||
c := &Client{Path: ln.Addr().String(), panes: map[string]string{"/worktree": "w1:p1"}, dial: func() (net.Conn, error) {
|
||||
return net.Dial("tcp", ln.Addr().String())
|
||||
}}
|
||||
_, err = c.StartAgent(context.Background(), "", "/worktree", "", "opencode", "silent-noop")
|
||||
if err == nil || !strings.Contains(err.Error(), "no agent attached") {
|
||||
t.Fatalf("StartAgent error = %v, want explicit missing attachment", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentNameIsBoundedAndValid(t *testing.T) {
|
||||
got := agentName("OpenCode", "TASK With spaces / and symbols !!! 0123456789")
|
||||
if len(got) > 32 || !regexp.MustCompile(`^[a-z0-9_-]+$`).MatchString(got) {
|
||||
|
||||
Reference in New Issue
Block a user