mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Live end-to-end verification (opencode 1.17.8 + grok-build-0.1) of the WHOLE
integration, then fixes for what it surfaced:
1) Intake draft card (FUNCTIONAL): opencode's synchronous serve reply
(POST /session/:id/message) returns only [step-start, text, step-finish] — it
does NOT include tool-call parts, so the driver could never extract the
propose_draft draft. intake-tools.js now POSTs the draft straight to the
prompter-live relay (/api/prompter/live/{session}/events, the same endpoint
the driver's relay sink uses), so the panel renders the card regardless.
Verified live: grok calls propose_draft -> the relay receives the draft.
2) Correct misattributed opencode "bugs" (DOCS): earlier comments asserted as
general opencode behavior that a provider.xai block / npm override / config
plugin:-array "break" registration. Re-testing showed those were artifacts of
a PROJECT-level .opencode/opencode.json; from the GLOBAL config (which
opencode_config writes) the built-in provider, model resolution, the plugin
array AND the auto-discovery dir all work, and MCP gateway verbs register
(delivery agents verified). Reframed the comments as design choices (built-in
provider + XAI_API_KEY env + plugins baked in the auto-discovery dir with
named exports) and dropped the false claims.
3) Reasoning --variant: passing it does not error, but whether opencode applies a
named reasoning variant to grok-build-0.1 (no provider-defined variants) is
UNVERIFIED — comment softened from a "~54% cut" claim to best-effort,
measure-on-NAS.
Verified live this session: one-shot delivery (model + MCP verbs + plugins +
hooks), secretary tools (read_company_state + submit_directive -> backend with
token), intake draft (relay), grok built-in-provider tool-calling. Remaining
NAS-only: full container assembly (SDK :9000 startup, entrypoint hooks, 429
parking) + the --variant cost measurement. Gate green (ruff/mypy + 51 tests;
node --check the plugins).
119 lines
4.6 KiB
JavaScript
119 lines
4.6 KiB
JavaScript
// opencode plugin — the Secretary's CEO-authority tools, on Grok.
|
|
//
|
|
// Parity with the Claude Secretary's SDK tools (roboco.agent_sdk.secretary_driver
|
|
// .build_secretary_options): read_company_state / read_task / submit_directive,
|
|
// each calling the backend /api/secretary/* routes with the container's HMAC
|
|
// agent token. Without these the Grok Secretary can chat but cannot read company
|
|
// state or act on a CEO directive — the integration blocker.
|
|
//
|
|
// Loaded ONLY into the roboco-agent-grok-secretary image via
|
|
// ROBOCO_OPENCODE_EXTRA_PLUGINS (so no other role gets CEO authority). The
|
|
// container already carries ROBOCO_AGENT_TOKEN / ROBOCO_API_URL / ROBOCO_AGENT_ID
|
|
// / ROBOCO_AGENT_ROLE (set by the orchestrator's _build_secretary_run_cmd), so
|
|
// the auth substrate matches the one-shot Grok path exactly.
|
|
//
|
|
// The backend gate-list queues high-impact directive kinds (charter,
|
|
// control_task, approve_pitch, announce) for the CEO's explicit confirmation and
|
|
// runs relay_message directly — that policy lives server-side; this plugin only
|
|
// forwards the call. Each tool returns the backend JSON as a string the model
|
|
// reads back (mirrors secretary_driver._text_result).
|
|
//
|
|
// UNVERIFIED-LIVE: the @opencode-ai/plugin tool-registration path against a live
|
|
// opencode serve + grok-build-0.1 — confirm a submit_directive round-trips with
|
|
// the HMAC token on the NAS before routing real CEO directives through Grok.
|
|
|
|
import { tool } from "@opencode-ai/plugin";
|
|
|
|
const API_BASE = (
|
|
process.env.ROBOCO_API_URL || "http://roboco-orchestrator:8000"
|
|
).replace(/\/+$/, "");
|
|
const TIMEOUT_MS = 30000;
|
|
|
|
function headers() {
|
|
const h = {
|
|
"Content-Type": "application/json",
|
|
"X-Agent-ID": process.env.ROBOCO_AGENT_ID || "",
|
|
"X-Agent-Role": process.env.ROBOCO_AGENT_ROLE || "secretary",
|
|
};
|
|
const token = process.env.ROBOCO_AGENT_TOKEN;
|
|
if (token) h["X-Agent-Token"] = token;
|
|
return h;
|
|
}
|
|
|
|
// Call /api/secretary{path}; never throw — a failure becomes an {error,...}
|
|
// object the model can read and report, exactly like secretary_driver._call_backend.
|
|
async function callBackend(method, path, body) {
|
|
let res;
|
|
try {
|
|
res = await fetch(`${API_BASE}/api/secretary${path}`, {
|
|
method,
|
|
headers: headers(),
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
});
|
|
} catch (e) {
|
|
return { error: "request_failed", detail: String(e) };
|
|
}
|
|
let data;
|
|
try {
|
|
data = await res.json();
|
|
} catch {
|
|
data = { detail: await res.text().catch(() => "") };
|
|
}
|
|
if (!res.ok) return { error: `http_${res.status}`, detail: data };
|
|
return data;
|
|
}
|
|
|
|
const asText = (data) => JSON.stringify(data);
|
|
|
|
// Named export (opencode's plugin convention) + baked into the plugin
|
|
// auto-discovery dir (~/.config/opencode/plugin/) at image build. Verified live
|
|
// against grok-build-0.1: the model called read_company_state + submit_directive
|
|
// and the backend received both requests with the X-Agent-Token.
|
|
export const RobocoSecretaryTools = async () => ({
|
|
tool: {
|
|
read_company_state: tool({
|
|
description:
|
|
"Read a compact snapshot of company state: the charter (goals), task " +
|
|
"counts by status, pending pitches, and any directives awaiting the " +
|
|
"CEO's confirmation.",
|
|
args: {},
|
|
async execute() {
|
|
return asText(await callBackend("GET", "/state"));
|
|
},
|
|
}),
|
|
read_task: tool({
|
|
description: "Read one task's detail by its id.",
|
|
args: { task_id: tool.schema.string().describe("The task id") },
|
|
async execute(args) {
|
|
const id = encodeURIComponent(String(args.task_id));
|
|
return asText(await callBackend("GET", `/tasks/${id}`));
|
|
},
|
|
}),
|
|
submit_directive: tool({
|
|
description:
|
|
"Act on the CEO's command. 'kind' is one of: relay_message " +
|
|
"(payload: channel, text), update_charter (payload: charter), " +
|
|
"control_task (payload: task_id, action[start|cancel|override], " +
|
|
"status?), approve_pitch (payload: pitch_id, notes?), announce " +
|
|
"(payload: text). High-impact kinds (charter, control_task, " +
|
|
"approve_pitch, announce) are queued for the CEO's explicit " +
|
|
"confirmation; relay_message runs directly.",
|
|
args: {
|
|
kind: tool.schema.string().describe("The directive kind"),
|
|
payload: tool.schema
|
|
.record(tool.schema.string(), tool.schema.any())
|
|
.describe("The directive payload object"),
|
|
},
|
|
async execute(args) {
|
|
return asText(
|
|
await callBackend("POST", "/directives", {
|
|
kind: args.kind,
|
|
payload: args.payload || {},
|
|
}),
|
|
);
|
|
},
|
|
}),
|
|
},
|
|
});
|