agent-relay-server 0.9.0 → 0.10.0
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 +12 -14
- package/package.json +18 -1
- package/public/index.html +979 -2575
- package/public/manifest.webmanifest +6 -6
- package/public/sw.js +16 -10
- package/recipes/code-review.yaml +26 -0
- package/recipes/debug.yaml +20 -0
- package/recipes/feature.yaml +26 -0
- package/recipes/refactor.yaml +20 -0
- package/recipes/test.yaml +20 -0
- package/runner/src/adapter.ts +69 -0
- package/runner/src/config.ts +144 -0
- package/scripts/orchestrator-spawn-smoke.ts +2 -9
- package/src/agent-spawn.ts +2 -94
- package/src/automations.ts +774 -0
- package/src/bus-outbox.ts +75 -0
- package/src/bus.ts +439 -0
- package/src/cli.ts +251 -5
- package/src/commands-db.ts +160 -0
- package/src/config.ts +1 -1
- package/src/connectors.ts +29 -9
- package/src/daemon.ts +1 -0
- package/src/db.ts +241 -34
- package/src/events.ts +33 -0
- package/src/index.ts +94 -5
- package/src/recipe-db.ts +163 -0
- package/src/recipe-loader.ts +100 -0
- package/src/recipe-runner.ts +206 -0
- package/src/recipe-validator.ts +85 -0
- package/src/routes.ts +649 -155
- package/src/security.ts +128 -2
- package/src/sse.ts +42 -31
- package/src/token-db.ts +96 -0
- package/src/types.ts +1 -493
- package/src/upgrade.ts +14 -28
- package/public/dashboard/actions.js +0 -819
- package/public/dashboard/api.js +0 -336
- package/public/dashboard/app.js +0 -34
- package/public/dashboard/charts.js +0 -128
- package/public/dashboard/computed.js +0 -693
- package/public/dashboard/constants.js +0 -28
- package/public/dashboard/display.js +0 -345
- package/public/dashboard/state.js +0 -129
- package/public/dashboard/utils.js +0 -207
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"name": "Agent Relay",
|
|
3
3
|
"short_name": "Relay",
|
|
4
4
|
"description": "Local control panel for Agent Relay agents, channels, tasks, and messages.",
|
|
5
|
-
"id": "
|
|
6
|
-
"start_url": "
|
|
7
|
-
"scope": "
|
|
5
|
+
"id": "./",
|
|
6
|
+
"start_url": "./",
|
|
7
|
+
"scope": "./",
|
|
8
8
|
"display": "standalone",
|
|
9
9
|
"background_color": "#0d1117",
|
|
10
10
|
"theme_color": "#0d1117",
|
|
@@ -12,19 +12,19 @@
|
|
|
12
12
|
"categories": ["developer", "productivity", "utilities"],
|
|
13
13
|
"icons": [
|
|
14
14
|
{
|
|
15
|
-
"src": "
|
|
15
|
+
"src": "icons/agent-relay.svg",
|
|
16
16
|
"sizes": "any",
|
|
17
17
|
"type": "image/svg+xml",
|
|
18
18
|
"purpose": "any maskable"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
"src": "
|
|
21
|
+
"src": "icons/agent-relay-192.png",
|
|
22
22
|
"sizes": "192x192",
|
|
23
23
|
"type": "image/png",
|
|
24
24
|
"purpose": "any maskable"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
"src": "
|
|
27
|
+
"src": "icons/agent-relay-512.png",
|
|
28
28
|
"sizes": "512x512",
|
|
29
29
|
"type": "image/png",
|
|
30
30
|
"purpose": "any maskable"
|
package/public/sw.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
const CACHE_NAME = "agent-relay-dashboard-v1";
|
|
2
|
+
const scopeUrl = new URL(self.registration.scope);
|
|
3
|
+
const scopePath = scopeUrl.pathname.endsWith("/") ? scopeUrl.pathname : `${scopeUrl.pathname}/`;
|
|
4
|
+
const appUrl = (path) => new URL(path, self.registration.scope).toString();
|
|
2
5
|
const APP_SHELL = [
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"/
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"/icons/agent-relay-512.png",
|
|
6
|
+
appUrl("./"),
|
|
7
|
+
appUrl("index.html"),
|
|
8
|
+
appUrl("manifest.webmanifest"),
|
|
9
|
+
appUrl("icons/agent-relay.svg"),
|
|
10
|
+
appUrl("icons/agent-relay-192.png"),
|
|
11
|
+
appUrl("icons/agent-relay-512.png"),
|
|
10
12
|
];
|
|
11
13
|
|
|
12
14
|
self.addEventListener("install", (event) => {
|
|
@@ -31,7 +33,11 @@ self.addEventListener("fetch", (event) => {
|
|
|
31
33
|
const request = event.request;
|
|
32
34
|
const url = new URL(request.url);
|
|
33
35
|
|
|
34
|
-
if (request.method !== "GET" || url.origin !== self.location.origin || url.pathname.startsWith(
|
|
36
|
+
if (request.method !== "GET" || url.origin !== self.location.origin || !url.pathname.startsWith(scopePath)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (url.pathname.startsWith(`${scopePath}api/`)) {
|
|
35
41
|
return;
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -42,7 +48,7 @@ self.addEventListener("fetch", (event) => {
|
|
|
42
48
|
event.respondWith(
|
|
43
49
|
fetch(request)
|
|
44
50
|
.then((response) => {
|
|
45
|
-
if (response.ok && APP_SHELL.includes(url.
|
|
51
|
+
if (response.ok && APP_SHELL.includes(url.href)) {
|
|
46
52
|
const copy = response.clone();
|
|
47
53
|
caches.open(CACHE_NAME).then((cache) => cache.put(request, copy));
|
|
48
54
|
}
|
|
@@ -51,7 +57,7 @@ self.addEventListener("fetch", (event) => {
|
|
|
51
57
|
.catch(async () => {
|
|
52
58
|
const cached = await caches.match(request);
|
|
53
59
|
if (cached) return cached;
|
|
54
|
-
if (request.mode === "navigate") return caches.match("
|
|
60
|
+
if (request.mode === "navigate") return caches.match(appUrl("index.html"));
|
|
55
61
|
throw new Error("offline");
|
|
56
62
|
}),
|
|
57
63
|
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Code Review
|
|
2
|
+
description: Three agents review code from architecture, security, and test angles.
|
|
3
|
+
version: "1.0"
|
|
4
|
+
agents:
|
|
5
|
+
- role: lead-reviewer
|
|
6
|
+
provider: claude
|
|
7
|
+
capabilities: [review, architecture]
|
|
8
|
+
label: lead reviewer
|
|
9
|
+
approvalMode: guarded
|
|
10
|
+
prompt: Review architecture, maintainability, and release risk.
|
|
11
|
+
- role: security-reviewer
|
|
12
|
+
provider: claude
|
|
13
|
+
capabilities: [review, security]
|
|
14
|
+
label: security reviewer
|
|
15
|
+
approvalMode: guarded
|
|
16
|
+
prompt: Review authentication, authorization, secrets, and data handling.
|
|
17
|
+
- role: test-reviewer
|
|
18
|
+
provider: codex
|
|
19
|
+
capabilities: [review, testing]
|
|
20
|
+
label: test reviewer
|
|
21
|
+
approvalMode: guarded
|
|
22
|
+
prompt: Review test coverage and failure modes.
|
|
23
|
+
workflow:
|
|
24
|
+
trigger: cap:review
|
|
25
|
+
fanOut: all
|
|
26
|
+
collect: lead-reviewer
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Debug
|
|
2
|
+
description: Investigator and fixer agents isolate and patch a bug.
|
|
3
|
+
version: "1.0"
|
|
4
|
+
agents:
|
|
5
|
+
- role: investigator
|
|
6
|
+
provider: claude
|
|
7
|
+
capabilities: [debug, analysis]
|
|
8
|
+
label: bug investigator
|
|
9
|
+
approvalMode: guarded
|
|
10
|
+
prompt: Reproduce the bug and identify the failing path.
|
|
11
|
+
- role: fixer
|
|
12
|
+
provider: codex
|
|
13
|
+
capabilities: [debug, code]
|
|
14
|
+
label: bug fixer
|
|
15
|
+
approvalMode: guarded
|
|
16
|
+
prompt: Implement the smallest safe fix and verify it.
|
|
17
|
+
workflow:
|
|
18
|
+
trigger: cap:debug
|
|
19
|
+
fanOut: first
|
|
20
|
+
collect: investigator
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Feature
|
|
2
|
+
description: Planner plus two implementers for feature work.
|
|
3
|
+
version: "1.0"
|
|
4
|
+
agents:
|
|
5
|
+
- role: planner
|
|
6
|
+
provider: claude
|
|
7
|
+
capabilities: [planning, architecture]
|
|
8
|
+
label: feature planner
|
|
9
|
+
approvalMode: read-only
|
|
10
|
+
prompt: Map the change and split implementation safely.
|
|
11
|
+
- role: implementer-a
|
|
12
|
+
provider: codex
|
|
13
|
+
capabilities: [implementation]
|
|
14
|
+
label: implementer a
|
|
15
|
+
approvalMode: guarded
|
|
16
|
+
prompt: Implement the assigned feature slice.
|
|
17
|
+
- role: implementer-b
|
|
18
|
+
provider: codex
|
|
19
|
+
capabilities: [implementation]
|
|
20
|
+
label: implementer b
|
|
21
|
+
approvalMode: guarded
|
|
22
|
+
prompt: Implement an independent feature slice.
|
|
23
|
+
workflow:
|
|
24
|
+
trigger: cap:feature
|
|
25
|
+
fanOut: all
|
|
26
|
+
collect: planner
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Refactor
|
|
2
|
+
description: Analyzer and refactorer agents improve structure safely.
|
|
3
|
+
version: "1.0"
|
|
4
|
+
agents:
|
|
5
|
+
- role: analyzer
|
|
6
|
+
provider: claude
|
|
7
|
+
capabilities: [refactor, analysis]
|
|
8
|
+
label: refactor analyzer
|
|
9
|
+
approvalMode: read-only
|
|
10
|
+
prompt: Map dependents and identify safe refactor boundaries.
|
|
11
|
+
- role: refactorer
|
|
12
|
+
provider: codex
|
|
13
|
+
capabilities: [refactor, implementation]
|
|
14
|
+
label: refactorer
|
|
15
|
+
approvalMode: guarded
|
|
16
|
+
prompt: Apply the refactor and keep behavior unchanged.
|
|
17
|
+
workflow:
|
|
18
|
+
trigger: cap:refactor
|
|
19
|
+
fanOut: first
|
|
20
|
+
collect: analyzer
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
description: Test writer and verifier agents improve coverage and run checks.
|
|
3
|
+
version: "1.0"
|
|
4
|
+
agents:
|
|
5
|
+
- role: test-writer
|
|
6
|
+
provider: codex
|
|
7
|
+
capabilities: [testing, implementation]
|
|
8
|
+
label: test writer
|
|
9
|
+
approvalMode: guarded
|
|
10
|
+
prompt: Add focused tests for the target behavior.
|
|
11
|
+
- role: verifier
|
|
12
|
+
provider: claude
|
|
13
|
+
capabilities: [testing, review]
|
|
14
|
+
label: test verifier
|
|
15
|
+
approvalMode: read-only
|
|
16
|
+
prompt: Verify tests are meaningful and not overfit.
|
|
17
|
+
workflow:
|
|
18
|
+
trigger: cap:test
|
|
19
|
+
fanOut: all
|
|
20
|
+
collect: verifier
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Message } from "agent-relay-sdk";
|
|
2
|
+
|
|
3
|
+
export type SemanticStatus = "idle" | "busy" | "offline" | "error";
|
|
4
|
+
|
|
5
|
+
export interface ProviderConfig {
|
|
6
|
+
command: string;
|
|
7
|
+
defaultArgs: string[];
|
|
8
|
+
env: Record<string, string>;
|
|
9
|
+
pluginDirs: string[];
|
|
10
|
+
defaultCapabilities: string[];
|
|
11
|
+
defaultApprovalMode: string;
|
|
12
|
+
defaultTags: string[];
|
|
13
|
+
headless: {
|
|
14
|
+
tmuxPrefix: string;
|
|
15
|
+
shutdownTimeoutMs: number;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RunnerSpawnConfig {
|
|
20
|
+
provider: string;
|
|
21
|
+
runnerId: string;
|
|
22
|
+
instanceId: string;
|
|
23
|
+
agentId: string;
|
|
24
|
+
relayUrl: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
headless: boolean;
|
|
27
|
+
approvalMode: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
prompt?: string;
|
|
30
|
+
providerArgs: string[];
|
|
31
|
+
providerConfig: ProviderConfig;
|
|
32
|
+
env: Record<string, string>;
|
|
33
|
+
controlPort: number;
|
|
34
|
+
monitor?: {
|
|
35
|
+
deliver(messages: Message[]): Promise<number[]>;
|
|
36
|
+
};
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SpawnArgs {
|
|
41
|
+
command: string;
|
|
42
|
+
args: string[];
|
|
43
|
+
cwd: string;
|
|
44
|
+
env: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ManagedProcess {
|
|
48
|
+
pid?: number;
|
|
49
|
+
process?: Bun.Subprocess;
|
|
50
|
+
meta?: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ProviderAdapter {
|
|
54
|
+
provider: string;
|
|
55
|
+
spawn(config: RunnerSpawnConfig): Promise<ManagedProcess>;
|
|
56
|
+
shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void>;
|
|
57
|
+
deliver(process: ManagedProcess, messages: Message[]): Promise<void>;
|
|
58
|
+
onStatusChange(cb: (status: SemanticStatus) => void): void;
|
|
59
|
+
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function providerMessageText(messages: Message[]): string {
|
|
63
|
+
return messages
|
|
64
|
+
.map((message) => {
|
|
65
|
+
const subject = message.subject ? `Subject: ${message.subject}\n` : "";
|
|
66
|
+
return `[relay message #${message.id} from ${message.from}]\n${subject}${message.body}`;
|
|
67
|
+
})
|
|
68
|
+
.join("\n\n");
|
|
69
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, hostname } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import type { ProviderConfig } from "./adapter";
|
|
5
|
+
|
|
6
|
+
interface GlobalRunnerConfig {
|
|
7
|
+
relayUrl: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
defaultCwd: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface LoadedProviderConfig extends ProviderConfig {
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_RELAY_URL = "http://127.0.0.1:4850";
|
|
17
|
+
|
|
18
|
+
function agentRelayHome(): string {
|
|
19
|
+
return process.env.AGENT_RELAY_HOME || join(homedir(), ".agent-relay");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function providersDir(home = agentRelayHome()): string {
|
|
23
|
+
return join(home, "providers");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function defaultProviderConfig(provider: string): ProviderConfig {
|
|
27
|
+
const command = provider === "claude" ? "claude-rig" : provider;
|
|
28
|
+
return {
|
|
29
|
+
command,
|
|
30
|
+
defaultArgs: provider === "claude" ? ["--dangerously-skip-permissions"] : [],
|
|
31
|
+
env: {},
|
|
32
|
+
pluginDirs: [],
|
|
33
|
+
defaultCapabilities: ["chat", "code", "review"],
|
|
34
|
+
defaultApprovalMode: "guarded",
|
|
35
|
+
defaultTags: [],
|
|
36
|
+
headless: {
|
|
37
|
+
tmuxPrefix: `${provider}-relay`,
|
|
38
|
+
shutdownTimeoutMs: 10_000,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function loadGlobalConfig(home = agentRelayHome()): GlobalRunnerConfig {
|
|
44
|
+
const path = join(home, "config.json");
|
|
45
|
+
const parsed = readJson(path);
|
|
46
|
+
return {
|
|
47
|
+
relayUrl: stringValue(parsed.relayUrl) ?? process.env.AGENT_RELAY_URL ?? DEFAULT_RELAY_URL,
|
|
48
|
+
token: stringValue(parsed.token) ?? process.env.AGENT_RELAY_TOKEN,
|
|
49
|
+
defaultCwd: stringValue(parsed.defaultCwd) ?? process.cwd(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadProviderConfig(provider: string, home = agentRelayHome()): LoadedProviderConfig {
|
|
54
|
+
const path = join(providersDir(home), `${provider}.json`);
|
|
55
|
+
const raw = readJson(path);
|
|
56
|
+
const defaults = defaultProviderConfig(provider);
|
|
57
|
+
return {
|
|
58
|
+
path,
|
|
59
|
+
command: stringValue(raw.command) ?? defaults.command,
|
|
60
|
+
defaultArgs: stringArray(raw.defaultArgs) ?? defaults.defaultArgs,
|
|
61
|
+
env: stringRecord(raw.env) ?? defaults.env,
|
|
62
|
+
pluginDirs: stringArray(raw.pluginDirs) ?? defaults.pluginDirs,
|
|
63
|
+
defaultCapabilities: stringArray(raw.defaultCapabilities) ?? defaults.defaultCapabilities,
|
|
64
|
+
defaultApprovalMode: stringValue(raw.defaultApprovalMode) ?? defaults.defaultApprovalMode,
|
|
65
|
+
defaultTags: stringArray(raw.defaultTags) ?? defaults.defaultTags,
|
|
66
|
+
headless: {
|
|
67
|
+
tmuxPrefix: stringValue(recordValue(raw.headless).tmuxPrefix) ?? defaults.headless.tmuxPrefix,
|
|
68
|
+
shutdownTimeoutMs: positiveInteger(recordValue(raw.headless).shutdownTimeoutMs) ?? defaults.headless.shutdownTimeoutMs,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function writeProviderConfig(provider: string, config: ProviderConfig, home = agentRelayHome()): LoadedProviderConfig {
|
|
74
|
+
const dir = providersDir(home);
|
|
75
|
+
mkdirSync(dir, { recursive: true });
|
|
76
|
+
const path = join(dir, `${provider}.json`);
|
|
77
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
78
|
+
return { ...config, path };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function providerConfigPublic(config: LoadedProviderConfig): Record<string, unknown> {
|
|
82
|
+
return {
|
|
83
|
+
path: config.path,
|
|
84
|
+
command: config.command,
|
|
85
|
+
defaultArgs: config.defaultArgs,
|
|
86
|
+
env: maskEnv(config.env),
|
|
87
|
+
pluginDirs: config.pluginDirs,
|
|
88
|
+
defaultCapabilities: config.defaultCapabilities,
|
|
89
|
+
defaultApprovalMode: config.defaultApprovalMode,
|
|
90
|
+
defaultTags: config.defaultTags,
|
|
91
|
+
headless: config.headless,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function runnerId(provider: string, cwd: string, label?: string): string {
|
|
96
|
+
const project = cwd.split("/").filter(Boolean).at(-1) || "workspace";
|
|
97
|
+
const cleanLabel = (label || project).replace(/[^a-zA-Z0-9._-]+/g, "-").toLowerCase();
|
|
98
|
+
return `${hostname()}-${provider}-${cleanLabel}-${crypto.randomUUID().slice(0, 8)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveCwd(value: string | undefined, fallback: string): string {
|
|
102
|
+
return resolve(value || fallback);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readJson(path: string): Record<string, unknown> {
|
|
106
|
+
if (!existsSync(path)) return {};
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
109
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
110
|
+
} catch {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stringValue(value: unknown): string | undefined {
|
|
116
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
120
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stringArray(value: unknown): string[] | undefined {
|
|
124
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function stringRecord(value: unknown): Record<string, string> | undefined {
|
|
128
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
129
|
+
const entries = Object.entries(value);
|
|
130
|
+
if (entries.some(([, item]) => typeof item !== "string")) return undefined;
|
|
131
|
+
return Object.fromEntries(entries) as Record<string, string>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function recordValue(value: unknown): Record<string, unknown> {
|
|
135
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function maskEnv(env: Record<string, string>): Record<string, string> {
|
|
139
|
+
const result: Record<string, string> = {};
|
|
140
|
+
for (const [key, value] of Object.entries(env)) {
|
|
141
|
+
result[key] = /token|secret|key|password/i.test(key) && !value.startsWith("$env:") ? "********" : value;
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
@@ -115,20 +115,13 @@ for (const provider of providers) {
|
|
|
115
115
|
});
|
|
116
116
|
console.log(`registered ${provider}: ${agent.id}`);
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
await waitFor(`waiting for ${provider} managed session`, async () => {
|
|
119
119
|
const latest = (await api<Orchestrator[]>("GET", "/orchestrators")).find((orch) => orch.id === orchestrator.id);
|
|
120
120
|
return latest?.managedAgents?.find((entry) => entry.label === label || entry.tmuxSession.includes(label)) || null;
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
await api("POST", `/
|
|
123
|
+
await api("POST", `/agents/${encodeURIComponent(agent.id)}/actions`, {
|
|
124
124
|
action: "shutdown",
|
|
125
|
-
agentId: managed.tmuxSession,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
await waitFor(`waiting for ${provider} managed session shutdown`, async () => {
|
|
129
|
-
const latest = (await api<Orchestrator[]>("GET", "/orchestrators")).find((orch) => orch.id === orchestrator.id);
|
|
130
|
-
const stillManaged = latest?.managedAgents?.some((managed) => managed.label === label || managed.tmuxSession.includes(label));
|
|
131
|
-
return stillManaged ? null : true;
|
|
132
125
|
});
|
|
133
126
|
await waitFor(`waiting for ${provider} agent cleanup`, async () => {
|
|
134
127
|
const current = await apiOptional<Agent>("GET", `/agents/${encodeURIComponent(agent.id)}`);
|
package/src/agent-spawn.ts
CHANGED
|
@@ -1,27 +1,6 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export type CodexSpawnApprovalMode = "open" | "guarded" | "read-only";
|
|
6
|
-
|
|
7
|
-
interface CodexSpawnInput {
|
|
8
|
-
cwd?: string;
|
|
9
|
-
approvalMode: CodexSpawnApprovalMode;
|
|
10
|
-
label?: string;
|
|
11
|
-
relayUrl: string;
|
|
12
|
-
token?: string;
|
|
13
|
-
dryRun?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface CodexSpawnResult {
|
|
17
|
-
provider: "codex";
|
|
18
|
-
pid?: number;
|
|
19
|
-
cwd: string;
|
|
20
|
-
approvalMode: CodexSpawnApprovalMode;
|
|
21
|
-
logPath: string;
|
|
22
|
-
command: string[];
|
|
23
|
-
dryRun?: boolean;
|
|
24
|
-
}
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
4
|
|
|
26
5
|
interface HostDirectoryEntry {
|
|
27
6
|
name: string;
|
|
@@ -64,74 +43,3 @@ export function listHostDirectories(raw: string | undefined): HostDirectoryListi
|
|
|
64
43
|
entries,
|
|
65
44
|
};
|
|
66
45
|
}
|
|
67
|
-
|
|
68
|
-
export function codexSpawnCommand(): string[] {
|
|
69
|
-
const override = process.env.AGENT_RELAY_CODEX_RELAY_BIN;
|
|
70
|
-
if (override) return [override];
|
|
71
|
-
|
|
72
|
-
const repoLauncher = resolve(import.meta.dir, "../codex/bin/agent-relay-codex.ts");
|
|
73
|
-
if (existsSync(repoLauncher)) return ["bun", "run", repoLauncher, "start"];
|
|
74
|
-
|
|
75
|
-
const fromPath = findOnPath("codex-relay");
|
|
76
|
-
if (fromPath) return [fromPath];
|
|
77
|
-
|
|
78
|
-
return ["codex-relay"];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function codexSpawnLogPath(cwd: string, now = Date.now()): string {
|
|
82
|
-
const project = basename(cwd).replace(/[^a-zA-Z0-9._-]+/g, "-") || "project";
|
|
83
|
-
return join(homedir(), ".agent-relay", "spawns", `codex-${project}-${now}.log`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function spawnCodexAgent(input: CodexSpawnInput): CodexSpawnResult {
|
|
87
|
-
const cwd = normalizeCodexSpawnCwd(input.cwd);
|
|
88
|
-
const command = [
|
|
89
|
-
...codexSpawnCommand(),
|
|
90
|
-
"--headless",
|
|
91
|
-
"--relay-url",
|
|
92
|
-
input.relayUrl,
|
|
93
|
-
];
|
|
94
|
-
const logPath = codexSpawnLogPath(cwd);
|
|
95
|
-
const env: Record<string, string | undefined> = {
|
|
96
|
-
...process.env,
|
|
97
|
-
AGENT_RELAY_CODEX_HEADLESS: "1",
|
|
98
|
-
AGENT_RELAY_APPROVAL: input.approvalMode,
|
|
99
|
-
AGENT_RELAY_TAGS: mergeCsv(process.env.AGENT_RELAY_TAGS, ["headless", "dashboard-spawned"]),
|
|
100
|
-
AGENT_RELAY_LABEL: input.label || process.env.AGENT_RELAY_LABEL,
|
|
101
|
-
AGENT_RELAY_URL: input.relayUrl,
|
|
102
|
-
AGENT_RELAY_TOKEN: input.token || process.env.AGENT_RELAY_TOKEN,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (input.dryRun) {
|
|
106
|
-
return { provider: "codex", cwd, approvalMode: input.approvalMode, logPath, command, dryRun: true };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
110
|
-
const log = Bun.file(logPath);
|
|
111
|
-
const child = Bun.spawn(command, {
|
|
112
|
-
cwd,
|
|
113
|
-
env,
|
|
114
|
-
stdin: "ignore",
|
|
115
|
-
stdout: log,
|
|
116
|
-
stderr: log,
|
|
117
|
-
});
|
|
118
|
-
child.unref();
|
|
119
|
-
|
|
120
|
-
return { provider: "codex", pid: child.pid, cwd, approvalMode: input.approvalMode, logPath, command };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function mergeCsv(raw: string | undefined, additions: string[]): string {
|
|
124
|
-
return [...new Set([
|
|
125
|
-
...(raw || "").split(",").map((item) => item.trim()).filter(Boolean),
|
|
126
|
-
...additions,
|
|
127
|
-
])].join(",");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function findOnPath(command: string): string | null {
|
|
131
|
-
for (const dir of (process.env.PATH || "").split(delimiter)) {
|
|
132
|
-
if (!dir) continue;
|
|
133
|
-
const candidate = join(dir, command);
|
|
134
|
-
if (existsSync(candidate)) return candidate;
|
|
135
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
}
|