@botcord/daemon 0.2.54 → 0.2.56

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.
@@ -15,6 +15,69 @@ import type { RuntimeProbeResult, RuntimeRunOptions, StreamBlock } from "../type
15
15
  const CODEX_DESKTOP_BUNDLE_PATH = "/Applications/Codex.app/Contents/Resources/codex";
16
16
  /** Codex UUIDv7 / v4 session ids are 36-char dashed hex; reject anything else to keep argv safe. */
17
17
  const CODEX_SESSION_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
18
+ const CODEX_FOREIGN_EXTRA_FLAGS_WITH_VALUE = new Set([
19
+ "--append-system-prompt",
20
+ "--permission-mode",
21
+ ]);
22
+ const CODEX_SANDBOX_MODES = new Set(["read-only", "workspace-write", "danger-full-access"]);
23
+
24
+ function extraFlagName(arg: string): string {
25
+ if (!arg.startsWith("-")) return arg;
26
+ const eq = arg.indexOf("=");
27
+ return eq === -1 ? arg : arg.slice(0, eq);
28
+ }
29
+
30
+ function nextExtraValue(args: string[], index: number): string | undefined {
31
+ const next = args[index + 1];
32
+ if (typeof next !== "string") return undefined;
33
+ if (!next.startsWith("-")) return next;
34
+ return /^-\d/.test(next) ? next : undefined;
35
+ }
36
+
37
+ function sanitizeCodexExtraArgs(extraArgs: string[] | undefined): string[] {
38
+ if (!extraArgs?.length) return [];
39
+ const out: string[] = [];
40
+ for (let i = 0; i < extraArgs.length; i += 1) {
41
+ const arg = extraArgs[i];
42
+ const name = extraFlagName(arg);
43
+ if (CODEX_FOREIGN_EXTRA_FLAGS_WITH_VALUE.has(name)) {
44
+ if (!arg.includes("=") && nextExtraValue(extraArgs, i) !== undefined) i += 1;
45
+ continue;
46
+ }
47
+ if (name === "-s" || name === "--sandbox") {
48
+ const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : nextExtraValue(extraArgs, i);
49
+ if (!arg.includes("=") && value !== undefined) i += 1;
50
+ if (value && CODEX_SANDBOX_MODES.has(value)) {
51
+ out.push("-c", `sandbox_mode="${value}"`);
52
+ }
53
+ continue;
54
+ }
55
+ if (arg === "--full-auto") {
56
+ out.push("--dangerously-bypass-approvals-and-sandbox");
57
+ continue;
58
+ }
59
+ out.push(arg);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function hasCodexSandboxOverride(args: string[]): boolean {
65
+ for (let i = 0; i < args.length; i += 1) {
66
+ const arg = args[i];
67
+ if (
68
+ arg === "--dangerously-bypass-approvals-and-sandbox" ||
69
+ arg.startsWith("-c sandbox_mode=") ||
70
+ arg.startsWith("-csandbox_mode=") ||
71
+ arg.startsWith("--config=sandbox_mode=")
72
+ ) {
73
+ return true;
74
+ }
75
+ if ((arg === "-c" || arg === "--config") && args[i + 1]?.startsWith("sandbox_mode=")) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
18
81
 
19
82
  /** Resolve the Codex CLI executable via PATH or macOS desktop bundle. */
20
83
  export function resolveCodexCommand(deps: ProbeDeps = {}): string | null {
@@ -167,6 +230,7 @@ export class CodexAdapter extends NdjsonStreamAdapter {
167
230
  */
168
231
  protected buildArgs(opts: RuntimeRunOptions): string[] {
169
232
  const tail: string[] = [];
233
+ const extraArgs = sanitizeCodexExtraArgs(opts.extraArgs);
170
234
 
171
235
  // Sandbox / approval policy. Expressed as `-c` overrides because
172
236
  // `codex exec resume` rejects `-s` / `--full-auto`. `-c` works on both
@@ -177,16 +241,7 @@ export class CodexAdapter extends NdjsonStreamAdapter {
177
241
  // relay back to the user yet. Default to bypassing both approvals and the
178
242
  // sandbox for every trust tier; operators who need a stricter posture can
179
243
  // still override with route/defaultRoute extraArgs.
180
- const hasSandboxOverride =
181
- opts.extraArgs?.some(
182
- (a) =>
183
- a === "-s" ||
184
- a.startsWith("--sandbox") ||
185
- a === "--full-auto" ||
186
- a === "--dangerously-bypass-approvals-and-sandbox" ||
187
- a.startsWith("-c sandbox_mode=") ||
188
- a.startsWith("-csandbox_mode="),
189
- ) ?? false;
244
+ const hasSandboxOverride = hasCodexSandboxOverride(extraArgs);
190
245
  if (!hasSandboxOverride) {
191
246
  tail.push(
192
247
  "-c",
@@ -196,7 +251,7 @@ export class CodexAdapter extends NdjsonStreamAdapter {
196
251
  );
197
252
  }
198
253
  tail.push("--skip-git-repo-check", "--json");
199
- if (opts.extraArgs?.length) tail.push(...opts.extraArgs);
254
+ if (extraArgs.length) tail.push(...extraArgs);
200
255
 
201
256
  // `--` separates flags from positionals so a prompt starting with `-`
202
257
  // can never be parsed as an option. `systemContext` is NOT prepended to
package/src/index.ts CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  updateWorkingMemory,
58
58
  DEFAULT_SECTION,
59
59
  } from "./working-memory.js";
60
+ import { createDiagnosticBundle } from "./diagnostics.js";
60
61
  import { resolveStartAuthAction } from "./start-auth.js";
61
62
  import {
62
63
  discoverLocalOpenclawGateways,
@@ -131,7 +132,9 @@ Commands:
131
132
  route list
132
133
  route remove --room <rm_xxx>|--prefix <rm_xxx>
133
134
  config Print resolved config
134
- doctor [--json] Scan local runtimes (${ADAPTER_LIST})
135
+ doctor [--json] [--bundle] Scan local runtimes (${ADAPTER_LIST});
136
+ --bundle also writes a zip under
137
+ ~/.botcord/diagnostics/
135
138
  memory get [--agent <ag_xxx>] [--json] Show current working memory
136
139
  memory set [--agent <ag_xxx>] --goal <text>
137
140
  Pin/update the agent's work goal
@@ -164,6 +167,7 @@ const BOOLEAN_FLAGS = new Set([
164
167
  "f",
165
168
  "follow",
166
169
  "json",
170
+ "bundle",
167
171
  "help",
168
172
  "h",
169
173
  "mentioned",
@@ -1342,6 +1346,18 @@ const fsFileReader: DoctorFileReader = {
1342
1346
  };
1343
1347
 
1344
1348
  async function cmdDoctor(args: ParsedArgs): Promise<void> {
1349
+ if (args.flags.bundle === true) {
1350
+ const bundle = await createDiagnosticBundle();
1351
+ if (args.flags.json === true) {
1352
+ console.log(JSON.stringify({ bundle }, null, 2));
1353
+ return;
1354
+ }
1355
+ console.log(`diagnostic bundle written: ${bundle.path}`);
1356
+ console.log(`size: ${bundle.sizeBytes} bytes`);
1357
+ console.log("Send this zip file to the BotCord developer/support contact.");
1358
+ return;
1359
+ }
1360
+
1345
1361
  const entries: import("./doctor.js").DoctorRuntimeEntry[] = detectRuntimes();
1346
1362
  // Doctor should not hard-fail when no config exists yet; channel probes
1347
1363
  // simply produce an empty list in that case.
package/src/provision.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  type UpdateAgentParams,
29
29
  } from "@botcord/protocol-core";
30
30
  import type { Gateway } from "./gateway/index.js";
31
+ import type { GatewayInboundMessage } from "./gateway/index.js";
31
32
  import type { PolicyResolverLike } from "./gateway/policy-resolver.js";
32
33
  import type { PolicyUpdatedParams } from "@botcord/protocol-core";
33
34
  import type {
@@ -398,6 +399,10 @@ export function createProvisioner(opts: ProvisionerOptions): (
398
399
  return { ok: true, result };
399
400
  }
400
401
 
402
+ case "wake_agent": {
403
+ return handleWakeAgent(gateway, frame.params);
404
+ }
405
+
401
406
  default:
402
407
  daemonLog.warn("provision.dispatch: unknown frame type", {
403
408
  type: frame.type,
@@ -411,6 +416,87 @@ export function createProvisioner(opts: ProvisionerOptions): (
411
416
  };
412
417
  }
413
418
 
419
+ interface WakeAgentParams {
420
+ agent_id?: string;
421
+ agentId?: string;
422
+ message?: string;
423
+ run_id?: string;
424
+ runId?: string;
425
+ schedule_id?: string;
426
+ scheduleId?: string;
427
+ dedupe_key?: string;
428
+ dedupeKey?: string;
429
+ }
430
+
431
+ async function handleWakeAgent(gateway: Gateway, raw: unknown): Promise<AckBody> {
432
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
433
+ return {
434
+ ok: false,
435
+ error: { code: "bad_params", message: "wake_agent params must be an object" },
436
+ };
437
+ }
438
+ const params = raw as WakeAgentParams;
439
+ const agentId = params.agent_id || params.agentId;
440
+ const message = params.message;
441
+ if (!agentId || typeof agentId !== "string") {
442
+ return {
443
+ ok: false,
444
+ error: { code: "bad_params", message: "wake_agent requires params.agent_id" },
445
+ };
446
+ }
447
+ if (!message || typeof message !== "string") {
448
+ return {
449
+ ok: false,
450
+ error: { code: "bad_params", message: "wake_agent requires params.message" },
451
+ };
452
+ }
453
+
454
+ const channels = gateway.snapshot().channels;
455
+ if (!channels[agentId]) {
456
+ return {
457
+ ok: false,
458
+ error: { code: "agent_not_loaded", message: `agent ${agentId} is not loaded in daemon gateway` },
459
+ };
460
+ }
461
+
462
+ const runId = params.run_id || params.runId || `wake-${Date.now()}`;
463
+ const scheduleId = params.schedule_id || params.scheduleId;
464
+ const dedupeKey = params.dedupe_key || params.dedupeKey;
465
+ const conversationId = `rm_schedule_${agentId}`;
466
+ const msg: GatewayInboundMessage = {
467
+ id: runId,
468
+ channel: agentId,
469
+ accountId: agentId,
470
+ conversation: {
471
+ id: conversationId,
472
+ kind: "direct",
473
+ title: "BotCord Scheduler",
474
+ threadId: scheduleId ?? null,
475
+ },
476
+ sender: {
477
+ id: "hub",
478
+ name: "BotCord Scheduler",
479
+ kind: "system",
480
+ },
481
+ text: message,
482
+ raw: {
483
+ source_type: "botcord_schedule",
484
+ schedule_id: scheduleId,
485
+ run_id: runId,
486
+ dedupe_key: dedupeKey,
487
+ },
488
+ mentioned: true,
489
+ receivedAt: Date.now(),
490
+ trace: {
491
+ id: runId,
492
+ streamable: false,
493
+ },
494
+ };
495
+
496
+ await gateway.injectInbound(msg);
497
+ return { ok: true, result: { agent_id: agentId } };
498
+ }
499
+
414
500
  // W8: hand-written runtime validator for the third-party gateway frame
415
501
  // params. Rejects malformed payloads with a structured `bad_params` ack
416
502
  // before they hit the per-handler logic, so an attacker can't smuggle a
@@ -57,6 +57,16 @@ function buildOwnerChatSceneContext(): string {
57
57
  ].join("\n");
58
58
  }
59
59
 
60
+ function buildGroupRoomEnvironmentContext(message: GatewayInboundMessage): string | null {
61
+ if (message.conversation.kind !== "group") return null;
62
+ return [
63
+ "[BotCord Runtime Environment]",
64
+ "You are running as a local agent process connected to a remote BotCord group room.",
65
+ "Other room members can read your messages and any uploaded/attached files, but they cannot access this machine's local filesystem, container paths, or absolute paths such as /var/..., /tmp/..., or /Users/....",
66
+ "Do not present a local file path as a useful report link or deliverable in group chat. If an artifact needs to be shared, upload or attach it through the available BotCord file/attachment mechanism, then refer to the uploaded attachment or summarize the content in the message.",
67
+ ].join("\n");
68
+ }
69
+
60
70
  /** Dependencies injected by the daemon bootstrap. */
61
71
  export interface SystemContextDeps {
62
72
  /** The owning daemon's agent id. Used to scope working-memory + activity lookups. */
@@ -133,6 +143,7 @@ export function createDaemonSystemContextBuilder(
133
143
  const gatherSyncBlocks = (message: GatewayInboundMessage): {
134
144
  identity: string | null;
135
145
  ownerScene: string | null;
146
+ environment: string | null;
136
147
  memory: string | null;
137
148
  digest: string | null;
138
149
  } => {
@@ -142,6 +153,7 @@ export function createDaemonSystemContextBuilder(
142
153
  classifyActivitySender(message).kind === "owner"
143
154
  ? buildOwnerChatSceneContext()
144
155
  : null;
156
+ const environment = ownerScene ? null : buildGroupRoomEnvironmentContext(message);
145
157
 
146
158
  const wm = safeReadWorkingMemory(deps.agentId);
147
159
  const memory = wm ? buildWorkingMemoryPrompt({ workingMemory: wm }) : null;
@@ -155,7 +167,7 @@ export function createDaemonSystemContextBuilder(
155
167
  }) || null
156
168
  : null;
157
169
 
158
- return { identity, ownerScene, memory, digest };
170
+ return { identity, ownerScene, environment, memory, digest };
159
171
  };
160
172
 
161
173
  const assemble = (parts: Array<string | null | undefined>): string | undefined => {
@@ -195,13 +207,13 @@ export function createDaemonSystemContextBuilder(
195
207
 
196
208
  if (!deps.roomContextBuilder) {
197
209
  const syncBuilder = (message: GatewayInboundMessage): string | undefined => {
198
- const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
210
+ const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
199
211
  // Loop-risk sits at the end so its "reply NO_REPLY unless…" guidance
200
212
  // is the last thing the model sees before the user turn body.
201
213
  // Identity sits at the very front so it frames every other block.
202
214
  const skillIndex = buildSkillIndex(message);
203
215
  const loopRisk = runLoopRisk(message);
204
- return assemble([identity, ownerScene, memory, digest, skillIndex, loopRisk]);
216
+ return assemble([identity, ownerScene, environment, memory, digest, skillIndex, loopRisk]);
205
217
  };
206
218
  // Compile-time witness that the narrower sync signature still satisfies
207
219
  // `SystemContextBuilder` (which allows async). Prevents the two contracts
@@ -215,7 +227,7 @@ export function createDaemonSystemContextBuilder(
215
227
  const asyncBuilder = async (
216
228
  message: GatewayInboundMessage,
217
229
  ): Promise<string | undefined> => {
218
- const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
230
+ const { identity, ownerScene, environment, memory, digest } = gatherSyncBlocks(message);
219
231
  // Room context landing order: after owner-scene / memory, before digest —
220
232
  // "what room am I in" belongs with the session's own identity, while the
221
233
  // cross-room digest deliberately describes OTHER rooms and should stay
@@ -233,7 +245,7 @@ export function createDaemonSystemContextBuilder(
233
245
  }
234
246
  const skillIndex = buildSkillIndex(message);
235
247
  const loopRisk = runLoopRisk(message);
236
- return assemble([identity, ownerScene, memory, roomBlock, digest, skillIndex, loopRisk]);
248
+ return assemble([identity, ownerScene, environment, memory, roomBlock, digest, skillIndex, loopRisk]);
237
249
  };
238
250
  const _typecheck: SystemContextBuilder = asyncBuilder;
239
251
  void _typecheck;
@@ -307,6 +307,11 @@ export function buildWorkingMemoryPrompt(opts: {
307
307
  "- sections: named buckets (contacts, pending_tasks, preferences, etc.).",
308
308
  "- Updating one section never touches others. Empty content deletes a section.",
309
309
  "",
310
+ "For cross-room work, update memory before or immediately after delegating:",
311
+ "- If you accept a request in one room and continue it in another, record a `pending_tasks` entry with source room id/name, target room id/name, requested deliverable, current status, and where to report completion.",
312
+ "- When a delegated room replies or delivers an artifact, consult `pending_tasks` before deciding `NO_REPLY`; if it matches a pending handoff, acknowledge, update status, and send the promised follow-up to the source room when appropriate.",
313
+ "- Remove or mark the entry done once the source room has been updated.",
314
+ "",
310
315
  "Only update when something meaningful changes. Keep each section tight.",
311
316
  ];
312
317