@botcord/daemon 0.2.29 → 0.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,22 +88,13 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
88
88
  args.push("--resume", opts.sessionId);
89
89
  }
90
90
  // Permission-mode policy:
91
- // - owner: bypassPermissions (owner fully trusts their own agent; daemon
92
- // has no authorization-relay UI yet, so any other mode causes Bash /
93
- // WebFetch / MCP tool calls to deadlock waiting for a prompt that
94
- // never reaches the user see issue #332 for the planned MCP-bridge
95
- // relay that will let us tighten this back up).
96
- // - non-owner (trusted/public): default (let Claude Code prompt / reject
97
- // per its own rules — we must NOT auto-bypass for agents the operator
98
- // doesn't own).
99
- // `extraArgs` still wins — operators who know what they're doing can override either.
91
+ // Daemon-driven Claude Code runs are non-interactive. Any mode that waits
92
+ // for a local permission prompt can deadlock tool use (Bash / WebFetch /
93
+ // MCP) because there is no prompt relay back to the user yet. Default to
94
+ // bypassPermissions for every trust tier; operators who need a stricter
95
+ // posture can still override with route/defaultRoute extraArgs.
100
96
  if (!opts.extraArgs?.some((a) => a.startsWith("--permission-mode"))) {
101
- if (opts.trustLevel === "owner") {
102
- args.push("--permission-mode", "bypassPermissions");
103
- }
104
- else {
105
- args.push("--permission-mode", "default");
106
- }
97
+ args.push("--permission-mode", "bypassPermissions");
107
98
  }
108
99
  // Claude Code's `--append-system-prompt` is applied per invocation and NOT
109
100
  // persisted in the resumed session transcript — ideal for memory / digest
@@ -159,8 +159,12 @@ export class CodexAdapter extends NdjsonStreamAdapter {
159
159
  // Sandbox / approval policy. Expressed as `-c` overrides because
160
160
  // `codex exec resume` rejects `-s` / `--full-auto`. `-c` works on both
161
161
  // the fresh `exec` and `exec resume` paths.
162
- // - owner turn: bypass approvals + sandbox (owner trusts their agent)
163
- // - non-owner turn: `workspace-write` sandbox + on-request approvals
162
+ //
163
+ // Daemon-driven Codex runs are non-interactive. Any mode that waits for a
164
+ // local approval prompt can deadlock tool use because there is no prompt
165
+ // relay back to the user yet. Default to bypassing both approvals and the
166
+ // sandbox for every trust tier; operators who need a stricter posture can
167
+ // still override with route/defaultRoute extraArgs.
164
168
  const hasSandboxOverride = opts.extraArgs?.some((a) => a === "-s" ||
165
169
  a.startsWith("--sandbox") ||
166
170
  a === "--full-auto" ||
@@ -168,12 +172,7 @@ export class CodexAdapter extends NdjsonStreamAdapter {
168
172
  a.startsWith("-c sandbox_mode=") ||
169
173
  a.startsWith("-csandbox_mode=")) ?? false;
170
174
  if (!hasSandboxOverride) {
171
- if (opts.trustLevel === "owner") {
172
- tail.push("-c", 'sandbox_mode="danger-full-access"', "-c", 'approval_policy="never"');
173
- }
174
- else {
175
- tail.push("-c", 'sandbox_mode="workspace-write"', "-c", 'approval_policy="on-request"');
176
- }
175
+ tail.push("-c", 'sandbox_mode="danger-full-access"', "-c", 'approval_policy="never"');
177
176
  }
178
177
  tail.push("--skip-git-repo-check", "--json");
179
178
  if (opts.extraArgs?.length)
@@ -115,6 +115,9 @@ export type WsEndpointProbeFn = (args: {
115
115
  name?: string;
116
116
  provider?: string;
117
117
  };
118
+ botcordBinding?: {
119
+ agentId: string;
120
+ };
118
121
  }>;
119
122
  error?: string;
120
123
  }>;
package/dist/provision.js CHANGED
@@ -1457,7 +1457,7 @@ export async function collectRuntimeSnapshotAsync(opts = {}) {
1457
1457
  entry.diagnostics = [{ code: "gateway_unreachable", message: res.error }];
1458
1458
  }
1459
1459
  if (res.agents)
1460
- entry.agents = res.agents;
1460
+ entry.agents = annotateOpenclawAgentsWithBotcordBindings(g.name, res.agents);
1461
1461
  return entry;
1462
1462
  }
