- commit
- 54f1c61
- parent
- 4733478
- author
- codex@macbookpro
- date
- 2026-04-01 18:03:21 +0800 CST
feat: make baa instruction policy configurable
6 files changed,
+209,
-15
+86,
-0
1@@ -30,10 +30,12 @@ import {
2 PersistentBaaInstructionDeduper,
3 PersistentBaaLiveInstructionMessageDeduper,
4 PersistentBaaLiveInstructionSnapshotStore,
5+ createDefaultBaaInstructionPolicyTargetTools,
6 createArtifactStoreBrowserRequestPolicyPersistence,
7 createFetchControlApiClient,
8 createRenewalDispatcherRunner,
9 createRenewalProjectorRunner,
10+ evaluateBaaInstructionPolicy,
11 executeBaaInstruction,
12 extractBaaInstructionBlocks,
13 handleConductorHttpRequest,
14@@ -838,6 +840,26 @@ test("routeBaaInstruction applies a 60s default timeout to conductor exec", () =
15 });
16 });
17
18+test("evaluateBaaInstructionPolicy keeps the default Phase 1 policy behavior", () => {
19+ const allowed = evaluateBaaInstructionPolicy(createInstructionEnvelope({
20+ target: "browser.chatgpt",
21+ tool: "send"
22+ }));
23+ const denied = evaluateBaaInstructionPolicy(createInstructionEnvelope({
24+ target: "browser.chatgpt",
25+ tool: "reload"
26+ }));
27+
28+ assert.deepEqual(allowed, {
29+ code: null,
30+ message: null,
31+ ok: true
32+ });
33+ assert.equal(denied.ok, false);
34+ assert.equal(denied.code, "unsupported_tool");
35+ assert.match(denied.message ?? "", /not supported in Phase 1/i);
36+});
37+
38 test("executeBaaInstruction returns a structured timeout failure when the local handler stalls", async () => {
39 const { controlPlane, snapshot } = await createLocalApiFixture();
40 const instruction = createInstructionEnvelope({
41@@ -893,6 +915,70 @@ test("executeBaaInstruction returns a structured timeout failure when the local
42 }
43 });
44
45+test("BaaInstructionCenter applies custom policy overrides for target and tool allowlists", async () => {
46+ const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
47+ const targets = createDefaultBaaInstructionPolicyTargetTools();
48+ const chatgptTools = targets.get("browser.chatgpt");
49+ assert.ok(chatgptTools);
50+ targets.delete("browser.gemini");
51+ chatgptTools.add("reload");
52+ const center = new BaaInstructionCenter({
53+ localApiContext: {
54+ fetchImpl: globalThis.fetch,
55+ repository,
56+ sharedToken,
57+ snapshotLoader: () => snapshot
58+ },
59+ policy: {
60+ targets
61+ }
62+ });
63+ const message = [
64+ "```baa",
65+ "@conductor::describe",
66+ "```",
67+ "",
68+ "```baa",
69+ "@browser.chatgpt::reload",
70+ "```",
71+ "",
72+ "```baa",
73+ "@browser.gemini::send::hello gemini",
74+ "```"
75+ ].join("\n");
76+
77+ try {
78+ const result = await center.processAssistantMessage({
79+ assistantMessageId: "msg-custom-policy-1",
80+ conversationId: "conv-custom-policy-1",
81+ platform: "claude",
82+ text: message
83+ });
84+
85+ assert.equal(result.status, "executed");
86+ assert.equal(result.executions.length, 1);
87+ assert.equal(result.executions[0]?.tool, "describe");
88+ assert.equal(result.denied.length, 2);
89+
90+ const expandedToolDeny = result.denied.find(
91+ (entry) => entry.instruction.target === "browser.chatgpt" && entry.instruction.tool === "reload"
92+ );
93+ assert.ok(expandedToolDeny);
94+ assert.equal(expandedToolDeny.stage, "route");
95+ assert.equal(expandedToolDeny.code, null);
96+ assert.match(expandedToolDeny.reason, /No Phase 1 route exists/i);
97+
98+ const removedTargetDeny = result.denied.find(
99+ (entry) => entry.instruction.target === "browser.gemini" && entry.instruction.tool === "send"
100+ );
101+ assert.ok(removedTargetDeny);
102+ assert.equal(removedTargetDeny.stage, "policy");
103+ assert.equal(removedTargetDeny.code, "unsupported_target");
104+ } finally {
105+ controlPlane.close();
106+ }
107+});
108+
109 test("BaaInstructionCenter runs the Phase 1 execution loop and dedupes replayed messages", async () => {
110 const { controlPlane, repository, sharedToken, snapshot } = await createLocalApiFixture();
111 const hostOpsDir = mkdtempSync(join(tmpdir(), "baa-instruction-center-"));
1@@ -11,6 +11,7 @@ import {
2 BaaInstructionCenterError,
3 type BaaInstructionCenterOptions
4 } from "./loop.js";
5+import type { BaaInstructionPolicyConfig } from "./policy.js";
6 import type { BaaLiveInstructionSnapshotStore } from "./store.js";
7 import type {
8 BaaInstructionParseErrorStage,
9@@ -110,6 +111,7 @@ export interface BaaLiveInstructionIngestOptions {
10 localApiContext?: ConductorLocalApiContext;
11 messageDeduper?: BaaLiveInstructionMessageDeduper;
12 now?: () => number;
13+ policy?: BaaInstructionPolicyConfig | null;
14 snapshotStore?: BaaLiveInstructionSnapshotStore | null;
15 }
16
17@@ -283,7 +285,8 @@ export class BaaLiveInstructionIngest {
18 this.center =
19 options.center
20 ?? new BaaInstructionCenter({
21- localApiContext: options.localApiContext as BaaInstructionCenterOptions["localApiContext"]
22+ localApiContext: options.localApiContext as BaaInstructionCenterOptions["localApiContext"],
23+ policy: options.policy ?? options.localApiContext?.instructionPolicy
24 });
25 this.artifactStore = options.localApiContext?.artifactStore ?? null;
26 this.historyLimit = normalizeHistoryLimit(options.historyLimit);
1@@ -15,7 +15,12 @@ import {
2 normalizeBaaInstructionSourceMessage
3 } from "./normalize.js";
4 import { parseBaaInstructionBlock } from "./parse.js";
5-import { evaluateBaaInstructionPolicy } from "./policy.js";
6+import {
7+ evaluateBaaInstructionPolicy,
8+ resolveBaaInstructionPolicy,
9+ type BaaInstructionPolicy,
10+ type BaaInstructionPolicyConfig
11+} from "./policy.js";
12 import { isAutomationControlInstruction, routeBaaInstruction } from "./router.js";
13 import type {
14 BaaAssistantMessageInput,
15@@ -42,6 +47,7 @@ export class BaaInstructionCenterError extends Error {
16 export interface BaaInstructionCenterOptions {
17 deduper?: BaaInstructionDeduper;
18 localApiContext: ConductorLocalApiContext;
19+ policy?: BaaInstructionPolicyConfig | null;
20 }
21
22 export interface BaaInstructionProcessOptions {
23@@ -53,10 +59,12 @@ export interface BaaInstructionProcessOptions {
24 export class BaaInstructionCenter {
25 private readonly deduper: BaaInstructionDeduper;
26 private readonly localApiContext: ConductorLocalApiContext;
27+ private readonly policy: BaaInstructionPolicy;
28
29 constructor(options: BaaInstructionCenterOptions) {
30 this.deduper = options.deduper ?? new InMemoryBaaInstructionDeduper();
31 this.localApiContext = options.localApiContext;
32+ this.policy = resolveBaaInstructionPolicy(options.policy ?? options.localApiContext.instructionPolicy);
33 }
34
35 async processAssistantMessage(
36@@ -320,7 +328,7 @@ export class BaaInstructionCenter {
37 const routed: Array<{ instruction: BaaInstructionEnvelope; route: BaaInstructionRoute }> = [];
38
39 for (const instruction of instructions) {
40- const decision = evaluateBaaInstructionPolicy(instruction);
41+ const decision = evaluateBaaInstructionPolicy(instruction, this.policy);
42
43 if (!decision.ok) {
44 denied.push({
1@@ -1,6 +1,6 @@
2 import type { BaaInstructionEnvelope } from "./types.js";
3
4-const CONDUCTOR_TOOLS = new Set([
5+const CONDUCTOR_TOOLS = [
6 "conversation/mode",
7 "conversation/pause",
8 "conversation/resume",
9@@ -13,12 +13,12 @@ const CONDUCTOR_TOOLS = new Set([
10 "status",
11 "system/pause",
12 "system/resume"
13-]);
14-const BROWSER_LEGACY_TARGET_TOOLS = new Set([
15+];
16+const BROWSER_LEGACY_TARGET_TOOLS = [
17 "current",
18 "send"
19-]);
20-const SUPPORTED_TARGET_TOOLS = new Map([
21+];
22+const DEFAULT_TARGET_TOOLS = new Map([
23 ["conductor", CONDUCTOR_TOOLS],
24 ["system", CONDUCTOR_TOOLS],
25 ["browser.claude", BROWSER_LEGACY_TARGET_TOOLS],
26@@ -26,16 +26,100 @@ const SUPPORTED_TARGET_TOOLS = new Map([
27 ["browser.gemini", BROWSER_LEGACY_TARGET_TOOLS]
28 ]);
29
30+export type BaaInstructionPolicyTargetToolsInput =
31+ | ReadonlyMap<string, Iterable<string> | null | undefined>
32+ | Record<string, Iterable<string> | null | undefined>;
33+
34+export interface BaaInstructionPolicyConfig {
35+ targets?: BaaInstructionPolicyTargetToolsInput | null;
36+}
37+
38+export interface BaaInstructionPolicy {
39+ targets: ReadonlyMap<string, ReadonlySet<string>>;
40+}
41+
42 export interface BaaInstructionPolicyDecision {
43 code: string | null;
44 message: string | null;
45 ok: boolean;
46 }
47
48+const DEFAULT_BAA_INSTRUCTION_POLICY = resolveBaaInstructionPolicy({
49+ targets: createDefaultBaaInstructionPolicyTargetTools()
50+});
51+
52+function normalizePolicyIdentifier(kind: "target" | "tool", value: string): string {
53+ const normalized = value.trim();
54+
55+ if (normalized === "") {
56+ throw new TypeError(`BAA instruction policy ${kind} names must be non-empty strings.`);
57+ }
58+
59+ return normalized;
60+}
61+
62+function normalizePolicyToolSet(
63+ target: string,
64+ tools: Iterable<string>
65+): ReadonlySet<string> {
66+ const normalizedTools = new Set<string>();
67+
68+ for (const tool of tools) {
69+ if (typeof tool !== "string") {
70+ throw new TypeError(`BAA instruction policy tools for "${target}" must be strings.`);
71+ }
72+
73+ normalizedTools.add(normalizePolicyIdentifier("tool", tool));
74+ }
75+
76+ return normalizedTools;
77+}
78+
79+function iteratePolicyTargetTools(
80+ input: BaaInstructionPolicyTargetToolsInput
81+): Iterable<[string, Iterable<string> | null | undefined]> {
82+ if (input instanceof Map) {
83+ return input.entries();
84+ }
85+
86+ return Object.entries(input);
87+}
88+
89+export function createDefaultBaaInstructionPolicyTargetTools(): Map<string, Set<string>> {
90+ return new Map(
91+ [...DEFAULT_TARGET_TOOLS.entries()].map(([target, tools]) => [target, new Set(tools)])
92+ );
93+}
94+
95+export function resolveBaaInstructionPolicy(
96+ config: BaaInstructionPolicyConfig | null | undefined = null
97+): BaaInstructionPolicy {
98+ const targetsInput = config?.targets ?? createDefaultBaaInstructionPolicyTargetTools();
99+ const targets = new Map<string, ReadonlySet<string>>();
100+
101+ for (const [target, tools] of iteratePolicyTargetTools(targetsInput)) {
102+ if (typeof target !== "string") {
103+ throw new TypeError("BAA instruction policy target names must be strings.");
104+ }
105+
106+ if (tools == null) {
107+ continue;
108+ }
109+
110+ const normalizedTarget = normalizePolicyIdentifier("target", target);
111+ targets.set(normalizedTarget, normalizePolicyToolSet(normalizedTarget, tools));
112+ }
113+
114+ return {
115+ targets
116+ };
117+}
118+
119 export function evaluateBaaInstructionPolicy(
120- instruction: BaaInstructionEnvelope
121+ instruction: BaaInstructionEnvelope,
122+ policy: BaaInstructionPolicy = DEFAULT_BAA_INSTRUCTION_POLICY
123 ): BaaInstructionPolicyDecision {
124- const supportedTools = SUPPORTED_TARGET_TOOLS.get(instruction.target);
125+ const supportedTools = policy.targets.get(instruction.target);
126
127 if (!supportedTools) {
128 return {
1@@ -82,6 +82,7 @@ import {
2 listRenewalConversationDetails,
3 setRenewalConversationAutomationStatus
4 } from "./renewal/conversations.js";
5+import type { BaaInstructionPolicyConfig } from "./instructions/policy.js";
6
7 interface FileStatsLike {
8 isDirectory(): boolean;
9@@ -354,6 +355,7 @@ export interface ConductorLocalApiContext {
10 browserStateLoader?: (() => BrowserBridgeStateSnapshot | null) | null;
11 codexdLocalApiBase?: string | null;
12 fetchImpl?: typeof fetch;
13+ instructionPolicy?: BaaInstructionPolicyConfig | null;
14 now?: () => number;
15 repository: ControlPlaneRepository | null;
16 sharedToken?: string | null;
+16,
-5
1@@ -2,7 +2,7 @@
2
3 ## 状态
4
5-- 当前状态:`待开始`
6+- 当前状态:`已完成`
7 - 规模预估:`M`
8 - 依赖任务:无
9 - 建议执行者:`Codex`
10@@ -117,21 +117,32 @@
11
12 ### 开始执行
13
14-- 执行者:
15-- 开始时间:
16+- 执行者:`Codex`
17+- 开始时间:`2026-04-01 17:40:00 +0800`
18 - 状态变更:`待开始` → `进行中`
19
20 ### 完成摘要
21
22-- 完成时间:
23+- 完成时间:`2026-04-01 18:02:34 +0800`
24 - 状态变更:`进行中` → `已完成`
25 - 修改了哪些文件:
26+ - `apps/conductor-daemon/src/instructions/policy.ts`
27+ - `apps/conductor-daemon/src/instructions/loop.ts`
28+ - `apps/conductor-daemon/src/instructions/ingest.ts`
29+ - `apps/conductor-daemon/src/local-api.ts`
30+ - `apps/conductor-daemon/src/index.test.js`
31+ - `tasks/T-S065.md`
32 - 核心实现思路:
33+ - 把硬编码 target/tool 白名单抽成 `policy` 配置结构,提供默认 policy 克隆入口和解析函数。
34+ - 在 `BaaInstructionCenter` 构造阶段支持注入 policy,并在 `BaaLiveInstructionIngest` 初始化阶段支持把 policy 继续透传到内部 center。
35+ - 为 `ConductorLocalApiContext` 增加可选 `instructionPolicy` 扩展点,保持默认行为不变,同时给后续 automation control / 新 target/tool 扩面留入口。
36+ - 补测试覆盖默认 policy 主线行为,以及自定义 policy 对 target/tool allowlist 的收缩与扩展。
37 - 跑了哪些测试:
38+ - `pnpm -C /Users/george/code/baa-conductor-policy-configurable/apps/conductor-daemon test`
39
40 ### 执行过程中遇到的问题
41
42-- 暂无
43+- 新 worktree 初始没有 `node_modules`,先执行 `pnpm -C /Users/george/code/baa-conductor-policy-configurable install --frozen-lockfile` 后再跑测试。
44
45 ### 剩余风险
46