agent-relay-runner 0.127.0 → 0.127.2
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/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/mcp-outbox.ts +13 -1
- package/src/provisioning.ts +121 -0
- package/src/runner-core.ts +1 -1
package/package.json
CHANGED
package/src/mcp-outbox.ts
CHANGED
|
@@ -67,7 +67,12 @@ export async function deliverRunnerOutboxEvent(input: RunnerOutboxDelivery): Pro
|
|
|
67
67
|
}
|
|
68
68
|
logger.warn("outbox", `dropping event with unknown kind: ${record.kind}`);
|
|
69
69
|
} catch (error) {
|
|
70
|
-
if (isHttpStatusError(error, 409))
|
|
70
|
+
if (isHttpStatusError(error, 409)) {
|
|
71
|
+
if (record.kind === "session-message-batch") {
|
|
72
|
+
logger.error("outbox", `relay reported duplicate session batch; acking row seq=${record.seq} batchKey=${record.idempotencyKey} itemKeys=${sessionBatchItemKeys(record).join(",")} error=${errMessage(error)}`);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
71
76
|
if (isHttpAuthError(error)) input.recoverRuntimeTokenAfterAuthFailure("outbox");
|
|
72
77
|
throw error;
|
|
73
78
|
}
|
|
@@ -89,6 +94,13 @@ function isDeterministicValidationError(error: unknown): boolean {
|
|
|
89
94
|
return status === 400 || status === 413 || status === 422;
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
function sessionBatchItemKeys(record: OutboxRecord): string[] {
|
|
98
|
+
const messages = Array.isArray((record.payload as { messages?: unknown }).messages)
|
|
99
|
+
? (record.payload as { messages: SendMessageInput[] }).messages
|
|
100
|
+
: [];
|
|
101
|
+
return messages.map((message, index) => message.idempotencyKey ?? `${record.idempotencyKey}:${index}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
export async function deliverBufferedMcpCall(input: {
|
|
93
105
|
record: OutboxRecord;
|
|
94
106
|
relayUrl: string;
|
package/src/provisioning.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
4
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
@@ -355,6 +356,126 @@ export function writeCodexHooksJson(codexHome: string, hooks: ProvisioningHookSe
|
|
|
355
356
|
return;
|
|
356
357
|
}
|
|
357
358
|
writeFileSync(join(codexHome, "hooks.json"), JSON.stringify({ hooks }, null, 2), { mode: 0o600 });
|
|
359
|
+
writeCodexHookTrustState(codexHome, hooks);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const CODEX_HOOK_EVENT_STATE_KEYS: Record<string, string> = {
|
|
363
|
+
PreToolUse: "pre_tool_use",
|
|
364
|
+
PermissionRequest: "permission_request",
|
|
365
|
+
PostToolUse: "post_tool_use",
|
|
366
|
+
PreCompact: "pre_compact",
|
|
367
|
+
PostCompact: "post_compact",
|
|
368
|
+
SessionStart: "session_start",
|
|
369
|
+
UserPromptSubmit: "user_prompt_submit",
|
|
370
|
+
SubagentStart: "subagent_start",
|
|
371
|
+
SubagentStop: "subagent_stop",
|
|
372
|
+
Stop: "stop",
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
interface CodexHookTrustEntry {
|
|
376
|
+
key: string;
|
|
377
|
+
trustedHash: string;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function codexHookTrustEntries(codexHome: string, hooks: ProvisioningHookSet): CodexHookTrustEntry[] {
|
|
381
|
+
const sourcePath = join(codexHome, "hooks.json");
|
|
382
|
+
const entries: CodexHookTrustEntry[] = [];
|
|
383
|
+
for (const [eventName, groups] of Object.entries(hooks)) {
|
|
384
|
+
const eventKey = CODEX_HOOK_EVENT_STATE_KEYS[eventName];
|
|
385
|
+
if (!eventKey) continue;
|
|
386
|
+
groups.forEach((group, groupIndex) => {
|
|
387
|
+
group.hooks.forEach((handler, handlerIndex) => {
|
|
388
|
+
if (handler.type !== "command") return;
|
|
389
|
+
entries.push({
|
|
390
|
+
key: `${sourcePath}:${eventKey}:${groupIndex}:${handlerIndex}`,
|
|
391
|
+
trustedHash: codexCommandHookHash(eventKey, group.matcher, group, handler),
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return entries;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function writeCodexHookTrustState(codexHome: string, hooks: ProvisioningHookSet): void {
|
|
400
|
+
const entries = codexHookTrustEntries(codexHome, hooks);
|
|
401
|
+
if (!entries.length) return;
|
|
402
|
+
const configPath = join(codexHome, "config.toml");
|
|
403
|
+
let config = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
config = upsertCodexHookTrustEntry(config, entry);
|
|
406
|
+
}
|
|
407
|
+
writeFileSync(configPath, config, { mode: 0o600 });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function upsertCodexHookTrustEntry(config: string, entry: CodexHookTrustEntry): string {
|
|
411
|
+
const header = `[hooks.state.${tomlQuotedString(entry.key)}]`;
|
|
412
|
+
const lines = config.length ? config.split(/\n/) : [];
|
|
413
|
+
const headerIndex = lines.findIndex((line) => line.trim() === header);
|
|
414
|
+
if (headerIndex < 0) {
|
|
415
|
+
const prefix = config.endsWith("\n") || config.length === 0 ? config : `${config}\n`;
|
|
416
|
+
return `${prefix}\n${header}\ntrusted_hash = ${tomlQuotedString(entry.trustedHash)}\n`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let nextHeaderIndex = lines.length;
|
|
420
|
+
for (let i = headerIndex + 1; i < lines.length; i += 1) {
|
|
421
|
+
if (/^\s*\[.*\]\s*$/.test(lines[i] ?? "")) {
|
|
422
|
+
nextHeaderIndex = i;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const trustedHashLine = `trusted_hash = ${tomlQuotedString(entry.trustedHash)}`;
|
|
428
|
+
const existingIndex = lines
|
|
429
|
+
.slice(headerIndex + 1, nextHeaderIndex)
|
|
430
|
+
.findIndex((line) => /^\s*trusted_hash\s*=/.test(line));
|
|
431
|
+
if (existingIndex >= 0) {
|
|
432
|
+
lines[headerIndex + 1 + existingIndex] = trustedHashLine;
|
|
433
|
+
} else {
|
|
434
|
+
lines.splice(nextHeaderIndex, 0, trustedHashLine);
|
|
435
|
+
}
|
|
436
|
+
return lines.join("\n");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function codexCommandHookHash(
|
|
440
|
+
eventKey: string,
|
|
441
|
+
matcher: string | undefined,
|
|
442
|
+
group: ProvisioningHookSet[string][number],
|
|
443
|
+
handler: ProvisioningHookSet[string][number]["hooks"][number],
|
|
444
|
+
): string {
|
|
445
|
+
// Mirrors Codex's command_hook_hash: hash the normalized hook identity, not the source file.
|
|
446
|
+
const normalizedHandler: Record<string, unknown> = {
|
|
447
|
+
type: "command",
|
|
448
|
+
command: handler.command,
|
|
449
|
+
async: false,
|
|
450
|
+
timeout: Math.max(1, handler.timeout ?? 600),
|
|
451
|
+
};
|
|
452
|
+
if (handler.statusMessage !== undefined) normalizedHandler.statusMessage = handler.statusMessage;
|
|
453
|
+
|
|
454
|
+
const identity: Record<string, unknown> = {
|
|
455
|
+
event_name: eventKey,
|
|
456
|
+
hooks: [normalizedHandler],
|
|
457
|
+
};
|
|
458
|
+
if (matcher !== undefined) identity.matcher = matcher;
|
|
459
|
+
|
|
460
|
+
const canonical = canonicalJson(identity);
|
|
461
|
+
const hash = createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
462
|
+
return `sha256:${hash}`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function canonicalJson(value: unknown): unknown {
|
|
466
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
467
|
+
if (value && typeof value === "object") {
|
|
468
|
+
return Object.fromEntries(
|
|
469
|
+
Object.entries(value as Record<string, unknown>)
|
|
470
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
471
|
+
.map(([key, val]) => [key, canonicalJson(val)]),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return value;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function tomlQuotedString(value: string): string {
|
|
478
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
358
479
|
}
|
|
359
480
|
|
|
360
481
|
// #1096 — write relay-owned settings into an ISOLATED Claude managed home's `settings.json`
|
package/src/runner-core.ts
CHANGED
|
@@ -1598,7 +1598,7 @@ export class AgentRunner {
|
|
|
1598
1598
|
});
|
|
1599
1599
|
this.sessionOutbox.enqueue({
|
|
1600
1600
|
kind: "session-message-batch",
|
|
1601
|
-
idempotencyKey: `session-batch:${this.agentId}:${++this.sessionBatchSeq}:${messages.length}`,
|
|
1601
|
+
idempotencyKey: `session-batch:${this.agentId}:${this.options.instanceId}:${this.options.startedAt}:${++this.sessionBatchSeq}:${messages.length}`,
|
|
1602
1602
|
payload: { messages },
|
|
1603
1603
|
});
|
|
1604
1604
|
}
|