agent-relay-runner 0.67.1 → 0.68.1
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 +1 -1
- package/src/adapter.ts +11 -0
- package/src/adapters/claude.ts +18 -29
- package/src/adapters/codex.ts +18 -23
- package/src/launch-assembly.ts +303 -0
- package/src/profile-home.ts +12 -0
- package/src/profile-projection.ts +112 -56
- package/src/provisioning.ts +157 -0
- package/src/relay-mcp.ts +61 -21
- package/src/runner-core.ts +3 -1
- package/src/runner-helpers.ts +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.1",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-sdk": "0.2.44"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapter.ts
CHANGED
|
@@ -191,6 +191,17 @@ export function profileAllowsRelayFeature(config: RunnerSpawnConfig, feature: ke
|
|
|
191
191
|
return config.agentProfile?.relay?.[feature] !== false;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// #552 — the strictest base. Beyond the isolated provider home that every
|
|
195
|
+
// non-host base already gets (fresh CLAUDE_CONFIG_DIR / CODEX_HOME, which drops
|
|
196
|
+
// the GLOBAL instruction + host MCP/hooks/plugins cascade), "vanilla" actively
|
|
197
|
+
// suppresses every remaining host customization channel at launch, including the
|
|
198
|
+
// REPO CLAUDE.md/AGENTS.md cascade that lives in the cwd and so survives home
|
|
199
|
+
// isolation. Claude: `--setting-sources "user"` (the --safe-mode pivot — safe-mode
|
|
200
|
+
// blocked even explicit provisioning). Codex: `project_doc_max_bytes=0`.
|
|
201
|
+
export function profileIsVanillaBase(config: Pick<RunnerSpawnConfig, "agentProfile">): boolean {
|
|
202
|
+
return config.agentProfile?.base === "vanilla";
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
export const RELAY_CONTEXT = `[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id — relay fills it from your token; use relay_whoami only if you need to reason about yourself. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own — find them later with relay_find_agents spawnedBy:me. CLI fallback: agent-relay /reply <messageId> --stdin < response.md; if a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide`;
|
|
195
206
|
|
|
196
207
|
// #306 — deliver the FULL message body by default. Only a pathological body beyond this
|
package/src/adapters/claude.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { profileAllowsRelayFeature, type ManagedProcess, type ProviderAdapter, t
|
|
|
10
10
|
import { collectClaudeSessionEvents } from "./claude-transcript";
|
|
11
11
|
import type { SessionEvent } from "../session-insights";
|
|
12
12
|
import { prepareClaudeProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
|
|
13
|
-
import {
|
|
13
|
+
import { assembleLaunch, materializeLaunchAssembly, sessionStatusLineSettingsArgs } from "../launch-assembly";
|
|
14
14
|
import { claudeProviderMessageText } from "./claude-delivery";
|
|
15
15
|
import { buildRateLimitProviderState, parseClaudeRateLimitPane } from "../rate-limit";
|
|
16
16
|
|
|
@@ -219,10 +219,18 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
// Create + bootstrap + auth-link the isolated config home (no-op for host base) so the
|
|
223
|
+
// assembler's materialize step has somewhere to write the provisioned skills/plugins.
|
|
224
|
+
prepareClaudeProfileHome(config);
|
|
224
225
|
const defaultArgs = profileUsesHostProviderGlobals(config) ? providerConfig.defaultArgs : [];
|
|
225
|
-
|
|
226
|
+
// #557 — ONE assembler owns the provisioning/vanilla/instruction launch surface
|
|
227
|
+
// (--setting-sources, --plugin-dir, --mcp-config, status-line, --append-system-prompt);
|
|
228
|
+
// profile-projection describes the SAME assembled result, so report == launch. The
|
|
229
|
+
// adapter only adds the bits the assembler does not own: claude-rig command resolution,
|
|
230
|
+
// headless permission flags, --model, and the positional prompt. materializeLaunchAssembly
|
|
231
|
+
// writes the provisioned skills/plugins the assembled args point at.
|
|
232
|
+
const assembled = assembleLaunch("claude", config, providerConfig);
|
|
233
|
+
materializeLaunchAssembly("claude", config);
|
|
226
234
|
const isClaudeRig = /claude-rig/.test(providerConfig.command);
|
|
227
235
|
// Isolated profiles run their own CLAUDE_CONFIG_DIR and must never route through
|
|
228
236
|
// claude-rig — claude-rig resolves host rig config (and a bare `claude-rig` with no
|
|
@@ -238,10 +246,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
238
246
|
: [...defaultArgs, ...config.providerArgs];
|
|
239
247
|
const args = [
|
|
240
248
|
...rigPrefix,
|
|
241
|
-
...
|
|
242
|
-
...(profileAllowsRelayFeature(config, "mcp") ? relayMcpClaudeConfigArg(config.relayUrl, config.relayMcpEndpoint) : []),
|
|
243
|
-
...(profileAllowsRelayFeature(config, "statusLine") ? sessionStatusLineSettingsArgs(defaultArgs, config.providerArgs) : []),
|
|
244
|
-
...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
|
|
249
|
+
...assembled.args,
|
|
245
250
|
...providerArgs,
|
|
246
251
|
];
|
|
247
252
|
if (config.model) args.push("--model", config.model);
|
|
@@ -256,7 +261,8 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
256
261
|
cwd: config.cwd,
|
|
257
262
|
env: {
|
|
258
263
|
...config.env,
|
|
259
|
-
|
|
264
|
+
// #557 — the assembler owns the isolated config-home env (CLAUDE_CONFIG_DIR).
|
|
265
|
+
...assembled.env,
|
|
260
266
|
// Plain `claude` (isolated profiles) runs its own CLAUDE_CONFIG_DIR and never
|
|
261
267
|
// routes through claude-rig, so it never inherits the host's long-lived
|
|
262
268
|
// CLAUDE_CODE_OAUTH_TOKEN. Without it, claude falls back to the (often stale)
|
|
@@ -431,23 +437,9 @@ export function claudeShutdownGraceMs(timeoutMs: number): number {
|
|
|
431
437
|
return Math.min(timeoutMs, 10_000);
|
|
432
438
|
}
|
|
433
439
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
statusLine: {
|
|
438
|
-
type: "command",
|
|
439
|
-
command: "agent-relay context-probe --wrap",
|
|
440
|
-
refreshInterval: 30,
|
|
441
|
-
},
|
|
442
|
-
// Force readable thinking text for managed sessions so the session-mirror can
|
|
443
|
-
// surface reasoning in the dashboard. With showThinkingSummaries:false the API
|
|
444
|
-
// redacts thinking to a signature-only stub (empty text), leaving the transcript
|
|
445
|
-
// tail nothing to mirror. --settings merges per-key, so this overrides only this
|
|
446
|
-
// key for managed sessions — a host rig default of false still governs the
|
|
447
|
-
// operator's own interactive TUI sessions.
|
|
448
|
-
showThinkingSummaries: true,
|
|
449
|
-
})];
|
|
450
|
-
}
|
|
440
|
+
// #557 — sessionStatusLineSettingsArgs now lives in ../launch-assembly (the single
|
|
441
|
+
// launch-arg home); re-exported so existing ./claude consumers + tests keep their path.
|
|
442
|
+
export { sessionStatusLineSettingsArgs };
|
|
451
443
|
|
|
452
444
|
export function claudeLaunchArgs(defaultArgs: string[], providerArgs: string[], approvalMode?: string): string[] {
|
|
453
445
|
const stripToolArgs = approvalMode === "read-only";
|
|
@@ -489,9 +481,6 @@ function stripClaudePermissionArgs(args: string[], stripToolArgs = false): strin
|
|
|
489
481
|
return result;
|
|
490
482
|
}
|
|
491
483
|
|
|
492
|
-
function hasSettingsArg(args: string[]): boolean {
|
|
493
|
-
return args.some((arg) => arg === "--settings" || arg.startsWith("--settings="));
|
|
494
|
-
}
|
|
495
484
|
|
|
496
485
|
export function tmuxSessionName(prefix: string, instanceId: string, label?: string): string {
|
|
497
486
|
if (label) return `${prefix}-${sanitizeFsName(label, { replacement: "-", collapse: false, lowercase: true })}`;
|
package/src/adapters/codex.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { isRecord, stringValue } from "agent-relay-sdk";
|
|
|
6
6
|
import { isPidAlive, killPid, processTreePids, processTreePidsFromTable, waitForPidsExit } from "agent-relay-sdk/process-utils";
|
|
7
7
|
import { profileAllowsRelayFeature, providerMessageText, RELAY_CONTEXT, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderSessionEvent, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
|
|
8
8
|
import { workspaceDepsNoteFromEnv } from "../relay-instructions";
|
|
9
|
-
import {
|
|
9
|
+
import { tomlString } from "../relay-mcp";
|
|
10
|
+
import { assembleLaunch, bundledCodexSkillDirs, bundledSkillConfigArgs, materializeLaunchAssembly } from "../launch-assembly";
|
|
10
11
|
import { logger } from "../logger";
|
|
11
12
|
import type { SessionEvent } from "../session-insights";
|
|
12
13
|
|
|
@@ -345,17 +346,24 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
345
346
|
|
|
346
347
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
347
348
|
const appServerUrl = String(config.appServerUrl || process.env.CODEX_APP_SERVER_URL || `ws://127.0.0.1:${config.controlPort + 1000}`);
|
|
348
|
-
|
|
349
|
+
// Create + bootstrap the isolated CODEX_HOME (no-op for host base) so the assembler's
|
|
350
|
+
// materialize step has somewhere to write the provisioned skills.
|
|
351
|
+
prepareCodexProfileHome(config);
|
|
349
352
|
const defaultArgs = profileUsesHostProviderGlobals(config) ? providerConfig.defaultArgs : [];
|
|
353
|
+
// #557 — ONE assembler owns the provisioning/vanilla surface (skills.config, relay +
|
|
354
|
+
// provisioned -c mcp_servers.*, project_doc_max_bytes); profile-projection describes the
|
|
355
|
+
// SAME assembled result, so report == launch. The adapter only adds core launch config the
|
|
356
|
+
// assembler does not own: app-server wiring, model/approval/tool-output, managed flags, --listen.
|
|
357
|
+
const assembled = assembleLaunch("codex", config, providerConfig);
|
|
358
|
+
materializeLaunchAssembly("codex", config);
|
|
350
359
|
const args = isCodexCliCommand(providerConfig.command)
|
|
351
360
|
? [
|
|
352
361
|
"app-server",
|
|
353
362
|
...codexAppServerConfigArgs(defaultArgs, config.providerArgs),
|
|
354
363
|
...codexModelConfigArgs(config.model, config.effort),
|
|
355
364
|
...codexApprovalConfigArgs(config.approvalMode),
|
|
356
|
-
...(profileAllowsRelayFeature(config, "skills") ? bundledSkillConfigArgs() : []),
|
|
357
|
-
...(profileAllowsRelayFeature(config, "mcp") ? relayMcpCodexConfigArgs(config.relayUrl, config.relayMcpEndpoint) : []),
|
|
358
365
|
...codexToolOutputTokenLimitConfigArgs(config),
|
|
366
|
+
...assembled.args,
|
|
359
367
|
...codexManagedConfigArgs(),
|
|
360
368
|
"--listen",
|
|
361
369
|
appServerUrl,
|
|
@@ -367,7 +375,8 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
367
375
|
cwd: config.cwd,
|
|
368
376
|
env: {
|
|
369
377
|
...config.env,
|
|
370
|
-
|
|
378
|
+
// #557 — the assembler owns the isolated config-home env (CODEX_HOME).
|
|
379
|
+
...assembled.env,
|
|
371
380
|
CODEX_APP_SERVER_URL: appServerUrl,
|
|
372
381
|
AGENT_RELAY_PROVIDER: "codex",
|
|
373
382
|
},
|
|
@@ -1166,24 +1175,10 @@ function isAgentRelayCodexShim(path: string): boolean {
|
|
|
1166
1175
|
return path.includes("/.agent-relay/codex/bin/codex");
|
|
1167
1176
|
}
|
|
1168
1177
|
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
.map((entry) => join(baseDir, entry.name))
|
|
1174
|
-
.sort();
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
export function bundledSkillConfigArgs(skillDirs = bundledCodexSkillDirs()): string[] {
|
|
1178
|
-
if (skillDirs.length === 0) return [];
|
|
1179
|
-
const skillsConfig = skillDirs
|
|
1180
|
-
.map((dir) => `{path=${tomlString(dir)},enabled=true}`)
|
|
1181
|
-
.join(",");
|
|
1182
|
-
return ["-c", `skills.config=[${skillsConfig}]`];
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// tomlString now lives in ../relay-mcp (shared with the relay MCP injection args);
|
|
1186
|
-
// re-exported here so existing codex.ts consumers/tests keep their import path.
|
|
1178
|
+
// #557 — bundledCodexSkillDirs + bundledSkillConfigArgs now live in ../launch-assembly
|
|
1179
|
+
// (the single launch-arg home, shared with the assembler); re-exported so existing
|
|
1180
|
+
// codex.ts consumers + tests keep their import path. tomlString likewise from ../relay-mcp.
|
|
1181
|
+
export { bundledCodexSkillDirs, bundledSkillConfigArgs };
|
|
1187
1182
|
export { tomlString };
|
|
1188
1183
|
|
|
1189
1184
|
export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import type { AgentProfile, AgentProfileBase, SpawnProvider } from "agent-relay-sdk";
|
|
4
|
+
import {
|
|
5
|
+
profileAllowsRelayFeature,
|
|
6
|
+
profileIsVanillaBase,
|
|
7
|
+
type ProviderConfig,
|
|
8
|
+
type RunnerSpawnConfig,
|
|
9
|
+
} from "./adapter";
|
|
10
|
+
import { profileUsesHostProviderGlobals, providerHomePathFor } from "./profile-home";
|
|
11
|
+
import {
|
|
12
|
+
materializeResolvedAssets,
|
|
13
|
+
resolveClaudeProvisionedPlugins,
|
|
14
|
+
resolveClaudeProvisionedSkills,
|
|
15
|
+
resolveCodexProvisionedSkills,
|
|
16
|
+
profileProvisioning,
|
|
17
|
+
} from "./provisioning";
|
|
18
|
+
import {
|
|
19
|
+
claudeMcpLaunchArgs,
|
|
20
|
+
codexProvisionedMcpArgs,
|
|
21
|
+
relayMcpCodexConfigArgs,
|
|
22
|
+
tomlString,
|
|
23
|
+
RELAY_MCP_SERVER_NAME,
|
|
24
|
+
} from "./relay-mcp";
|
|
25
|
+
|
|
26
|
+
// #557 — THE single launch-assembly contract. Given a resolved AgentProfile (+ its
|
|
27
|
+
// spawn-time ResolvedProvisioning), `assembleLaunch` produces ONE provider-neutral
|
|
28
|
+
// description of the launch: the relay-provisioned skills/plugins/MCP, the bundled relay
|
|
29
|
+
// surface, the instruction-source disposition, and `base:"vanilla"` as the zero-host
|
|
30
|
+
// floor. Both adapters' buildSpawnArgs splice `assembled.args`/`assembled.env`, and
|
|
31
|
+
// profile-projection describes `assembled` directly — so projection and reality come
|
|
32
|
+
// from one source and cannot drift.
|
|
33
|
+
//
|
|
34
|
+
// `assembleLaunch` is PURE (it reads the filesystem to decide which declared dirs
|
|
35
|
+
// resolve, but writes nothing). `materializeLaunchAssembly` performs the disk writes the
|
|
36
|
+
// description implies, driven by the SAME resolvers — so what's written equals what's
|
|
37
|
+
// reported equals what's launched.
|
|
38
|
+
|
|
39
|
+
export interface AssembledAsset {
|
|
40
|
+
name: string;
|
|
41
|
+
/** Materialized/in-place dir feeding the launch (e.g. a --plugin-dir or skills.config path). */
|
|
42
|
+
dir: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type AssembledInstructionDisposition = "allow" | "suppressed" | "isolated" | "unsupported";
|
|
46
|
+
|
|
47
|
+
export interface AssembledInstructions {
|
|
48
|
+
/** Repo CLAUDE.md/AGENTS.md cascade: "allow" left available, "suppressed" by the
|
|
49
|
+
* vanilla launch mechanism, "unsupported" requested-but-not-enforced (non-vanilla isolated). */
|
|
50
|
+
repo: AssembledInstructionDisposition;
|
|
51
|
+
/** Host/global provider instructions: "allow", "isolated" (dropped by the isolated home), or "unsupported". */
|
|
52
|
+
global: AssembledInstructionDisposition;
|
|
53
|
+
/** A composed systemPromptAppend block (project preamble + profile system/append +
|
|
54
|
+
* caller context, composed relay-side by composeSpawnInstructions) flows into this launch. */
|
|
55
|
+
systemPromptAppend: boolean;
|
|
56
|
+
// ── #559 SEAM ──────────────────────────────────────────────────────────────────
|
|
57
|
+
// Scoped {profile|user} memory does not exist yet (#559 generalizes Projects' anchor
|
|
58
|
+
// to project|profile|user). When it lands, the assembled instruction world becomes
|
|
59
|
+
// "instructions + provisioned assets + scoped knowledge": the relay-side composer
|
|
60
|
+
// (composeSpawnInstructions) gains a profile/user-scoped preamble beside the existing
|
|
61
|
+
// project preamble, and THIS field reports which scopes contributed. Until then it is
|
|
62
|
+
// always "none" — a thin add for #559, not a refactor.
|
|
63
|
+
scopedKnowledge: "none";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AssembledMcp {
|
|
67
|
+
/** The identity-bearing relay HTTP MCP endpoint is injected. */
|
|
68
|
+
relayEndpoint: boolean;
|
|
69
|
+
/** Only the assembled servers may load (Claude --strict-mcp-config) — the vanilla floor. */
|
|
70
|
+
strict: boolean;
|
|
71
|
+
/** Relay-provisioned server names (excludes the relay endpoint itself). */
|
|
72
|
+
servers: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AssembledLaunch {
|
|
76
|
+
provider: SpawnProvider;
|
|
77
|
+
/** The resolved profile this launch was assembled from; undefined for a profile-less
|
|
78
|
+
* (legacy default) spawn, which behaves as host base with relay features default-on. */
|
|
79
|
+
profile?: AgentProfile;
|
|
80
|
+
base: AgentProfileBase;
|
|
81
|
+
vanilla: boolean;
|
|
82
|
+
/** Isolated provider config home (CLAUDE_CONFIG_DIR / CODEX_HOME), or undefined for host base. */
|
|
83
|
+
configHome?: string;
|
|
84
|
+
relayContext: boolean;
|
|
85
|
+
/** Bundled relay skills are available (Codex skills.config / Claude via the relay plugin). */
|
|
86
|
+
relaySkills: boolean;
|
|
87
|
+
/** Bundled relay plugin dir is passed (Claude only). */
|
|
88
|
+
relayPlugin: boolean;
|
|
89
|
+
/** Relay status-line context probe is injected (Claude only). */
|
|
90
|
+
statusLine: boolean;
|
|
91
|
+
/** Relay-provisioned skills materialized for this launch. */
|
|
92
|
+
skills: AssembledAsset[];
|
|
93
|
+
/** Relay-provisioned plugins (Claude --plugin-dir). Empty for Codex (no plugin surface). */
|
|
94
|
+
plugins: AssembledAsset[];
|
|
95
|
+
mcp: AssembledMcp;
|
|
96
|
+
instructions: AssembledInstructions;
|
|
97
|
+
/** Provider provisioning/vanilla/instruction launch-arg block, in launch order. */
|
|
98
|
+
args: string[];
|
|
99
|
+
/** Provisioning-derived env additions (the isolated config-home var). */
|
|
100
|
+
env: Record<string, string>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Bundled relay assets (relocated from the adapters so the assembler — not each
|
|
104
|
+
// adapter — owns launch-arg construction; #557). Adapters re-export for test parity. ──
|
|
105
|
+
|
|
106
|
+
const CLAUDE_RELAY_PLUGIN_DIR = resolve(import.meta.dir, "../plugins/claude");
|
|
107
|
+
const CODEX_BUNDLED_SKILLS_DIR = resolve(import.meta.dir, "../plugins/codex/skills");
|
|
108
|
+
|
|
109
|
+
export function bundledCodexSkillDirs(baseDir = CODEX_BUNDLED_SKILLS_DIR): string[] {
|
|
110
|
+
if (!existsSync(baseDir)) return [];
|
|
111
|
+
return readdirSync(baseDir, { withFileTypes: true })
|
|
112
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(baseDir, entry.name, "SKILL.md")))
|
|
113
|
+
.map((entry) => join(baseDir, entry.name))
|
|
114
|
+
.sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function bundledSkillConfigArgs(skillDirs = bundledCodexSkillDirs()): string[] {
|
|
118
|
+
if (skillDirs.length === 0) return [];
|
|
119
|
+
const skillsConfig = skillDirs.map((dir) => `{path=${tomlString(dir)},enabled=true}`).join(",");
|
|
120
|
+
return ["-c", `skills.config=[${skillsConfig}]`];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// #552 vanilla base (Codex): suppress the AGENTS.md project-doc cascade. project_doc_max_bytes
|
|
124
|
+
// (default 32768) caps bytes Codex reads from AGENTS.md walking root→cwd; 0 stops before adding
|
|
125
|
+
// any, so no repo (or fresh-CODEX_HOME global) project doc loads. The isolated CODEX_HOME already
|
|
126
|
+
// drops host config.toml MCP/hooks; this closes the cwd-resident repo instruction channel.
|
|
127
|
+
export function codexProjectDocSuppressionArgs(config: Pick<RunnerSpawnConfig, "agentProfile">): string[] {
|
|
128
|
+
return profileIsVanillaBase(config) ? ["-c", "project_doc_max_bytes=0"] : [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasSettingsArg(args: string[]): boolean {
|
|
132
|
+
return args.some((arg) => arg === "--settings" || arg.startsWith("--settings="));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Claude relay status-line + readable-thinking settings, injected only when no caller
|
|
136
|
+
// --settings is already present (caller wins). Relocated from claude.ts (#557).
|
|
137
|
+
export function sessionStatusLineSettingsArgs(...argLists: string[][]): string[] {
|
|
138
|
+
if (argLists.some(hasSettingsArg)) return [];
|
|
139
|
+
return ["--settings", JSON.stringify({
|
|
140
|
+
statusLine: { type: "command", command: "agent-relay context-probe --wrap", refreshInterval: 30 },
|
|
141
|
+
showThinkingSummaries: true,
|
|
142
|
+
})];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Instruction disposition (the vanilla floor's effect on the host instruction cascade) ──
|
|
146
|
+
|
|
147
|
+
function repoInstructionDisposition(profile: AgentProfile): AssembledInstructionDisposition {
|
|
148
|
+
if (profile.instructions.repoInstructions === "allow") return "allow";
|
|
149
|
+
if (profile.base === "vanilla") return "suppressed";
|
|
150
|
+
// Non-vanilla isolated bases can request ignore but have no launch-time enforcement yet.
|
|
151
|
+
return "unsupported";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function globalInstructionDisposition(profile: AgentProfile): AssembledInstructionDisposition {
|
|
155
|
+
if (profile.instructions.globalInstructions === "allow") return "allow";
|
|
156
|
+
if (profile.base !== "host") return "isolated";
|
|
157
|
+
return "unsupported";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function assembleInstructions(config: RunnerSpawnConfig): AssembledInstructions {
|
|
161
|
+
const profile = config.agentProfile;
|
|
162
|
+
return {
|
|
163
|
+
// A profile-less spawn is host-base: the full host instruction cascade is left available.
|
|
164
|
+
repo: profile ? repoInstructionDisposition(profile) : "allow",
|
|
165
|
+
global: profile ? globalInstructionDisposition(profile) : "allow",
|
|
166
|
+
systemPromptAppend: Boolean(config.systemPromptAppend?.trim()),
|
|
167
|
+
scopedKnowledge: "none",
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Per-provider arg assembly ──────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]): AssembledLaunch {
|
|
174
|
+
const configHome = providerHomePathFor("claude", config);
|
|
175
|
+
const vanilla = profileIsVanillaBase(config);
|
|
176
|
+
const relayPlugin = profileAllowsRelayFeature(config, "plugins");
|
|
177
|
+
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
178
|
+
const statusLine = profileAllowsRelayFeature(config, "statusLine");
|
|
179
|
+
|
|
180
|
+
const skills = configHome ? resolveClaudeProvisionedSkills(configHome, config) : [];
|
|
181
|
+
const provisionedPlugins = configHome ? resolveClaudeProvisionedPlugins(configHome, config) : [];
|
|
182
|
+
|
|
183
|
+
// Plugin dirs (--plugin-dir loads explicitly, independent of --setting-sources):
|
|
184
|
+
// relay-bundled plugin → only when relay.plugins (carries the relay monitor +
|
|
185
|
+
// turn-lifecycle hooks #556, not reproducible via settings.json).
|
|
186
|
+
// relay-provisioned plugins (#556) → materialized into the isolated home.
|
|
187
|
+
// host plugin dirs → ONLY host-base profiles; isolated/vanilla must not pull them back in.
|
|
188
|
+
const relayPluginDirs = relayPlugin ? [CLAUDE_RELAY_PLUGIN_DIR] : [];
|
|
189
|
+
const provisionedPluginDirs = provisionedPlugins.map((p) => p.dir);
|
|
190
|
+
const hostPluginDirs = profileUsesHostProviderGlobals(config) ? providerConfig.pluginDirs : [];
|
|
191
|
+
const pluginDirs = [...new Set([...relayPluginDirs, ...provisionedPluginDirs, ...hostPluginDirs])];
|
|
192
|
+
|
|
193
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
194
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
195
|
+
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
196
|
+
|
|
197
|
+
const instructions = assembleInstructions(config);
|
|
198
|
+
|
|
199
|
+
// Launch order matches the historical claude buildSpawnArgs block exactly so existing
|
|
200
|
+
// arg assertions hold: vanilla scope → plugin dirs → MCP → status-line → append.
|
|
201
|
+
const args = [
|
|
202
|
+
...(vanilla ? ["--setting-sources", "user"] : []),
|
|
203
|
+
...pluginDirs.flatMap((dir) => ["--plugin-dir", dir]),
|
|
204
|
+
...claudeMcpLaunchArgs({
|
|
205
|
+
relayUrl: config.relayUrl,
|
|
206
|
+
...(config.relayMcpEndpoint ? { endpoint: config.relayMcpEndpoint } : {}),
|
|
207
|
+
includeRelay: relayEndpoint,
|
|
208
|
+
servers: provisionedMcp,
|
|
209
|
+
strict: vanilla,
|
|
210
|
+
}),
|
|
211
|
+
...(statusLine ? sessionStatusLineSettingsArgs(hostDefaultArgs, config.providerArgs) : []),
|
|
212
|
+
...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
provider: "claude",
|
|
217
|
+
...(config.agentProfile ? { profile: config.agentProfile } : {}),
|
|
218
|
+
base: config.agentProfile?.base ?? "host",
|
|
219
|
+
vanilla,
|
|
220
|
+
...(configHome ? { configHome } : {}),
|
|
221
|
+
relayContext: profileAllowsRelayFeature(config, "context"),
|
|
222
|
+
relaySkills,
|
|
223
|
+
relayPlugin,
|
|
224
|
+
statusLine,
|
|
225
|
+
skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
226
|
+
plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
|
|
227
|
+
mcp: { relayEndpoint, strict: vanilla, servers: mcpServers },
|
|
228
|
+
instructions,
|
|
229
|
+
args,
|
|
230
|
+
env: configHome ? { CLAUDE_CONFIG_DIR: configHome } : {},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfig): AssembledLaunch {
|
|
235
|
+
const configHome = providerHomePathFor("codex", config);
|
|
236
|
+
const vanilla = profileIsVanillaBase(config);
|
|
237
|
+
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
238
|
+
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
239
|
+
|
|
240
|
+
const provisionedSkills = configHome ? resolveCodexProvisionedSkills(configHome, config) : [];
|
|
241
|
+
const bundledSkillDirs = relaySkills ? bundledCodexSkillDirs() : [];
|
|
242
|
+
const skillDirs = [...bundledSkillDirs, ...provisionedSkills.map((s) => s.dir)];
|
|
243
|
+
|
|
244
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
245
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
246
|
+
|
|
247
|
+
const instructions = assembleInstructions(config);
|
|
248
|
+
|
|
249
|
+
// The skills/MCP/project-doc block; core launch config (model/approval/listen) stays
|
|
250
|
+
// in the adapter. -c overrides are order-independent, so the assembled block is contiguous.
|
|
251
|
+
const args = [
|
|
252
|
+
...bundledSkillConfigArgs(skillDirs),
|
|
253
|
+
...(relayEndpoint ? relayMcpCodexConfigArgs(config.relayUrl, config.relayMcpEndpoint) : []),
|
|
254
|
+
...codexProvisionedMcpArgs(provisionedMcp),
|
|
255
|
+
...codexProjectDocSuppressionArgs(config),
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
provider: "codex",
|
|
260
|
+
...(config.agentProfile ? { profile: config.agentProfile } : {}),
|
|
261
|
+
base: config.agentProfile?.base ?? "host",
|
|
262
|
+
vanilla,
|
|
263
|
+
...(configHome ? { configHome } : {}),
|
|
264
|
+
relayContext: profileAllowsRelayFeature(config, "context"),
|
|
265
|
+
relaySkills,
|
|
266
|
+
relayPlugin: false,
|
|
267
|
+
statusLine: false,
|
|
268
|
+
skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
269
|
+
plugins: [],
|
|
270
|
+
mcp: { relayEndpoint, strict: false, servers: mcpServers },
|
|
271
|
+
instructions,
|
|
272
|
+
args,
|
|
273
|
+
env: configHome ? { CODEX_HOME: configHome } : {},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Assemble the launch for `provider` from the resolved profile + provisioning. PURE:
|
|
279
|
+
* resolves which declared assets exist (filesystem reads) but writes nothing. A missing
|
|
280
|
+
* `agentProfile` assembles the legacy default (host base, relay features on, no provisioning).
|
|
281
|
+
*/
|
|
282
|
+
export function assembleLaunch(provider: SpawnProvider, config: RunnerSpawnConfig, providerConfig: ProviderConfig): AssembledLaunch {
|
|
283
|
+
if (provider === "codex") return assembleCodex(config, providerConfig);
|
|
284
|
+
const hostDefaultArgs = profileUsesHostProviderGlobals(config) ? providerConfig.defaultArgs : [];
|
|
285
|
+
return assembleClaude(config, providerConfig, hostDefaultArgs);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Write the provisioned assets the assembly describes (Claude skills + plugins, Codex
|
|
290
|
+
* skills). Driven by the SAME resolvers `assembleLaunch` uses, so disk == launch ==
|
|
291
|
+
* report. Idempotent; the isolated home must already exist (prepare*ProfileHome). No-op
|
|
292
|
+
* for a host-base profile.
|
|
293
|
+
*/
|
|
294
|
+
export function materializeLaunchAssembly(provider: SpawnProvider, config: RunnerSpawnConfig): void {
|
|
295
|
+
const configHome = providerHomePathFor(provider, config);
|
|
296
|
+
if (!configHome) return;
|
|
297
|
+
if (provider === "claude") {
|
|
298
|
+
materializeResolvedAssets(resolveClaudeProvisionedSkills(configHome, config));
|
|
299
|
+
materializeResolvedAssets(resolveClaudeProvisionedPlugins(configHome, config));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
materializeResolvedAssets(resolveCodexProvisionedSkills(configHome, config));
|
|
303
|
+
}
|
package/src/profile-home.ts
CHANGED
|
@@ -19,6 +19,15 @@ function profileRequiresIsolatedHome(config: RunnerSpawnConfig): boolean {
|
|
|
19
19
|
return Boolean(config.agentProfile && config.agentProfile.base !== "host");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// #557 — PURE: the isolated provider config home path a launch WILL use, or undefined
|
|
23
|
+
// for a host-base profile (which uses the provider's normal home). The launch assembler
|
|
24
|
+
// computes the home this way to report it + build CLAUDE_CONFIG_DIR/skill/plugin paths
|
|
25
|
+
// without side effects; prepare*ProfileHome creates it at the same path at spawn time.
|
|
26
|
+
export function providerHomePathFor(provider: "claude" | "codex", config: RunnerSpawnConfig): string | undefined {
|
|
27
|
+
if (!profileRequiresIsolatedHome(config)) return undefined;
|
|
28
|
+
return providerHomePath(provider, config);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
// First-run bootstrap (provider layer)
|
|
23
32
|
// -------------------------------------
|
|
24
33
|
// Isolated profile homes are keyed by instanceId, so every spawn gets a brand
|
|
@@ -70,6 +79,9 @@ function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig):
|
|
|
70
79
|
// agent-relay communication instructions written into its config home.
|
|
71
80
|
if (profileAllowsRelayFeature(config, "context")) writeClaudeRelayManual(claudeHome);
|
|
72
81
|
seedClaudeConfigIfMissing(claudeHome, config);
|
|
82
|
+
// #554/#557 — relay-provisioned skills are materialized into <home>/skills by the launch
|
|
83
|
+
// assembler (materializeLaunchAssembly), the single owner of all provisioned-asset writes;
|
|
84
|
+
// the bootstrap only creates/auths the home so the assembler has somewhere to write.
|
|
73
85
|
const sourceHome = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
74
86
|
return linkExistingAuthItems(sourceHome, claudeHome, CLAUDE_AUTH_ITEMS);
|
|
75
87
|
}
|
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
import type { AgentProfile, AgentProfileProjectionEntry, AgentProfileProjectionReport, SpawnProvider } from "agent-relay-sdk";
|
|
2
|
+
import type { AssembledLaunch } from "./launch-assembly";
|
|
3
|
+
|
|
4
|
+
// #557 — the projection report DESCRIBES an AssembledLaunch, the same object the adapters
|
|
5
|
+
// turn into launch args. It is no longer a separate re-derivation from the profile, so the
|
|
6
|
+
// report and the real launch share one source of truth and cannot drift: change the
|
|
7
|
+
// assembler and both the args AND this report change together. Every entry below reads a
|
|
8
|
+
// field the assembler actually computed (relay gates, provisioned sets, instruction
|
|
9
|
+
// disposition, vanilla mechanism, config home) — only the not-yet-enforced category modes
|
|
10
|
+
// (mcp/hooks) and the purely-declarative env/permissions read straight off the profile.
|
|
2
11
|
|
|
3
12
|
interface AgentProfileProjectionInput {
|
|
4
|
-
|
|
5
|
-
profile: AgentProfile;
|
|
13
|
+
assembled: AssembledLaunch;
|
|
6
14
|
generatedAt?: number;
|
|
7
15
|
}
|
|
8
16
|
|
|
9
17
|
export function agentProfileProjectionReport(input: AgentProfileProjectionInput): AgentProfileProjectionReport {
|
|
18
|
+
const { assembled } = input;
|
|
19
|
+
const { provider } = assembled;
|
|
20
|
+
const profile = assembled.profile;
|
|
21
|
+
const generatedAt = input.generatedAt ?? Date.now();
|
|
10
22
|
const entries: AgentProfileProjectionEntry[] = [];
|
|
11
23
|
const add = (entry: AgentProfileProjectionEntry) => entries.push(entry);
|
|
12
|
-
const { provider, profile } = input;
|
|
13
|
-
const generatedAt = input.generatedAt ?? Date.now();
|
|
14
24
|
|
|
15
|
-
add(relayContextEntry(
|
|
16
|
-
add(relaySkillsEntry(
|
|
17
|
-
add(relayPluginsEntry(
|
|
18
|
-
add(relayStatusLineEntry(
|
|
19
|
-
add(repoInstructionsEntry(
|
|
20
|
-
add(globalInstructionsEntry(
|
|
21
|
-
add(mcpEntry(
|
|
22
|
-
add(hooksEntry(
|
|
23
|
-
add(
|
|
24
|
-
add(
|
|
25
|
-
add(
|
|
25
|
+
add(relayContextEntry(assembled));
|
|
26
|
+
add(relaySkillsEntry(assembled));
|
|
27
|
+
add(relayPluginsEntry(assembled));
|
|
28
|
+
add(relayStatusLineEntry(assembled));
|
|
29
|
+
add(repoInstructionsEntry(assembled));
|
|
30
|
+
add(globalInstructionsEntry(assembled));
|
|
31
|
+
add(mcpEntry(assembled));
|
|
32
|
+
add(hooksEntry(assembled));
|
|
33
|
+
add(provisioningSkillsEntry(assembled));
|
|
34
|
+
add(provisioningPluginsEntry(assembled));
|
|
35
|
+
add(provisioningMcpEntry(assembled));
|
|
36
|
+
if (profile) add(filesystemEntry(profile));
|
|
37
|
+
add(configHomeEntry(assembled));
|
|
38
|
+
if (assembled.vanilla) add(vanillaLaunchEntry(provider));
|
|
39
|
+
if (profile) add(envEntry(profile));
|
|
26
40
|
|
|
27
41
|
const warnings = entries
|
|
28
42
|
.filter((entry) => entry.result === "partial" || entry.result === "unsupported")
|
|
@@ -32,9 +46,9 @@ export function agentProfileProjectionReport(input: AgentProfileProjectionInput)
|
|
|
32
46
|
.map((entry) => entry.capability);
|
|
33
47
|
|
|
34
48
|
return {
|
|
35
|
-
profileName: profile
|
|
49
|
+
profileName: profile?.name ?? "default",
|
|
36
50
|
provider,
|
|
37
|
-
base:
|
|
51
|
+
base: assembled.base,
|
|
38
52
|
generatedAt,
|
|
39
53
|
entries,
|
|
40
54
|
warnings,
|
|
@@ -42,74 +56,116 @@ export function agentProfileProjectionReport(input: AgentProfileProjectionInput)
|
|
|
42
56
|
};
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
function relayContextEntry(
|
|
46
|
-
return
|
|
59
|
+
function relayContextEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
60
|
+
return a.relayContext
|
|
47
61
|
? applied("relay.context", "enabled", "Relay context is injected before Relay-delivered provider turns.")
|
|
48
62
|
: applied("relay.context", "disabled", "Relay context injection is disabled by the profile.");
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
function relaySkillsEntry(
|
|
52
|
-
if (provider === "codex") {
|
|
53
|
-
return
|
|
65
|
+
function relaySkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
66
|
+
if (a.provider === "codex") {
|
|
67
|
+
return a.relaySkills
|
|
54
68
|
? applied("relay.skills", "enabled", "Bundled Agent Relay Codex skills are passed through Codex skills.config.")
|
|
55
69
|
: applied("relay.skills", "disabled", "Bundled Agent Relay Codex skills are omitted.");
|
|
56
70
|
}
|
|
57
|
-
return
|
|
71
|
+
return a.relaySkills
|
|
58
72
|
? applied("relay.skills", "enabled", "Agent Relay Claude skills are available through the bundled Relay plugin.")
|
|
59
|
-
: applied("relay.skills", "disabled",
|
|
73
|
+
: applied("relay.skills", "disabled", a.relayPlugin
|
|
60
74
|
? "Claude does not expose an independent Relay skill switch; disabling skills requires disabling the Relay plugin."
|
|
61
75
|
: "Bundled Agent Relay Claude plugin dirs are omitted, so Relay plugin-provided skills are unavailable.");
|
|
62
76
|
}
|
|
63
77
|
|
|
64
|
-
function relayPluginsEntry(
|
|
65
|
-
if (provider === "claude") {
|
|
66
|
-
return
|
|
78
|
+
function relayPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
79
|
+
if (a.provider === "claude") {
|
|
80
|
+
return a.relayPlugin
|
|
67
81
|
? applied("relay.plugins", "enabled", "Bundled Agent Relay Claude plugin dir is passed to Claude.")
|
|
68
82
|
: applied("relay.plugins", "disabled", "Bundled Agent Relay Claude plugin dirs are omitted.");
|
|
69
83
|
}
|
|
70
|
-
return notApplicable("relay.plugins",
|
|
84
|
+
return notApplicable("relay.plugins", a.relaySkills ? "enabled" : "disabled", "Codex Relay integration uses skills/app-server delivery, not a Relay plugin dir.");
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
function relayStatusLineEntry(
|
|
74
|
-
if (provider === "claude") {
|
|
75
|
-
return
|
|
87
|
+
function relayStatusLineEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
88
|
+
if (a.provider === "claude") {
|
|
89
|
+
return a.statusLine
|
|
76
90
|
? applied("relay.statusLine", "enabled", "Agent Relay status-line context probe settings are injected when caller settings allow it.")
|
|
77
91
|
: applied("relay.statusLine", "disabled", "Agent Relay status-line settings are omitted.");
|
|
78
92
|
}
|
|
79
|
-
return notApplicable("relay.statusLine",
|
|
93
|
+
return notApplicable("relay.statusLine", "disabled", "Codex reports context through App Server events instead of a Relay status-line setting.");
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
function repoInstructionsEntry(
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
function repoInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
97
|
+
const disposition = a.instructions.repo;
|
|
98
|
+
if (disposition === "allow") return applied("instructions.repo", "allow", "Repo-local provider instructions are left available.");
|
|
99
|
+
if (disposition === "suppressed") {
|
|
100
|
+
return applied("instructions.repo", "ignore", a.provider === "claude"
|
|
101
|
+
? "Repo CLAUDE.md is suppressed: Claude launches with --setting-sources \"user\" (the project scope carrying repo CLAUDE.md is excluded; only the relay-built config home loads)."
|
|
102
|
+
: "Repo AGENTS.md is suppressed: Codex launches with project_doc_max_bytes=0 (no project doc loaded).");
|
|
85
103
|
}
|
|
86
|
-
return unsupported("instructions.repo", "ignore", "
|
|
104
|
+
return unsupported("instructions.repo", "ignore", "Repo instruction suppression requires the vanilla base; isolated/minimal do not enforce it at launch yet.");
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
// #552/#554/#555/#556 — the vanilla base's launch-time host suppression, naming the exact
|
|
108
|
+
// provider mechanism the assembler emitted (Claude --setting-sources "user"; Codex
|
|
109
|
+
// project_doc_max_bytes=0). OS-level admin/managed policy is beyond any client flag and is
|
|
110
|
+
// called out so it isn't mistaken for a leak.
|
|
111
|
+
function vanillaLaunchEntry(provider: SpawnProvider): AgentProfileProjectionEntry {
|
|
112
|
+
return provider === "claude"
|
|
113
|
+
? applied("provider.vanilla", "--setting-sources user", "Claude launches the isolated config home under --setting-sources \"user\": host CLAUDE.md (repo + global), host skills/plugins/hooks, and host MCP (.mcp.json / ~/.claude.json) are all excluded; relay-provisioned assets in the home load. Auth, model, and built-in tools work normally. OS-level admin-managed policy still applies.")
|
|
114
|
+
: applied("provider.vanilla", "project_doc_max_bytes=0", "Codex launches with an isolated CODEX_HOME (no host config.toml MCP/hooks) and project_doc_max_bytes=0 (no AGENTS.md project doc, repo or global).");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function provisioningSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
118
|
+
const count = a.skills.length;
|
|
119
|
+
if (!count) return notApplicable("provisioning.skills", "none", "No relay-provisioned skills materialized for this launch.");
|
|
120
|
+
const names = ` (${a.skills.map((s) => s.name).join(", ")})`;
|
|
121
|
+
return applied("provisioning.skills", `${count}`, a.provider === "claude"
|
|
122
|
+
? `Relay-provisioned skills materialized into the isolated config home skills/ dir, auto-discovered under --setting-sources "user"${names}.`
|
|
123
|
+
: `Relay-provisioned skills added to Codex skills.config from the isolated CODEX_HOME${names}.`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function provisioningPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
127
|
+
if (a.provider === "codex") {
|
|
128
|
+
const declared = (a.profile?.plugins ?? []).filter((p) => p.enabled).length;
|
|
129
|
+
return notApplicable("provisioning.plugins", `${declared}`, "Codex has no --plugin-dir surface; provisioned plugins apply to Claude only.");
|
|
95
130
|
}
|
|
96
|
-
|
|
131
|
+
const count = a.plugins.length;
|
|
132
|
+
if (!count) return notApplicable("provisioning.plugins", "none", "No relay-provisioned plugins materialized for this launch.");
|
|
133
|
+
const names = ` (${a.plugins.map((p) => p.name).join(", ")})`;
|
|
134
|
+
return applied("provisioning.plugins", `${count}`, `Relay-provisioned plugins passed to Claude via --plugin-dir${names}.`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function provisioningMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
138
|
+
const names = a.mcp.servers;
|
|
139
|
+
if (!names.length) return notApplicable("provisioning.mcp", "none", "No relay-provisioned MCP servers materialized for this launch.");
|
|
140
|
+
const label = `${names.length} server${names.length === 1 ? "" : "s"}`;
|
|
141
|
+
return applied("provisioning.mcp", label, a.provider === "claude"
|
|
142
|
+
? `Relay-provisioned MCP servers composed into --mcp-config (${names.join(", ")})${a.mcp.strict ? "; --strict-mcp-config guarantees only these load" : ""}.`
|
|
143
|
+
: `Relay-provisioned MCP servers emitted as -c mcp_servers.* overrides (${names.join(", ")}).`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function globalInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
147
|
+
const disposition = a.instructions.global;
|
|
148
|
+
if (disposition === "allow") return applied("instructions.global", "allow", "Host/global provider instructions are left available.");
|
|
149
|
+
if (disposition === "isolated") return applied("instructions.global", "ignore", `${a.provider} uses an isolated provider config home, so host/global provider instructions are not loaded.`);
|
|
150
|
+
return unsupported("instructions.global", "ignore", `${a.provider} global instruction isolation requires an isolated provider config home.`);
|
|
97
151
|
}
|
|
98
152
|
|
|
99
|
-
function mcpEntry(
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
153
|
+
function mcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
154
|
+
const mode = a.profile?.mcp.mode ?? "host";
|
|
155
|
+
if (mode === "host") return applied("mcp", "host", "Host provider MCP configuration is left available.");
|
|
156
|
+
if (mode === "none" && a.base !== "host") {
|
|
157
|
+
return applied("mcp", "none", `${a.provider} uses an isolated provider config home without host MCP configuration.`);
|
|
103
158
|
}
|
|
104
|
-
return unsupported("mcp",
|
|
159
|
+
return unsupported("mcp", mode, `${a.provider} MCP category projection is not enforced yet.`);
|
|
105
160
|
}
|
|
106
161
|
|
|
107
|
-
function hooksEntry(
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
162
|
+
function hooksEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
163
|
+
const mode = a.profile?.hooks.mode ?? "host";
|
|
164
|
+
if (mode === "host") return applied("hooks", "host", "Host provider hooks are left available.");
|
|
165
|
+
if (mode === "none" && a.base !== "host") {
|
|
166
|
+
return applied("hooks", "none", `${a.provider} uses an isolated provider config home without host hook configuration.`);
|
|
111
167
|
}
|
|
112
|
-
return unsupported("hooks",
|
|
168
|
+
return unsupported("hooks", mode, `${a.provider} hook category projection is not enforced yet.`);
|
|
113
169
|
}
|
|
114
170
|
|
|
115
171
|
function filesystemEntry(profile: AgentProfile): AgentProfileProjectionEntry {
|
|
@@ -117,9 +173,9 @@ function filesystemEntry(profile: AgentProfile): AgentProfileProjectionEntry {
|
|
|
117
173
|
return partial("permissions.filesystem", profile.permissions.filesystem, "Relay constrains spawn cwd through the orchestrator; provider-level filesystem sandbox projection is not complete yet.");
|
|
118
174
|
}
|
|
119
175
|
|
|
120
|
-
function configHomeEntry(
|
|
121
|
-
if (
|
|
122
|
-
return applied("provider.configHome",
|
|
176
|
+
function configHomeEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
177
|
+
if (!a.configHome) return applied("provider.configHome", "host", "Provider uses the host's normal configuration home.");
|
|
178
|
+
return applied("provider.configHome", a.base, `${a.provider} is launched with an Agent Relay managed provider config home and linked host auth where available.`);
|
|
123
179
|
}
|
|
124
180
|
|
|
125
181
|
function envEntry(profile: AgentProfile): AgentProfileProjectionEntry {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
4
|
+
import type { ProvisioningAssetFile, ResolvedProvisioning } from "agent-relay-sdk";
|
|
5
|
+
import type { RunnerSpawnConfig } from "./adapter";
|
|
6
|
+
|
|
7
|
+
// Runner-side materialization of the relay-resolved provisioning bundle
|
|
8
|
+
// (#554/#555/#556). The relay resolves a profile's refs against the DB registry and
|
|
9
|
+
// attaches the concrete definitions to profile.provisioning; here we turn those into
|
|
10
|
+
// real on-disk dirs (skills/plugins). MCP is handled in relay-mcp.ts.
|
|
11
|
+
//
|
|
12
|
+
// #557 — split into PURE RESOLUTION (resolve* → where each asset lands, no writes) and
|
|
13
|
+
// MATERIALIZATION (materialize* → the disk writes). The single launch assembler
|
|
14
|
+
// (launch-assembly.ts) calls the resolvers to build BOTH the launch args and the
|
|
15
|
+
// projection report, so what's reported can never drift from what's written; the
|
|
16
|
+
// materialize* writers consume the same resolved entries, so what's written can never
|
|
17
|
+
// drift from what's reported either. One resolution, three consumers.
|
|
18
|
+
|
|
19
|
+
const EMPTY: ResolvedProvisioning = { skills: [], plugins: [], mcpServers: {} };
|
|
20
|
+
|
|
21
|
+
export function profileProvisioning(config: Pick<RunnerSpawnConfig, "agentProfile">): ResolvedProvisioning {
|
|
22
|
+
const p = config.agentProfile?.provisioning;
|
|
23
|
+
if (!p) return EMPTY;
|
|
24
|
+
return { skills: p.skills ?? [], plugins: p.plugins ?? [], mcpServers: p.mcpServers ?? {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A resolved provisioning asset: WHERE it lands (`dir`) and HOW it materializes
|
|
28
|
+
// (`source`). `files` writes the inline set into `dir`; `link` symlinks a pre-existing
|
|
29
|
+
// source dir at `dir`; `inPlace` uses the source dir as-is (no write, dir IS the source).
|
|
30
|
+
export type ResolvedAssetSource =
|
|
31
|
+
| { kind: "files"; files: ProvisioningAssetFile[] }
|
|
32
|
+
| { kind: "link"; source: string }
|
|
33
|
+
| { kind: "inPlace" };
|
|
34
|
+
export interface ResolvedAsset {
|
|
35
|
+
name: string;
|
|
36
|
+
dir: string;
|
|
37
|
+
source: ResolvedAssetSource;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function assetDirName(name: string): string {
|
|
41
|
+
return sanitizeFsName(name, { replacement: "-", collapse: false, maxLen: 120, fallback: "asset" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveExistingDir(dir: string, cwd: string): string | null {
|
|
45
|
+
const abs = isAbsolute(dir) ? dir : resolve(cwd, dir);
|
|
46
|
+
return existsSync(abs) ? abs : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasRootSkillManifest(files: ProvisioningAssetFile[]): boolean {
|
|
50
|
+
return files.some((f) => f.path === "SKILL.md");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Write an inline file set into <dir>, guarding against path escapes from a malformed
|
|
54
|
+
// registry entry. Shared by every files-source materialization.
|
|
55
|
+
function writeInlineFiles(dir: string, files: ProvisioningAssetFile[]): void {
|
|
56
|
+
mkdirSync(dir, { recursive: true });
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const dest = resolve(dir, file.path);
|
|
59
|
+
if (!dest.startsWith(resolve(dir))) continue;
|
|
60
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
61
|
+
writeFileSync(dest, file.content, { mode: 0o600 });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Perform the disk write a resolved asset describes. files → write inline; link →
|
|
66
|
+
// (re)create the symlink; inPlace → nothing (the dir already exists at the source).
|
|
67
|
+
// Best-effort for links: a failed link must not abort the launch.
|
|
68
|
+
function materializeAsset(asset: ResolvedAsset): boolean {
|
|
69
|
+
if (asset.source.kind === "files") {
|
|
70
|
+
writeInlineFiles(asset.dir, asset.source.files);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (asset.source.kind === "link") {
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(dirname(asset.dir), { recursive: true });
|
|
76
|
+
if (existsSync(asset.dir)) rmSync(asset.dir, { recursive: true, force: true });
|
|
77
|
+
symlinkSync(asset.source.source, asset.dir);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return existsSync(asset.dir);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// #554 (Claude) — each provisioned skill lands at <claudeHome>/skills/<name> so it's
|
|
87
|
+
// auto-discovered under the launch's `--setting-sources "user"` scope (host skills
|
|
88
|
+
// never bleed). Inline files are written there; a pre-existing dir is symlinked in.
|
|
89
|
+
export function resolveClaudeProvisionedSkills(claudeHome: string, config: RunnerSpawnConfig): ResolvedAsset[] {
|
|
90
|
+
const { skills } = profileProvisioning(config);
|
|
91
|
+
const skillsRoot = join(claudeHome, "skills");
|
|
92
|
+
const resolved: ResolvedAsset[] = [];
|
|
93
|
+
for (const skill of skills) {
|
|
94
|
+
const dir = join(skillsRoot, assetDirName(skill.name));
|
|
95
|
+
if (skill.files?.length) {
|
|
96
|
+
resolved.push({ name: skill.name, dir, source: { kind: "files", files: skill.files } });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (skill.dir) {
|
|
100
|
+
const source = resolveExistingDir(skill.dir, config.cwd);
|
|
101
|
+
if (source) resolved.push({ name: skill.name, dir, source: { kind: "link", source } });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// #556 (Claude) — each provisioned plugin resolves to a --plugin-dir. Inline plugins
|
|
108
|
+
// materialize under <claudeHome>/relay-provisioned-plugins/<name>; declared dirs are
|
|
109
|
+
// used in place. The resolved `dir` is appended to the launch's --plugin-dir set.
|
|
110
|
+
export function resolveClaudeProvisionedPlugins(claudeHome: string, config: RunnerSpawnConfig): ResolvedAsset[] {
|
|
111
|
+
const { plugins } = profileProvisioning(config);
|
|
112
|
+
const parent = join(claudeHome, "relay-provisioned-plugins");
|
|
113
|
+
const resolved: ResolvedAsset[] = [];
|
|
114
|
+
for (const plugin of plugins) {
|
|
115
|
+
if (plugin.files?.length) {
|
|
116
|
+
resolved.push({ name: plugin.name, dir: join(parent, assetDirName(plugin.name)), source: { kind: "files", files: plugin.files } });
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (plugin.dir) {
|
|
120
|
+
const source = resolveExistingDir(plugin.dir, config.cwd);
|
|
121
|
+
if (source) resolved.push({ name: plugin.name, dir: source, source: { kind: "inPlace" } });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// #554 (Codex) — each provisioned skill resolves to a dir for `-c skills.config`.
|
|
128
|
+
// Inline skills materialize under <codexHome>/relay-provisioned-skills/<name>; declared
|
|
129
|
+
// dirs are used in place. Codex requires a SKILL.md at the dir root to register it, so
|
|
130
|
+
// inline sets without one and dirs missing it are dropped at resolution time.
|
|
131
|
+
export function resolveCodexProvisionedSkills(codexHome: string, config: RunnerSpawnConfig): ResolvedAsset[] {
|
|
132
|
+
const { skills } = profileProvisioning(config);
|
|
133
|
+
const parent = join(codexHome, "relay-provisioned-skills");
|
|
134
|
+
const resolved: ResolvedAsset[] = [];
|
|
135
|
+
for (const skill of skills) {
|
|
136
|
+
if (skill.files?.length) {
|
|
137
|
+
if (!hasRootSkillManifest(skill.files)) continue;
|
|
138
|
+
resolved.push({ name: skill.name, dir: join(parent, assetDirName(skill.name)), source: { kind: "files", files: skill.files } });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (skill.dir) {
|
|
142
|
+
const source = resolveExistingDir(skill.dir, config.cwd);
|
|
143
|
+
if (source && existsSync(join(source, "SKILL.md"))) resolved.push({ name: skill.name, dir: source, source: { kind: "inPlace" } });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return resolved;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Write the resolved assets to disk; returns the dirs that materialized successfully,
|
|
150
|
+
// in resolution order. Idempotent — safe to re-run against an existing home.
|
|
151
|
+
export function materializeResolvedAssets(assets: ResolvedAsset[]): string[] {
|
|
152
|
+
const dirs: string[] = [];
|
|
153
|
+
for (const asset of assets) {
|
|
154
|
+
if (materializeAsset(asset)) dirs.push(asset.dir);
|
|
155
|
+
}
|
|
156
|
+
return dirs;
|
|
157
|
+
}
|
package/src/relay-mcp.ts
CHANGED
|
@@ -23,27 +23,6 @@ export function relayMcpEndpoint(relayUrl: string): string {
|
|
|
23
23
|
return `${relayUrl.replace(/\/+$/, "")}${RELAY_MCP_PATH}`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// Claude: additive `--mcp-config` JSON (NOT --strict-mcp-config, which would clobber
|
|
27
|
-
// the user's own servers). HTTP transport, token via env-var expansion so it never
|
|
28
|
-
// hits argv. Returns the full ["--mcp-config", "<json>"] arg pair.
|
|
29
|
-
//
|
|
30
|
-
// `endpoint` overrides the target URL: the runner passes its local MCP proxy URL (Stage 2,
|
|
31
|
-
// #215) so the agent connects to the Runner, not the relay. Omitted → the direct relay endpoint.
|
|
32
|
-
export function relayMcpClaudeConfigArg(relayUrl: string, endpoint?: string): string[] {
|
|
33
|
-
return [
|
|
34
|
-
"--mcp-config",
|
|
35
|
-
JSON.stringify({
|
|
36
|
-
mcpServers: {
|
|
37
|
-
[RELAY_MCP_SERVER_NAME]: {
|
|
38
|
-
type: "http",
|
|
39
|
-
url: endpoint ?? relayMcpEndpoint(relayUrl),
|
|
40
|
-
headers: { Authorization: `Bearer \${${RELAY_MCP_TOKEN_ENV}}` },
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
}),
|
|
44
|
-
];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
26
|
// Codex: `-c mcp_servers.<name>.*` overrides. `bearer_token_env_var` tells Codex to
|
|
48
27
|
// read the token from the env var itself → transport resolves to streamable_http.
|
|
49
28
|
// `endpoint` overrides the target URL (runner-local proxy, Stage 2 #215) — see above.
|
|
@@ -62,3 +41,64 @@ export function relayMcpCodexConfigArgs(relayUrl: string, endpoint?: string): st
|
|
|
62
41
|
export function tomlString(value: string): string {
|
|
63
42
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
64
43
|
}
|
|
44
|
+
|
|
45
|
+
// #555 — compose the launch MCP set from (the relay endpoint, if relay.mcp) PLUS the
|
|
46
|
+
// profile's relay-provisioned servers. One `--mcp-config` JSON carries them all.
|
|
47
|
+
// `strict` (vanilla) adds `--strict-mcp-config` so ONLY these load — host `.mcp.json`
|
|
48
|
+
// / `~/.claude.json` are ignored even if a stray config exists (belt-and-suspenders
|
|
49
|
+
// on top of the `--setting-sources "user"` scope that already excludes repo MCP).
|
|
50
|
+
import type { ProvisioningMcpServer } from "agent-relay-sdk";
|
|
51
|
+
|
|
52
|
+
function claudeMcpServerEntry(server: ProvisioningMcpServer): Record<string, unknown> {
|
|
53
|
+
const entry: Record<string, unknown> = {};
|
|
54
|
+
if (server.type) entry.type = server.type;
|
|
55
|
+
if (server.command) entry.command = server.command;
|
|
56
|
+
if (server.args?.length) entry.args = server.args;
|
|
57
|
+
if (server.env && Object.keys(server.env).length) entry.env = server.env;
|
|
58
|
+
if (server.url) entry.url = server.url;
|
|
59
|
+
if (server.headers && Object.keys(server.headers).length) entry.headers = server.headers;
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function claudeMcpLaunchArgs(opts: {
|
|
64
|
+
relayUrl: string;
|
|
65
|
+
endpoint?: string;
|
|
66
|
+
includeRelay: boolean;
|
|
67
|
+
servers?: Record<string, ProvisioningMcpServer>;
|
|
68
|
+
strict?: boolean;
|
|
69
|
+
}): string[] {
|
|
70
|
+
const mcpServers: Record<string, unknown> = {};
|
|
71
|
+
if (opts.includeRelay) {
|
|
72
|
+
mcpServers[RELAY_MCP_SERVER_NAME] = {
|
|
73
|
+
type: "http",
|
|
74
|
+
url: opts.endpoint ?? relayMcpEndpoint(opts.relayUrl),
|
|
75
|
+
headers: { Authorization: `Bearer \${${RELAY_MCP_TOKEN_ENV}}` },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
for (const [name, server] of Object.entries(opts.servers ?? {})) {
|
|
79
|
+
// Never let a declared server shadow the relay endpoint (identity-bearing).
|
|
80
|
+
if (name === RELAY_MCP_SERVER_NAME) continue;
|
|
81
|
+
const entry = claudeMcpServerEntry(server);
|
|
82
|
+
if (Object.keys(entry).length) mcpServers[name] = entry;
|
|
83
|
+
}
|
|
84
|
+
// Nothing to inject and not enforcing strict isolation → don't pass an empty config.
|
|
85
|
+
if (Object.keys(mcpServers).length === 0 && !opts.strict) return [];
|
|
86
|
+
return ["--mcp-config", JSON.stringify({ mcpServers }), ...(opts.strict ? ["--strict-mcp-config"] : [])];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// #555 (Codex) — `-c mcp_servers.<name>.*` overrides for each relay-provisioned
|
|
90
|
+
// server, the same shape relayMcpCodexConfigArgs uses for the relay endpoint.
|
|
91
|
+
export function codexProvisionedMcpArgs(servers?: Record<string, ProvisioningMcpServer>): string[] {
|
|
92
|
+
const args: string[] = [];
|
|
93
|
+
for (const [name, server] of Object.entries(servers ?? {})) {
|
|
94
|
+
if (name === RELAY_MCP_SERVER_NAME) continue;
|
|
95
|
+
const key = `mcp_servers.${name}`;
|
|
96
|
+
if (server.command) args.push("-c", `${key}.command=${tomlString(server.command)}`);
|
|
97
|
+
if (server.args?.length) args.push("-c", `${key}.args=[${server.args.map(tomlString).join(",")}]`);
|
|
98
|
+
if (server.url) args.push("-c", `${key}.url=${tomlString(server.url)}`);
|
|
99
|
+
for (const [envKey, envVal] of Object.entries(server.env ?? {})) {
|
|
100
|
+
args.push("-c", `${key}.env.${envKey}=${tomlString(envVal)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return args;
|
|
104
|
+
}
|
package/src/runner-core.ts
CHANGED
|
@@ -378,7 +378,9 @@ export class AgentRunner {
|
|
|
378
378
|
model: options.model ?? null,
|
|
379
379
|
effort: options.effort ?? null,
|
|
380
380
|
profile: options.profile ?? null,
|
|
381
|
-
|
|
381
|
+
// #557 — pass a spawn-shaped config so the projection describes the SAME assembled
|
|
382
|
+
// launch the adapter builds (env/controlPort are placeholders the pure assembler ignores).
|
|
383
|
+
agentProfile: options.agentProfile ? appliedAgentProfileMetadata(options.provider, options.agentProfile, { ...options, agentId: this.agentId, env: {}, controlPort: 0 } as RunnerSpawnConfig, options.providerConfig) : null,
|
|
382
384
|
runnerId: options.runnerId,
|
|
383
385
|
startedAt: options.startedAt,
|
|
384
386
|
tmuxSession: this.providerTerminalSession() ?? options.tmuxSession ?? null,
|
package/src/runner-helpers.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
|
2
2
|
import type { AgentProfile, ContextState, Message, ProviderCapabilities } from "agent-relay-sdk";
|
|
3
3
|
import { errMessage } from "agent-relay-sdk";
|
|
4
4
|
import { targetMatchesAgentIdentity } from "agent-relay-sdk/agent-target";
|
|
5
|
-
import type { ProviderAdapter, SemanticStatus } from "./adapter";
|
|
5
|
+
import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig, SemanticStatus } from "./adapter";
|
|
6
6
|
import { agentProfileProjectionReport } from "./profile-projection";
|
|
7
|
+
import { assembleLaunch } from "./launch-assembly";
|
|
7
8
|
|
|
8
9
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
9
10
|
const LOG_TAIL_BYTES = 128 * 1024;
|
|
@@ -272,9 +273,17 @@ function runtimeProviderTerminalCapabilities(options: RuntimeProviderOptions): P
|
|
|
272
273
|
return {};
|
|
273
274
|
}
|
|
274
275
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
// #557 — the agent-card projection now DESCRIBES the assembled launch (same source the
|
|
277
|
+
// adapter turns into args), so it can't drift from reality. `config`/`providerConfig` are
|
|
278
|
+
// the spawn inputs; when present we assemble (pure — no disk writes here) and report that.
|
|
279
|
+
export function appliedAgentProfileMetadata(
|
|
280
|
+
provider: string,
|
|
281
|
+
profile: AgentProfile,
|
|
282
|
+
config?: RunnerSpawnConfig,
|
|
283
|
+
providerConfig?: ProviderConfig,
|
|
284
|
+
): Record<string, unknown> {
|
|
285
|
+
const projection = (provider === "claude" || provider === "codex") && config && providerConfig
|
|
286
|
+
? agentProfileProjectionReport({ assembled: assembleLaunch(provider, config, providerConfig) })
|
|
278
287
|
: undefined;
|
|
279
288
|
return {
|
|
280
289
|
name: profile.name,
|