1463
1463
  catch (err) {
@@ -1476,6 +1476,27 @@ export async function collectRuntimeSnapshotAsync(opts = {}) {
1476
1476
  out.runtimes = base.runtimes.map((r) => r.id === "openclaw-acp" ? { ...r, endpoints } : r);
1477
1477
  return out;
1478
1478
  }
1479
+ function annotateOpenclawAgentsWithBotcordBindings(gateway, agents) {
1480
+ const bindings = openclawBindingIndex();
1481
+ return agents.map((agent) => {
1482
+ const botcordAgentId = bindings.get(`${gateway}\0${agent.id}`);
1483
+ if (!botcordAgentId)
1484
+ return agent;
1485
+ return { ...agent, botcordBinding: { agentId: botcordAgentId } };
1486
+ });
1487
+ }
1488
+ function openclawBindingIndex() {
1489
+ const out = new Map();
1490
+ const discovered = discoverAgentCredentials({
1491
+ credentialsDir: path.join(homedir(), ".botcord", "credentials"),
1492
+ });
1493
+ for (const agent of discovered.agents) {
1494
+ if (!agent.openclawGateway || !agent.openclawAgent)
1495
+ continue;
1496
+ out.set(`${agent.openclawGateway}\0${agent.openclawAgent}`, agent.agentId);
1497
+ }
1498
+ return out;
1499
+ }
1479
1500
  /**
1480
1501
  * Reconcile every agent identity carried by the `hello.agents` snapshot
1481
1502
  * against the on-disk `identity.md`. Best-effort: a malformed entry or a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -146,6 +146,54 @@ describe("collectRuntimeSnapshotAsync", () => {
146
146
  ]);
147
147
  });
148
148
 
149
+ it("marks OpenClaw agent profiles that already have a BotCord binding", async () => {
150
+ const tmp = mkdtempSync(path.join(tmpdir(), "daemon-runtime-binding-"));
151
+ const prevHome = process.env.HOME;
152
+ process.env.HOME = tmp;
153
+ try {
154
+ mkdirSync(path.join(tmp, ".botcord", "credentials"), { recursive: true });
155
+ writeFileSync(
156
+ path.join(tmp, ".botcord", "credentials", "ag_bound.json"),
157
+ JSON.stringify({
158
+ hubUrl: "https://api.preview.botcord.chat",
159
+ agentId: "ag_bound",
160
+ keyId: "kid_bound",
161
+ privateKey: Buffer.alloc(32, 1).toString("base64"),
162
+ runtime: "openclaw-acp",
163
+ openclawGateway: "local",
164
+ openclawAgent: "swe",
165
+ }),
166
+ );
167
+ setRuntimes([
168
+ {
169
+ id: "openclaw-acp",
170
+ displayName: "OpenClaw",
171
+ binary: "openclaw",
172
+ supportsRun: true,
173
+ result: { available: true },
174
+ },
175
+ ]);
176
+
177
+ const snap = await collectRuntimeSnapshotAsync({
178
+ cfg: { openclawGateways: [{ name: "local", url: "ws://127.0.0.1:18789" }] },
179
+ wsProbe: async () => ({
180
+ ok: true,
181
+ agents: [{ id: "default" }, { id: "swe", name: "SWE" }],
182
+ }),
183
+ });
184
+
185
+ const runtime = snap.runtimes.find((r) => r.id === "openclaw-acp");
186
+ expect(runtime?.endpoints?.[0]?.agents).toEqual([
187
+ { id: "default" },
188
+ { id: "swe", name: "SWE", botcordBinding: { agentId: "ag_bound" } },
189
+ ]);
190
+ } finally {
191
+ if (prevHome === undefined) delete process.env.HOME;
192
+ else process.env.HOME = prevHome;
193
+ rmSync(tmp, { recursive: true, force: true });
194
+ }
195
+ });
196
+
149
197
  it("reports acp_disabled without probing the gateway", async () => {
150
198
  const tmp = mkdtempSync(path.join(tmpdir(), "daemon-runtime-openclaw-"));
151
199
  const prevHome = process.env.HOME;
@@ -263,7 +263,7 @@ process.stdout.write(JSON.stringify({type:"result", subtype:"success", session_i
263
263
  expect(argv[modeIdx + 1]).toBe("bypassPermissions");
264
264
  });
265
265
 
266
- it("public → --permission-mode default", async () => {
266
+ it("public → --permission-mode bypassPermissions", async () => {
267
267
  const adapter = new ClaudeCodeAdapter({ binary: echoScript() });
268
268
  const ctrl = new AbortController();
269
269
  const res = await adapter.run({
@@ -277,10 +277,10 @@ process.stdout.write(JSON.stringify({type:"result", subtype:"success", session_i
277
277
  const argv = JSON.parse(res.text) as string[];
278
278
  const modeIdx = argv.indexOf("--permission-mode");
279
279
  expect(modeIdx).toBeGreaterThanOrEqual(0);
280
- expect(argv[modeIdx + 1]).toBe("default");
280
+ expect(argv[modeIdx + 1]).toBe("bypassPermissions");
281
281
  });
282
282
 
283
- it("trusted → --permission-mode default", async () => {
283
+ it("trusted → --permission-mode bypassPermissions", async () => {
284
284
  const adapter = new ClaudeCodeAdapter({ binary: echoScript() });
285
285
  const ctrl = new AbortController();
286
286
  const res = await adapter.run({
@@ -294,7 +294,7 @@ process.stdout.write(JSON.stringify({type:"result", subtype:"success", session_i
294
294
  const argv = JSON.parse(res.text) as string[];
295
295
  const modeIdx = argv.indexOf("--permission-mode");
296
296
  expect(modeIdx).toBeGreaterThanOrEqual(0);
297
- expect(argv[modeIdx + 1]).toBe("default");
297
+ expect(argv[modeIdx + 1]).toBe("bypassPermissions");
298
298
  });
299
299
 
300
300
  it("systemContext → --append-system-prompt <text>", async () => {
@@ -338,7 +338,7 @@ process.stdout.write(JSON.stringify({type:"item.completed", item:{id:"i0", type:
338
338
  expect(argv).not.toContain("-s");
339
339
  });
340
340
 
341
- it("public → sandbox_mode=\"workspace-write\" + approval_policy=\"on-request\"", async () => {
341
+ it("public → sandbox_mode=\"danger-full-access\" + approval_policy=\"never\"", async () => {
342
342
  const adapter = new CodexAdapter({ binary: echoScript() });
343
343
  const ctrl = new AbortController();
344
344
  const res = await adapter.run({
@@ -350,12 +350,12 @@ process.stdout.write(JSON.stringify({type:"item.completed", item:{id:"i0", type:
350
350
  trustLevel: "public",
351
351
  });
352
352
  const argv = JSON.parse(res.text) as string[];
353
- expect(argv).toContain('sandbox_mode="workspace-write"');
354
- expect(argv).toContain('approval_policy="on-request"');
353
+ expect(argv).toContain('sandbox_mode="danger-full-access"');
354
+ expect(argv).toContain('approval_policy="never"');
355
355
  expect(argv).not.toContain("--dangerously-bypass-approvals-and-sandbox");
356
356
  });
357
357
 
358
- it("trusted → sandbox_mode=\"workspace-write\" + approval_policy=\"on-request\"", async () => {
358
+ it("trusted → sandbox_mode=\"danger-full-access\" + approval_policy=\"never\"", async () => {
359
359
  const adapter = new CodexAdapter({ binary: echoScript() });
360
360
  const ctrl = new AbortController();
361
361
  const res = await adapter.run({
@@ -367,8 +367,8 @@ process.stdout.write(JSON.stringify({type:"item.completed", item:{id:"i0", type:
367
367
  trustLevel: "trusted",
368
368
  });
369
369
  const argv = JSON.parse(res.text) as string[];
370
- expect(argv).toContain('sandbox_mode="workspace-write"');
371
- expect(argv).toContain('approval_policy="on-request"');
370
+ expect(argv).toContain('sandbox_mode="danger-full-access"');
371
+ expect(argv).toContain('approval_policy="never"');
372
372
  });
373
373
 
374
374
  it("extraArgs `-s read-only` suppresses the default sandbox `-c`s", async () => {
@@ -107,21 +107,13 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
107
107
  args.push("--resume", opts.sessionId);
108
108
  }
109
109
  // Permission-mode policy:
110
- // - owner: bypassPermissions (owner fully trusts their own agent; daemon
111
- // has no authorization-relay UI yet, so any other mode causes Bash /
112
- // WebFetch / MCP tool calls to deadlock waiting for a prompt that
113
- // never reaches the user see issue #332 for the planned MCP-bridge
114
- // relay that will let us tighten this back up).
115
- // - non-owner (trusted/public): default (let Claude Code prompt / reject
116
- // per its own rules — we must NOT auto-bypass for agents the operator
117
- // doesn't own).
118
- // `extraArgs` still wins — operators who know what they're doing can override either.
110
+ // Daemon-driven Claude Code runs are non-interactive. Any mode that waits
111
+ // for a local permission prompt can deadlock tool use (Bash / WebFetch /
112
+ // MCP) because there is no prompt relay back to the user yet. Default to
113
+ // bypassPermissions for every trust tier; operators who need a stricter
114
+ // posture can still override with route/defaultRoute extraArgs.
119
115
  if (!opts.extraArgs?.some((a) => a.startsWith("--permission-mode"))) {
120
- if (opts.trustLevel === "owner") {
121
- args.push("--permission-mode", "bypassPermissions");
122
- } else {
123
- args.push("--permission-mode", "default");
124
- }
116
+ args.push("--permission-mode", "bypassPermissions");
125
117
  }
126
118
  // Claude Code's `--append-system-prompt` is applied per invocation and NOT
127
119
  // persisted in the resumed session transcript — ideal for memory / digest
@@ -171,8 +171,12 @@ export class CodexAdapter extends NdjsonStreamAdapter {
171
171
  // Sandbox / approval policy. Expressed as `-c` overrides because
172
172
  // `codex exec resume` rejects `-s` / `--full-auto`. `-c` works on both
173
173
  // the fresh `exec` and `exec resume` paths.
174
- // - owner turn: bypass approvals + sandbox (owner trusts their agent)
175
- // - non-owner turn: `workspace-write` sandbox + on-request approvals
174
+ //
175
+ // Daemon-driven Codex runs are non-interactive. Any mode that waits for a
176
+ // local approval prompt can deadlock tool use because there is no prompt
177
+ // relay back to the user yet. Default to bypassing both approvals and the
178
+ // sandbox for every trust tier; operators who need a stricter posture can
179
+ // still override with route/defaultRoute extraArgs.
176
180
  const hasSandboxOverride =
177
181
  opts.extraArgs?.some(
178
182
  (a) =>
@@ -184,21 +188,12 @@ export class CodexAdapter extends NdjsonStreamAdapter {
184
188
  a.startsWith("-csandbox_mode="),
185
189
  ) ?? false;
186
190
  if (!hasSandboxOverride) {
187
- if (opts.trustLevel === "owner") {
188
- tail.push(
189
- "-c",
190
- 'sandbox_mode="danger-full-access"',
191
- "-c",
192
- 'approval_policy="never"',
193
- );
194
- } else {
195
- tail.push(
196
- "-c",
197
- 'sandbox_mode="workspace-write"',
198
- "-c",
199
- 'approval_policy="on-request"',
200
- );
201
- }
191
+ tail.push(
192
+ "-c",
193
+ 'sandbox_mode="danger-full-access"',
194
+ "-c",
195
+ 'approval_policy="never"',
196
+ );
202
197
  }
203
198
  tail.push("--skip-git-repo-check", "--json");
204
199
  if (opts.extraArgs?.length) tail.push(...opts.extraArgs);
package/src/provision.ts CHANGED
@@ -1326,6 +1326,7 @@ export type WsEndpointProbeFn = (args: {
1326
1326
  name?: string;
1327
1327
  workspace?: string;
1328
1328
  model?: { name?: string; provider?: string };
1329
+ botcordBinding?: { agentId: string };
1329
1330
  }>;
1330
1331
  error?: string;
1331
1332
  }>;
@@ -1381,6 +1382,7 @@ async function defaultWsProbe(args: {
1381
1382
  name?: string;
1382
1383
  workspace?: string;
1383
1384
  model?: { name?: string; provider?: string };
1385
+ botcordBinding?: { agentId: string };
1384
1386
  }>;
1385
1387
  error?: string;
1386
1388
  }> {
@@ -1715,7 +1717,7 @@ export async function collectRuntimeSnapshotAsync(opts: {
1715
1717
  entry.error = res.error;
1716
1718
  entry.diagnostics = [{ code: "gateway_unreachable", message: res.error }];
1717
1719
  }
1718
- if (res.agents) entry.agents = res.agents;
1720
+ if (res.agents) entry.agents = annotateOpenclawAgentsWithBotcordBindings(g.name, res.agents);
1719
1721
  return entry;
1720
1722
  } catch (err) {
1721
1723
  const message = err instanceof Error ? err.message : String(err);
@@ -1737,6 +1739,41 @@ export async function collectRuntimeSnapshotAsync(opts: {
1737
1739
  return out;
1738
1740
  }
1739
1741
 
1742
+ function annotateOpenclawAgentsWithBotcordBindings(
1743
+ gateway: string,
1744
+ agents: Array<{
1745
+ id: string;
1746
+ name?: string;
1747
+ workspace?: string;
1748
+ model?: { name?: string; provider?: string };
1749
+ }>,
1750
+ ): Array<{
1751
+ id: string;
1752
+ name?: string;
1753
+ workspace?: string;
1754
+ model?: { name?: string; provider?: string };
1755
+ botcordBinding?: { agentId: string };
1756
+ }> {
1757
+ const bindings = openclawBindingIndex();
1758
+ return agents.map((agent) => {
1759
+ const botcordAgentId = bindings.get(`${gateway}\0${agent.id}`);
1760
+ if (!botcordAgentId) return agent;
1761
+ return { ...agent, botcordBinding: { agentId: botcordAgentId } };
1762
+ });
1763
+ }
1764
+
1765
+ function openclawBindingIndex(): Map<string, string> {
1766
+ const out = new Map<string, string>();
1767
+ const discovered = discoverAgentCredentials({
1768
+ credentialsDir: path.join(homedir(), ".botcord", "credentials"),
1769
+ });
1770
+ for (const agent of discovered.agents) {
1771
+ if (!agent.openclawGateway || !agent.openclawAgent) continue;
1772
+ out.set(`${agent.openclawGateway}\0${agent.openclawAgent}`, agent.agentId);
1773
+ }
1774
+ return out;
1775
+ }
1776
+
1740
1777
  // ---------------------------------------------------------------------------
1741
1778
  // hello agents snapshot (lightweight identity sync)
1742
1779
  // ---------------------------------------------------------------------------