mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(grok): give the Grok Secretary its CEO-authority tools (blocker)
The Grok Secretary could chat but had zero directive tools — it could not read company state or act on a CEO command, so it was non-functional. This is the integration blocker. - secretary-tools.js: opencode plugin registering read_company_state / read_task / submit_directive via the Hooks.tool API, each calling /api/secretary/* with the container's HMAC agent token — a direct port of the Claude Secretary's SDK tools (secretary_driver.build_secretary_options). The high-impact directive kinds stay gated server-side (queued for CEO confirm). - agent-grok-secretary.Dockerfile: bake the plugin and scope it to this image via ROBOCO_OPENCODE_EXTRA_PLUGINS, so only the Secretary carries CEO authority. - grok_secretary_main: correct the docstring that falsely claimed the tools reached the API "through the mounted MCP gateway" (there is no gateway mount; they're an opencode plugin). - secretary.md: name the three tools and restate the confirm-before-act gate. Verified locally that opencode loads a file-path plugin importing @opencode-ai/plugin and resolves the package; the live model-tool-call + backend round-trip is flagged UNVERIFIED-LIVE for the NAS.
This commit is contained in:
@@ -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.
|
- 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.
|
- 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
|
## Anti-patterns
|
||||||
|
|
||||||
- ❌ Doing anything the CEO did not ask for.
|
- ❌ Doing anything the CEO did not ask for.
|
||||||
|
|||||||
@@ -3,13 +3,22 @@
|
|||||||
# The Grok analogue of agent-secretary. Holds a PERSISTENT `opencode serve`
|
# 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
|
# 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-
|
# 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
|
# authority tools (read_company_state / read_task / submit_directive) are
|
||||||
# API through the mounted MCP gateway + the HMAC agent token. Builds on the Grok
|
# 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.
|
# runtime image; the driver renders opencode.json from the spawn env first.
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
FROM roboco-agent-grok
|
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
|
USER agent
|
||||||
|
|
||||||
LABEL role="grok-secretary"
|
LABEL role="grok-secretary"
|
||||||
|
|||||||
@@ -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 || {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
The Grok analogue of ``secretary_main``: the same in-container ``POST /turn``
|
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
|
receiver and the same relay sink to ``/api/secretary/live/{id}/events``, but the
|
||||||
held-open session is an :class:`OpencodeServeSession` (``opencode serve``) rather
|
held-open session is an :class:`OpencodeServeSession` (``opencode serve``) rather
|
||||||
than a ``ClaudeSDKClient``. ``opencode.json`` (xAI provider + MCP gateway +
|
than a ``ClaudeSDKClient``. ``opencode.json`` (xAI provider + system prompt) is
|
||||||
system prompt) is rendered first so the serve process is gateway-wired. The
|
rendered first. The Secretary's CEO-authority tools (read_company_state /
|
||||||
Secretary's CEO-authority tools (read_company_state / read_task /
|
read_task / submit_directive) are registered as opencode tools by the
|
||||||
submit_directive) reach the API through the mounted MCP gateway and the HMAC
|
``secretary-tools.js`` plugin (baked into the grok-secretary image and wired in
|
||||||
agent token, identically to the one-shot Grok path.
|
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
|
from __future__ import annotations
|
||||||
|
|||||||
Reference in New Issue
Block a user