agent-relay-runner 0.113.0 → 0.115.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.113.0",
3
+ "version": "0.115.0",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "agent-relay-providers": "0.104.1",
24
- "agent-relay-sdk": "0.2.96",
24
+ "agent-relay-sdk": "0.2.97",
25
25
  "callmux": "0.23.0"
26
26
  },
27
27
  "devDependencies": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.113.0",
4
+ "version": "0.115.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  projectConfigProjectionOn,
12
12
  hostUserScopeMcpServers,
13
+ profileAllowsHostAssets,
13
14
  profileUsesProviderHostGlobals,
14
15
  providerHomePathFor,
15
16
  providerProvisioningHomePathFor,
@@ -19,6 +20,7 @@ import { claudeLaunchPromptGateSettings } from "./claude-prompt-gates";
19
20
  import {
20
21
  materializeResolvedAssets,
21
22
  resolveClaudeProjectSkills,
23
+ resolveClaudeHostSkills,
22
24
  resolveProjectClaudeSettings,
23
25
  resolveClaudeProvisionedPlugins,
24
26
  resolveClaudeProvisionedSkills,
@@ -127,10 +129,14 @@ export interface AssembledLaunch {
127
129
  skills: AssembledAsset[];
128
130
  /** Project `.claude/skills` materialized for this Claude launch (#668). */
129
131
  projectSkills: AssembledAsset[];
132
+ /** Host/global Claude skills explicitly inherited without enabling host MCP. */
133
+ hostSkills: AssembledAsset[];
130
134
  /** Project `.claude/settings.json` sanitized and injected for this Claude launch (#669). */
131
135
  projectSettings: { applied: boolean; keys: string[]; ignoredKeys: string[]; file?: string };
132
136
  /** Relay-provisioned plugins (Claude --plugin-dir). Empty for Codex (no plugin surface). */
133
137
  plugins: AssembledAsset[];
138
+ /** Host/global provider plugin dirs explicitly inherited without enabling host MCP. */
139
+ hostPluginDirs: string[];
134
140
  mcp: AssembledMcp;
135
141
  instructions: AssembledInstructions;
136
142
  /** Provider provisioning/vanilla/instruction launch-arg block, in launch order. */
@@ -236,8 +242,11 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
236
242
  const statusLine = profileAllowsRelayFeature(config, "statusLine");
237
243
 
238
244
  const skills = configHome ? resolveClaudeProvisionedSkills(configHome, config) : [];
245
+ const hostSkills = configHome
246
+ ? resolveClaudeHostSkills(configHome, config, new Set(skills.map((s) => s.name)))
247
+ : [];
239
248
  const projectSkills = configHome
240
- ? resolveClaudeProjectSkills(configHome, config, new Set(skills.map((s) => s.name)))
249
+ ? resolveClaudeProjectSkills(configHome, config, new Set([...skills, ...hostSkills].map((s) => s.name)))
241
250
  : [];
242
251
  const projectSettings = resolveProjectClaudeSettings(config);
243
252
  const provisionedPlugins = assetHome ? resolveClaudeProvisionedPlugins(assetHome, config) : [];
@@ -249,7 +258,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
249
258
  // host plugin dirs → ONLY host-base profiles; isolated/vanilla must not pull them back in.
250
259
  const relayPluginDirs = !config.agentProfile && relayPlugin ? [CLAUDE_RELAY_PLUGIN_DIR] : [];
251
260
  const provisionedPluginDirs = provisionedPlugins.map((p) => p.dir);
252
- const hostPluginDirs = profileUsesProviderHostGlobals(config) ? providerConfig.pluginDirs : [];
261
+ const hostPluginDirs = profileAllowsHostAssets(config, "plugins") ? providerConfig.pluginDirs : [];
253
262
  const pluginDirs = [...new Set([...relayPluginDirs, ...provisionedPluginDirs, ...hostPluginDirs])];
254
263
 
255
264
  const provisionedMcp = profileProvisioning(config).mcpServers;
@@ -333,6 +342,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
333
342
  relayPlugin,
334
343
  statusLine,
335
344
  skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
345
+ hostSkills: hostSkills.map((s) => ({ name: s.name, dir: s.dir })),
336
346
  projectSkills: projectSkills.map((s) => ({ name: s.name, dir: s.dir })),
337
347
  projectSettings: {
338
348
  applied: projectSettings.applied,
@@ -341,6 +351,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
341
351
  ...(projectSettings.file ? { file: projectSettings.file } : {}),
342
352
  },
343
353
  plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
354
+ hostPluginDirs,
344
355
  mcp: {
345
356
  relayEndpoint, strict: strictMcp, servers: mcpServers, projectServers, hostServers: hostMcpServers,
346
357
  ...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
@@ -410,9 +421,11 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
410
421
  relayPlugin: false,
411
422
  statusLine: false,
412
423
  skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
424
+ hostSkills: [],
413
425
  projectSkills: [],
414
426
  projectSettings: { applied: false, keys: [], ignoredKeys: [] },
415
427
  plugins: [],
428
+ hostPluginDirs: [],
416
429
  mcp: {
417
430
  relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [],
418
431
  ...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
@@ -451,7 +464,9 @@ function materializeClaudeAssets(config: RunnerSpawnConfig): void {
451
464
  if (configHome) {
452
465
  const skills = resolveClaudeProvisionedSkills(configHome, config);
453
466
  materializeResolvedAssets(skills);
454
- materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set(skills.map((s) => s.name))));
467
+ const hostSkills = resolveClaudeHostSkills(configHome, config, new Set(skills.map((s) => s.name)));
468
+ materializeResolvedAssets(hostSkills);
469
+ materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set([...skills, ...hostSkills].map((s) => s.name))));
455
470
  }
456
471
  if (assetHome) materializeResolvedAssets(resolveClaudeProvisionedPlugins(assetHome, config));
457
472
  }
