@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.
@@ -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 —
@@ -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 that's the only thing the daemon is allowed to author when
338
- // attached to a user-owned profile.
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
- // - 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)
@@ -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 — the profile is wholly owned
238
- // by the user, and AGENTS.md is written under the per-agent
239
- // hermes-workspace cwd (NOT into the profile root) by `prepareTurn`.
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 });
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.28",
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": {
@@ -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;
@@ -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 that's the only thing the daemon is allowed to author when
374
- // attached to a user-owned profile.
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 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);
@@ -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 — the profile is wholly owned
281
- // by the user, and AGENTS.md is written under the per-agent
282
- // hermes-workspace cwd (NOT into the profile root) by `prepareTurn`.
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
  // ---------------------------------------------------------------------------