diff --git a/agents/prompts/roles/secretary.md b/agents/prompts/roles/secretary.md index f13094d9..a7562eb1 100644 --- a/agents/prompts/roles/secretary.md +++ b/agents/prompts/roles/secretary.md @@ -33,6 +33,16 @@ When you carry out a directive, you act with the CEO's authority — but that au - Never invent agents, channels, tasks, or numbers. If you don't know, say so and offer to look it up. - You do not write code, open PRs, or merge. You coordinate and inform; the cells and PMs execute, and the CEO decides. +## Your tools + +You have read-only file tools to inspect the repos, plus three action tools: + +- **`read_company_state`** — a compact snapshot of the company: charter (goals), task counts by status, pending pitches, and any directives awaiting the CEO's confirmation. Reading is always free; ground every claim about state in what you actually read. +- **`read_task`** — one task's detail by its id. +- **`submit_directive`** — act on the CEO's command. `kind` is one of `relay_message`, `update_charter`, `control_task`, `approve_pitch`, `announce`; `payload` carries that kind's fields. The high-impact kinds (`update_charter`, `control_task`, `approve_pitch`, `announce`) are gated server-side and queued for the CEO's explicit confirmation — so restate the action and wait for a clear "yes" before you call `submit_directive` for any of them. `relay_message` runs directly. + +You have no `say`/`dm`/`notify` and no lifecycle verbs — you never talk to other agents or run the delivery lifecycle. You inform the CEO by writing in this chat, and you act only through `submit_directive`. + ## Anti-patterns - ❌ Doing anything the CEO did not ask for. diff --git a/docker/agent-grok-secretary.Dockerfile b/docker/agent-grok-secretary.Dockerfile index 83a67a05..447c64c8 100644 --- a/docker/agent-grok-secretary.Dockerfile +++ b/docker/agent-grok-secretary.Dockerfile @@ -3,13 +3,22 @@ # The Grok analogue of agent-secretary. Holds a PERSISTENT `opencode serve` # session open, receives the CEO's messages over HTTP (POST /turn on :9000), and # streams each reply back to the panel via the relay. The Secretary's CEO- -# authority tools (read_company_state / read_task / submit_directive) reach the -# API through the mounted MCP gateway + the HMAC agent token. Builds on the Grok +# authority tools (read_company_state / read_task / submit_directive) are +# registered as opencode tools by the secretary-tools.js plugin, which calls +# /api/secretary/* with the container's HMAC agent token. Builds on the Grok # runtime image; the driver renders opencode.json from the spawn env first. # ============================================================================= FROM roboco-agent-grok +USER root + +# The CEO-authority tool plugin (read_company_state / read_task / submit_directive). +# Scoped to THIS image via ROBOCO_OPENCODE_EXTRA_PLUGINS so only the Secretary +# carries CEO authority; opencode_config appends it to the plugin array. +COPY docker/grok/secretary-tools.js /app/opencode-plugins/secretary-tools.js +ENV ROBOCO_OPENCODE_EXTRA_PLUGINS=/app/opencode-plugins/secretary-tools.js + USER agent LABEL role="grok-secretary" diff --git a/docker/grok/secretary-tools.js b/docker/grok/secretary-tools.js new file mode 100644 index 00000000..4803b689 --- /dev/null +++ b/docker/grok/secretary-tools.js @@ -0,0 +1,114 @@ +// 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); + +export default 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 || {}, + }), + ); + }, + }), + }, +}); diff --git a/roboco/agent_sdk/grok_secretary_main.py b/roboco/agent_sdk/grok_secretary_main.py index 5f973ab1..6b6349da 100644 --- a/roboco/agent_sdk/grok_secretary_main.py +++ b/roboco/agent_sdk/grok_secretary_main.py @@ -3,11 +3,12 @@ The Grok analogue of ``secretary_main``: the same in-container ``POST /turn`` receiver and the same relay sink to ``/api/secretary/live/{id}/events``, but the held-open session is an :class:`OpencodeServeSession` (``opencode serve``) rather -than a ``ClaudeSDKClient``. ``opencode.json`` (xAI provider + MCP gateway + -system prompt) is rendered first so the serve process is gateway-wired. The -Secretary's CEO-authority tools (read_company_state / read_task / -submit_directive) reach the API through the mounted MCP gateway and the HMAC -agent token, identically to the one-shot Grok path. +than a ``ClaudeSDKClient``. ``opencode.json`` (xAI provider + system prompt) is +rendered first. The Secretary's CEO-authority tools (read_company_state / +read_task / submit_directive) are registered as opencode tools by the +``secretary-tools.js`` plugin (baked into the grok-secretary image and wired in +via ``ROBOCO_OPENCODE_EXTRA_PLUGINS``); they call ``/api/secretary/*`` with the +container's HMAC agent token, the same auth the one-shot Grok path uses. """ from __future__ import annotations