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 +11 -3
- package/bin/agent-relay-codex.ts +93 -22
- package/codex/README.md +12 -3
- package/codex/smoke/fallback.ts +4 -5
- package/package.json +1 -1
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
|
|
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`
|
|
86
|
-
`--
|
|
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
|
package/bin/agent-relay-codex.ts
CHANGED
|
@@ -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
|
|
287
|
+
return !isAgentRelaySessionStartCommand(hook.command);
|
|
230
288
|
}),
|
|
231
289
|
}))
|
|
232
290
|
.filter((group) => (group.hooks ?? []).length > 0);
|
|
233
291
|
|
|
234
|
-
hooksJson.hooks.SessionStart.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
581
|
-
console.
|
|
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
|
-
|
|
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
|
-
##
|
|
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`
|
|
61
|
-
`--
|
|
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
|
|
package/codex/smoke/fallback.ts
CHANGED
|
@@ -48,8 +48,8 @@ exit 64
|
|
|
48
48
|
chmodSync(path, 0o755);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function parseRuntimeDir(
|
|
52
|
-
const match =
|
|
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
|
-
|