mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(grok): start the in-container SDK server + budget feed (Claude parity)
The keystone of the Grok parity work (CEO's "take Claude as baseline, create what's missing" call): the one-shot Grok container now starts the same SDK server the Claude path runs, so the per-verb circuit breaker (the flow/do MCP servers already POST /verb/attempted to it), the per-session budget/loop counters, the terminal-verb tracking, and the SessionEnd post-mortem all work on Grok instead of being silently absent. - entrypoint: launch roboco.agent_sdk.server (bare venv python, not `uv run` which would re-sync the drifted clone lock and stall), wait for /health, reset counters; run opencode WITHOUT exec so the script regains control to run the post-mortem and the silent-exit substitute after the run returns. - budget-feed.js: opencode plugin that gates on /budget/status in tool.execute.before (halt/loop deny — the only place to stop a runaway one-shot run; opencode has no PostToolUse-deny) and records the executed tool + args-hash in tool.execute.after. Fail-open; bare-verb normalization for MCP-namespaced terminal verbs. - silent-exit substitute: on a graceful exit with no terminal verb the entrypoint posts /terminal/force_substitute so the task isn't left stuck claimed/in_progress (Stop-hook parity at the boundary). - opencode_config: wire budget-feed into the plugin array; add ROBOCO_OPENCODE_EXTRA_PLUGINS so per-image role tool plugins load scoped to one role; read the per-role ROBOCO_GROK_EDIT_PERMISSION. Targeted gate green (ruff/mypy/xenon + opencode_config tests; node --check on the plugins; bash -n on the entrypoint).
This commit is contained in:
@@ -1,33 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
# Entrypoint for the roboco-agent-grok image.
|
||||
# Entrypoint for the roboco-agent-grok image (one-shot delivery roles).
|
||||
#
|
||||
# Renders opencode.json from the RoboCo spawn env (OPENAI_* + ROBOCO_*, set by
|
||||
# GrokProvider) plus the mounted Claude Code mcp-config.json, then runs opencode
|
||||
# non-interactively. opencode speaks the OpenAI protocol, so grok-build-0.1 runs
|
||||
# natively against api.x.ai/v1 with no shim, while still reaching the RoboCo MCP
|
||||
# gateway (roboco-flow / roboco-do / ...) translated into opencode's mcp config.
|
||||
# GrokProvider) plus the mounted Claude Code mcp-config.json, starts the
|
||||
# in-container SDK server (parity with the Claude SessionStart sdk-startup-hook),
|
||||
# then runs opencode non-interactively. opencode speaks the OpenAI protocol, so
|
||||
# grok-build-0.1 runs natively against api.x.ai/v1 with no shim, while still
|
||||
# reaching the RoboCo MCP gateway (roboco-flow / roboco-do / ...) translated into
|
||||
# opencode's mcp config.
|
||||
set -euo pipefail
|
||||
|
||||
SDK_PORT="${ROBOCO_SDK_PORT:-9000}"
|
||||
SDK_URL="http://localhost:${SDK_PORT}"
|
||||
|
||||
# Generate opencode.json (provider + model + MCP gateway + permissions +
|
||||
# instructions). Writes to opencode's global config dir by default.
|
||||
python -m roboco.llm.providers.opencode_config
|
||||
|
||||
# Run the agent. The prompt comes from an env var (never an untrusted argv
|
||||
# positional); `--` separates it from flags so a prompt starting with `--`
|
||||
# cannot be parsed as CLI options. The model also comes from the rendered
|
||||
# config; --model is passed explicitly as belt-and-suspenders.
|
||||
#
|
||||
# `< /dev/null` is REQUIRED: without a closed stdin, `opencode run` hangs after
|
||||
# init in a headless / no-TTY environment (it blocks waiting on stdin). Verified
|
||||
# live — closing stdin lets the run proceed to the model call and exit cleanly.
|
||||
#
|
||||
# Reasoning effort: GrokProvider sets ROBOCO_GROK_VARIANT per role (e.g.
|
||||
# "minimal" for coordination/docs roles to cut reasoning cost). Absent =
|
||||
# opencode default (full reasoning).
|
||||
variant_arg=()
|
||||
if [ -n "${ROBOCO_GROK_VARIANT:-}" ]; then
|
||||
variant_arg=(--variant "$ROBOCO_GROK_VARIANT")
|
||||
# --- SDK server bring-up (Claude-parity) ----------------------------------
|
||||
# The flow/do MCP servers POST /verb/attempted here for the per-verb circuit
|
||||
# breaker; the budget-feed opencode plugin POSTs /budget/* + /terminal/* here;
|
||||
# the post-exit hook below reads /terminal/status and writes the post-mortem.
|
||||
# Bare `python` (the baked venv) — NOT `uv run`, which would re-sync the clone's
|
||||
# drifted lock and stall (the #179 fix the Claude hook needs `--no-sync` for).
|
||||
if ! curl -sf -m 2 "${SDK_URL}/health" >/dev/null 2>&1; then
|
||||
nohup python -m roboco.agent_sdk.server >/tmp/sdk-server.log 2>&1 &
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if curl -sf -m 2 "${SDK_URL}/health" >/dev/null 2>&1; then break; fi
|
||||
sleep 0.5
|
||||
done
|
||||
fi
|
||||
# Zero the budget/terminal counters at the start of the session.
|
||||
curl -sf -m 2 -X POST "${SDK_URL}/budget/reset" >/dev/null 2>&1 || true
|
||||
|
||||
# Prompt-injection guard (parity with the Claude UserPromptSubmit hook): the
|
||||
# task prompt is DATA, not instructions — refuse a poisoned one before it
|
||||
@@ -37,7 +41,54 @@ if ! python -m roboco.agent_sdk.prompt_guard "${ROBOCO_INITIAL_PROMPT:-}"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec opencode run \
|
||||
# Reasoning effort: GrokProvider sets ROBOCO_GROK_VARIANT per role (e.g.
|
||||
# "minimal" for coordination/docs roles to cut reasoning cost). Absent =
|
||||
# opencode default (full reasoning).
|
||||
variant_arg=()
|
||||
if [ -n "${ROBOCO_GROK_VARIANT:-}" ]; then
|
||||
variant_arg=(--variant "$ROBOCO_GROK_VARIANT")
|
||||
fi
|
||||
|
||||
# Run the agent. The prompt comes from an env var (never an untrusted argv
|
||||
# positional); `--` separates it from flags so a prompt starting with `--`
|
||||
# cannot be parsed as CLI options. `< /dev/null` is REQUIRED: without a closed
|
||||
# stdin, `opencode run` hangs after init in a headless / no-TTY environment.
|
||||
#
|
||||
# We do NOT `exec`: the script must regain control after opencode exits to run
|
||||
# the post-mortem + silent-exit substitute below (the Claude SessionEnd / Stop
|
||||
# hooks have no opencode equivalent, so the boundary handles them). `set +e`
|
||||
# around the run so a non-zero opencode exit doesn't abort before the post-run
|
||||
# hooks; PIPESTATUS preserves opencode's real code through the `tee`.
|
||||
set +e
|
||||
opencode run \
|
||||
--model "xai/${ROBOCO_AGENT_MODEL:-grok-build-0.1}" \
|
||||
"${variant_arg[@]}" \
|
||||
-- "${ROBOCO_INITIAL_PROMPT:-}" < /dev/null
|
||||
run_rc=$?
|
||||
set -e
|
||||
|
||||
# --- Post-run hooks (Claude SessionEnd + Stop parity) ----------------------
|
||||
# Read terminal state once, then (a) write a post-mortem journal entry and
|
||||
# (b) if the agent exited WITHOUT a terminal verb (i_am_idle / i_am_done /
|
||||
# pass / fail / ...), auto-substitute the task so it is not left stuck in
|
||||
# claimed/in_progress for a human to hand-unstick. Best-effort; never change
|
||||
# the exit code the orchestrator observes.
|
||||
terminal=$(curl -sf -m 2 "${SDK_URL}/terminal/status" 2>/dev/null || echo "")
|
||||
last_tool="null"
|
||||
had_terminal="false"
|
||||
if [ -n "$terminal" ]; then
|
||||
last_tool=$(echo "$terminal" | jq -r '.last_tool // "null"' 2>/dev/null || echo "null")
|
||||
had_terminal=$(echo "$terminal" | jq -r '.had_terminal_recently // false' 2>/dev/null || echo "false")
|
||||
fi
|
||||
|
||||
curl -sf -m 3 -X POST "${SDK_URL}/journal/post_mortem" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"terminal_tool\":\"${last_tool}\",\"reason\":\"session_end\"}" \
|
||||
>/dev/null 2>&1 || true
|
||||
|
||||
if [ "$had_terminal" != "true" ]; then
|
||||
curl -sf -m 3 -X POST "${SDK_URL}/terminal/force_substitute" >/dev/null 2>&1 || true
|
||||
echo "[grok] exited without a terminal verb (last tool: ${last_tool}) — auto-substituted." >&2
|
||||
fi
|
||||
|
||||
exit "$run_rc"
|
||||
|
||||
Reference in New Issue
Block a user