agent-relay-runner 0.91.3 → 0.91.4
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/adapters/claude.ts +3 -3
- package/src/launch-assembly.ts +53 -9
- package/src/profile-home.ts +14 -4
- package/src/profile-projection.ts +23 -5
- package/src/provisioning.ts +83 -1
- package/src/relay-injection-events.ts +6 -0
- package/src/runner-core.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.91.
|
|
3
|
+
"version": "0.91.4",
|
|
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.71"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapters/claude.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
|
9
9
|
import { profileAllowsRelayFeature, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderStatusUpdate, type RunnerSpawnConfig, type SemanticStatus, type SpawnArgs } from "../adapter";
|
|
10
10
|
import { collectClaudeSessionEvents } from "./claude-transcript";
|
|
11
11
|
import type { SessionEvent } from "../session-insights";
|
|
12
|
-
import { prepareClaudeProfileHome,
|
|
12
|
+
import { prepareClaudeProfileHome, profileUsesClaudeHostProviderGlobals } from "../profile-home";
|
|
13
13
|
import { assembleLaunch, materializeLaunchAssembly, sessionStatusLineSettingsArgs } from "../launch-assembly";
|
|
14
14
|
import { claudeProviderMessageText } from "./claude-delivery";
|
|
15
15
|
import { buildRateLimitProviderState, gatedClaudeRateLimitStatus, parseClaudeRateLimitPane } from "../rate-limit";
|
|
@@ -223,7 +223,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
223
223
|
// Create + bootstrap + auth-link the isolated config home (no-op for host base) so the
|
|
224
224
|
// assembler's materialize step has somewhere to write the provisioned skills/plugins.
|
|
225
225
|
prepareClaudeProfileHome(config);
|
|
226
|
-
const defaultArgs =
|
|
226
|
+
const defaultArgs = profileUsesClaudeHostProviderGlobals(config) ? providerConfig.defaultArgs : [];
|
|
227
227
|
// #557 — ONE assembler owns the provisioning/vanilla/instruction launch surface
|
|
228
228
|
// (--setting-sources, --plugin-dir, --mcp-config, status-line, --append-system-prompt);
|
|
229
229
|
// profile-projection describes the SAME assembled result, so report == launch. The
|
|
@@ -236,7 +236,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
236
236
|
// Isolated profiles run their own CLAUDE_CONFIG_DIR and must never route through
|
|
237
237
|
// claude-rig — claude-rig resolves host rig config (and a bare `claude-rig` with no
|
|
238
238
|
// rig key just errors out). Only host-base profiles may use claude-rig.
|
|
239
|
-
const usesClaudeRig = isClaudeRig &&
|
|
239
|
+
const usesClaudeRig = isClaudeRig && profileUsesClaudeHostProviderGlobals(config);
|
|
240
240
|
const bypassRigDefaults = config.headless && usesClaudeRig && config.approvalMode !== "open";
|
|
241
241
|
const rigPrefix = usesClaudeRig && !bypassRigDefaults && config.rig ? ["launch", config.rig] : [];
|
|
242
242
|
const command = !usesClaudeRig || bypassRigDefaults || (!config.rig && !findClaudeRigRC(config.cwd))
|
package/src/launch-assembly.ts
CHANGED
|
@@ -7,9 +7,16 @@ import {
|
|
|
7
7
|
type ProviderConfig,
|
|
8
8
|
type RunnerSpawnConfig,
|
|
9
9
|
} from "./adapter";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
claudeProjectConfigProjectionOn,
|
|
12
|
+
hostUserScopeMcpServers,
|
|
13
|
+
profileUsesClaudeHostProviderGlobals,
|
|
14
|
+
providerHomePathFor,
|
|
15
|
+
} from "./profile-home";
|
|
11
16
|
import {
|
|
12
17
|
materializeResolvedAssets,
|
|
18
|
+
resolveClaudeProjectSkills,
|
|
19
|
+
resolveProjectClaudeSettings,
|
|
13
20
|
resolveClaudeProvisionedPlugins,
|
|
14
21
|
resolveClaudeProvisionedSkills,
|
|
15
22
|
resolveCodexProvisionedSkills,
|
|
@@ -115,6 +122,10 @@ export interface AssembledLaunch {
|
|
|
115
122
|
statusLine: boolean;
|
|
116
123
|
/** Relay-provisioned skills materialized for this launch. */
|
|
117
124
|
skills: AssembledAsset[];
|
|
125
|
+
/** Project `.claude/skills` materialized for this Claude launch (#668). */
|
|
126
|
+
projectSkills: AssembledAsset[];
|
|
127
|
+
/** Project `.claude/settings.json` sanitized and injected for this Claude launch (#669). */
|
|
128
|
+
projectSettings: { applied: boolean; keys: string[]; ignoredKeys: string[]; file?: string };
|
|
118
129
|
/** Relay-provisioned plugins (Claude --plugin-dir). Empty for Codex (no plugin surface). */
|
|
119
130
|
plugins: AssembledAsset[];
|
|
120
131
|
mcp: AssembledMcp;
|
|
@@ -161,10 +172,27 @@ function hasSettingsArg(args: string[]): boolean {
|
|
|
161
172
|
// --settings is already present (caller wins). Relocated from claude.ts (#557).
|
|
162
173
|
export function sessionStatusLineSettingsArgs(...argLists: string[][]): string[] {
|
|
163
174
|
if (argLists.some(hasSettingsArg)) return [];
|
|
164
|
-
return ["--settings", JSON.stringify(
|
|
175
|
+
return ["--settings", JSON.stringify(relayStatusLineSettings())];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function relayStatusLineSettings(): Record<string, unknown> {
|
|
179
|
+
return {
|
|
165
180
|
statusLine: { type: "command", command: "agent-relay context-probe --wrap", refreshInterval: 30 },
|
|
166
181
|
showThinkingSummaries: true,
|
|
167
|
-
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function claudeSettingsArgs(
|
|
186
|
+
projectSettings: Record<string, unknown>,
|
|
187
|
+
includeStatusLine: boolean,
|
|
188
|
+
...argLists: string[][]
|
|
189
|
+
): string[] {
|
|
190
|
+
if (argLists.some(hasSettingsArg)) return [];
|
|
191
|
+
const settings = {
|
|
192
|
+
...projectSettings,
|
|
193
|
+
...(includeStatusLine ? relayStatusLineSettings() : {}),
|
|
194
|
+
};
|
|
195
|
+
return Object.keys(settings).length ? ["--settings", JSON.stringify(settings)] : [];
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
// ── Instruction disposition (the vanilla floor's effect on the host instruction cascade) ──
|
|
@@ -198,11 +226,16 @@ function assembleInstructions(config: RunnerSpawnConfig): AssembledInstructions
|
|
|
198
226
|
function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]): AssembledLaunch {
|
|
199
227
|
const configHome = providerHomePathFor("claude", config);
|
|
200
228
|
const vanilla = profileIsVanillaBase(config);
|
|
229
|
+
const projectConfigProjection = claudeProjectConfigProjectionOn(config);
|
|
201
230
|
const relayPlugin = profileAllowsRelayFeature(config, "plugins");
|
|
202
231
|
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
203
232
|
const statusLine = profileAllowsRelayFeature(config, "statusLine");
|
|
204
233
|
|
|
205
234
|
const skills = configHome ? resolveClaudeProvisionedSkills(configHome, config) : [];
|
|
235
|
+
const projectSkills = configHome
|
|
236
|
+
? resolveClaudeProjectSkills(configHome, config, new Set(skills.map((s) => s.name)))
|
|
237
|
+
: [];
|
|
238
|
+
const projectSettings = resolveProjectClaudeSettings(config);
|
|
206
239
|
const provisionedPlugins = configHome ? resolveClaudeProvisionedPlugins(configHome, config) : [];
|
|
207
240
|
|
|
208
241
|
// Plugin dirs (--plugin-dir loads explicitly, independent of --setting-sources):
|
|
@@ -212,7 +245,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
212
245
|
// host plugin dirs → ONLY host-base profiles; isolated/vanilla must not pull them back in.
|
|
213
246
|
const relayPluginDirs = relayPlugin ? [CLAUDE_RELAY_PLUGIN_DIR] : [];
|
|
214
247
|
const provisionedPluginDirs = provisionedPlugins.map((p) => p.dir);
|
|
215
|
-
const hostPluginDirs =
|
|
248
|
+
const hostPluginDirs = profileUsesClaudeHostProviderGlobals(config) ? providerConfig.pluginDirs : [];
|
|
216
249
|
const pluginDirs = [...new Set([...relayPluginDirs, ...provisionedPluginDirs, ...hostPluginDirs])];
|
|
217
250
|
|
|
218
251
|
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
@@ -265,7 +298,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
265
298
|
// still excluded) when the launch both uses the host config home AND its mcp mode is host.
|
|
266
299
|
// An isolated/minimal base has its own config home and never had the real host's user MCP,
|
|
267
300
|
// so it stays relay+provisioned only even if a profile sets mode:"host".
|
|
268
|
-
const usesHostMcp =
|
|
301
|
+
const usesHostMcp = profileUsesClaudeHostProviderGlobals(config)
|
|
269
302
|
&& (!config.agentProfile || config.agentProfile.mcp.mode === "host");
|
|
270
303
|
const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
|
|
271
304
|
const hostMcpServers = Object.keys(hostMcp).filter(
|
|
@@ -277,7 +310,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
277
310
|
// Launch order matches the historical claude buildSpawnArgs block exactly so existing
|
|
278
311
|
// arg assertions hold: vanilla scope → plugin dirs → MCP → status-line → append.
|
|
279
312
|
const args = [
|
|
280
|
-
...(vanilla ? ["--setting-sources", "user"] : []),
|
|
313
|
+
...(vanilla || projectConfigProjection ? ["--setting-sources", "user"] : []),
|
|
281
314
|
...pluginDirs.flatMap((dir) => ["--plugin-dir", dir]),
|
|
282
315
|
...claudeMcpLaunchArgs({
|
|
283
316
|
relayUrl: config.relayUrl,
|
|
@@ -287,7 +320,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
287
320
|
hostServers: hostMcp,
|
|
288
321
|
strict: strictMcp,
|
|
289
322
|
}),
|
|
290
|
-
...(statusLine
|
|
323
|
+
...claudeSettingsArgs(projectSettings.settings, statusLine, hostDefaultArgs, config.providerArgs),
|
|
291
324
|
...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
|
|
292
325
|
];
|
|
293
326
|
|
|
@@ -302,6 +335,13 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
302
335
|
relayPlugin,
|
|
303
336
|
statusLine,
|
|
304
337
|
skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
338
|
+
projectSkills: projectSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
339
|
+
projectSettings: {
|
|
340
|
+
applied: projectSettings.applied,
|
|
341
|
+
keys: projectSettings.keys,
|
|
342
|
+
ignoredKeys: projectSettings.ignoredKeys,
|
|
343
|
+
...(projectSettings.file ? { file: projectSettings.file } : {}),
|
|
344
|
+
},
|
|
305
345
|
plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
|
|
306
346
|
mcp: {
|
|
307
347
|
relayEndpoint, strict: strictMcp, servers: mcpServers, projectServers, hostServers: hostMcpServers,
|
|
@@ -371,6 +411,8 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
|
|
|
371
411
|
relayPlugin: false,
|
|
372
412
|
statusLine: false,
|
|
373
413
|
skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
414
|
+
projectSkills: [],
|
|
415
|
+
projectSettings: { applied: false, keys: [], ignoredKeys: [] },
|
|
374
416
|
plugins: [],
|
|
375
417
|
mcp: {
|
|
376
418
|
relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [],
|
|
@@ -389,7 +431,7 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
|
|
|
389
431
|
*/
|
|
390
432
|
export function assembleLaunch(provider: SpawnProvider, config: RunnerSpawnConfig, providerConfig: ProviderConfig): AssembledLaunch {
|
|
391
433
|
if (provider === "codex") return assembleCodex(config, providerConfig);
|
|
392
|
-
const hostDefaultArgs =
|
|
434
|
+
const hostDefaultArgs = profileUsesClaudeHostProviderGlobals(config) ? providerConfig.defaultArgs : [];
|
|
393
435
|
return assembleClaude(config, providerConfig, hostDefaultArgs);
|
|
394
436
|
}
|
|
395
437
|
|
|
@@ -403,7 +445,9 @@ export function materializeLaunchAssembly(provider: SpawnProvider, config: Runne
|
|
|
403
445
|
const configHome = providerHomePathFor(provider, config);
|
|
404
446
|
if (!configHome) return;
|
|
405
447
|
if (provider === "claude") {
|
|
406
|
-
|
|
448
|
+
const skills = resolveClaudeProvisionedSkills(configHome, config);
|
|
449
|
+
materializeResolvedAssets(skills);
|
|
450
|
+
materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set(skills.map((s) => s.name))));
|
|
407
451
|
materializeResolvedAssets(resolveClaudeProvisionedPlugins(configHome, config));
|
|
408
452
|
return;
|
|
409
453
|
}
|
package/src/profile-home.ts
CHANGED
|
@@ -16,8 +16,18 @@ export function profileUsesHostProviderGlobals(config: { agentProfile?: RunnerSp
|
|
|
16
16
|
return !config.agentProfile || config.agentProfile.base === "host";
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
return Boolean(config.agentProfile
|
|
19
|
+
export function claudeProjectConfigProjectionOn(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
|
|
20
|
+
return Boolean(config.agentProfile?.projectSkills || config.agentProfile?.projectSettings);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function profileUsesClaudeHostProviderGlobals(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
|
|
24
|
+
return profileUsesHostProviderGlobals(config) && !claudeProjectConfigProjectionOn(config);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function profileRequiresIsolatedHome(provider: "claude" | "codex", config: RunnerSpawnConfig): boolean {
|
|
28
|
+
if (!config.agentProfile) return false;
|
|
29
|
+
if (config.agentProfile.base !== "host") return true;
|
|
30
|
+
return provider === "claude" && claudeProjectConfigProjectionOn(config);
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
// #557 — PURE: the isolated provider config home path a launch WILL use, or undefined
|
|
@@ -25,7 +35,7 @@ function profileRequiresIsolatedHome(config: RunnerSpawnConfig): boolean {
|
|
|
25
35
|
// computes the home this way to report it + build CLAUDE_CONFIG_DIR/skill/plugin paths
|
|
26
36
|
// without side effects; prepare*ProfileHome creates it at the same path at spawn time.
|
|
27
37
|
export function providerHomePathFor(provider: "claude" | "codex", config: RunnerSpawnConfig): string | undefined {
|
|
28
|
-
if (!profileRequiresIsolatedHome(config)) return undefined;
|
|
38
|
+
if (!profileRequiresIsolatedHome(provider, config)) return undefined;
|
|
29
39
|
return providerHomePath(provider, config);
|
|
30
40
|
}
|
|
31
41
|
|
|
@@ -56,7 +66,7 @@ const CLAUDE_AUTH_ITEMS = [".credentials.json", "statsig"];
|
|
|
56
66
|
// instance-keyed home, run the provider-specific first-run bootstrap. The
|
|
57
67
|
// bootstrap step is the only genuinely provider-specific part.
|
|
58
68
|
function prepareProviderHome(provider: "claude" | "codex", config: RunnerSpawnConfig): ProviderHome | undefined {
|
|
59
|
-
if (!profileRequiresIsolatedHome(config)) return undefined;
|
|
69
|
+
if (!profileRequiresIsolatedHome(provider, config)) return undefined;
|
|
60
70
|
const target = providerHomePath(provider, config);
|
|
61
71
|
mkdirSync(target, { recursive: true });
|
|
62
72
|
const authLinked = provider === "codex"
|
|
@@ -34,6 +34,8 @@ export function agentProfileProjectionReport(input: AgentProfileProjectionInput)
|
|
|
34
34
|
add(provisioningPluginsEntry(assembled));
|
|
35
35
|
add(provisioningMcpEntry(assembled));
|
|
36
36
|
add(projectMcpEntry(assembled));
|
|
37
|
+
add(projectSkillsEntry(assembled));
|
|
38
|
+
add(projectSettingsEntry(assembled));
|
|
37
39
|
add(sharedMcpEntry(assembled));
|
|
38
40
|
if (profile) add(filesystemEntry(profile));
|
|
39
41
|
add(configHomeEntry(assembled));
|
|
@@ -159,14 +161,30 @@ function projectMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
|
159
161
|
: `Project .mcp.json servers emitted as -c mcp_servers.* overrides (${names.join(", ")}).`);
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
function projectSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
165
|
+
const optedIn = Boolean(a.profile?.projectSkills);
|
|
166
|
+
if (!optedIn) return notApplicable("projectSkills", "off", "Project .claude/skills projection is off (opt in via the projectSkills profile/spawn option).");
|
|
167
|
+
if (a.provider !== "claude") return notApplicable("projectSkills", "unsupported", "Project .claude/skills projection applies to Claude launches only.");
|
|
168
|
+
if (!a.projectSkills.length) return applied("projectSkills", "none", "Project .claude/skills projection is on, but the spawn cwd declared no valid skill dirs.");
|
|
169
|
+
const names = a.projectSkills.map((s) => s.name).join(", ");
|
|
170
|
+
return applied("projectSkills", `${a.projectSkills.length}`, `Project .claude skills materialized into Relay's managed Claude config home (${names}); host/global skills are excluded.`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function projectSettingsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
174
|
+
const optedIn = Boolean(a.profile?.projectSettings);
|
|
175
|
+
if (!optedIn) return notApplicable("projectSettings", "off", "Project .claude/settings.json projection is off (opt in via the projectSettings profile/spawn option).");
|
|
176
|
+
if (a.provider !== "claude") return notApplicable("projectSettings", "unsupported", "Project .claude/settings.json projection applies to Claude launches only.");
|
|
177
|
+
const ignored = a.projectSettings.ignoredKeys.length ? `; ignored security keys: ${a.projectSettings.ignoredKeys.join(", ")}` : "";
|
|
178
|
+
return applied("projectSettings", a.projectSettings.keys.length ? `${a.projectSettings.keys.length} keys` : "none", `Project .claude/settings.json was sanitized and injected through --settings; Relay approvalMode/permission gates remain authoritative${ignored}.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// #672/#689 — shared host-listener MCP provenance. `applied` reports the per-agent `callmux
|
|
182
|
+
// bridge` route. `not-applicable` means the resolved profile kept shared MCP off (for example a
|
|
183
|
+
// zero-host profile, explicit false, or global kill-switch normalization before launch).
|
|
166
184
|
function sharedMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
167
185
|
const optedIn = Boolean(a.profile?.sharedMcp);
|
|
168
186
|
const listener = a.mcp.sharedListener;
|
|
169
|
-
if (!optedIn || !listener) return notApplicable("sharedMcp", "off", "Shared host-listener MCP is off
|
|
187
|
+
if (!optedIn || !listener) return notApplicable("sharedMcp", "off", "Shared host-listener MCP is off for this resolved profile; non-identity MCP is injected per-agent.");
|
|
170
188
|
return applied("sharedMcp", "on", a.provider === "claude"
|
|
171
189
|
? `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"', injected into --mcp-config (survives --strict-mcp-config); session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent (#215).`
|
|
172
190
|
: `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"' emitted as -c mcp_servers.* overrides; session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent (#215).`);
|
package/src/provisioning.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
4
4
|
import type { ProvisioningAssetFile, ProvisioningMcpServer, ResolvedProvisioning } from "agent-relay-sdk";
|
|
@@ -74,6 +74,88 @@ export function resolveProjectMcpServers(
|
|
|
74
74
|
return out;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export function resolveClaudeProjectSkills(
|
|
78
|
+
claudeHome: string,
|
|
79
|
+
config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">,
|
|
80
|
+
reservedNames: ReadonlySet<string> = new Set(),
|
|
81
|
+
): ResolvedAsset[] {
|
|
82
|
+
if (!config.agentProfile?.projectSkills) return [];
|
|
83
|
+
const skillsRoot = join(config.cwd, ".claude", "skills");
|
|
84
|
+
if (!existsSync(skillsRoot)) return [];
|
|
85
|
+
const targetRoot = join(claudeHome, "skills");
|
|
86
|
+
const resolved: ResolvedAsset[] = [];
|
|
87
|
+
for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
88
|
+
if (!entry.isDirectory() || reservedNames.has(entry.name)) continue;
|
|
89
|
+
const source = join(skillsRoot, entry.name);
|
|
90
|
+
if (!existsSync(join(source, "SKILL.md"))) continue;
|
|
91
|
+
resolved.push({
|
|
92
|
+
name: entry.name,
|
|
93
|
+
dir: join(targetRoot, assetDirName(entry.name)),
|
|
94
|
+
source: { kind: "link", source },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return resolved.sort((a, b) => a.name.localeCompare(b.name));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const UNSAFE_PROJECT_CLAUDE_SETTINGS = new Set([
|
|
101
|
+
"apiKeyHelper",
|
|
102
|
+
"allowDangerouslySkipPermissions",
|
|
103
|
+
"allowedTools",
|
|
104
|
+
"disabledMcpjsonServers",
|
|
105
|
+
"disallowedTools",
|
|
106
|
+
"enableAllProjectMcpServers",
|
|
107
|
+
"enabledMcpjsonServers",
|
|
108
|
+
"env",
|
|
109
|
+
"hooks",
|
|
110
|
+
"mcpServers",
|
|
111
|
+
"permissionMode",
|
|
112
|
+
"permissions",
|
|
113
|
+
"statusLine",
|
|
114
|
+
"tools",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
export interface ResolvedProjectClaudeSettings {
|
|
118
|
+
applied: boolean;
|
|
119
|
+
settings: Record<string, unknown>;
|
|
120
|
+
keys: string[];
|
|
121
|
+
ignoredKeys: string[];
|
|
122
|
+
file?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveProjectClaudeSettings(
|
|
126
|
+
config: Pick<RunnerSpawnConfig, "agentProfile" | "cwd">,
|
|
127
|
+
): ResolvedProjectClaudeSettings {
|
|
128
|
+
if (!config.agentProfile?.projectSettings) return { applied: false, settings: {}, keys: [], ignoredKeys: [] };
|
|
129
|
+
const file = join(config.cwd, ".claude", "settings.json");
|
|
130
|
+
if (!existsSync(file)) return { applied: true, settings: {}, keys: [], ignoredKeys: [], file };
|
|
131
|
+
let parsed: unknown;
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn(`project .claude/settings.json projection: failed to parse ${file}: ${(err as Error).message}`);
|
|
136
|
+
return { applied: true, settings: {}, keys: [], ignoredKeys: [], file };
|
|
137
|
+
}
|
|
138
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
139
|
+
return { applied: true, settings: {}, keys: [], ignoredKeys: [], file };
|
|
140
|
+
}
|
|
141
|
+
const settings: Record<string, unknown> = {};
|
|
142
|
+
const ignoredKeys: string[] = [];
|
|
143
|
+
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
|
|
144
|
+
if (UNSAFE_PROJECT_CLAUDE_SETTINGS.has(key)) {
|
|
145
|
+
ignoredKeys.push(key);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
settings[key] = value;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
applied: true,
|
|
152
|
+
settings,
|
|
153
|
+
keys: Object.keys(settings).sort(),
|
|
154
|
+
ignoredKeys: ignoredKeys.sort(),
|
|
155
|
+
file,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
77
159
|
// A resolved provisioning asset: WHERE it lands (`dir`) and HOW it materializes
|
|
78
160
|
// (`source`). `files` writes the inline set into `dir`; `link` symlinks a pre-existing
|
|
79
161
|
// source dir at `dir`; `inPlace` uses the source dir as-is (no write, dir IS the source).
|
|
@@ -39,7 +39,9 @@ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInj
|
|
|
39
39
|
if (!supportsLaunchAssembly(input.provider)) return events;
|
|
40
40
|
const assembled = assembleLaunch(input.provider, input.config, input.providerConfig);
|
|
41
41
|
const toolCount = assembled.skills.length + assembled.plugins.length + assembled.mcp.servers.length
|
|
42
|
+
+ assembled.projectSkills.length
|
|
42
43
|
+ assembled.mcp.projectServers.length
|
|
44
|
+
+ (assembled.projectSettings.applied ? 1 : 0)
|
|
43
45
|
+ (assembled.relaySkills ? 1 : 0)
|
|
44
46
|
+ (assembled.relayPlugin ? 1 : 0)
|
|
45
47
|
+ (assembled.mcp.relayEndpoint ? 1 : 0)
|
|
@@ -51,6 +53,8 @@ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInj
|
|
|
51
53
|
assembled.statusLine ? "status line" : "",
|
|
52
54
|
assembled.mcp.relayEndpoint ? "relay MCP" : "",
|
|
53
55
|
assembled.skills.length ? `${assembled.skills.length} skill${assembled.skills.length === 1 ? "" : "s"}` : "",
|
|
56
|
+
assembled.projectSkills.length ? `${assembled.projectSkills.length} project skill${assembled.projectSkills.length === 1 ? "" : "s"}` : "",
|
|
57
|
+
assembled.projectSettings.applied ? "project settings" : "",
|
|
54
58
|
assembled.plugins.length ? `${assembled.plugins.length} plugin${assembled.plugins.length === 1 ? "" : "s"}` : "",
|
|
55
59
|
assembled.mcp.servers.length ? `${assembled.mcp.servers.length} MCP server${assembled.mcp.servers.length === 1 ? "" : "s"}` : "",
|
|
56
60
|
assembled.mcp.projectServers.length ? `${assembled.mcp.projectServers.length} project MCP server${assembled.mcp.projectServers.length === 1 ? "" : "s"}` : "",
|
|
@@ -70,6 +74,8 @@ function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInj
|
|
|
70
74
|
relayPlugin: assembled.relayPlugin,
|
|
71
75
|
statusLine: assembled.statusLine,
|
|
72
76
|
skills: assembled.skills,
|
|
77
|
+
projectSkills: assembled.projectSkills,
|
|
78
|
+
projectSettings: assembled.projectSettings,
|
|
73
79
|
plugins: assembled.plugins,
|
|
74
80
|
mcp: assembled.mcp,
|
|
75
81
|
},
|
package/src/runner-core.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { startControlServer, type ControlServer } from "./control-server";
|
|
|
16
16
|
import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
|
|
17
17
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
18
18
|
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
19
|
-
import { profileUsesHostProviderGlobals } from "./profile-home";
|
|
19
|
+
import { profileUsesClaudeHostProviderGlobals, profileUsesHostProviderGlobals } from "./profile-home";
|
|
20
20
|
import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint, resolveSharedMcpUrl } from "./relay-mcp";
|
|
21
21
|
import { RelayMcpProxy } from "./relay-mcp-proxy";
|
|
22
22
|
import { runtimeMetadata } from "./version";
|
|
@@ -526,7 +526,7 @@ export class AgentRunner {
|
|
|
526
526
|
private async spawnProvider(opts: { resumeId?: string; suppressPrompt?: boolean } = {}): Promise<ManagedProcess> {
|
|
527
527
|
this.providerSessionId = crypto.randomUUID();
|
|
528
528
|
this.lastTranscriptPath = undefined;
|
|
529
|
-
const includeProviderGlobals = profileUsesHostProviderGlobals(this.options);
|
|
529
|
+
const includeProviderGlobals = this.options.provider === "claude" ? profileUsesClaudeHostProviderGlobals(this.options) : profileUsesHostProviderGlobals(this.options);
|
|
530
530
|
const env = {
|
|
531
531
|
...process.env as Record<string, string>,
|
|
532
532
|
...(includeProviderGlobals ? this.options.providerConfig.env : {}),
|