From ae908a82fdbca80a39fe608737166f3bde41a6ef Mon Sep 17 00:00:00 2001 From: thatguywob Date: Mon, 26 Oct 2026 18:01:33 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20support=20Bob=202.x=20in?= =?UTF-8?q?=20contributor=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Select contributor launch flags by Bob version and bridge current and legacy API key environment names. Pin contributor and pod-agent argv against the same fake CLI and exercise the contributor startup key check. Hive-Run: hivecommons/hive#14334 Hive-Plan: contributor-bob-version-compatibility Hive-Spec: issue-14334#expected-fix Co-authored-by: odttlnt <91499691+odttlnt@users.noreply.github.com> Signed-off-by: thatguywob --- bin/contributor-agent.sh | 15 +++---- changelog.d/fixed-contributor-bob-2.md | 1 + config/backends.conf | 46 ++++++++------------ src/crux/agent/bob_launch_shell_test.go | 57 +++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 41 deletions(-) create mode 100644 changelog.d/fixed-contributor-bob-2.md diff --git a/bin/contributor-agent.sh b/bin/contributor-agent.sh index 8fe368f6fd..2c1f6f328b 100755 --- a/bin/contributor-agent.sh +++ b/bin/contributor-agent.sh @@ -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 diff --git a/changelog.d/fixed-contributor-bob-2.md b/changelog.d/fixed-contributor-bob-2.md new file mode 100644 index 0000000000..12999d95a1 --- /dev/null +++ b/changelog.d/fixed-contributor-bob-2.md @@ -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). diff --git a/config/backends.conf b/config/backends.conf index 8f5a648544..3202332e7d 100644 --- a/config/backends.conf +++ b/config/backends.conf @@ -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 diff --git a/src/crux/agent/bob_launch_shell_test.go b/src/crux/agent/bob_launch_shell_test.go index e3bef53c07..53a43be096 100644 --- a/src/crux/agent/bob_launch_shell_test.go +++ b/src/crux/agent/bob_launch_shell_test.go @@ -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) { @@ -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) + } + }) + } +}