agent-relay-runner 0.10.19 → 0.10.21
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 +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +4 -1
- package/plugins/claude/hooks/hooks.json +114 -0
- package/plugins/claude/hooks/permission-request.sh +20 -0
- package/plugins/claude/hooks/post-compact.sh +5 -0
- package/plugins/claude/hooks/pre-compact.sh +5 -0
- package/plugins/claude/hooks/relay-status.sh +66 -0
- package/plugins/claude/hooks/session-end.sh +16 -3
- package/plugins/claude/hooks/session-start.sh +14 -0
- package/plugins/claude/hooks/stop-failure.sh +15 -0
- package/plugins/claude/hooks/stop.sh +13 -3
- package/plugins/claude/hooks/subagent-start.sh +12 -0
- package/plugins/claude/hooks/subagent-stop.sh +12 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +2 -3
- package/plugins/claude/monitors/relay-monitor.ts +16 -4
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +7 -3
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +6 -2
- package/src/adapter.ts +207 -6
- package/src/adapters/claude-delivery.ts +108 -0
- package/src/adapters/claude.ts +232 -31
- package/src/adapters/codex-client.ts +27 -1
- package/src/adapters/codex.ts +635 -26
- package/src/attachment-cache.ts +190 -0
- package/src/claim-tracker.ts +48 -5
- package/src/control-server.ts +193 -6
- package/src/index.ts +203 -6
- package/src/profile-home.ts +85 -0
- package/src/profile-projection.ts +146 -0
- package/src/relay-instructions.ts +25 -0
- package/src/runner.ts +811 -40
- package/src/version.ts +39 -0
package/src/index.ts
CHANGED
|
@@ -1,58 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { hostname } from "node:os";
|
|
2
3
|
import { basename } from "node:path";
|
|
3
4
|
import { isatty } from "node:tty";
|
|
4
5
|
import { ClaudeAdapter } from "./adapters/claude";
|
|
5
6
|
import { CodexAdapter } from "./adapters/codex";
|
|
6
7
|
import { AgentRunner } from "./runner";
|
|
7
8
|
import { loadGlobalConfig, loadProviderConfig, resolveCwd, runnerId } from "./config";
|
|
9
|
+
import { VERSION } from "./version";
|
|
10
|
+
import type { AgentProfile, WorkspaceMetadata } from "agent-relay-sdk";
|
|
8
11
|
|
|
9
12
|
interface CliOptions {
|
|
10
13
|
provider: "claude" | "codex";
|
|
14
|
+
model?: string;
|
|
15
|
+
effort?: string;
|
|
11
16
|
headless: boolean;
|
|
12
17
|
interactive: boolean;
|
|
13
18
|
cwd?: string;
|
|
14
19
|
rig?: string;
|
|
20
|
+
profile?: string;
|
|
15
21
|
relayUrl?: string;
|
|
16
22
|
token?: string;
|
|
17
23
|
approvalMode?: string;
|
|
18
24
|
label?: string;
|
|
19
25
|
agentId?: string;
|
|
20
26
|
prompt?: string;
|
|
27
|
+
systemPromptAppend?: string;
|
|
21
28
|
tags: string[];
|
|
22
29
|
caps: string[];
|
|
23
30
|
providerArgs: string[];
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export async function main(argv = process.argv): Promise<void> {
|
|
34
|
+
if (isVersionRequest(argv)) {
|
|
35
|
+
console.log(VERSION);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
27
38
|
const opts = parseArgs(argv);
|
|
28
39
|
const globalConfig = loadGlobalConfig();
|
|
29
40
|
const providerConfig = loadProviderConfig(opts.provider);
|
|
30
41
|
const cwd = resolveCwd(opts.cwd, globalConfig.defaultCwd);
|
|
31
42
|
const id = runnerId(opts.provider, cwd, opts.label);
|
|
32
43
|
const adapter = opts.provider === "claude" ? new ClaudeAdapter() : new CodexAdapter();
|
|
44
|
+
const relayUrl = opts.relayUrl ?? globalConfig.relayUrl;
|
|
45
|
+
const runtimeAuth = await resolveRunnerToken({
|
|
46
|
+
interactive: opts.interactive,
|
|
47
|
+
relayUrl,
|
|
48
|
+
token: opts.token ?? globalConfig.token,
|
|
49
|
+
runtimeTokenProfile: process.env.AGENT_RELAY_TOKEN_PROFILE,
|
|
50
|
+
runtimeTokenJti: process.env.AGENT_RELAY_TOKEN_JTI,
|
|
51
|
+
runtimeTokenExpiresAt: parseTokenExpiresAt(process.env.AGENT_RELAY_TOKEN_EXPIRES_AT),
|
|
52
|
+
provider: opts.provider,
|
|
53
|
+
cwd,
|
|
54
|
+
runnerId: id,
|
|
55
|
+
agentId: opts.agentId ?? id,
|
|
56
|
+
label: opts.label,
|
|
57
|
+
});
|
|
33
58
|
|
|
34
59
|
let exitResolve: (code: number) => void;
|
|
35
60
|
const exitPromise = new Promise<number>((resolve) => { exitResolve = resolve; });
|
|
36
61
|
|
|
62
|
+
const approvalMode = opts.approvalMode
|
|
63
|
+
?? detectApprovalModeFromArgs(opts.provider, providerConfig.defaultArgs, opts.providerArgs)
|
|
64
|
+
?? providerConfig.defaultApprovalMode;
|
|
65
|
+
|
|
37
66
|
const runner = new AgentRunner({
|
|
38
67
|
provider: opts.provider,
|
|
68
|
+
model: opts.model,
|
|
69
|
+
effort: opts.effort,
|
|
39
70
|
runnerId: id,
|
|
40
71
|
instanceId: crypto.randomUUID(),
|
|
41
72
|
agentId: opts.agentId,
|
|
42
|
-
relayUrl
|
|
43
|
-
token:
|
|
73
|
+
relayUrl,
|
|
74
|
+
token: runtimeAuth.token,
|
|
75
|
+
tokenJti: runtimeAuth.jti,
|
|
76
|
+
tokenProfileId: runtimeAuth.profileId,
|
|
77
|
+
tokenExpiresAt: runtimeAuth.expiresAt,
|
|
78
|
+
rootTokenFallback: runtimeAuth.rootFallback,
|
|
44
79
|
cwd,
|
|
45
80
|
headless: opts.headless,
|
|
46
|
-
approvalMode
|
|
81
|
+
approvalMode,
|
|
47
82
|
label: opts.label,
|
|
48
83
|
rig: opts.rig,
|
|
84
|
+
profile: opts.profile ?? process.env.AGENT_RELAY_AGENT_PROFILE,
|
|
85
|
+
agentProfile: parseAgentProfileEnv(process.env.AGENT_RELAY_AGENT_PROFILE_JSON),
|
|
86
|
+
workspace: parseWorkspaceEnv(process.env.AGENT_RELAY_WORKSPACE_JSON),
|
|
49
87
|
prompt: opts.prompt,
|
|
88
|
+
systemPromptAppend: opts.systemPromptAppend,
|
|
89
|
+
tmuxSession: process.env.AGENT_RELAY_TMUX_SESSION,
|
|
50
90
|
tags: opts.tags,
|
|
51
91
|
capabilities: opts.caps,
|
|
52
92
|
providerArgs: opts.providerArgs,
|
|
53
93
|
policyName: process.env.AGENT_RELAY_POLICY,
|
|
54
94
|
spawnRequestId: process.env.AGENT_RELAY_SPAWN_REQUEST_ID,
|
|
55
|
-
|
|
95
|
+
automationId: process.env.AGENT_RELAY_AUTOMATION_ID,
|
|
96
|
+
automationRunId: process.env.AGENT_RELAY_AUTOMATION_RUN_ID,
|
|
56
97
|
startedAt: Date.now(),
|
|
57
98
|
providerConfig,
|
|
58
99
|
adapter,
|
|
@@ -65,6 +106,108 @@ export async function main(argv = process.argv): Promise<void> {
|
|
|
65
106
|
process.exit(code);
|
|
66
107
|
}
|
|
67
108
|
|
|
109
|
+
export async function resolveRunnerToken(input: {
|
|
110
|
+
interactive: boolean;
|
|
111
|
+
relayUrl: string;
|
|
112
|
+
token?: string;
|
|
113
|
+
runtimeTokenProfile?: string;
|
|
114
|
+
runtimeTokenJti?: string;
|
|
115
|
+
runtimeTokenExpiresAt?: number;
|
|
116
|
+
provider: string;
|
|
117
|
+
cwd: string;
|
|
118
|
+
runnerId: string;
|
|
119
|
+
agentId: string;
|
|
120
|
+
label?: string;
|
|
121
|
+
}): Promise<{ token?: string; jti?: string; profileId?: string; expiresAt?: number; rootFallback?: boolean }> {
|
|
122
|
+
if (!input.token) return {};
|
|
123
|
+
const claims = decodeComponentTokenClaims(input.token);
|
|
124
|
+
if (input.runtimeTokenProfile) {
|
|
125
|
+
return {
|
|
126
|
+
token: input.token,
|
|
127
|
+
jti: input.runtimeTokenJti ?? claims?.jti,
|
|
128
|
+
profileId: input.runtimeTokenProfile,
|
|
129
|
+
expiresAt: input.runtimeTokenExpiresAt ?? claims?.exp,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const inferredRuntime = inferRuntimeTokenMetadata(claims);
|
|
133
|
+
if (!input.interactive && inferredRuntime) return { token: input.token, ...inferredRuntime };
|
|
134
|
+
if (!input.interactive) return { token: input.token };
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const url = new URL("/api/runtime-tokens/interactive-runner", input.relayUrl);
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"X-Agent-Relay-Token": input.token,
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify({
|
|
145
|
+
provider: input.provider,
|
|
146
|
+
cwd: input.cwd,
|
|
147
|
+
runnerId: input.runnerId,
|
|
148
|
+
agentId: input.agentId,
|
|
149
|
+
label: input.label,
|
|
150
|
+
host: hostname(),
|
|
151
|
+
}),
|
|
152
|
+
signal: AbortSignal.timeout(5_000),
|
|
153
|
+
});
|
|
154
|
+
const body = await res.json().catch(() => null) as { token?: string; record?: { jti?: string; profileId?: string; expiresAt?: number }; error?: string } | null;
|
|
155
|
+
if (res.ok && body?.token) return { token: body.token, jti: body.record?.jti, profileId: body.record?.profileId ?? "provider-interactive", expiresAt: body.record?.expiresAt };
|
|
156
|
+
const detail = body?.error ? `: ${body.error}` : "";
|
|
157
|
+
console.warn(`[agent-relay] interactive scoped-token exchange failed (${res.status}${detail}); continuing with existing token. Root-token runtime fallback is deprecated.`);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.warn(`[agent-relay] interactive scoped-token exchange failed (${error instanceof Error ? error.message : String(error)}); continuing with existing token. Root-token runtime fallback is deprecated.`);
|
|
160
|
+
}
|
|
161
|
+
return { token: input.token, rootFallback: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function decodeComponentTokenClaims(token: string | undefined): { sub?: string; role?: string; exp?: number; jti?: string } | undefined {
|
|
165
|
+
const payload = token?.split(".")[1];
|
|
166
|
+
if (!payload) return undefined;
|
|
167
|
+
try {
|
|
168
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
169
|
+
const decoded = JSON.parse(Buffer.from(normalized, "base64").toString("utf8")) as Record<string, unknown>;
|
|
170
|
+
const exp = typeof decoded.exp === "number" && Number.isSafeInteger(decoded.exp) && decoded.exp > 0 ? decoded.exp : undefined;
|
|
171
|
+
return {
|
|
172
|
+
...(typeof decoded.sub === "string" ? { sub: decoded.sub } : {}),
|
|
173
|
+
...(typeof decoded.role === "string" ? { role: decoded.role } : {}),
|
|
174
|
+
...(typeof decoded.jti === "string" ? { jti: decoded.jti } : {}),
|
|
175
|
+
...(exp ? { exp } : {}),
|
|
176
|
+
};
|
|
177
|
+
} catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function inferRuntimeTokenMetadata(claims: { sub?: string; role?: string; exp?: number; jti?: string } | undefined): { jti?: string; profileId?: string; expiresAt?: number } | undefined {
|
|
183
|
+
if (claims?.role !== "provider" || !claims.exp) return undefined;
|
|
184
|
+
const sub = claims.sub ?? "";
|
|
185
|
+
const profileId = sub.startsWith("runner:interactive:")
|
|
186
|
+
? "provider-interactive"
|
|
187
|
+
: sub.startsWith("runner:spawn:") || sub.startsWith("runner:policy:") || /^runner:(codex|claude|provider):/.test(sub)
|
|
188
|
+
? "provider-agent"
|
|
189
|
+
: undefined;
|
|
190
|
+
if (!profileId) return undefined;
|
|
191
|
+
return {
|
|
192
|
+
...(claims.jti ? { jti: claims.jti } : {}),
|
|
193
|
+
profileId,
|
|
194
|
+
expiresAt: claims.exp,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseTokenExpiresAt(value: string | undefined): number | undefined {
|
|
199
|
+
if (!value) return undefined;
|
|
200
|
+
const parsed = Number(value);
|
|
201
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function isVersionRequest(argv: string[]): boolean {
|
|
205
|
+
const relayArgs = argv.slice(2);
|
|
206
|
+
const providerSep = relayArgs.indexOf("--");
|
|
207
|
+
const ownArgs = providerSep >= 0 ? relayArgs.slice(0, providerSep) : relayArgs;
|
|
208
|
+
return ownArgs.some((arg) => arg === "--version" || arg === "-v");
|
|
209
|
+
}
|
|
210
|
+
|
|
68
211
|
function parseArgs(argv: string[]): CliOptions {
|
|
69
212
|
const bin = [argv[1], process.env._].map((s) => basename(s || "")).join(" ");
|
|
70
213
|
let provider: "claude" | "codex" = bin.includes("claude") ? "claude" : "codex";
|
|
@@ -73,14 +216,18 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
73
216
|
const ownArgs = providerSep >= 0 ? relayArgs.slice(0, providerSep) : relayArgs;
|
|
74
217
|
const providerArgs = providerSep >= 0 ? relayArgs.slice(providerSep + 1) : [];
|
|
75
218
|
let headless = false;
|
|
219
|
+
let model: string | undefined;
|
|
220
|
+
let effort: string | undefined;
|
|
76
221
|
let cwd: string | undefined;
|
|
77
222
|
let rig: string | undefined;
|
|
223
|
+
let profile: string | undefined;
|
|
78
224
|
let relayUrl: string | undefined;
|
|
79
225
|
let token: string | undefined;
|
|
80
226
|
let approvalMode: string | undefined;
|
|
81
227
|
let label: string | undefined;
|
|
82
228
|
let agentId: string | undefined;
|
|
83
229
|
let prompt: string | undefined;
|
|
230
|
+
let systemPromptAppend: string | undefined;
|
|
84
231
|
let tags: string[] = [];
|
|
85
232
|
let caps: string[] = [];
|
|
86
233
|
|
|
@@ -88,14 +235,18 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
88
235
|
const arg = ownArgs[i];
|
|
89
236
|
if (arg === "claude" || arg === "codex") provider = arg;
|
|
90
237
|
else if (arg === "--headless") headless = true;
|
|
238
|
+
else if (arg === "--model" && ownArgs[i + 1]) model = ownArgs[++i];
|
|
239
|
+
else if (arg === "--effort" && ownArgs[i + 1]) effort = ownArgs[++i];
|
|
91
240
|
else if (arg === "--cwd" && ownArgs[i + 1]) cwd = ownArgs[++i];
|
|
92
241
|
else if (arg === "--rig" && ownArgs[i + 1]) rig = ownArgs[++i];
|
|
242
|
+
else if (arg === "--profile" && ownArgs[i + 1]) profile = ownArgs[++i];
|
|
93
243
|
else if (arg === "--relay-url" && ownArgs[i + 1]) relayUrl = ownArgs[++i];
|
|
94
244
|
else if (arg === "--token" && ownArgs[i + 1]) token = ownArgs[++i];
|
|
95
245
|
else if ((arg === "--approval" || arg === "--approval-mode") && ownArgs[i + 1]) approvalMode = ownArgs[++i];
|
|
96
246
|
else if (arg === "--label" && ownArgs[i + 1]) label = ownArgs[++i];
|
|
97
247
|
else if (arg === "--agent-id" && ownArgs[i + 1]) agentId = ownArgs[++i];
|
|
98
248
|
else if (arg === "--prompt" && ownArgs[i + 1]) prompt = ownArgs[++i];
|
|
249
|
+
else if ((arg === "--system-prompt-append" || arg === "--append-system-prompt") && ownArgs[i + 1]) systemPromptAppend = ownArgs[++i];
|
|
99
250
|
else if (arg === "--tags" && ownArgs[i + 1]) tags = csvSplit(ownArgs[++i]!);
|
|
100
251
|
else if (arg === "--caps" && ownArgs[i + 1]) caps = csvSplit(ownArgs[++i]!);
|
|
101
252
|
else if (arg === "--help" || arg === "-h") {
|
|
@@ -107,15 +258,35 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
107
258
|
}
|
|
108
259
|
|
|
109
260
|
const interactive = !headless && isatty(0);
|
|
110
|
-
return { provider, headless, interactive, cwd, rig, relayUrl, token, approvalMode, label, agentId, prompt, tags, caps, providerArgs };
|
|
261
|
+
return { provider, model, effort, headless, interactive, cwd, rig, profile, relayUrl, token, approvalMode, label, agentId, prompt, systemPromptAppend, tags, caps, providerArgs };
|
|
111
262
|
}
|
|
112
263
|
|
|
113
264
|
function csvSplit(value: string): string[] {
|
|
114
265
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
115
266
|
}
|
|
116
267
|
|
|
268
|
+
function parseAgentProfileEnv(raw: string | undefined): AgentProfile | undefined {
|
|
269
|
+
if (!raw) return undefined;
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(raw) as Partial<AgentProfile>;
|
|
272
|
+
return parsed && typeof parsed === "object" && typeof parsed.name === "string" ? parsed as AgentProfile : undefined;
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseWorkspaceEnv(raw: string | undefined): WorkspaceMetadata | undefined {
|
|
279
|
+
if (!raw) return undefined;
|
|
280
|
+
try {
|
|
281
|
+
const parsed = JSON.parse(raw) as Partial<WorkspaceMetadata>;
|
|
282
|
+
return parsed && typeof parsed === "object" && typeof parsed.mode === "string" ? parsed as WorkspaceMetadata : undefined;
|
|
283
|
+
} catch {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
117
288
|
function printHelp(provider: string): void {
|
|
118
|
-
console.log(`${provider}-relay [--headless] [--rig NAME] [--cwd PATH] [--relay-url URL] [--approval MODE] [--label NAME] [--agent-id ID] [--tags a,b] [--caps x,y] [-- provider-args...]`);
|
|
289
|
+
console.log(`${provider}-relay [--headless] [--model ALIAS] [--effort LEVEL] [--rig NAME] [--profile NAME] [--cwd PATH] [--relay-url URL] [--approval MODE] [--label NAME] [--agent-id ID] [--prompt TEXT] [--system-prompt-append TEXT] [--tags a,b] [--caps x,y] [-- provider-args...]`);
|
|
119
290
|
}
|
|
120
291
|
|
|
121
292
|
function signalPromise(): Promise<number> {
|
|
@@ -125,6 +296,32 @@ function signalPromise(): Promise<number> {
|
|
|
125
296
|
});
|
|
126
297
|
}
|
|
127
298
|
|
|
299
|
+
export function detectApprovalModeFromArgs(provider: string, defaultArgs: string[], providerArgs: string[]): string | undefined {
|
|
300
|
+
const allArgs = [...defaultArgs, ...providerArgs];
|
|
301
|
+
if (provider === "claude") {
|
|
302
|
+
if (allArgs.includes("--dangerously-skip-permissions")) return "open";
|
|
303
|
+
const modeIdx = allArgs.lastIndexOf("--permission-mode");
|
|
304
|
+
if (modeIdx >= 0) {
|
|
305
|
+
const mode = allArgs[modeIdx + 1];
|
|
306
|
+
if (mode === "dontAsk") return "read-only";
|
|
307
|
+
if (mode === "default") return "guarded";
|
|
308
|
+
}
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
if (provider === "codex") {
|
|
312
|
+
if (allArgs.includes("--yolo")) return "open";
|
|
313
|
+
for (let i = 0; i < allArgs.length; i++) {
|
|
314
|
+
if (allArgs[i] === "-c" || allArgs[i] === "--config") {
|
|
315
|
+
const value = allArgs[i + 1] ?? "";
|
|
316
|
+
if (/sandbox_mode\s*=\s*"?danger-full-access"?/.test(value)) return "open";
|
|
317
|
+
if (/sandbox_mode\s*=\s*"?read-only"?/.test(value)) return "read-only";
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
128
325
|
if (import.meta.main) {
|
|
129
326
|
await main();
|
|
130
327
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
|
|
5
|
+
import { CLAUDE_RELAY_MANUAL } from "./relay-instructions";
|
|
6
|
+
|
|
7
|
+
type ProviderHome = {
|
|
8
|
+
path: string;
|
|
9
|
+
authLinked: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function profileUsesHostProviderGlobals(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
|
|
13
|
+
return !config.agentProfile || config.agentProfile.base === "host";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function profileRequiresIsolatedHome(config: RunnerSpawnConfig): boolean {
|
|
17
|
+
return Boolean(config.agentProfile && config.agentProfile.base !== "host");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function prepareCodexProfileHome(config: RunnerSpawnConfig): ProviderHome | undefined {
|
|
21
|
+
if (!profileRequiresIsolatedHome(config)) return undefined;
|
|
22
|
+
const target = providerHomePath("codex", config);
|
|
23
|
+
mkdirSync(target, { recursive: true });
|
|
24
|
+
trustCodexProjectIfAllowed(target, config);
|
|
25
|
+
const sourceHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
26
|
+
const authLinked = linkExistingAuthItems(sourceHome, target, ["auth.json", "installation_id"]);
|
|
27
|
+
return { path: target, authLinked };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function prepareClaudeProfileHome(config: RunnerSpawnConfig): ProviderHome | undefined {
|
|
31
|
+
if (!profileRequiresIsolatedHome(config)) return undefined;
|
|
32
|
+
const target = providerHomePath("claude", config);
|
|
33
|
+
mkdirSync(target, { recursive: true });
|
|
34
|
+
// Only inject the Relay usage manual when the profile actually wants a Relay
|
|
35
|
+
// surface. An isolated-research profile (relay.context disabled) must not get
|
|
36
|
+
// agent-relay communication instructions written into its config home.
|
|
37
|
+
if (profileAllowsRelayFeature(config, "context")) writeClaudeRelayManual(target);
|
|
38
|
+
const sourceHome = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
39
|
+
const authLinked = linkExistingAuthItems(sourceHome, target, [".credentials.json", "statsig"]);
|
|
40
|
+
return { path: target, authLinked };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function providerHomePath(provider: "claude" | "codex", config: RunnerSpawnConfig): string {
|
|
44
|
+
const root = process.env.AGENT_RELAY_PROVIDER_HOME_ROOT || join(homedir(), ".agent-relay", "provider-homes");
|
|
45
|
+
const profileName = sanitizePathPart(config.agentProfile?.name || config.profile || "profile");
|
|
46
|
+
const instance = sanitizePathPart(config.instanceId || config.runnerId);
|
|
47
|
+
return join(root, provider, profileName, instance);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sanitizePathPart(value: string): string {
|
|
51
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 120) || "profile";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function linkExistingAuthItems(sourceHome: string, targetHome: string, items: string[]): string[] {
|
|
55
|
+
const linked: string[] = [];
|
|
56
|
+
for (const item of items) {
|
|
57
|
+
const source = join(sourceHome, item);
|
|
58
|
+
const target = join(targetHome, item);
|
|
59
|
+
if (!existsSync(source) || existsSync(target)) continue;
|
|
60
|
+
symlinkSync(source, target);
|
|
61
|
+
linked.push(item);
|
|
62
|
+
}
|
|
63
|
+
return linked;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function trustCodexProjectIfAllowed(codexHome: string, config: RunnerSpawnConfig): void {
|
|
67
|
+
if (config.agentProfile?.instructions.repoInstructions === "ignore") return;
|
|
68
|
+
const path = join(codexHome, "config.toml");
|
|
69
|
+
const project = tomlBasicString(resolve(config.cwd));
|
|
70
|
+
const section = `[projects.${project}]`;
|
|
71
|
+
const entry = `${section}\ntrust_level = "trusted"\n`;
|
|
72
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
73
|
+
if (current.includes(section)) return;
|
|
74
|
+
const prefix = current.trimEnd();
|
|
75
|
+
writeFileSync(path, `${prefix ? `${prefix}\n\n` : ""}${entry}`, { mode: 0o600 });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writeClaudeRelayManual(claudeHome: string): void {
|
|
79
|
+
const path = join(claudeHome, "CLAUDE.md");
|
|
80
|
+
writeFileSync(path, CLAUDE_RELAY_MANUAL, { mode: 0o600 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function tomlBasicString(value: string): string {
|
|
84
|
+
return JSON.stringify(value);
|
|
85
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { AgentProfile, AgentProfileProjectionEntry, AgentProfileProjectionReport, SpawnProvider } from "agent-relay-sdk";
|
|
2
|
+
|
|
3
|
+
interface AgentProfileProjectionInput {
|
|
4
|
+
provider: SpawnProvider;
|
|
5
|
+
profile: AgentProfile;
|
|
6
|
+
generatedAt?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function agentProfileProjectionReport(input: AgentProfileProjectionInput): AgentProfileProjectionReport {
|
|
10
|
+
const entries: AgentProfileProjectionEntry[] = [];
|
|
11
|
+
const add = (entry: AgentProfileProjectionEntry) => entries.push(entry);
|
|
12
|
+
const { provider, profile } = input;
|
|
13
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
14
|
+
|
|
15
|
+
add(relayContextEntry(profile));
|
|
16
|
+
add(relaySkillsEntry(provider, profile));
|
|
17
|
+
add(relayPluginsEntry(provider, profile));
|
|
18
|
+
add(relayStatusLineEntry(provider, profile));
|
|
19
|
+
add(repoInstructionsEntry(profile));
|
|
20
|
+
add(globalInstructionsEntry(provider, profile));
|
|
21
|
+
add(mcpEntry(provider, profile));
|
|
22
|
+
add(hooksEntry(provider, profile));
|
|
23
|
+
add(filesystemEntry(profile));
|
|
24
|
+
add(configHomeEntry(provider, profile));
|
|
25
|
+
add(envEntry(profile));
|
|
26
|
+
|
|
27
|
+
const warnings = entries
|
|
28
|
+
.filter((entry) => entry.result === "partial" || entry.result === "unsupported")
|
|
29
|
+
.map((entry) => `${entry.capability}: ${entry.detail}`);
|
|
30
|
+
const unsupported = entries
|
|
31
|
+
.filter((entry) => entry.result === "unsupported")
|
|
32
|
+
.map((entry) => entry.capability);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
profileName: profile.name,
|
|
36
|
+
provider,
|
|
37
|
+
base: profile.base,
|
|
38
|
+
generatedAt,
|
|
39
|
+
entries,
|
|
40
|
+
warnings,
|
|
41
|
+
unsupported,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function relayContextEntry(profile: AgentProfile): AgentProfileProjectionEntry {
|
|
46
|
+
return profile.relay.context
|
|
47
|
+
? applied("relay.context", "enabled", "Relay context is injected before Relay-delivered provider turns.")
|
|
48
|
+
: applied("relay.context", "disabled", "Relay context injection is disabled by the profile.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function relaySkillsEntry(provider: SpawnProvider, profile: AgentProfile): AgentProfileProjectionEntry {
|
|
52
|
+
if (provider === "codex") {
|
|
53
|
+
return profile.relay.skills
|
|
54
|
+
? applied("relay.skills", "enabled", "Bundled Agent Relay Codex skills are passed through Codex skills.config.")
|
|
55
|
+
: applied("relay.skills", "disabled", "Bundled Agent Relay Codex skills are omitted.");
|
|
56
|
+
}
|
|
57
|
+
return profile.relay.skills
|
|
58
|
+
? applied("relay.skills", "enabled", "Agent Relay Claude skills are available through the bundled Relay plugin.")
|
|
59
|
+
: applied("relay.skills", "disabled", profile.relay.plugins
|
|
60
|
+
? "Claude does not expose an independent Relay skill switch; disabling skills requires disabling the Relay plugin."
|
|
61
|
+
: "Bundled Agent Relay Claude plugin dirs are omitted, so Relay plugin-provided skills are unavailable.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function relayPluginsEntry(provider: SpawnProvider, profile: AgentProfile): AgentProfileProjectionEntry {
|
|
65
|
+
if (provider === "claude") {
|
|
66
|
+
return profile.relay.plugins
|
|
67
|
+
? applied("relay.plugins", "enabled", "Bundled Agent Relay Claude plugin dir is passed to Claude.")
|
|
68
|
+
: applied("relay.plugins", "disabled", "Bundled Agent Relay Claude plugin dirs are omitted.");
|
|
69
|
+
}
|
|
70
|
+
return notApplicable("relay.plugins", profile.relay.plugins ? "enabled" : "disabled", "Codex Relay integration uses skills/app-server delivery, not a Relay plugin dir.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function relayStatusLineEntry(provider: SpawnProvider, profile: AgentProfile): AgentProfileProjectionEntry {
|
|
74
|
+
if (provider === "claude") {
|
|
75
|
+
return profile.relay.statusLine
|
|
76
|
+
? applied("relay.statusLine", "enabled", "Agent Relay status-line context probe settings are injected when caller settings allow it.")
|
|
77
|
+
: applied("relay.statusLine", "disabled", "Agent Relay status-line settings are omitted.");
|
|
78
|
+
}
|
|
79
|
+
return notApplicable("relay.statusLine", profile.relay.statusLine ? "enabled" : "disabled", "Codex reports context through App Server events instead of a Relay status-line setting.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function repoInstructionsEntry(profile: AgentProfile): AgentProfileProjectionEntry {
|
|
83
|
+
if (profile.instructions.repoInstructions === "allow") {
|
|
84
|
+
return applied("instructions.repo", "allow", "Repo-local provider instructions are left available.");
|
|
85
|
+
}
|
|
86
|
+
return unsupported("instructions.repo", "ignore", "Provider-specific repo instruction suppression is not projected yet.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function globalInstructionsEntry(provider: SpawnProvider, profile: AgentProfile): AgentProfileProjectionEntry {
|
|
90
|
+
if (profile.instructions.globalInstructions === "allow") {
|
|
91
|
+
return applied("instructions.global", "allow", "Host/global provider instructions are left available.");
|
|
92
|
+
}
|
|
93
|
+
if (profile.base !== "host") {
|
|
94
|
+
return applied("instructions.global", "ignore", `${provider} uses an isolated provider config home, so host/global provider instructions are not loaded.`);
|
|
95
|
+
}
|
|
96
|
+
return unsupported("instructions.global", "ignore", `${provider} global instruction isolation requires an isolated provider config home.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mcpEntry(provider: SpawnProvider, profile: AgentProfile): AgentProfileProjectionEntry {
|
|
100
|
+
if (profile.mcp.mode === "host") return applied("mcp", "host", "Host provider MCP configuration is left available.");
|
|
101
|
+
if (profile.mcp.mode === "none" && profile.base !== "host") {
|
|
102
|
+
return applied("mcp", "none", `${provider} uses an isolated provider config home without host MCP configuration.`);
|
|
103
|
+
}
|
|
104
|
+
return unsupported("mcp", profile.mcp.mode, `${provider} MCP category projection is not enforced yet.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hooksEntry(provider: SpawnProvider, profile: AgentProfile): AgentProfileProjectionEntry {
|
|
108
|
+
if (profile.hooks.mode === "host") return applied("hooks", "host", "Host provider hooks are left available.");
|
|
109
|
+
if (profile.hooks.mode === "none" && profile.base !== "host") {
|
|
110
|
+
return applied("hooks", "none", `${provider} uses an isolated provider config home without host hook configuration.`);
|
|
111
|
+
}
|
|
112
|
+
return unsupported("hooks", profile.hooks.mode, `${provider} hook category projection is not enforced yet.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function filesystemEntry(profile: AgentProfile): AgentProfileProjectionEntry {
|
|
116
|
+
if (profile.permissions.filesystem === "host") return applied("permissions.filesystem", "host", "No profile-level filesystem boundary requested.");
|
|
117
|
+
return partial("permissions.filesystem", profile.permissions.filesystem, "Relay constrains spawn cwd through the orchestrator; provider-level filesystem sandbox projection is not complete yet.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function configHomeEntry(provider: SpawnProvider, profile: AgentProfile): AgentProfileProjectionEntry {
|
|
121
|
+
if (profile.base === "host") return applied("provider.configHome", "host", "Provider uses the host's normal configuration home.");
|
|
122
|
+
return applied("provider.configHome", profile.base, `${provider} is launched with an Agent Relay managed provider config home and linked host auth where available.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function envEntry(profile: AgentProfile): AgentProfileProjectionEntry {
|
|
126
|
+
const count = Object.keys(profile.env).length;
|
|
127
|
+
return count
|
|
128
|
+
? applied("env", `${count} vars`, "Profile environment variables are passed through the orchestrator runner environment.")
|
|
129
|
+
: applied("env", "none", "No profile environment variables requested.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function applied(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
|
|
133
|
+
return { capability, requested, result: "applied", detail };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function partial(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
|
|
137
|
+
return { capability, requested, result: "partial", detail };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function unsupported(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
|
|
141
|
+
return { capability, requested, result: "unsupported", detail };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function notApplicable(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
|
|
145
|
+
return { capability, requested, result: "not-applicable", detail };
|
|
146
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const CLAUDE_RELAY_MANUAL = `# Agent Relay
|
|
2
|
+
|
|
3
|
+
- Agent Relay messages may come from humans, channels, or other agents.
|
|
4
|
+
- Read the delivered message body and do the requested work.
|
|
5
|
+
- Reply through Relay only when the sender still needs an answer.
|
|
6
|
+
- Prefer \`agent-relay /reply <messageId> --stdin < response.md\` for the actual answer to an incoming Relay message.
|
|
7
|
+
- Do not send \`/message\` with the useful answer and then a separate \`/reply\` that only says it was sent.
|
|
8
|
+
- If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.
|
|
9
|
+
- If the useful response was already delivered through Relay, do not send an extra "sent", "done", or "drafts sent" confirmation unless the user explicitly asked for one.
|
|
10
|
+
- No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.
|
|
11
|
+
- Use \`agent-relay /react <messageId> <emoji>\` instead of a text reply for lightweight acknowledgement, approval, thanks, or "good job" after a completed work update.
|
|
12
|
+
- Good reaction uses: acknowledge praise with 👍 or ❤️, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.
|
|
13
|
+
- Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.
|
|
14
|
+
- A thumbs-up reaction to your question means approval to proceed. A thumbs-up reaction to your completed status means no further action.
|
|
15
|
+
- Use \`agent-relay get-message <id>\` when a delivered preview is truncated.
|
|
16
|
+
- Use \`agent-relay /status --json\` to inspect your current Relay identity and \`agent-relay /guide\` for command details.
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
export const CLAUDE_RELAY_CONTEXT = `[agent-relay] Relay is available. Follow the Agent Relay rules in CLAUDE.md when present, or run agent-relay /guide. Reply through Relay only when a response is needed; do not send status-only follow-ups after the useful Relay response was already delivered.`;
|
|
20
|
+
|
|
21
|
+
export const CLAUDE_READ_ONLY_RELAY_CONTEXT = `${CLAUDE_RELAY_CONTEXT}
|
|
22
|
+
|
|
23
|
+
This Claude session is running with restricted read-only Relay permissions. Do not invoke Agent Relay skills. If you need to reply, use this Bash command shape:
|
|
24
|
+
|
|
25
|
+
agent-relay /reply <messageId> "<your reply>"`;
|