@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.
- package/dist/gateway/runtimes/claude-code.js +6 -15
- package/dist/gateway/runtimes/codex.js +7 -8
- package/dist/provision.d.ts +3 -0
- package/dist/provision.js +22 -1
- package/package.json +1 -1
- package/src/__tests__/runtime-discovery.test.ts +48 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +4 -4
- package/src/gateway/__tests__/codex-adapter.test.ts +6 -6
- package/src/gateway/runtimes/claude-code.ts +6 -14
- package/src/gateway/runtimes/codex.ts +12 -17
- package/src/provision.ts +38 -1
|
@@ -88,22 +88,13 @@ export class ClaudeCodeAdapter extends NdjsonStreamAdapter {
|
|
|
88
88
|
args.push("--resume", opts.sessionId);
|
|
89
89
|
}
|
|
90
90
|
// Permission-mode policy:
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
163
|
-
//
|
|
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
|
-
|
|
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)
|
package/dist/provision.d.ts
CHANGED
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
|
@@ -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
|
|
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("
|
|
280
|
+
expect(argv[modeIdx + 1]).toBe("bypassPermissions");
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
-
it("trusted → --permission-mode
|
|
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("
|
|
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=\"
|
|
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="
|
|
354
|
-
expect(argv).toContain('approval_policy="
|
|
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=\"
|
|
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="
|
|
371
|
-
expect(argv).toContain('approval_policy="
|
|
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
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
175
|
-
//
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|