@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.
- package/dist/daemon.js +25 -1
- package/dist/diagnostics.d.ts +30 -0
- package/dist/diagnostics.js +286 -0
- package/dist/gateway/gateway.d.ts +6 -0
- package/dist/gateway/gateway.js +8 -0
- package/dist/gateway/runtimes/claude-code.js +79 -5
- package/dist/gateway/runtimes/codex.js +67 -8
- package/dist/index.js +16 -1
- package/dist/provision.js +68 -0
- package/dist/system-context.js +16 -5
- package/dist/working-memory.js +5 -0
- package/package.json +1 -1
- package/src/__tests__/diagnostics.test.ts +46 -0
- package/src/__tests__/provision.test.ts +45 -0
- package/src/__tests__/system-context.test.ts +32 -12
- package/src/__tests__/working-memory.test.ts +9 -1
- package/src/daemon.ts +25 -1
- package/src/diagnostics.ts +348 -0
- package/src/gateway/__tests__/claude-code-adapter.test.ts +35 -0
- package/src/gateway/__tests__/codex-adapter.test.ts +80 -4
- package/src/gateway/gateway.ts +9 -0
- package/src/gateway/runtimes/claude-code.ts +76 -4
- package/src/gateway/runtimes/codex.ts +66 -11
- package/src/index.ts +17 -1
- package/src/provision.ts +86 -0
- package/src/system-context.ts +17 -5
- package/src/working-memory.ts +5 -0
|
@@ -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 (
|
|
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]
|
|
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
|
package/src/system-context.ts
CHANGED
|
@@ -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;
|
package/src/working-memory.ts
CHANGED
|
@@ -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
|
|