agent-relay-server 0.3.7 → 0.3.9

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/README.md CHANGED
@@ -77,16 +77,24 @@ codex-relay
77
77
  incoming messages as live turns, and cleans up sidecar processes when Codex
78
78
  exits.
79
79
 
80
- ### Codex approval prompts
80
+ ### Codex approval mode
81
81
 
82
82
  Replying to relay messages is usually done with a shell command (`curl` to
83
83
  `/api/messages`), so Codex may prompt for approval in stricter modes.
84
84
 
85
- `codex-relay` now forwards your Codex runtime mode to the sidecar, including
86
- `--ask-for-approval`, `--sandbox`, `--full-auto`, and `--yolo`.
85
+ By default, `codex-relay` starts Codex with
86
+ `--dangerously-bypass-approvals-and-sandbox` so relay turns do not get stuck on
87
+ approval prompts. If you pass an explicit Codex runtime mode, `codex-relay`
88
+ leaves it alone and forwards it to the sidecar, including `--ask-for-approval`,
89
+ `--sandbox`, `--full-auto`, and `--yolo`.
87
90
 
88
91
  Useful setups:
89
92
 
93
+ ```bash
94
+ # default: no approval prompts, no Codex sandbox
95
+ codex-relay
96
+ ```
97
+
90
98
  ```bash
91
99
  # no approval prompts, still sandboxed to workspace boundaries
92
100
  codex-relay -- --ask-for-approval never --sandbox workspace-write
@@ -76,6 +76,47 @@ function readJsonFile<T>(path: string, fallback: T): T {
76
76
  return JSON.parse(readFileSync(path, "utf8")) as T;
77
77
  }
78
78
 
79
+ function isAgentRelaySessionStartCommand(command: string): boolean {
80
+ return /agent-relay.*codex\/hooks\/session-start\.ts/.test(command);
81
+ }
82
+
83
+ function removeAgentRelaySessionStartToml(input: string): string {
84
+ const lines = input.split(/\r?\n/);
85
+ const blocks: Array<{ header: string | null; text: string }> = [];
86
+
87
+ for (let index = 0; index < lines.length; ) {
88
+ const line = lines[index] ?? "";
89
+ const header = /^\[\[?/.test(line) ? line : null;
90
+ const block: string[] = [];
91
+ do {
92
+ block.push(lines[index] ?? "");
93
+ index += 1;
94
+ } while (index < lines.length && !/^\[\[?/.test(lines[index] ?? ""));
95
+ blocks.push({ header, text: block.join("\n") });
96
+ }
97
+
98
+ let output = "";
99
+ for (let index = 0; index < blocks.length; ) {
100
+ const block = blocks[index];
101
+ if (block?.header?.startsWith("[[hooks.SessionStart")) {
102
+ const group: typeof blocks = [];
103
+ while (index < blocks.length && blocks[index]?.header?.startsWith("[[hooks.SessionStart")) {
104
+ group.push(blocks[index]!);
105
+ index += 1;
106
+ }
107
+
108
+ const groupText = group.map((block) => block.text).join("\n");
109
+ if (!isAgentRelaySessionStartCommand(groupText)) output += `${groupText}\n`;
110
+ continue;
111
+ }
112
+
113
+ output += `${block?.text ?? ""}\n`;
114
+ index += 1;
115
+ }
116
+
117
+ return output.replace(/\s+$/, "\n");
118
+ }
119
+
79
120
  function compareVersions(left: string, right: string): number {
80
121
  const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
81
122
  const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0);
@@ -215,35 +256,43 @@ function installMarketplace(quiet = false): void {
215
256
 
216
257
  function installHook(): void {
217
258
  mkdirSync(join(home, ".codex"), { recursive: true });
259
+ const command = `bun ${shellQuote(installedHookScript)}`;
260
+
261
+ const configPath = join(home, ".codex", "config.toml");
262
+ const existingConfig = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
263
+ let output = removeAgentRelaySessionStartToml(existingConfig);
264
+ if (!output.endsWith("\n\n")) output += output.endsWith("\n") ? "\n" : "\n\n";
265
+ output += `[[hooks.SessionStart]]
266
+ matcher = "startup|resume"
267
+
268
+ [[hooks.SessionStart.hooks]]
269
+ type = "command"
270
+ command = "${command.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"
271
+ statusMessage = "Starting Agent Relay"
272
+ timeout = 10
273
+ `;
274
+
275
+ writeFileSync(configPath, output);
276
+
218
277
  const hooksPath = join(home, ".codex", "hooks.json");
278
+ if (!existsSync(hooksPath)) return;
279
+
219
280
  const hooksJson = readJsonFile<HooksJson>(hooksPath, { hooks: {} });
220
281
  hooksJson.hooks ??= {};
221
- hooksJson.hooks.SessionStart ??= [];
222
-
223
- const command = `bun ${shellQuote(installedHookScript)}`;
224
- hooksJson.hooks.SessionStart = hooksJson.hooks.SessionStart
282
+ hooksJson.hooks.SessionStart = (hooksJson.hooks.SessionStart ?? [])
225
283
  .map((group) => ({
226
284
  ...group,
227
285
  hooks: (group.hooks ?? []).filter((hook) => {
228
286
  if (hook.type !== "command" || typeof hook.command !== "string") return true;
229
- return !/agent-relay.*codex\/hooks\/session-start\.ts/.test(hook.command);
287
+ return !isAgentRelaySessionStartCommand(hook.command);
230
288
  }),
231
289
  }))
232
290
  .filter((group) => (group.hooks ?? []).length > 0);
233
291
 
234
- hooksJson.hooks.SessionStart.push({
235
- matcher: "startup|resume",
236
- hooks: [
237
- {
238
- type: "command",
239
- command,
240
- statusMessage: "Starting Agent Relay",
241
- timeout: 10,
242
- },
243
- ],
244
- });
292
+ if (hooksJson.hooks.SessionStart.length === 0) delete hooksJson.hooks.SessionStart;
245
293
 
246
- writeFileSync(hooksPath, `${JSON.stringify(hooksJson, null, 2)}\n`);
294
+ if (Object.keys(hooksJson.hooks).length === 0) rmSync(hooksPath, { force: true });
295
+ else writeFileSync(hooksPath, `${JSON.stringify(hooksJson, null, 2)}\n`);
247
296
  }
248
297
 
249
298
  async function pickLoopbackUrl(): Promise<string> {
@@ -334,6 +383,25 @@ type SessionPermissions = {
334
383
  sandbox?: string;
335
384
  };
336
385
 
386
+ function hasCodexPermissionMode(codexArgs: string[]): boolean {
387
+ for (const arg of codexArgs) {
388
+ if (
389
+ arg === "--yolo" ||
390
+ arg === "--dangerously-bypass-approvals-and-sandbox" ||
391
+ arg === "--full-auto" ||
392
+ arg === "--ask-for-approval" ||
393
+ arg === "-a" ||
394
+ arg.startsWith("--ask-for-approval=") ||
395
+ arg === "--sandbox" ||
396
+ arg === "-s" ||
397
+ arg.startsWith("--sandbox=")
398
+ ) {
399
+ return true;
400
+ }
401
+ }
402
+ return false;
403
+ }
404
+
337
405
  function resolveSessionPermissions(codexArgs: string[]): SessionPermissions {
338
406
  let approvalPolicy: string | undefined;
339
407
  let sandbox: string | undefined;
@@ -407,8 +475,9 @@ function cleanupRun(runDir: string, appServer: ReturnType<typeof Bun.spawn> | nu
407
475
  function installCodexSupport(quiet = false): void {
408
476
  if (!commandExists("bun")) throw new Error("Bun is required: https://bun.sh");
409
477
  findCodexBinary();
410
- installMarketplace(quiet);
478
+ syncInstalledPackage();
411
479
  installHook();
480
+ installMarketplace(quiet);
412
481
  }
413
482
 
414
483
  function writeLauncherShim(name: string): void {
@@ -438,7 +507,6 @@ function installLauncherShims(includeCodexAlias: boolean): void {
438
507
  mkdirSync(aliasBinDir, { recursive: true });
439
508
  writeLauncherShim("codex-relay");
440
509
  if (includeCodexAlias) writeLauncherShim("codex");
441
- else removeLauncherShim("codex");
442
510
  }
443
511
 
444
512
  function isAliasBinOnPath(): boolean {
@@ -539,6 +607,7 @@ async function start(args: string[]): Promise<void> {
539
607
  }
540
608
 
541
609
  if (!listenUrl) listenUrl = await pickLoopbackUrl();
610
+ if (!hasCodexPermissionMode(codexArgs)) codexArgs.unshift("--dangerously-bypass-approvals-and-sandbox");
542
611
  const permissions = resolveSessionPermissions(codexArgs);
543
612
 
544
613
  mkdirSync(runtimeRoot, { recursive: true });
@@ -577,8 +646,8 @@ async function start(args: string[]): Promise<void> {
577
646
  process.once("exit", shutdown);
578
647
 
579
648
  await waitForPort(listenUrl, appServer);
580
- console.error(`Agent Relay Codex session: ${listenUrl}`);
581
- console.error(`Runtime: ${runDir}`);
649
+ console.log(`Agent Relay Codex session: ${listenUrl}`);
650
+ console.log(`Runtime: ${runDir}`);
582
651
 
583
652
  const codex = Bun.spawn([codexBinary, "--remote", listenUrl, ...codexArgs], {
584
653
  env,
@@ -602,7 +671,9 @@ async function doctor(): Promise<void> {
602
671
  const checks: Array<[string, boolean, string]> = [];
603
672
  checks.push(["bun", commandExists("bun"), "Bun is required to run the sidecar"]);
604
673
  checks.push(["codex", findOnPath("codex", [aliasBinDir]) !== null, "Codex CLI is required"]);
605
- checks.push(["hook", existsSync(join(home, ".codex", "hooks.json")), "~/.codex/hooks.json exists"]);
674
+ const configPath = join(home, ".codex", "config.toml");
675
+ const config = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
676
+ checks.push(["hook", isAgentRelaySessionStartCommand(config), "~/.codex/config.toml has Agent Relay SessionStart hook"]);
606
677
  checks.push(["marketplace", existsSync(marketplaceFile), "Agent Relay marketplace is installed"]);
607
678
  checks.push(["launcher", existsSync(join(aliasBinDir, process.platform === "win32" ? "codex-relay.cmd" : "codex-relay")), "codex-relay launcher shim"]);
608
679
 
package/codex/README.md CHANGED
@@ -52,13 +52,22 @@ starts Codex with
52
52
  `--remote`, lets the SessionStart hook attach a sidecar to the actual thread,
53
53
  and kills sidecars plus the app-server when Codex exits.
54
54
 
55
- ## Approvals and prompts
55
+ ## Approval mode
56
56
 
57
57
  Relay replies are usually sent with a shell command (`curl` to
58
58
  `/api/messages`), so Codex can prompt for approval in stricter modes.
59
59
 
60
- `codex-relay` forwards your launch mode to the sidecar, including
61
- `--ask-for-approval`, `--sandbox`, `--full-auto`, and `--yolo`.
60
+ By default, `codex-relay` starts Codex with
61
+ `--dangerously-bypass-approvals-and-sandbox` so relay turns do not get stuck on
62
+ approval prompts. If you pass an explicit Codex runtime mode, `codex-relay`
63
+ leaves it alone and forwards it to the sidecar, including `--ask-for-approval`,
64
+ `--sandbox`, `--full-auto`, and `--yolo`.
65
+
66
+ Default:
67
+
68
+ ```bash
69
+ codex-relay
70
+ ```
62
71
 
63
72
  Example: no prompt loop, still workspace sandboxing:
64
73
 
@@ -48,8 +48,8 @@ exit 64
48
48
  chmodSync(path, 0o755);
49
49
  }
50
50
 
51
- function parseRuntimeDir(stderr: string): string | null {
52
- const match = stderr.match(/^Runtime:\s+(.+)$/m);
51
+ function parseRuntimeDir(output: string): string | null {
52
+ const match = output.match(/^Runtime:\s+(.+)$/m);
53
53
  return match?.[1]?.trim() || null;
54
54
  }
55
55
 
@@ -85,9 +85,9 @@ async function main(): Promise<void> {
85
85
  throw new Error(`launcher exited ${exitCode}\nstdout:\n${stdout}\nstderr:\n${stderr}`);
86
86
  }
87
87
 
88
- const runtimeDir = parseRuntimeDir(stderr);
88
+ const runtimeDir = parseRuntimeDir(`${stdout}\n${stderr}`);
89
89
  if (!runtimeDir) {
90
- throw new Error(`missing runtime directory in launcher output\nstderr:\n${stderr}`);
90
+ throw new Error(`missing runtime directory in launcher output\nstdout:\n${stdout}\nstderr:\n${stderr}`);
91
91
  }
92
92
 
93
93
  if (!stderr.includes("SessionStart hook did not start sidecar; started fallback sidecar pid")) {
@@ -122,4 +122,3 @@ main().catch((error) => {
122
122
  log(error instanceof Error ? error.stack || error.message : String(error));
123
123
  process.exitCode = 1;
124
124
  });
125
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",