@@ -18,6 +18,17 @@ export function profileUsesHostProviderGlobals(config: { agentProfile?: RunnerSp
18
18
  return !config.agentProfile || config.agentProfile.base === "host";
19
19
  }
20
20
 
21
+ export function profileAllowsHostAssets(
22
+ config: { agentProfile?: RunnerSpawnConfig["agentProfile"] },
23
+ asset: "skills" | "plugins",
24
+ ): boolean {
25
+ return profileUsesProviderHostGlobals(config) || Boolean(config.agentProfile?.hostAssets?.[asset]);
26
+ }
27
+
28
+ export function profileAllowsHostGlobalInstructions(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
29
+ return !config.agentProfile || config.agentProfile.instructions.globalInstructions === "allow";
30
+ }
31
+
21
32
  export function projectConfigProjectionOn(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
22
33
  return Boolean(config.agentProfile?.projectSkills || config.agentProfile?.projectSettings);
23
34
  }
@@ -90,6 +101,7 @@ function prepareProviderHome(provider: SpawnProvider, config: RunnerSpawnConfig)
90
101
  const authItems = manifest?.home?.authItems ?? [];
91
102
  const sourceHome = providerSourceHome(provider);
92
103
  const authLinked = linkExistingAuthItems(sourceHome, target, authItems);
104
+ linkHostGlobalInstructionFiles(provider, sourceHome, target, config);
93
105
  return { path: target, authLinked };
94
106
  }
95
107
 
@@ -101,14 +113,16 @@ function bootstrapCodexFirstRun(codexHome: string, config: RunnerSpawnConfig): v
101
113
  }
102
114
 
103
115
  function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig): void {
104
- if (profileAllowsRelayFeature(config, "context")) writeClaudeRelayManual(claudeHome);
116
+ if (profileAllowsRelayFeature(config, "context") && !profileAllowsHostGlobalInstructions(config)) {
117
+ writeClaudeRelayManual(claudeHome);
118
+ }
105
119
  seedClaudeConfigIfMissing(claudeHome, config);
106
120
  }
107
121
 
108
122
  BOOTSTRAP_HOOKS.set("codex", bootstrapCodexFirstRun);
109
123
  BOOTSTRAP_HOOKS.set("claude", bootstrapClaudeFirstRun);
110
124
 
