@botcord/daemon 0.2.28 → 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/agent-workspace.d.ts +8 -0
- package/dist/agent-workspace.js +12 -2
- package/dist/gateway/runtimes/claude-code.js +6 -15
- package/dist/gateway/runtimes/codex.js +7 -8
- package/dist/gateway/runtimes/hermes-agent.js +8 -4
- package/dist/provision.d.ts +3 -0
- package/dist/provision.js +26 -2
- package/package.json +1 -1
- package/src/__tests__/agent-workspace.test.ts +18 -0
- package/src/__tests__/provision.test.ts +5 -0
- package/src/__tests__/runtime-discovery.test.ts +48 -0
- package/src/agent-workspace.ts +13 -2
- 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/gateway/runtimes/hermes-agent.ts +8 -3
- package/src/provision.ts +42 -1
|
@@ -54,6 +54,14 @@ export declare function ensureAgentHermesWorkspace(agentId: string, opts?: {
|
|
|
54
54
|
hermesHome: string;
|
|
55
55
|
hermesWorkspace: string;
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Seed BotCord's bundled Hermes skills into a user-owned Hermes profile used
|
|
59
|
+
* by attach mode. Unlike `ensureAgentHermesWorkspace({ attached: true })`,
|
|
60
|
+
* this intentionally writes only the managed `botcord*` skill directories
|
|
61
|
+
* under the profile's `skills/` directory; it does not touch `.env`,
|
|
62
|
+
* `config.yaml`, sessions, or any user-authored skills.
|
|
63
|
+
*/
|
|
64
|
+
export declare function ensureAttachedHermesProfileSkills(profileHome: string): void;
|
|
57
65
|
/**
|
|
58
66
|
* Idempotently create the agent's home / workspace / state directories and
|
|
59
67
|
* seed the workspace Markdown files. Existing files are never overwritten —
|
package/dist/agent-workspace.js
CHANGED
|
@@ -334,8 +334,8 @@ export function ensureAgentHermesWorkspace(agentId, opts = {}) {
|
|
|
334
334
|
// Attach mode: HERMES_HOME points at the user's `~/.hermes/profiles/<n>/`
|
|
335
335
|
// so we MUST NOT touch the per-agent isolated home. The cwd
|
|
336
336
|
// (`hermesWorkspace`) is still ours and `prepareTurn` writes AGENTS.md
|
|
337
|
-
// there
|
|
338
|
-
//
|
|
337
|
+
// there. Profile-owned skill seeding is handled separately by
|
|
338
|
+
// `ensureAttachedHermesProfileSkills`.
|
|
339
339
|
if (opts.attached) {
|
|
340
340
|
return { hermesHome, hermesWorkspace };
|
|
341
341
|
}
|
|
@@ -420,6 +420,16 @@ function seedCodexSkills(codexHome) {
|
|
|
420
420
|
function seedHermesAgentSkills(hermesHome) {
|
|
421
421
|
copyBundledSkills(path.join(hermesHome, "skills"));
|
|
422
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Seed BotCord's bundled Hermes skills into a user-owned Hermes profile used
|
|
425
|
+
* by attach mode. Unlike `ensureAgentHermesWorkspace({ attached: true })`,
|
|
426
|
+
* this intentionally writes only the managed `botcord*` skill directories
|
|
427
|
+
* under the profile's `skills/` directory; it does not touch `.env`,
|
|
428
|
+
* `config.yaml`, sessions, or any user-authored skills.
|
|
429
|
+
*/
|
|
430
|
+
export function ensureAttachedHermesProfileSkills(profileHome) {
|
|
431
|
+
seedHermesAgentSkills(profileHome);
|
|
432
|
+
}
|
|
423
433
|
/**
|
|
424
434
|
* Idempotently create the agent's home / workspace / state directories and
|
|
425
435
|
* seed the workspace Markdown files. Existing files are never overwritten —
|
|
@@ -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)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { agentHermesHomeDir, agentHermesWorkspaceDir, ensureAgentHermesWorkspace, } from "../../agent-workspace.js";
|
|
4
|
+
import { agentHermesHomeDir, agentHermesWorkspaceDir, ensureAttachedHermesProfileSkills, ensureAgentHermesWorkspace, } from "../../agent-workspace.js";
|
|
5
5
|
import { buildCliEnv } from "../cli-resolver.js";
|
|
6
6
|
import { AcpRuntimeAdapter, } from "./acp-stream.js";
|
|
7
7
|
import { firstExistingPath, readCommandVersion, resolveCommandOnPath, resolveHomePath, } from "./probe.js";
|
|
@@ -234,9 +234,10 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
234
234
|
};
|
|
235
235
|
// Attach mode: BotCord agent shares a hermes profile (state.db /
|
|
236
236
|
// sessions / skills / .env) with the user's command-line `hermes`. In
|
|
237
|
-
// this mode we DO NOT seed a private home —
|
|
238
|
-
//
|
|
239
|
-
//
|
|
237
|
+
// this mode we DO NOT seed a private home — AGENTS.md is written under
|
|
238
|
+
// the per-agent hermes-workspace cwd (NOT into the profile root) by
|
|
239
|
+
// `prepareTurn`, while bundled BotCord skills are installed into the
|
|
240
|
+
// attached profile's `skills/` directory so hermes can discover them.
|
|
240
241
|
if (opts.hermesProfile) {
|
|
241
242
|
env.HERMES_HOME = hermesProfileHomeDir(opts.hermesProfile);
|
|
242
243
|
}
|
|
@@ -262,6 +263,9 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
262
263
|
const { hermesWorkspace } = ensureAgentHermesWorkspace(opts.accountId, {
|
|
263
264
|
attached: !!opts.hermesProfile,
|
|
264
265
|
});
|
|
266
|
+
if (opts.hermesProfile) {
|
|
267
|
+
ensureAttachedHermesProfileSkills(hermesProfileHomeDir(opts.hermesProfile));
|
|
268
|
+
}
|
|
265
269
|
const target = path.join(hermesWorkspace, "AGENTS.md");
|
|
266
270
|
const tmp = path.join(hermesWorkspace, `.AGENTS.md.${process.pid}.tmp`);
|
|
267
271
|
mkdirSync(hermesWorkspace, { recursive: true, mode: 0o700 });
|
package/dist/provision.d.ts
CHANGED
package/dist/provision.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from "node:path";
|
|
|
10
10
|
import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, writeCredentialsFile, } from "@botcord/protocol-core";
|
|
11
11
|
import { loadConfig, resolveConfiguredAgentIds, saveConfig, } from "./config.js";
|
|
12
12
|
import { BOTCORD_CHANNEL_TYPE, buildManagedRoutes, prepareGatewayProfile, } from "./daemon-config-map.js";
|
|
13
|
-
import { agentHomeDir, agentStateDir, agentWorkspaceDir, applyAgentIdentity, ensureAgentWorkspace, } from "./agent-workspace.js";
|
|
13
|
+
import { agentHomeDir, agentStateDir, agentWorkspaceDir, applyAgentIdentity, ensureAttachedHermesProfileSkills, ensureAgentWorkspace, } from "./agent-workspace.js";
|
|
14
14
|
import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
|
|
15
15
|
import { hermesProfileHomeDir, isValidHermesProfileName, listHermesProfiles, } from "./gateway/runtimes/hermes-agent.js";
|
|
16
16
|
import { log as daemonLog } from "./log.js";
|
|
@@ -285,6 +285,9 @@ async function installLocalAgent(credentials, ctx) {
|
|
|
285
285
|
keyId: credentials.keyId,
|
|
286
286
|
savedAt: credentials.savedAt,
|
|
287
287
|
});
|
|
288
|
+
if (credentials.runtime === "hermes-agent" && credentials.hermesProfile) {
|
|
289
|
+
ensureAttachedHermesProfileSkills(hermesProfileHomeDir(credentials.hermesProfile));
|
|
290
|
+
}
|
|
288
291
|
}
|
|
289
292
|
catch (err) {
|
|
290
293
|
try {
|
|
@@ -1454,7 +1457,7 @@ export async function collectRuntimeSnapshotAsync(opts = {}) {
|
|
|
1454
1457
|
entry.diagnostics = [{ code: "gateway_unreachable", message: res.error }];
|
|
1455
1458
|
}
|
|
1456
1459
|
if (res.agents)
|
|
1457
|
-
entry.agents = res.agents;
|
|
1460
|
+
entry.agents = annotateOpenclawAgentsWithBotcordBindings(g.name, res.agents);
|
|
1458
1461
|
return entry;
|
|
1459
1462
|
}
|
|
1460
1463
|
catch (err) {
|
|
@@ -1473,6 +1476,27 @@ export async function collectRuntimeSnapshotAsync(opts = {}) {
|
|
|
1473
1476
|
out.runtimes = base.runtimes.map((r) => r.id === "openclaw-acp" ? { ...r, endpoints } : r);
|
|
1474
1477
|
return out;
|
|
1475
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
|
+
}
|
|
1476
1500
|
/**
|
|
1477
1501
|
* Reconcile every agent identity carried by the `hello.agents` snapshot
|
|
1478
1502
|
* against the on-disk `identity.md`. Best-effort: a malformed entry or a
|
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
agentStateDir,
|
|
19
19
|
agentWorkspaceDir,
|
|
20
20
|
applyAgentIdentity,
|
|
21
|
+
ensureAttachedHermesProfileSkills,
|
|
21
22
|
ensureAgentCodexHome,
|
|
22
23
|
ensureAgentHermesWorkspace,
|
|
23
24
|
ensureAgentWorkspace,
|
|
@@ -150,6 +151,23 @@ describe("ensureAgentWorkspace", () => {
|
|
|
150
151
|
expect(reseeded).toContain("name: botcord");
|
|
151
152
|
});
|
|
152
153
|
|
|
154
|
+
it("seeds bundled skills into an attached Hermes profile without creating private home state", () => {
|
|
155
|
+
const profileHome = path.join(tmpHome, ".hermes", "profiles", "coder");
|
|
156
|
+
mkdirSync(profileHome, { recursive: true });
|
|
157
|
+
|
|
158
|
+
const { hermesHome, hermesWorkspace } = ensureAgentHermesWorkspace("ag_hermes_attach", {
|
|
159
|
+
attached: true,
|
|
160
|
+
});
|
|
161
|
+
ensureAttachedHermesProfileSkills(profileHome);
|
|
162
|
+
|
|
163
|
+
expect(existsSync(path.join(profileHome, "skills", "botcord", "SKILL.md"))).toBe(true);
|
|
164
|
+
expect(existsSync(path.join(profileHome, "skills", "botcord-user-guide", "SKILL.md"))).toBe(
|
|
165
|
+
true,
|
|
166
|
+
);
|
|
167
|
+
expect(existsSync(hermesWorkspace)).toBe(true);
|
|
168
|
+
expect(existsSync(hermesHome)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
153
171
|
it("does not overwrite a user-modified memory.md on a second call", () => {
|
|
154
172
|
ensureAgentWorkspace("ag_keep", {});
|
|
155
173
|
const memoryPath = path.join(agentWorkspaceDir("ag_keep"), "memory.md");
|
|
@@ -1527,6 +1527,11 @@ describe("provision_agent hermes profile attach", () => {
|
|
|
1527
1527
|
>;
|
|
1528
1528
|
expect(saved.hermesProfile).toBe("coder");
|
|
1529
1529
|
expect(saved.runtime).toBe("hermes-agent");
|
|
1530
|
+
expect(
|
|
1531
|
+
fs.existsSync(
|
|
1532
|
+
nodePath.join(tmp, ".hermes", "profiles", "coder", "skills", "botcord", "SKILL.md"),
|
|
1533
|
+
),
|
|
1534
|
+
).toBe(true);
|
|
1530
1535
|
});
|
|
1531
1536
|
});
|
|
1532
1537
|
|
|
@@ -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;
|
package/src/agent-workspace.ts
CHANGED
|
@@ -370,8 +370,8 @@ export function ensureAgentHermesWorkspace(
|
|
|
370
370
|
// Attach mode: HERMES_HOME points at the user's `~/.hermes/profiles/<n>/`
|
|
371
371
|
// so we MUST NOT touch the per-agent isolated home. The cwd
|
|
372
372
|
// (`hermesWorkspace`) is still ours and `prepareTurn` writes AGENTS.md
|
|
373
|
-
// there
|
|
374
|
-
//
|
|
373
|
+
// there. Profile-owned skill seeding is handled separately by
|
|
374
|
+
// `ensureAttachedHermesProfileSkills`.
|
|
375
375
|
if (opts.attached) {
|
|
376
376
|
return { hermesHome, hermesWorkspace };
|
|
377
377
|
}
|
|
@@ -462,6 +462,17 @@ function seedHermesAgentSkills(hermesHome: string): void {
|
|
|
462
462
|
copyBundledSkills(path.join(hermesHome, "skills"));
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
/**
|
|
466
|
+
* Seed BotCord's bundled Hermes skills into a user-owned Hermes profile used
|
|
467
|
+
* by attach mode. Unlike `ensureAgentHermesWorkspace({ attached: true })`,
|
|
468
|
+
* this intentionally writes only the managed `botcord*` skill directories
|
|
469
|
+
* under the profile's `skills/` directory; it does not touch `.env`,
|
|
470
|
+
* `config.yaml`, sessions, or any user-authored skills.
|
|
471
|
+
*/
|
|
472
|
+
export function ensureAttachedHermesProfileSkills(profileHome: string): void {
|
|
473
|
+
seedHermesAgentSkills(profileHome);
|
|
474
|
+
}
|
|
475
|
+
|
|
465
476
|
/**
|
|
466
477
|
* Idempotently create the agent's home / workspace / state directories and
|
|
467
478
|
* seed the workspace Markdown files. Existing files are never overwritten —
|
|
@@ -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);
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import {
|
|
5
5
|
agentHermesHomeDir,
|
|
6
6
|
agentHermesWorkspaceDir,
|
|
7
|
+
ensureAttachedHermesProfileSkills,
|
|
7
8
|
ensureAgentHermesWorkspace,
|
|
8
9
|
} from "../../agent-workspace.js";
|
|
9
10
|
import { buildCliEnv } from "../cli-resolver.js";
|
|
@@ -277,9 +278,10 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
277
278
|
};
|
|
278
279
|
// Attach mode: BotCord agent shares a hermes profile (state.db /
|
|
279
280
|
// sessions / skills / .env) with the user's command-line `hermes`. In
|
|
280
|
-
// this mode we DO NOT seed a private home —
|
|
281
|
-
//
|
|
282
|
-
//
|
|
281
|
+
// this mode we DO NOT seed a private home — AGENTS.md is written under
|
|
282
|
+
// the per-agent hermes-workspace cwd (NOT into the profile root) by
|
|
283
|
+
// `prepareTurn`, while bundled BotCord skills are installed into the
|
|
284
|
+
// attached profile's `skills/` directory so hermes can discover them.
|
|
283
285
|
if (opts.hermesProfile) {
|
|
284
286
|
env.HERMES_HOME = hermesProfileHomeDir(opts.hermesProfile);
|
|
285
287
|
} else if (opts.accountId) {
|
|
@@ -304,6 +306,9 @@ export class HermesAgentAdapter extends AcpRuntimeAdapter {
|
|
|
304
306
|
const { hermesWorkspace } = ensureAgentHermesWorkspace(opts.accountId, {
|
|
305
307
|
attached: !!opts.hermesProfile,
|
|
306
308
|
});
|
|
309
|
+
if (opts.hermesProfile) {
|
|
310
|
+
ensureAttachedHermesProfileSkills(hermesProfileHomeDir(opts.hermesProfile));
|
|
311
|
+
}
|
|
307
312
|
const target = path.join(hermesWorkspace, "AGENTS.md");
|
|
308
313
|
const tmp = path.join(hermesWorkspace, `.AGENTS.md.${process.pid}.tmp`);
|
|
309
314
|
mkdirSync(hermesWorkspace, { recursive: true, mode: 0o700 });
|
package/src/provision.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
agentStateDir,
|
|
53
53
|
agentWorkspaceDir,
|
|
54
54
|
applyAgentIdentity,
|
|
55
|
+
ensureAttachedHermesProfileSkills,
|
|
55
56
|
ensureAgentWorkspace,
|
|
56
57
|
} from "./agent-workspace.js";
|
|
57
58
|
import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
|
|
@@ -431,6 +432,9 @@ async function installLocalAgent(
|
|
|
431
432
|
keyId: credentials.keyId,
|
|
432
433
|
savedAt: credentials.savedAt,
|
|
433
434
|
});
|
|
435
|
+
if (credentials.runtime === "hermes-agent" && credentials.hermesProfile) {
|
|
436
|
+
ensureAttachedHermesProfileSkills(hermesProfileHomeDir(credentials.hermesProfile));
|
|
437
|
+
}
|
|
434
438
|
} catch (err) {
|
|
435
439
|
try {
|
|
436
440
|
unlinkSync(credentialsFile);
|
|
@@ -1322,6 +1326,7 @@ export type WsEndpointProbeFn = (args: {
|
|
|
1322
1326
|
name?: string;
|
|
1323
1327
|
workspace?: string;
|
|
1324
1328
|
model?: { name?: string; provider?: string };
|
|
1329
|
+
botcordBinding?: { agentId: string };
|
|
1325
1330
|
}>;
|
|
1326
1331
|
error?: string;
|
|
1327
1332
|
}>;
|
|
@@ -1377,6 +1382,7 @@ async function defaultWsProbe(args: {
|
|
|
1377
1382
|
name?: string;
|
|
1378
1383
|
workspace?: string;
|
|
1379
1384
|
model?: { name?: string; provider?: string };
|
|
1385
|
+
botcordBinding?: { agentId: string };
|
|
1380
1386
|
}>;
|
|
1381
1387
|
error?: string;
|
|
1382
1388
|
}> {
|
|
@@ -1711,7 +1717,7 @@ export async function collectRuntimeSnapshotAsync(opts: {
|
|
|
1711
1717
|
entry.error = res.error;
|
|
1712
1718
|
entry.diagnostics = [{ code: "gateway_unreachable", message: res.error }];
|
|
1713
1719
|
}
|
|
1714
|
-
if (res.agents) entry.agents = res.agents;
|
|
1720
|
+
if (res.agents) entry.agents = annotateOpenclawAgentsWithBotcordBindings(g.name, res.agents);
|
|
1715
1721
|
return entry;
|
|
1716
1722
|
} catch (err) {
|
|
1717
1723
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1733,6 +1739,41 @@ export async function collectRuntimeSnapshotAsync(opts: {
|
|
|
1733
1739
|
return out;
|
|
1734
1740
|
}
|
|
1735
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
|
+
|
|
1736
1777
|
// ---------------------------------------------------------------------------
|
|
1737
1778
|
// hello agents snapshot (lightweight identity sync)
|
|
1738
1779
|
// ---------------------------------------------------------------------------
|