Skip to content
Closed
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
15 changes: 5 additions & 10 deletions bin/contributor-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -901,17 +901,12 @@ case "$STATUS" in
;;
esac

# bob is launched with --auth-method api-key (see backend_perm_flag in
# config/backends.conf), which makes BOBSHELL_API_KEY mandatory: without it bob
# comes up sitting at "Enter Bob-Shell API Key" and simply waits. Nobody is
# attached to an unattended relay, so that is an indefinite silent hang rather
# than a failure anyone can see. Fail loudly at startup instead.
# Only the presence of the key is tested — never its value, and it is never
# echoed.
if [[ "$AGENT_BACKEND" == "bob" ]] && [[ -z "${BOBSHELL_API_KEY:-}" ]]; then
echo "ERROR: backend 'bob' requires BOBSHELL_API_KEY to be set."
# Fail before an unattended Bob waits indefinitely for a key. Export both
# names for Bob 1.x/2.x compatibility without printing either value.
if [[ "$AGENT_BACKEND" == "bob" ]] && ! backend_bob_prepare_env; then
echo "ERROR: backend 'bob' requires BOB_API_KEY (or legacy BOBSHELL_API_KEY) to be set."
echo " Get a key at https://bob.ibm.com (Scope: Inference), then:"
echo " export BOBSHELL_API_KEY=..."
echo " export BOB_API_KEY=..."
echo " The key stays on your machine and is never sent to the hive."
exit 1
fi
Expand Down
1 change: 1 addition & 0 deletions changelog.d/fixed-contributor-bob-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Contributor images now select Bob 1.x/2.x launch flags at runtime and accept both `BOB_API_KEY` and legacy `BOBSHELL_API_KEY` credentials, matching hub-side agents (#14334).
46 changes: 18 additions & 28 deletions config/backends.conf
Original file line number Diff line number Diff line change
Expand Up @@ -548,39 +548,29 @@ codex_default_sandbox_mode() {
fi
}

# Bob renamed its key in 2.x. Fill either missing alias so both versions
# work, preserving explicit values when contributors supply both names.
# Call in the parent shell so the relay and CLI inherit the exports.
backend_bob_prepare_env() {
export BOB_API_KEY="${BOB_API_KEY:-${BOBSHELL_API_KEY:-}}"
export BOBSHELL_API_KEY="${BOBSHELL_API_KEY:-${BOB_API_KEY:-}}"
[[ -n "$BOB_API_KEY" ]]
}

# ── Backend → permission flag ───────────────────────────────────────────
backend_perm_flag() {
case "$1" in
claude) claude_family_perm_flag ;;
copilot) echo "--allow-all" ;;
# bob (IBM bobshell). Mirrors the hub-side launcher (bobLaunchCmd in
# src/pkg/agent/manager.go) so relay and pod agents behave identically:
# --accept-license bob hard-errors before doing any work unless
# licenseConsent is persisted; the prompt that
# collects it is interactive and nobody is attached
# to an unattended agent loop. Idempotent.
# --auth-method api-key bob otherwise defaults to W3ID browser SSO. The
# relay is unattended, so it must use the API key
# from BOBSHELL_API_KEY. The flag is HIDDEN from
# `bob --help` (the bundle registers it then calls
# t.hide()) but is live and validated — an invalid
# value reports Choices: "sso", "api-key".
# It is also the only input that outranks the
# persisted security.auth.selectedType AND
# suppresses bob's write-back of that value, so it
# does NOT stomp a contributor's own ~/.bob config.
# --approval-mode yolo without it bob reports `Auto-approve: Off` and
# blocks forever on its first tool call. Note that
# `-y`/`--yolo` is a BOOLEAN flag in bobshell 1.0.6
# ([boolean] [default: false]), so the former
# `-y yolo` spelling left `yolo` as a stray
# positional argument — i.e. a prompt.
# --trust bob otherwise treats the workdir as untrusted
# and gates tool availability on it.
# No --model is passed anywhere for bob: it auto-selects its own model and
# any --model makes every prompt die. See the bob case in the MODEL_FLAG
# block of bin/contributor-agent.sh.
bob) echo "--accept-license --auth-method api-key --approval-mode yolo --trust" ;;
# Match bobLaunchCmd in src/crux/agent/manager_launch.go. Only 1.x
# accepts --auth-method/--approval-mode; 2.x uses the chat subcommand.
# Unknown versions follow the current (2.x) CLI, just like pod agents.
bob)
case "$(bob --version 2>/dev/null | sed -n '1p')" in
1.*) echo "--accept-license --auth-method api-key --approval-mode yolo --trust" ;;
*) echo "chat --accept-license --auto-approve --trust" ;;
esac
;;