111
- function providerSourceHome(provider: string): string {
125
+ export function providerSourceHome(provider: string): string {
112
126
  const manifest = getManifest(provider);
113
127
  const envVar = manifest?.home?.configDirEnvVar;
114
128
  const configDir = manifest?.home?.configDir ?? `.${provider}`;
@@ -161,15 +175,23 @@ function sanitizePathPart(value: string): string {
161
175
  function linkExistingAuthItems(sourceHome: string, targetHome: string, items: string[]): string[] {
162
176
  const linked: string[] = [];
163
177
  for (const item of items) {
164
- const source = join(sourceHome, item);
165
- const target = join(targetHome, item);
166
- if (!existsSync(source) || existsSync(target)) continue;
167
- symlinkSync(source, target);
168
- linked.push(item);
178
+ if (linkExistingPath(join(sourceHome, item), join(targetHome, item))) linked.push(item);
169
179
  }
170
180
  return linked;
171
181
  }
172
182
 
183
+ function linkExistingPath(source: string, target: string): boolean {
184
+ if (!existsSync(source) || existsSync(target)) return false;
185
+ symlinkSync(source, target);
186
+ return true;
187
+ }
188
+
189
+ function linkHostGlobalInstructionFiles(provider: SpawnProvider, sourceHome: string, targetHome: string, config: RunnerSpawnConfig): void {
190
+ if (!profileAllowsHostGlobalInstructions(config)) return;
191
+ const files = provider === "claude" ? ["CLAUDE.md"] : provider === "codex" ? ["AGENTS.md"] : [];
192
+ for (const file of files) linkExistingPath(join(sourceHome, file), join(targetHome, file));
193
+ }
194
+
173
195
  function trustWorkspaceForCodex(codexHome: string, config: RunnerSpawnConfig): void {
174
196
  if (!config.cwd) return;
175
197
  const path = join(codexHome, "config.toml");
@@ -33,6 +33,8 @@ export function agentProfileProjectionReport(input: AgentProfileProjectionInput)
33
33
  add(hooksEntry(assembled));
34
34
  add(provisioningSkillsEntry(assembled));
35
35
  add(provisioningPluginsEntry(assembled));
36
+ add(hostSkillsEntry(assembled));
37
+ add(hostPluginsEntry(assembled));
36
38
  add(provisioningMcpEntry(assembled));
37
39
  for (const entry of provisioningSkippedEntries(assembled)) add(entry);
38
40
  add(projectMcpEntry(assembled));
@@ -166,6 +168,29 @@ function provisioningPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEnt
166
168
  return applied("provisioning.plugins", `${count}`, `Relay-provisioned plugins passed to ${label} via the plugin dir mechanism${names}.`);
167
169
  }
168
170
 
171
+ function hostSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
172
+ const optedIn = Boolean(a.profile?.hostAssets?.skills);
173
+ if (!optedIn) return notApplicable("hostAssets.skills", "off", "Host/global skill inheritance is off for this resolved profile.");
174
+ const label = getManifest(a.provider)?.label ?? a.provider;
175
+ if (!getManifest(a.provider)?.provisioning?.bundledPlugin) {
176
+ return notApplicable("hostAssets.skills", "unsupported", `${label} does not expose host skill-dir projection through Agent Relay.`);
177
+ }
178
+ if (!a.hostSkills.length) return applied("hostAssets.skills", "none", "Host/global skill inheritance is on, but no valid host skill dirs were found.");
179
+ const names = a.hostSkills.map((s) => s.name).join(", ");
180
+ return applied("hostAssets.skills", `${a.hostSkills.length}`, `Host/global skills materialized into Relay's managed ${label} config home (${names}).`);
181
+ }
182
+
183
+ function hostPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
184
+ const optedIn = Boolean(a.profile?.hostAssets?.plugins);
185
+ if (!optedIn) return notApplicable("hostAssets.plugins", "off", "Host/global plugin inheritance is off for this resolved profile.");
186
+ const label = getManifest(a.provider)?.label ?? a.provider;
187
+ if (!getManifest(a.provider)?.provisioning?.bundledPlugin) {
188
+ return notApplicable("hostAssets.plugins", "unsupported", `${label} has no plugin-dir surface for host plugin inheritance.`);
189
+ }
190
+ if (!a.hostPluginDirs.length) return applied("hostAssets.plugins", "none", "Host/global plugin inheritance is on, but no host plugin dirs are configured.");
191
+ return applied("hostAssets.plugins", `${a.hostPluginDirs.length}`, `Host/global plugin dirs passed to ${label} (${a.hostPluginDirs.join(", ")}).`);
192
+ }
193
+
169
194
  function provisioningMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
170
195
  const names = a.mcp.servers;
171
196
  if (!names.length) return notApplicable("provisioning.mcp", "none", "No relay-provisioned MCP servers materialized for this launch.");
@@ -3,6 +3,7 @@ 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";
5
5
  import type { RunnerSpawnConfig } from "./adapter";
6
+ import { profileAllowsHostAssets, providerSourceHome } from "./profile-home";
6
7
 
7
8
  // Runner-side materialization of the relay-resolved provisioning bundle
8
9
  // (#554/#555/#556). The relay resolves a profile's refs against the DB registry and
@@ -97,6 +98,29 @@ export function resolveClaudeProjectSkills(
97
98
  return resolved.sort((a, b) => a.name.localeCompare(b.name));
98
99
  }
99
100
 
101
+ export function resolveClaudeHostSkills(
102
+ claudeHome: string,
103
+ config: Pick<RunnerSpawnConfig, "agentProfile">,
104
+ reservedNames: ReadonlySet<string> = new Set(),
105
+ ): ResolvedAsset[] {
106
+ if (!profileAllowsHostAssets(config, "skills")) return [];
107
+ const skillsRoot = join(providerSourceHome("claude"), "skills");
108
+ if (!existsSync(skillsRoot)) return [];
109
+ const targetRoot = join(claudeHome, "skills");
110
+ const resolved: ResolvedAsset[] = [];
111
+ for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
112
+ if (!entry.isDirectory() || reservedNames.has(entry.name)) continue;
113
+ const source = join(skillsRoot, entry.name);
114
+ if (!existsSync(join(source, "SKILL.md"))) continue;
115
+ resolved.push({
116
+ name: entry.name,
117
+ dir: join(targetRoot, assetDirName(entry.name)),
118
+ source: { kind: "link", source },
119
+ });
120
+ }
121
+ return resolved.sort((a, b) => a.name.localeCompare(b.name));
122
+ }
123
+
100
124
  const UNSAFE_PROJECT_CLAUDE_SETTINGS = new Set([
101
125
  "apiKeyHelper",
102
126
  "allowDangerouslySkipPermissions",