baa-conductor

git clone 

commit
1d0871e
parent
a6e3a34
author
im_wower
date
2026-03-28 02:54:19 +0800 CST
feat: add phase1 browser claude target
3 files changed,  +242, -10
M apps/conductor-daemon/src/index.test.js
+154, -3
  1@@ -27,6 +27,7 @@ import {
  2   normalizeBaaInstruction,
  3   parseBaaInstructionBlock,
  4   parseConductorCliRequest,
  5+  routeBaaInstruction,
  6   writeHttpResponse
  7 } from "../dist/index.js";
  8 
  9@@ -236,6 +237,44 @@ async function createLocalApiFixture(options = {}) {
 10   };
 11 }
 12 
 13+function createInstructionEnvelope({
 14+  blockIndex = 0,
 15+  params = null,
 16+  paramsKind = params == null
 17+    ? "none"
 18+    : typeof params === "string"
 19+      ? "inline_string"
 20+      : "inline_json",
 21+  target = "conductor",
 22+  tool = "describe"
 23+} = {}) {
 24+  return {
 25+    assistantMessageId: "msg-route-test",
 26+    blockIndex,
 27+    conversationId: "conv-route-test",
 28+    dedupeBasis: {
 29+      assistant_message_id: "msg-route-test",
 30+      block_index: blockIndex,
 31+      conversation_id: "conv-route-test",
 32+      params,
 33+      platform: "claude",
 34+      target,
 35+      tool,
 36+      version: "baa.v1"
 37+    },
 38+    dedupeKey: `dedupe:${blockIndex}:${target}:${tool}`,
 39+    envelopeVersion: "baa.v1",
 40+    instructionId: `instruction-${blockIndex}`,
 41+    params,
 42+    paramsKind,
 43+    platform: "claude",
 44+    rawBlock: "```baa\nplaceholder\n```",
 45+    rawInstruction: `@${target}::${tool}`,
 46+    target,
 47+    tool
 48+  };
 49+}
 50+
 51 async function startCodexdStubServer() {
 52   const requests = [];
 53   const sessions = [
 54@@ -646,6 +685,35 @@ test("BAA instruction normalization keeps auditable fields and stable dedupe key
 55   });
 56 });
 57 
 58+test("routeBaaInstruction maps browser.claude send/current to the existing local browser routes", () => {
 59+  const sendRoute = routeBaaInstruction(createInstructionEnvelope({
 60+    params: "hello claude from baa",
 61+    target: "browser.claude",
 62+    tool: "send"
 63+  }));
 64+  assert.deepEqual(sendRoute, {
 65+    body: {
 66+      prompt: "hello claude from baa"
 67+    },
 68+    key: "local.browser.claude.send",
 69+    method: "POST",
 70+    path: "/v1/browser/claude/send",
 71+    requiresSharedToken: false
 72+  });
 73+
 74+  const currentRoute = routeBaaInstruction(createInstructionEnvelope({
 75+    target: "browser.claude",
 76+    tool: "current"
 77+  }));
 78+  assert.deepEqual(currentRoute, {
 79+    body: null,
 80+    key: "local.browser.claude.current",
 81+    method: "GET",
 82+    path: "/v1/browser/claude/current",
 83+    requiresSharedToken: false
 84+  });
 85+});
 86+
 87 test("BaaInstructionCenter runs the Phase 1 execution loop and dedupes replayed messages", async () => {
 88   const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
 89   const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-"));
 90@@ -749,6 +817,74 @@ test("BaaInstructionCenter runs the Phase 1 execution loop and dedupes replayed
 91   }
 92 });
 93 
 94+test("BaaInstructionCenter executes browser.claude send/current through the Phase 1 route layer", async () => {
 95+  const { controlPlane, repository, snapshot } = await createLocalApiFixture();
 96+  const browser = createBrowserBridgeStub();
 97+  const center = new BaaInstructionCenter({
 98+    localApiContext: {
 99+      ...browser.context,
100+      fetchImpl: globalThis.fetch,
101+      repository,
102+      snapshotLoader: () => snapshot
103+    }
104+  });
105+  const message = [
106+    "```baa",
107+    "@browser.claude::send::hello from instruction center",
108+    "```",
109+    "",
110+    "```baa",
111+    "@browser.claude::current",
112+    "```"
113+  ].join("\n");
114+
115+  try {
116+    const result = await center.processAssistantMessage({
117+      assistantMessageId: "msg-browser-claude-1",
118+      conversationId: "conv-browser-claude-1",
119+      platform: "claude",
120+      text: message
121+    });
122+
123+    assert.equal(result.status, "executed");
124+    assert.equal(result.denied.length, 0);
125+    assert.equal(result.executions.length, 2);
126+
127+    const sendExecution = result.executions.find((execution) => execution.tool === "send");
128+    assert.ok(sendExecution);
129+    assert.equal(sendExecution.ok, true);
130+    assert.equal(sendExecution.route.path, "/v1/browser/claude/send");
131+    assert.equal(sendExecution.data.conversation.conversation_id, "conv-1");
132+    assert.equal(sendExecution.data.response.accepted, true);
133+
134+    const currentExecution = result.executions.find((execution) => execution.tool === "current");
135+    assert.ok(currentExecution);
136+    assert.equal(currentExecution.ok, true);
137+    assert.equal(currentExecution.route.path, "/v1/browser/claude/current");
138+    assert.equal(currentExecution.data.conversation.conversation_id, "conv-1");
139+    assert.equal(currentExecution.data.messages.length, 2);
140+
141+    const completionCall = browser.calls.find(
142+      (call) =>
143+        call.kind === "apiRequest"
144+        && call.path === "/api/organizations/org-1/chat_conversations/conv-1/completion"
145+    );
146+    assert.ok(completionCall);
147+    assert.deepEqual(completionCall.body, {
148+      prompt: "hello from instruction center"
149+    });
150+
151+    const currentConversationCall = browser.calls.find(
152+      (call) =>
153+        call.kind === "apiRequest"
154+        && call.path === "/api/organizations/org-1/chat_conversations/conv-1"
155+    );
156+    assert.ok(currentConversationCall);
157+  } finally {
158+    controlPlane.close();
159+  }
160+});
161+
162 test("BaaInstructionCenter keeps supported instructions running when one instruction is denied", async () => {
163   const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
164   const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-partial-deny-"));
165@@ -830,7 +966,7 @@ test("BaaInstructionCenter returns denied_only when every pending instruction is
166     "```",
167     "",
168     "```baa",
169-    "@browser.claude::current",
170+    "@browser.claude::reload",
171     "```"
172   ].join("\n");
173 
174@@ -846,8 +982,23 @@ test("BaaInstructionCenter returns denied_only when every pending instruction is
175     assert.equal(result.executions.length, 0);
176     assert.equal(result.denied.length, 2);
177     assert.deepEqual(
178-      result.denied.map((entry) => entry.instruction.target),
179-      ["browser.chatgpt", "browser.claude"]
180+      result.denied.map((entry) => ({
181+        code: entry.code,
182+        target: entry.instruction.target,
183+        tool: entry.instruction.tool
184+      })),
185+      [
186+        {
187+          code: "unsupported_target",
188+          target: "browser.chatgpt",
189+          tool: "send"
190+        },
191+        {
192+          code: "unsupported_tool",
193+          target: "browser.claude",
194+          tool: "reload"
195+        }
196+      ]
197     );
198   } finally {
199     controlPlane.close();
M apps/conductor-daemon/src/instructions/policy.ts
+14, -4
 1@@ -1,7 +1,6 @@
 2 import type { BaaInstructionEnvelope } from "./types.js";
 3 
 4-const SUPPORTED_TARGETS = new Set(["conductor", "system"]);
 5-const SUPPORTED_TOOLS = new Set([
 6+const CONDUCTOR_TOOLS = new Set([
 7   "describe",
 8   "describe/business",
 9   "describe/control",
10@@ -10,6 +9,15 @@ const SUPPORTED_TOOLS = new Set([
11   "files/write",
12   "status"
13 ]);
14+const BROWSER_CLAUDE_TOOLS = new Set([
15+  "current",
16+  "send"
17+]);
18+const SUPPORTED_TARGET_TOOLS = new Map([
19+  ["conductor", CONDUCTOR_TOOLS],
20+  ["system", CONDUCTOR_TOOLS],
21+  ["browser.claude", BROWSER_CLAUDE_TOOLS]
22+]);
23 
24 export interface BaaInstructionPolicyDecision {
25   code: string | null;
26@@ -20,7 +28,9 @@ export interface BaaInstructionPolicyDecision {
27 export function evaluateBaaInstructionPolicy(
28   instruction: BaaInstructionEnvelope
29 ): BaaInstructionPolicyDecision {
30-  if (!SUPPORTED_TARGETS.has(instruction.target)) {
31+  const supportedTools = SUPPORTED_TARGET_TOOLS.get(instruction.target);
32+
33+  if (!supportedTools) {
34     return {
35       code: "unsupported_target",
36       message: `Target "${instruction.target}" is not supported in Phase 1.`,
37@@ -28,7 +38,7 @@ export function evaluateBaaInstructionPolicy(
38     };
39   }
40 
41-  if (!SUPPORTED_TOOLS.has(instruction.tool)) {
42+  if (!supportedTools.has(instruction.tool)) {
43     return {
44       code: "unsupported_tool",
45       message: `Tool "${instruction.tool}" is not supported in Phase 1.`,
M apps/conductor-daemon/src/instructions/router.ts
+74, -3
 1@@ -1,6 +1,5 @@
 2 import type {
 3   BaaInstructionEnvelope,
 4-  BaaInstructionParams,
 5   BaaInstructionRoute,
 6   BaaJsonObject
 7 } from "./types.js";
 8@@ -129,7 +128,37 @@ function normalizeFileWriteBody(instruction: BaaInstructionEnvelope): BaaJsonObj
 9   return params;
10 }
11 
12-export function routeBaaInstruction(instruction: BaaInstructionEnvelope): BaaInstructionRoute {
13+function normalizeBrowserClaudeSendBody(instruction: BaaInstructionEnvelope): BaaJsonObject {
14+  if (typeof instruction.params === "string") {
15+    return {
16+      prompt: requireNonEmptyStringParam(instruction, "prompt", true)
17+    };
18+  }
19+
20+  const params = requireJsonObjectParams(instruction);
21+  const prompt = params.prompt;
22+  const unexpectedKeys = Object.keys(params).filter((key) => key !== "prompt");
23+
24+  if (typeof prompt !== "string" || prompt.trim() === "") {
25+    throw new BaaInstructionRouteError(
26+      instruction.blockIndex,
27+      `${instruction.target}::${instruction.tool} JSON params must include a non-empty "prompt".`
28+    );
29+  }
30+
31+  if (unexpectedKeys.length > 0) {
32+    throw new BaaInstructionRouteError(
33+      instruction.blockIndex,
34+      `${instruction.target}::${instruction.tool} JSON params only support string "prompt".`
35+    );
36+  }
37+
38+  return {
39+    prompt: prompt.trim()
40+  };
41+}
42+
43+function routeLocalInstruction(instruction: BaaInstructionEnvelope): BaaInstructionRoute {
44   switch (instruction.tool) {
45     case "describe":
46       requireNoParams(instruction);
47@@ -194,7 +223,49 @@ export function routeBaaInstruction(instruction: BaaInstructionEnvelope): BaaIns
48     default:
49       throw new BaaInstructionRouteError(
50         instruction.blockIndex,
51-        `No Phase 1 route exists for tool "${instruction.tool}".`
52+        `No Phase 1 route exists for ${instruction.target}::${instruction.tool}.`
53+      );
54+  }
55+}
56+
57+function routeBrowserClaudeInstruction(instruction: BaaInstructionEnvelope): BaaInstructionRoute {
58+  switch (instruction.tool) {
59+    case "send":
60+      return {
61+        body: normalizeBrowserClaudeSendBody(instruction),
62+        key: "local.browser.claude.send",
63+        method: "POST",
64+        path: "/v1/browser/claude/send",
65+        requiresSharedToken: false
66+      };
67+    case "current":
68+      requireNoParams(instruction);
69+      return {
70+        body: null,
71+        key: "local.browser.claude.current",
72+        method: "GET",
73+        path: "/v1/browser/claude/current",
74+        requiresSharedToken: false
75+      };
76+    default:
77+      throw new BaaInstructionRouteError(
78+        instruction.blockIndex,
79+        `No Phase 1 route exists for ${instruction.target}::${instruction.tool}.`
80+      );
81+  }
82+}
83+
84+export function routeBaaInstruction(instruction: BaaInstructionEnvelope): BaaInstructionRoute {
85+  switch (instruction.target) {
86+    case "conductor":
87+    case "system":
88+      return routeLocalInstruction(instruction);
89+    case "browser.claude":
90+      return routeBrowserClaudeInstruction(instruction);
91+    default:
92+      throw new BaaInstructionRouteError(
93+        instruction.blockIndex,
94+        `No Phase 1 route exists for ${instruction.target}::${instruction.tool}.`
95       );
96   }
97 }