codex)
if is_truthy "${HIVE_CODEX_DANGEROUSLY_BYPASS_APPROVALS_AND_SANDBOX:-}"; then
Expand Down
57 changes: 54 additions & 3 deletions src/crux/agent/bob_launch_shell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ func TestBobLaunchCmdRunsBehindEnvPrefix(t *testing.T) {
version string
wantArgs string
}{
{"2.0.4", "chat --accept-license --auto-approve --trust"},
{"2.0.6", "chat --accept-license --auto-approve --trust"},
{"1.0.6", "--accept-license --auth-method api-key --approval-mode yolo --trust"},
{"unknown", "chat --accept-license --auto-approve --trust"},
}
for _, tc := range cases {
t.Run(tc.version, func(t *testing.T) {
Expand All @@ -35,15 +36,65 @@ func TestBobLaunchCmdRunsBehindEnvPrefix(t *testing.T) {
t.Fatal(err)
}

// Exercise the contributor's actual flag helper against the same
// fake CLI and expected argv as the hub's pod-agent launcher.
conf, err := filepath.Abs("../../../config/backends.conf")
if err != nil {
t.Fatal(err)
}
contributor := exec.Command(bash, "-c", `source "$1"; eval "bob $(backend_perm_flag_shell bob)"`, "bash", conf)
contributor.Env = append(os.Environ(), "PATH="+filepath.Dir(fake)+":"+os.Getenv("PATH"), "HIVE_AGENT=quality")
out, err := contributor.CombinedOutput()
want := "ARGS=" + tc.wantArgs + " HIVE_AGENT=quality"
if err != nil || strings.TrimSpace(string(out)) != want {
t.Fatalf("contributor launch: err=%v output=%q, want %q", err, out, want)
}

line := shellEnvVar("HIVE_AGENT", "quality") + " " + bobLaunchCmd(fake)
out, err := exec.Command(bash, "-c", line).CombinedOutput()
out, err = exec.Command(bash, "-c", line).CombinedOutput()
if err != nil {
t.Fatalf("launch line failed: %v\nline: %s\noutput: %s", err, line, out)
}
want := "ARGS=" + tc.wantArgs + " HIVE_AGENT=quality"
if got := strings.TrimSpace(string(out)); got != want {
t.Errorf("output = %q, want %q", got, want)
}
})
}
}

// Run the real contributor startup key check without starting tmux or a relay.
func TestContributorBobKeyCompatibility(t *testing.T) {
startup, err := os.ReadFile("../../../bin/contributor-agent.sh")
if err != nil {
t.Fatal(err)
}
start := strings.Index(string(startup), "# Fail before an unattended Bob")
end := strings.Index(string(startup), "# Ensure metrics directory")
if start < 0 || end <= start {
t.Fatal("contributor Bob startup check not found")
}
for _, tc := range []struct {
name, current, legacy, want string
wantFailure bool
}{
{"current", "current-key", "", "current-key/current-key", false},
{"legacy", "", "legacy-key", "legacy-key/legacy-key", false},
{"both", "current-key", "legacy-key", "current-key/legacy-key", false},
{"missing", "", "", "", true},
} {
t.Run(tc.name, func(t *testing.T) {
script := `set -eu; source ../../../config/backends.conf; AGENT_BACKEND=bob; BOB_API_KEY=$1; BOBSHELL_API_KEY=$2; ` +
string(startup[start:end]) + `bash -c 'printf "%s/%s" "$BOB_API_KEY" "$BOBSHELL_API_KEY"'`
out, err := exec.Command("bash", "-c", script, "bash", tc.current, tc.legacy).CombinedOutput()
if tc.wantFailure {
if err == nil || !strings.Contains(string(out), "requires BOB_API_KEY") {
t.Fatalf("missing key: err=%v output=%q", err, out)
}
return
}
if err != nil || string(out) != tc.want {
t.Fatalf("key aliases: err=%v output=%q, want %q", err, out, tc.want)
}
})
}
}
Loading