Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions backend/internal/adapters/agent/activitydispatch/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ func Derive(agent, event string, payload []byte) (domain.ActivityState, bool) {
}
return derive(event, payload)
}

// SupportsHarness reports whether a harness has an activity pipeline at all:
// a registered deriver here means its adapter installs `ao hooks <harness>`
// callbacks that can reach the daemon. Status derivation uses this to decide
// whether prolonged silence is suspicious (no_signal) or simply all a hook-less
// harness can ever report (idle). Harness names and `ao hooks` agent tokens are
// the same strings by convention.
func SupportsHarness(h domain.AgentHarness) bool {
_, ok := Derivers[string(h)]
return ok
}
33 changes: 33 additions & 0 deletions backend/internal/adapters/agent/activitydispatch/dispatch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package activitydispatch

import (
"testing"

"github.com/aoagents/agent-orchestrator/backend/internal/domain"
)

// Every deriver key must be a known harness name: SupportsHarness equates the
// two, so a token that drifts from its harness constant would silently report
// the harness as hook-less.
func TestDeriverTokensAreKnownHarnesses(t *testing.T) {
for token := range Derivers {
if !domain.AgentHarness(token).IsKnown() {
t.Errorf("deriver token %q is not a known AgentHarness", token)
}
}
}

func TestSupportsHarness(t *testing.T) {
for _, h := range []domain.AgentHarness{domain.HarnessCodex, domain.HarnessClaudeCode, domain.HarnessOpenCode} {
if !SupportsHarness(h) {
t.Errorf("SupportsHarness(%q) = false, want true", h)
}
}
// Harnesses whose adapters install no hooks must read as unsupported so
// their silence never derives no_signal.
for _, h := range []domain.AgentHarness{domain.HarnessAmp, domain.HarnessAider, domain.HarnessCrush, domain.AgentHarness("")} {
if SupportsHarness(h) {
t.Errorf("SupportsHarness(%q) = true, want false", h)
}
}
}
52 changes: 44 additions & 8 deletions backend/internal/adapters/agent/codex/codex.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ func (p *Plugin) GetConfigSpec(ctx context.Context) (ports.ConfigSpec, error) {
}

// GetLaunchCommand builds the argv to start a new Codex session, applying the
// no-update-check, hook-trust bypass, and approval flags, optional
// system-prompt instructions, and the initial prompt (passed after `--` so a
// leading "-" is not read as a flag).
// no-update-check, hook-trust bypass, and approval flags, AO's session-flag
// activity hooks, the workspace trust override, optional system-prompt
// instructions, and the initial prompt (passed after `--` so a leading "-" is
// not read as a flag).
func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) (cmd []string, err error) {
binary, err := p.codexBinary(ctx)
if err != nil {
Expand All @@ -68,8 +69,11 @@ func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) (

cmd = []string{binary}
appendNoUpdateCheckFlag(&cmd)
appendHideRateLimitNudgeFlag(&cmd)
appendHookTrustBypassFlag(&cmd)
appendApprovalFlags(&cmd, cfg.Permissions)
appendSessionHookFlags(&cmd)
appendWorkspaceTrustFlag(&cmd, cfg.WorkspacePath)

if cfg.SystemPromptFile != "" {
cmd = append(cmd, "-c", "model_instructions_file="+cfg.SystemPromptFile)
Expand Down Expand Up @@ -111,11 +115,14 @@ func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig)
return nil, false, err
}

cmd = make([]string, 0, 8)
cmd = make([]string, 0, 24)
cmd = append(cmd, binary, "resume")
appendNoUpdateCheckFlag(&cmd)
appendHideRateLimitNudgeFlag(&cmd)
appendHookTrustBypassFlag(&cmd)
appendApprovalFlags(&cmd, cfg.Permissions)
appendSessionHookFlags(&cmd)
appendWorkspaceTrustFlag(&cmd, cfg.Session.WorkspacePath)
cmd = append(cmd, agentSessionID)
return cmd, true, nil
}
Expand Down Expand Up @@ -222,15 +229,44 @@ func (p *Plugin) codexBinary(ctx context.Context) (string, error) {
return binary, nil
}

// DoctorLaunchProbes returns argv tails `ao doctor` runs against the installed
// codex binary to smoke-test the launch surface AO's hook delivery depends on.
// Probe 1 confirms --dangerously-bypass-hook-trust still exists (clap rejects
// unknown flags with a non-zero exit even alongside --version). Probe 2 loads
// codex's config with AO's `-c` session-flag overrides through the offline
// `features list` subcommand, so an override-parse regression surfaces as a
// non-zero exit or warning output. Both are built from the same flag builders
// the launch command uses, so the probes cannot drift from the real spawn argv.
func DoctorLaunchProbes() [][]string {
flagProbe := make([]string, 0, 2)
appendHookTrustBypassFlag(&flagProbe)
flagProbe = append(flagProbe, "--version")

overrideProbe := []string{"features", "list"}
appendNoUpdateCheckFlag(&overrideProbe)
appendHideRateLimitNudgeFlag(&overrideProbe)
appendSessionHookFlags(&overrideProbe)
appendWorkspaceTrustFlag(&overrideProbe, os.TempDir())
return [][]string{flagProbe, overrideProbe}
}

func appendNoUpdateCheckFlag(cmd *[]string) {
*cmd = append(*cmd, "-c", "check_for_update_on_startup=false")
}

func appendHideRateLimitNudgeFlag(cmd *[]string) {
// When the account nears its rate limit, the Codex TUI interposes an
// interactive "switch to a cheaper model?" dialog before the first turn.
// In a headless AO pane that dialog hangs the session invisibly and
// swallows the auto-submitted spawn prompt, so suppress it.
*cmd = append(*cmd, "-c", "notice.hide_rate_limit_model_nudge=true")
}

func appendHookTrustBypassFlag(cmd *[]string) {
// AO installs deterministic workspace-local Codex hooks immediately before
// launch/restore. Without this flag, a fresh per-session worktree can skip
// those hooks until an interactive /hooks trust review happens, leaving AO
// without activity signals.
// AO's activity hooks ride the launch command as session-flag config (see
// appendSessionHookFlags) and carry no persisted trust hash in the user's
// `[hooks.state]`. Without this flag Codex would hold them for an
// interactive hooks review, leaving AO without activity signals.
*cmd = append(*cmd, "--dangerously-bypass-hook-trust")
}

Expand Down
Loading
Loading