diff --git a/.env.example b/.env.example index c6c65be..1863e69 100644 --- a/.env.example +++ b/.env.example @@ -14,7 +14,10 @@ WORKFLOW_CURSOR_MODEL= # Timeout in milliseconds for Cursor agent operations WORKFLOW_CURSOR_TIMEOUT= -# ── Hermes Agent (used by workflow-template-solve-issue) ── +# ── Hermes Agent (used by develop tester/committer + solve-issue) ── + +# CLI command to invoke the Hermes agent (absolute path required) +WORKFLOW_HERMES_COMMAND= # Model override for Hermes agent WORKFLOW_HERMES_MODEL= @@ -29,7 +32,7 @@ WORKFLOW_HERMES_TIMEOUT= WORKFLOW_STORAGE_ROOT= # Gateway secret for the serve command -WORKFLOW_GATEWAY_SECRET= +WORKFLOW_DASHBOARD_SECRET= # ── Display ── diff --git a/packages/cli-workflow/src/commands/connect/connect.ts b/packages/cli-workflow/src/commands/connect/connect.ts index 4e72922..478393e 100644 --- a/packages/cli-workflow/src/commands/connect/connect.ts +++ b/packages/cli-workflow/src/commands/connect/connect.ts @@ -23,7 +23,7 @@ function requireNextArg(argv: string[], i: number, flag: string): Result { let name = osHostname().split(".")[0].toLowerCase(); let gatewayUrl = DEFAULT_GATEWAY_URL; - const gatewaySecret = process.env.WORKFLOW_GATEWAY_SECRET ?? ""; + const gatewaySecret = process.env.WORKFLOW_DASHBOARD_SECRET ?? ""; const stringFlags: Record void> = { "--name": (v) => { name = v; @@ -56,7 +56,7 @@ export async function dispatchConnect(storageRoot: string, argv: string[]): Prom const options = parsed.value; if (options.gatewaySecret === "") { - printCliLine("error: WORKFLOW_GATEWAY_SECRET is required"); + printCliLine("error: WORKFLOW_DASHBOARD_SECRET is required"); return 1; } diff --git a/packages/workflow-gateway/src/client-socket.ts b/packages/workflow-gateway/src/client-socket.ts index 6c91c86..417b5ac 100644 --- a/packages/workflow-gateway/src/client-socket.ts +++ b/packages/workflow-gateway/src/client-socket.ts @@ -4,7 +4,7 @@ import { DurableObject } from "cloudflare:workers"; import { parseWsRequestJson, parseWsResponseJson, type WsResponse } from "./ws-protocol.js"; type ClientSocketEnv = { - GATEWAY_SECRET: string; + WORKFLOW_DASHBOARD_SECRET: string; }; export const CLIENT_SOCKET_INTERNAL_STATUS_PATH = "/internal/client-socket/status"; @@ -37,7 +37,7 @@ export class ClientSocket extends DurableObject { private requireAuth(request: Request): Response | null { const auth = request.headers.get("Authorization"); - if (auth !== `Bearer ${this.env.GATEWAY_SECRET}`) { + if (auth !== `Bearer ${this.env.WORKFLOW_DASHBOARD_SECRET}`) { return jsonResponse(401, { error: "unauthorized" }); } return null; diff --git a/packages/workflow-gateway/src/index.ts b/packages/workflow-gateway/src/index.ts index b49e422..7635563 100644 --- a/packages/workflow-gateway/src/index.ts +++ b/packages/workflow-gateway/src/index.ts @@ -13,8 +13,7 @@ export { ClientSocket }; type Env = { Bindings: { ENDPOINTS: KVNamespace; - GATEWAY_SECRET: string; - DASHBOARD_API_KEY: string; + WORKFLOW_DASHBOARD_SECRET: string; CLIENT_SOCKET: DurableObjectNamespace; }; }; @@ -40,7 +39,7 @@ function checkDashboardAuth(c: { const bearer = c.req.header("Authorization")?.replace("Bearer ", ""); const query = c.req.query("key"); const key = bearer ?? query; - return key === c.env.DASHBOARD_API_KEY; + return key === c.env.WORKFLOW_DASHBOARD_SECRET; } function isLocalClientUrl(url: string): boolean { @@ -153,7 +152,7 @@ async function fetchClientSocketStatus( const resp = await stub.fetch( new Request(`https://do${CLIENT_SOCKET_INTERNAL_STATUS_PATH}`, { method: "GET", - headers: { Authorization: `Bearer ${env.GATEWAY_SECRET}` }, + headers: { Authorization: `Bearer ${env.WORKFLOW_DASHBOARD_SECRET}` }, }), ); if (!resp.ok) { @@ -184,14 +183,14 @@ function endpointStatusFromKvAndDo(record: EndpointRecord, doConnected: boolean // ── Health ────────────────────────────────────────────────────────── app.get("/healthz", (c) => c.json({ ok: true })); -// ── Client reverse WebSocket (GATEWAY_SECRET query param) ──────────── +// ── Client reverse WebSocket (WORKFLOW_DASHBOARD_SECRET query param) ──────────── app.get("/ws/connect", async (c) => { const secret = c.req.query("secret"); const name = c.req.query("name"); if (name === undefined || name === "") { return c.json({ error: "name required" }, 400); } - if (secret !== c.env.GATEWAY_SECRET) { + if (secret !== c.env.WORKFLOW_DASHBOARD_SECRET) { return c.json({ error: "unauthorized" }, 401); } if (c.req.header("Upgrade") !== "websocket") { @@ -202,7 +201,7 @@ app.get("/ws/connect", async (c) => { return stub.fetch(c.req.raw); }); -// ── Gateway management (GATEWAY_SECRET auth) ──────────────────────── +// ── Gateway management (WORKFLOW_DASHBOARD_SECRET auth) ──────────────────────── const gateway = new Hono(); gateway.post("/register", async (c) => { @@ -217,7 +216,7 @@ gateway.post("/register", async (c) => { if (!name || !url) { return c.json({ error: "name and url required" }, 400); } - if (secret !== c.env.GATEWAY_SECRET) { + if (secret !== c.env.WORKFLOW_DASHBOARD_SECRET) { return c.json({ error: "unauthorized" }, 401); } @@ -242,7 +241,7 @@ gateway.post("/register", async (c) => { gateway.delete("/register/:name", async (c) => { const auth = c.req.header("Authorization"); - if (auth !== `Bearer ${c.env.GATEWAY_SECRET}`) { + if (auth !== `Bearer ${c.env.WORKFLOW_DASHBOARD_SECRET}`) { return c.json({ error: "unauthorized" }, 401); } @@ -308,7 +307,7 @@ app.all("/api/clients/:client/*", async (c) => { const proxyResp = await fetchThroughClientSocket( c.env, client, - c.env.GATEWAY_SECRET, + c.env.WORKFLOW_DASHBOARD_SECRET, wsRequest, ); if (proxyResp.status !== 503) { diff --git a/packages/workflow-gateway/wrangler.toml b/packages/workflow-gateway/wrangler.toml index 253be5b..56cf1ba 100644 --- a/packages/workflow-gateway/wrangler.toml +++ b/packages/workflow-gateway/wrangler.toml @@ -17,4 +17,4 @@ new_sqlite_classes = ["AgentSocket"] tag = "rename-agent-to-client" renamed_classes = [{ from = "AgentSocket", to = "ClientSocket" }] -# GATEWAY_SECRET is set via `wrangler secret put` +# WORKFLOW_DASHBOARD_SECRET is set via `wrangler secret put` diff --git a/packages/workflow-template-develop/bundle-entry.ts b/packages/workflow-template-develop/bundle-entry.ts index ab3231c..fd79fd6 100644 --- a/packages/workflow-template-develop/bundle-entry.ts +++ b/packages/workflow-template-develop/bundle-entry.ts @@ -1,14 +1,16 @@ /** * develop bundle entry — 小橘 🍊 * - * All roles use cursor-agent with workspace auto-extracted from context. + * planner/coder/reviewer → cursor-agent (needs code editing) + * tester/committer → hermes-agent (lightweight, no editing needed) */ import { createCursorAgent } from "@uncaged/workflow-agent-cursor"; +import { createHermesAgent } from "@uncaged/workflow-agent-hermes"; import { createWorkflow } from "@uncaged/workflow-runtime"; import { optionalEnv, requireEnv } from "@uncaged/workflow-util"; import { buildDevelopDescriptor, developWorkflowDefinition } from "./src/index.js"; -const adapter = createCursorAgent({ +const cursorAdapter = createCursorAgent({ command: requireEnv("WORKFLOW_CURSOR_COMMAND", "set WORKFLOW_CURSOR_COMMAND (e.g. cursor-agent)"), model: optionalEnv("WORKFLOW_CURSOR_MODEL"), timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT") @@ -17,7 +19,21 @@ const adapter = createCursorAgent({ workspace: null, }); -const wf = createWorkflow(developWorkflowDefinition, { adapter, overrides: null }); +const hermesAdapter = createHermesAgent({ + command: requireEnv("WORKFLOW_HERMES_COMMAND", "set WORKFLOW_HERMES_COMMAND (absolute path to hermes CLI)"), + model: optionalEnv("WORKFLOW_HERMES_MODEL"), + timeout: optionalEnv("WORKFLOW_HERMES_TIMEOUT") + ? Number(optionalEnv("WORKFLOW_HERMES_TIMEOUT")) + : null, +}); + +const wf = createWorkflow(developWorkflowDefinition, { + adapter: cursorAdapter, + overrides: { + tester: hermesAdapter, + committer: hermesAdapter, + }, +}); export const descriptor = buildDevelopDescriptor(); export const run = wf;