agent-relay-server 0.121.5 → 0.122.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.
Files changed (179) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +6 -6
  3. package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
  4. package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
  7. package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
  8. package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
  10. package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
  12. package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
  13. package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
  14. package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
  15. package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
  16. package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
  17. package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
  18. package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
  20. package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
  21. package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
  22. package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
  23. package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
  24. package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
  25. package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
  26. package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
  27. package/public/assets/display-CJzSoTMq.js +3 -0
  28. package/public/assets/display-CJzSoTMq.js.map +1 -0
  29. package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
  30. package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
  31. package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
  32. package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
  33. package/public/assets/index-D0xMQQb_.js +25 -0
  34. package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
  35. package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
  36. package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
  37. package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
  38. package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
  39. package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
  40. package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
  41. package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
  42. package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
  43. package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
  44. package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
  45. package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
  46. package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
  47. package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
  48. package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
  49. package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
  50. package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
  51. package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
  52. package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
  53. package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
  54. package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
  55. package/public/assets/projects-Dzdf-gkK.js +2 -0
  56. package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
  57. package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
  58. package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
  59. package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
  60. package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
  61. package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
  62. package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
  63. package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
  64. package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
  65. package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
  66. package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
  67. package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
  68. package/public/assets/store-BPrVuKiv.js.map +1 -0
  69. package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
  70. package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
  71. package/public/assets/teams-DWv6UwFV.js +2 -0
  72. package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
  73. package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
  74. package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
  75. package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
  76. package/public/assets/types-BeJUSfDj.js.map +1 -0
  77. package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
  78. package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
  79. package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
  80. package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
  81. package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
  82. package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
  83. package/public/index.html +6 -6
  84. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  85. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
  86. package/runner/src/adapter.ts +53 -18
  87. package/runner/src/adapters/claude-delivery.ts +127 -0
  88. package/runner/src/adapters/claude-quota-harvest.ts +92 -0
  89. package/runner/src/adapters/claude-session-probe.ts +225 -0
  90. package/runner/src/adapters/claude-status-detectors.ts +147 -0
  91. package/runner/src/adapters/claude-transcript-tail.ts +128 -0
  92. package/runner/src/adapters/claude-transcript.ts +414 -0
  93. package/runner/src/adapters/claude.ts +672 -0
  94. package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
  95. package/runner/src/adapters/codex-client.ts +279 -0
  96. package/runner/src/adapters/codex.ts +1345 -0
  97. package/runner/src/attachment-cache.ts +189 -0
  98. package/runner/src/busy-reconciler.ts +102 -0
  99. package/runner/src/claim-tracker.ts +140 -0
  100. package/runner/src/claude-prompt-gates.ts +268 -0
  101. package/runner/src/config.ts +1 -4
  102. package/runner/src/context-probe-state.ts +6 -0
  103. package/runner/src/continuation-archive.ts +179 -0
  104. package/runner/src/control-server.ts +639 -0
  105. package/runner/src/index.ts +396 -0
  106. package/runner/src/launch-assembly.ts +508 -0
  107. package/runner/src/liveness.ts +50 -0
  108. package/runner/src/logger.ts +99 -0
  109. package/runner/src/mcp-outbox.ts +120 -0
  110. package/runner/src/message-body-config/config.ts +3 -0
  111. package/runner/src/message-body-config.ts +1 -0
  112. package/runner/src/native-self-resume.ts +202 -0
  113. package/runner/src/outbox.ts +342 -0
  114. package/runner/src/process-meta.ts +9 -0
  115. package/runner/src/profile-home.ts +260 -0
  116. package/runner/src/profile-projection.ts +305 -0
  117. package/runner/src/providers.ts +20 -0
  118. package/runner/src/provisioning.ts +314 -0
  119. package/runner/src/quota.ts +40 -0
  120. package/runner/src/rate-limit.ts +189 -0
  121. package/runner/src/relay-injection-events.ts +102 -0
  122. package/runner/src/relay-instructions.ts +60 -0
  123. package/runner/src/relay-mcp-proxy.ts +383 -0
  124. package/runner/src/relay-mcp.ts +156 -0
  125. package/runner/src/reply-obligation-cache.ts +136 -0
  126. package/runner/src/response-capture-report.ts +63 -0
  127. package/runner/src/runner-core.ts +2409 -0
  128. package/runner/src/runner-helpers.ts +435 -0
  129. package/runner/src/runner-insights.ts +105 -0
  130. package/runner/src/runner.ts +14 -0
  131. package/runner/src/session-destroy.ts +22 -0
  132. package/runner/src/session-insights.ts +118 -0
  133. package/runner/src/session-scratch.ts +271 -0
  134. package/runner/src/template-resolver.ts +29 -0
  135. package/runner/src/version.ts +43 -0
  136. package/src/agent-card-projection.ts +5 -1
  137. package/src/agent-command-activity.ts +39 -0
  138. package/src/automations/reconcile.ts +2 -2
  139. package/src/cli/workspace.ts +3 -1
  140. package/src/config.ts +10 -1
  141. package/src/db/agents.ts +118 -37
  142. package/src/db/message-reads.ts +50 -0
  143. package/src/db/messages.ts +48 -9
  144. package/src/db/migrations.ts +54 -0
  145. package/src/db/provider-quota-burn.ts +96 -67
  146. package/src/db/provider-quotas.ts +29 -7
  147. package/src/db/scheduled-timers.ts +21 -0
  148. package/src/db/schema.ts +1 -0
  149. package/src/db/tasks.ts +33 -3
  150. package/src/db/work-evidence.ts +10 -7
  151. package/src/event-loop-lag.ts +15 -0
  152. package/src/gate-resolver.ts +19 -1
  153. package/src/maintenance/jobs.ts +23 -6
  154. package/src/managed-policy.ts +5 -12
  155. package/src/mcp-plan-tools.ts +13 -1
  156. package/src/project-instructions.ts +36 -2
  157. package/src/quota-advisory.ts +6 -4
  158. package/src/quota-band-transition.ts +23 -14
  159. package/src/routes/agent-sessions.ts +39 -7
  160. package/src/routes/agents.ts +3 -2
  161. package/src/routes/tasks.ts +16 -4
  162. package/src/routes/workspaces.ts +35 -10
  163. package/src/services/plan-graph.ts +27 -9
  164. package/src/services/register-agent.ts +12 -12
  165. package/src/services/send-message.ts +43 -15
  166. package/src/services/spawn-agent.ts +3 -0
  167. package/src/spawn-command.ts +8 -0
  168. package/src/spawn-targets.ts +19 -34
  169. package/src/sse.ts +25 -3
  170. package/src/workspace-merge.ts +25 -5
  171. package/src/workspace-orphans.ts +13 -6
  172. package/public/assets/display-exQDWum3.js +0 -3
  173. package/public/assets/display-exQDWum3.js.map +0 -1
  174. package/public/assets/index-D9OogaB5.js +0 -25
  175. package/public/assets/projects-DdPNPFJI.js +0 -2
  176. package/public/assets/prompt-settings-BpZse7-X.js +0 -2
  177. package/public/assets/store-DKTLubBT.js.map +0 -1
  178. package/public/assets/teams-B8f2pGJe.js +0 -2
  179. package/public/assets/types-uVOXgvMT.js.map +0 -1
@@ -0,0 +1,260 @@
1
+ import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { sanitizeFsName } from "agent-relay-sdk/fs-name";
5
+ import type { ProvisioningMcpServer, SpawnProvider } from "agent-relay-sdk";
6
+ import { getManifest } from "agent-relay-providers";
7
+ import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
8
+ import { claudeRelayManual } from "./relay-instructions";
9
+ import { providerHomeRootFromEnv } from "./config";
10
+ import { applyClaudeConfigPromptGatePreventions } from "./claude-prompt-gates";
11
+
12
+ type ProviderHome = {
13
+ path: string;
14
+ authLinked: string[];
15
+ };
16
+
17
+ export function profileUsesHostProviderGlobals(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
18
+ return !config.agentProfile || config.agentProfile.base === "host";
19
+ }
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
+
32
+ export function projectConfigProjectionOn(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
33
+ return Boolean(config.agentProfile?.projectSkills || config.agentProfile?.projectSettings);
34
+ }
35
+
36
+ /** @deprecated Use {@link projectConfigProjectionOn}. */
37
+ export { projectConfigProjectionOn as claudeProjectConfigProjectionOn };
38
+
39
+ export function profileUsesProviderHostGlobals(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
40
+ return profileUsesHostProviderGlobals(config) && !projectConfigProjectionOn(config);
41
+ }
42
+
43
+ /** @deprecated Use {@link profileUsesProviderHostGlobals}. */
44
+ export { profileUsesProviderHostGlobals as profileUsesClaudeHostProviderGlobals };
45
+
46
+ function profileRequiresIsolatedHome(_provider: SpawnProvider, config: RunnerSpawnConfig): boolean {
47
+ if (!config.agentProfile) return false;
48
+ if (config.agentProfile.base !== "host") return true;
49
+ return projectConfigProjectionOn(config);
50
+ }
51
+
52
+ // #557 — PURE: the isolated provider config home path a launch WILL use, or undefined
53
+ // for a host-base profile (which uses the provider's normal home). The launch assembler
54
+ // computes the home this way to report it + build CLAUDE_CONFIG_DIR/skill/plugin paths
55
+ // without side effects; prepare*ProfileHome creates it at the same path at spawn time.
56
+ export function providerHomePathFor(provider: SpawnProvider, config: RunnerSpawnConfig): string | undefined {
57
+ if (!profileRequiresIsolatedHome(provider, config)) return undefined;
58
+ return providerHomePath(provider, config);
59
+ }
60
+
61
+ // Provisioned file-backed assets need a relay-owned materialization root even for
62
+ // host-base profiles, where the provider's real config home stays in use. This path is
63
+ // only passed explicitly through launch args such as Codex skills.config or Claude
64
+ // --plugin-dir; it does not imply CODEX_HOME/CLAUDE_CONFIG_DIR isolation.
65
+ export function providerProvisioningHomePathFor(provider: SpawnProvider, config: RunnerSpawnConfig): string | undefined {
66
+ if (!config.agentProfile) return undefined;
67
+ return providerHomePathFor(provider, config) ?? join(providerHomePath(provider, config), "provisioning");
68
+ }
69
+
70
+ // First-run bootstrap (provider layer)
71
+ // -------------------------------------
72
+ // Isolated profile homes are keyed by instanceId, so every spawn gets a brand
73
+ // new CLAUDE_CONFIG_DIR / CODEX_HOME — to the provider it always looks like the
74
+ // first time it has ever run, even in a repo we've used before. Without
75
+ // bootstrapping, the CLI stalls on first-run gates (theme/onboarding picker,
76
+ // "do you trust this folder?") and the agent never starts; the runner then sees
77
+ // repeated ~2s exits and gives up. bootstrap<Provider>FirstRun makes a fresh
78
+ // home launch-ready non-interactively, guaranteeing three things:
79
+ // 1. host auth is linked in,
80
+ // 2. first-run / onboarding flags are pre-accepted (where the provider has them),
81
+ // 3. the cwd workspace is trusted.
82
+ // Workspace trust is independent of repoInstructions: "ignore" means don't load
83
+ // the repo's CLAUDE.md / AGENTS.md as behavioral instructions, NOT "don't
84
+ // operate here" — an isolated agent must still read/edit/execute in its own cwd.
85
+ // (claude-rig / host-base launches dodge all of this via onboarded host config +
86
+ // --dangerously-skip-permissions; only isolated homes need the bootstrap.)
87
+
88
+ // Provider-specific first-run bootstrap hooks, registered at module load.
89
+ type BootstrapHook = (home: string, config: RunnerSpawnConfig) => void;
90
+ const BOOTSTRAP_HOOKS = new Map<string, BootstrapHook>();
91
+
92
+ // Shared skeleton: gate on isolated-profile, make the instance-keyed home,
93
+ // link auth items from the manifest, and run any registered first-run hook.
94
+ function prepareProviderHome(provider: SpawnProvider, config: RunnerSpawnConfig): ProviderHome | undefined {
95
+ if (!profileRequiresIsolatedHome(provider, config)) return undefined;
96
+ const target = providerHomePath(provider, config);
97
+ mkdirSync(target, { recursive: true });
98
+ const hook = BOOTSTRAP_HOOKS.get(provider);
99
+ if (hook) hook(target, config);
100
+ const manifest = getManifest(provider);
101
+ const authItems = manifest?.home?.authItems ?? [];
102
+ const sourceHome = providerSourceHome(provider);
103
+ const authLinked = linkExistingAuthItems(sourceHome, target, authItems);
104
+ linkHostGlobalInstructionFiles(sourceHome, target, config);
105
+ return { path: target, authLinked };
106
+ }
107
+
108
+ export const prepareCodexProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("codex", config);
109
+ export const prepareClaudeProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("claude", config);
110
+
111
+ function bootstrapCodexFirstRun(codexHome: string, config: RunnerSpawnConfig): void {
112
+ disableCodexAppsConnectors(codexHome);
113
+ trustWorkspaceForCodex(codexHome, config);
114
+ }
115
+
116
+ function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig): void {
117
+ if (profileAllowsRelayFeature(config, "context") && !profileAllowsHostGlobalInstructions(config)) {
118
+ writeClaudeRelayManual(claudeHome);
119
+ }
120
+ seedClaudeConfigIfMissing(claudeHome, config);
121
+ }
122
+
123
+ BOOTSTRAP_HOOKS.set("codex", bootstrapCodexFirstRun);
124
+ BOOTSTRAP_HOOKS.set("claude", bootstrapClaudeFirstRun);
125
+
126
+ export function providerSourceHome(provider: string): string {
127
+ const manifest = getManifest(provider);
128
+ const envVar = manifest?.home?.configDirEnvVar;
129
+ const configDir = manifest?.home?.configDir ?? `.${provider}`;
130
+ return (envVar && process.env[envVar]) || join(homedir(), configDir);
131
+ }
132
+
133
+ function seedClaudeConfigIfMissing(claudeHome: string, config: RunnerSpawnConfig): void {
134
+ const path = join(claudeHome, ".claude.json");
135
+ if (existsSync(path)) return;
136
+ const host = readHostClaudeConfig();
137
+ const seed = applyClaudeConfigPromptGatePreventions({}, host, config);
138
+ writeFileSync(path, JSON.stringify(seed, null, 2), { mode: 0o600 });
139
+ }
140
+
141
+ function readHostClaudeConfig(): Record<string, unknown> | undefined {
142
+ try {
143
+ const hostPath = join(homedir(), ".claude.json");
144
+ if (!existsSync(hostPath)) return undefined;
145
+ return JSON.parse(readFileSync(hostPath, "utf8")) as Record<string, unknown>;
146
+ } catch {
147
+ return undefined;
148
+ }
149
+ }
150
+
151
+ // #662 — the host USER-scope MCP servers: the top-level `mcpServers` of `~/.claude.json`.
152
+ // These are the ambient host MCP a host-base Claude auto-loads on EVERY launch when NOT
153
+ // strict (user scope is global across all projects — callmux/qmd/gh on this host). Since
154
+ // the #662 wedge fix makes every managed launch `--strict-mcp-config` (so Claude ignores
155
+ // all on-disk config and never renders the project-`.mcp.json` enable modal), an
156
+ // `mcp.mode:"host"` profile must re-inject these explicitly into `--mcp-config` to keep
157
+ // honoring host mode. Deliberately NOT the project-cwd `.mcp.json` (project scope — the
158
+ // modal's source, which must stay excluded) nor `projects[cwd].mcpServers` (local scope).
159
+ export function hostUserScopeMcpServers(): Record<string, ProvisioningMcpServer> {
160
+ const servers = readHostClaudeConfig()?.mcpServers;
161
+ if (!servers || typeof servers !== "object") return {};
162
+ return servers as Record<string, ProvisioningMcpServer>;
163
+ }
164
+
165
+ function providerHomePath(provider: SpawnProvider, config: RunnerSpawnConfig): string {
166
+ const root = providerHomeRootFromEnv();
167
+ const profileName = sanitizePathPart(config.agentProfile?.name || config.profile || "profile");
168
+ const instance = sanitizePathPart(config.instanceId || config.runnerId);
169
+ return join(root, provider, profileName, instance);
170
+ }
171
+
172
+ function sanitizePathPart(value: string): string {
173
+ return sanitizeFsName(value, { replacement: "-", collapse: false, maxLen: 120, fallback: "profile" });
174
+ }
175
+
176
+ function linkExistingAuthItems(sourceHome: string, targetHome: string, items: string[]): string[] {
177
+ const linked: string[] = [];
178
+ for (const item of items) {
179
+ if (linkExistingPath(join(sourceHome, item), join(targetHome, item))) linked.push(item);
180
+ }
181
+ return linked;
182
+ }
183
+
184
+ function linkExistingPath(source: string, target: string): boolean {
185
+ if (!existsSync(source) || existsSync(target)) return false;
186
+ symlinkSync(source, target);
187
+ return true;
188
+ }
189
+
190
+ function linkHostGlobalInstructionFiles(sourceHome: string, targetHome: string, config: RunnerSpawnConfig): void {
191
+ if (!profileAllowsHostGlobalInstructions(config)) return;
192
+ for (const file of config.providerConfig.globalInstructionFiles ?? []) {
193
+ linkExistingPath(join(sourceHome, file), join(targetHome, file));
194
+ }
195
+ }
196
+
197
+ function trustWorkspaceForCodex(codexHome: string, config: RunnerSpawnConfig): void {
198
+ if (!config.cwd) return;
199
+ const path = join(codexHome, "config.toml");
200
+ const project = tomlBasicString(resolve(config.cwd));
201
+ const section = `[projects.${project}]`;
202
+ const entry = `${section}\ntrust_level = "trusted"\n`;
203
+ const current = existsSync(path) ? readFileSync(path, "utf8") : "";
204
+ if (current.includes(section)) return;
205
+ const prefix = current.trimEnd();
206
+ writeFileSync(path, `${prefix ? `${prefix}\n\n` : ""}${entry}`, { mode: 0o600 });
207
+ }
208
+
209
+ function disableCodexAppsConnectors(codexHome: string): void {
210
+ const path = join(codexHome, "config.toml");
211
+ const current = existsSync(path) ? readFileSync(path, "utf8") : "";
212
+ const next = withTomlFeatureFlag(current, "apps", false);
213
+ if (next === current) return;
214
+ writeFileSync(path, next, { mode: 0o600 });
215
+ }
216
+
217
+ function withTomlFeatureFlag(input: string, key: string, value: boolean): string {
218
+ const lines = input.length ? input.split("\n") : [];
219
+ const headerIndex = lines.findIndex((line) => line.trim() === "[features]");
220
+ const entry = `${key} = ${value ? "true" : "false"}`;
221
+ if (headerIndex === -1) {
222
+ const prefix = input.trimEnd();
223
+ return `${prefix ? `${prefix}\n\n` : ""}[features]\n${entry}\n`;
224
+ }
225
+
226
+ let sectionEnd = lines.length;
227
+ for (let i = headerIndex + 1; i < lines.length; i += 1) {
228
+ const line = lines[i] ?? "";
229
+ if (/^\s*\[[^\]]+\]\s*(?:#.*)?$/.test(line)) {
230
+ sectionEnd = i;
231
+ break;
232
+ }
233
+ }
234
+
235
+ const flagPattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*=`);
236
+ for (let i = headerIndex + 1; i < sectionEnd; i += 1) {
237
+ const line = lines[i] ?? "";
238
+ if (flagPattern.test(line)) {
239
+ if (lines[i] === entry) return input;
240
+ lines[i] = entry;
241
+ return lines.join("\n");
242
+ }
243
+ }
244
+
245
+ lines.splice(headerIndex + 1, 0, entry);
246
+ return lines.join("\n");
247
+ }
248
+
249
+ function escapeRegExp(value: string): string {
250
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
251
+ }
252
+
253
+ function writeClaudeRelayManual(claudeHome: string): void {
254
+ const path = join(claudeHome, "CLAUDE.md");
255
+ writeFileSync(path, claudeRelayManual(), { mode: 0o600 });
256
+ }
257
+
258
+ function tomlBasicString(value: string): string {
259
+ return JSON.stringify(value);
260
+ }
@@ -0,0 +1,305 @@
1
+ import type { AgentProfile, AgentProfileProjectionEntry, AgentProfileProjectionReport, SpawnProvider } from "agent-relay-sdk";
2
+ import { getManifest } from "agent-relay-providers";
3
+ import type { AssembledLaunch } from "./launch-assembly";
4
+
5
+ // #557 — the projection report DESCRIBES an AssembledLaunch, the same object the adapters
6
+ // turn into launch args. It is no longer a separate re-derivation from the profile, so the
7
+ // report and the real launch share one source of truth and cannot drift: change the
8
+ // assembler and both the args AND this report change together. Every entry below reads a
9
+ // field the assembler actually computed (relay gates, provisioned sets, instruction
10
+ // disposition, vanilla mechanism, config home) — only the not-yet-enforced category modes
11
+ // (mcp/hooks) and the purely-declarative env/permissions read straight off the profile.
12
+
13
+ interface AgentProfileProjectionInput {
14
+ assembled: AssembledLaunch;
15
+ generatedAt?: number;
16
+ }
17
+
18
+ export function agentProfileProjectionReport(input: AgentProfileProjectionInput): AgentProfileProjectionReport {
19
+ const { assembled } = input;
20
+ const { provider } = assembled;
21
+ const profile = assembled.profile;
22
+ const generatedAt = input.generatedAt ?? Date.now();
23
+ const entries: AgentProfileProjectionEntry[] = [];
24
+ const add = (entry: AgentProfileProjectionEntry) => entries.push(entry);
25
+
26
+ add(relayContextEntry(assembled));
27
+ add(relaySkillsEntry(assembled));
28
+ add(relayPluginsEntry(assembled));
29
+ add(relayStatusLineEntry(assembled));
30
+ add(repoInstructionsEntry(assembled));
31
+ add(globalInstructionsEntry(assembled));
32
+ add(mcpEntry(assembled));
33
+ add(hooksEntry(assembled));
34
+ add(provisioningSkillsEntry(assembled));
35
+ add(provisioningPluginsEntry(assembled));
36
+ add(hostSkillsEntry(assembled));
37
+ add(hostPluginsEntry(assembled));
38
+ add(provisioningMcpEntry(assembled));
39
+ for (const entry of provisioningSkippedEntries(assembled)) add(entry);
40
+ add(projectMcpEntry(assembled));
41
+ add(projectSkillsEntry(assembled));
42
+ add(projectSettingsEntry(assembled));
43
+ add(sharedMcpEntry(assembled));
44
+ if (profile) add(filesystemEntry(profile));
45
+ add(configHomeEntry(assembled));
46
+ if (assembled.vanilla) add(vanillaLaunchEntry(provider));
47
+ if (profile) add(envEntry(profile));
48
+
49
+ const warnings = entries
50
+ .filter((entry) => entry.result === "partial" || entry.result === "unsupported")
51
+ .map((entry) => `${entry.capability}: ${entry.detail}`);
52
+ const unsupported = entries
53
+ .filter((entry) => entry.result === "unsupported")
54
+ .map((entry) => entry.capability);
55
+
56
+ return {
57
+ profileName: profile?.name ?? "default",
58
+ provider,
59
+ base: assembled.base,
60
+ generatedAt,
61
+ entries,
62
+ warnings,
63
+ unsupported,
64
+ };
65
+ }
66
+
67
+ function relayContextEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
68
+ return a.relayContext
69
+ ? applied("relay.context", "enabled", "Relay context is injected before Relay-delivered provider turns.")
70
+ : applied("relay.context", "disabled", "Relay context injection is disabled by the profile.");
71
+ }
72
+
73
+ function relaySkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
74
+ const manifest = getManifest(a.provider);
75
+ const label = manifest?.label ?? a.provider;
76
+ const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
77
+ if (!hasPluginSurface) {
78
+ return a.relaySkills
79
+ ? applied("relay.skills", "enabled", `Bundled Agent Relay ${label} skills are passed through the provider's skill delivery mechanism.`)
80
+ : applied("relay.skills", "disabled", `Bundled Agent Relay ${label} skills are omitted.`);
81
+ }
82
+ return a.relaySkills
83
+ ? applied("relay.skills", "enabled", `Agent Relay ${label} skills are available through the bundled Relay plugin.`)
84
+ : applied("relay.skills", "disabled", a.relayPlugin
85
+ ? `${label} does not expose an independent Relay skill switch; disabling skills requires disabling the Relay plugin.`
86
+ : `Bundled Agent Relay ${label} plugin dirs are omitted, so Relay plugin-provided skills are unavailable.`);
87
+ }
88
+
89
+ function relayPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
90
+ const manifest = getManifest(a.provider);
91
+ const label = manifest?.label ?? a.provider;
92
+ const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
93
+ if (hasPluginSurface) {
94
+ return a.relayPlugin
95
+ ? applied("relay.plugins", "enabled", `Bundled Agent Relay ${label} plugin dir is passed to ${label}.`)
96
+ : applied("relay.plugins", "disabled", `Bundled Agent Relay ${label} plugin dirs are omitted.`);
97
+ }
98
+ return notApplicable("relay.plugins", a.relaySkills ? "enabled" : "disabled", `${label} Relay integration uses skills/app-server delivery, not a Relay plugin dir.`);
99
+ }
100
+
101
+ function relayStatusLineEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
102
+ const manifest = getManifest(a.provider);
103
+ const label = manifest?.label ?? a.provider;
104
+ if (a.statusLine) {
105
+ return applied("relay.statusLine", "enabled", "Agent Relay status-line context probe settings are injected when caller settings allow it.");
106
+ }
107
+ const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
108
+ if (hasPluginSurface) {
109
+ return applied("relay.statusLine", "disabled", "Agent Relay status-line settings are omitted.");
110
+ }
111
+ return notApplicable("relay.statusLine", "disabled", `${label} reports context through provider-native events instead of a Relay status-line setting.`);
112
+ }
113
+
114
+ function repoInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
115
+ const disposition = a.instructions.repo;
116
+ if (disposition === "allow") return applied("instructions.repo", "allow", "Repo-local provider instructions are left available.");
117
+ if (disposition === "suppressed") {
118
+ const label = getManifest(a.provider)?.label ?? a.provider;
119
+ return applied("instructions.repo", "ignore", `Repo project instructions are suppressed: ${label} launches with the vanilla base mechanism (provider-specific suppression of repo instructions).`);
120
+ }
121
+ return unsupported("instructions.repo", "ignore", "Repo instruction suppression requires the vanilla base; isolated/minimal do not enforce it at launch yet.");
122
+ }
123
+
124
+ // #552/#554/#555/#556 — the vanilla base's launch-time host suppression, naming the exact
125
+ // provider mechanism the assembler emitted (Claude --setting-sources "user"; Codex
126
+ // project_doc_max_bytes=0). OS-level admin/managed policy is beyond any client flag and is
127
+ // called out so it isn't mistaken for a leak.
128
+ const VANILLA_MECHANISM: Record<string, { requested: string; detail: string }> = {
129
+ claude: {
130
+ requested: "--setting-sources user",
131
+ detail: "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.",
132
+ },
133
+ codex: {
134
+ requested: "project_doc_max_bytes=0",
135
+ detail: "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).",
136
+ },
137
+ };
138
+
139
+ function vanillaLaunchEntry(provider: SpawnProvider): AgentProfileProjectionEntry {
140
+ const label = getManifest(provider)?.label ?? provider;
141
+ const mechanism = VANILLA_MECHANISM[provider];
142
+ return applied(
143
+ "provider.vanilla",
144
+ mechanism?.requested ?? "active",
145
+ mechanism?.detail ?? `${label} launches with the vanilla base: an isolated provider config home and provider-specific repo instruction suppression. Auth, model, and built-in tools work normally.`,
146
+ );
147
+ }
148
+
149
+ function provisioningSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
150
+ const count = a.skills.length;
151
+ if (!count) return notApplicable("provisioning.skills", "none", "No relay-provisioned skills materialized for this launch.");
152
+ const names = ` (${a.skills.map((s) => s.name).join(", ")})`;
153
+ const label = getManifest(a.provider)?.label ?? a.provider;
154
+ return applied("provisioning.skills", `${count}`, `Relay-provisioned skills materialized for ${label} from the isolated provider config home${names}.`);
155
+ }
156
+
157
+ function provisioningPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
158
+ const manifest = getManifest(a.provider);
159
+ const label = manifest?.label ?? a.provider;
160
+ const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
161
+ if (!hasPluginSurface) {
162
+ const declared = (a.profile?.plugins ?? []).filter((p) => p.enabled).length;
163
+ return notApplicable("provisioning.plugins", `${declared}`, `${label} has no plugin surface; provisioned plugins apply to plugin-capable providers only.`);
164
+ }
165
+ const count = a.plugins.length;
166
+ if (!count) return notApplicable("provisioning.plugins", "none", "No relay-provisioned plugins materialized for this launch.");
167
+ const names = ` (${a.plugins.map((p) => p.name).join(", ")})`;
168
+ return applied("provisioning.plugins", `${count}`, `Relay-provisioned plugins passed to ${label} via the plugin dir mechanism${names}.`);
169
+ }
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
+
194
+ function provisioningMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
195
+ const names = a.mcp.servers;
196
+ if (!names.length) return notApplicable("provisioning.mcp", "none", "No relay-provisioned MCP servers materialized for this launch.");
197
+ const count = `${names.length} server${names.length === 1 ? "" : "s"}`;
198
+ const providerLabel = getManifest(a.provider)?.label ?? a.provider;
199
+ return applied("provisioning.mcp", count, `Relay-provisioned MCP servers injected for ${providerLabel} (${names.join(", ")})${a.mcp.strict ? "; strict mode guarantees only declared servers load" : ""}.`);
200
+ }
201
+
202
+ function provisioningSkippedEntries(a: AssembledLaunch): AgentProfileProjectionEntry[] {
203
+ return (a.profile?.provisioning?.skipped ?? []).map((item) => partial(
204
+ `provisioning.${item.kind}.${item.ref}`,
205
+ item.reason,
206
+ item.detail,
207
+ ));
208
+ }
209
+
210
+ // #667 — project `.mcp.json` projection provenance. `not-applicable` when the profile/spawn
211
+ // didn't opt in (the default); `applied` lists the spawn-cwd servers relay injected through
212
+ // the controlled --mcp-config / -c path (sourced from the project, not ambient host MCP).
213
+ function projectMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
214
+ const optedIn = Boolean(a.profile?.projectMcp);
215
+ const names = a.mcp.projectServers;
216
+ if (!optedIn) return notApplicable("projectMcp", "off", "Project .mcp.json projection is off (opt in via the projectMcp profile/spawn option).");
217
+ if (!names.length) return applied("projectMcp", "none", "Project .mcp.json projection is on, but the spawn cwd declared no injectable MCP servers.");
218
+ const count = `${names.length} server${names.length === 1 ? "" : "s"}`;
219
+ return applied("projectMcp", count, `Project .mcp.json servers injected for this launch (${names.join(", ")})${a.mcp.strict ? "; survives strict MCP mode" : ""}.`);
220
+ }
221
+
222
+ function projectSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
223
+ const optedIn = Boolean(a.profile?.projectSkills);
224
+ if (!optedIn) return notApplicable("projectSkills", "off", "Project .claude/skills projection is off (opt in via the projectSkills profile/spawn option).");
225
+ if (!getManifest(a.provider)?.provisioning?.bundledPlugin) return notApplicable("projectSkills", "unsupported", "Project skills projection applies to plugin-capable providers only.");
226
+ if (!a.projectSkills.length) return applied("projectSkills", "none", "Project .claude/skills projection is on, but the spawn cwd declared no valid skill dirs.");
227
+ const names = a.projectSkills.map((s) => s.name).join(", ");
228
+ return applied("projectSkills", `${a.projectSkills.length}`, `Project .claude skills materialized into Relay's managed Claude config home (${names}); host/global skills are excluded.`);
229
+ }
230
+
231
+ function projectSettingsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
232
+ const optedIn = Boolean(a.profile?.projectSettings);
233
+ if (!optedIn) return notApplicable("projectSettings", "off", "Project .claude/settings.json projection is off (opt in via the projectSettings profile/spawn option).");
234
+ if (!getManifest(a.provider)?.provisioning?.bundledPlugin) return notApplicable("projectSettings", "unsupported", "Project settings projection applies to plugin-capable providers only.");
235
+ const ignored = a.projectSettings.ignoredKeys.length ? `; ignored security keys: ${a.projectSettings.ignoredKeys.join(", ")}` : "";
236
+ 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}.`);
237
+ }
238
+
239
+ // #672/#689 — shared host-listener MCP provenance. `applied` reports the per-agent `callmux
240
+ // bridge` route. `not-applicable` means the resolved profile kept shared MCP off (for example a
241
+ // zero-host profile, explicit false, or global kill-switch normalization before launch).
242
+ function sharedMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
243
+ const optedIn = Boolean(a.profile?.sharedMcp);
244
+ const listener = a.mcp.sharedListener;
245
+ if (!optedIn || !listener) return notApplicable("sharedMcp", "off", "Shared host-listener MCP is off for this resolved profile; non-identity MCP is injected per-agent.");
246
+ return applied("sharedMcp", "on", `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"'; session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent.`);
247
+ }
248
+
249
+ function globalInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
250
+ const disposition = a.instructions.global;
251
+ if (disposition === "allow") return applied("instructions.global", "allow", "Host/global provider instructions are left available.");
252
+ if (disposition === "isolated") return applied("instructions.global", "ignore", `${a.provider} uses an isolated provider config home, so host/global provider instructions are not loaded.`);
253
+ return unsupported("instructions.global", "ignore", `${a.provider} global instruction isolation requires an isolated provider config home.`);
254
+ }
255
+
256
+ function mcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
257
+ const mode = a.profile?.mcp.mode ?? "host";
258
+ if (mode === "host") return applied("mcp", "host", "Host provider MCP configuration is left available.");
259
+ if (mode === "none" && a.base !== "host") {
260
+ return applied("mcp", "none", `${a.provider} uses an isolated provider config home without host MCP configuration.`);
261
+ }
262
+ return unsupported("mcp", mode, `${a.provider} MCP category projection is not enforced yet.`);
263
+ }
264
+
265
+ function hooksEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
266
+ const mode = a.profile?.hooks.mode ?? "host";
267
+ if (mode === "host") return applied("hooks", "host", "Host provider hooks are left available.");
268
+ if (mode === "none" && a.base !== "host") {
269
+ return applied("hooks", "none", `${a.provider} uses an isolated provider config home without host hook configuration.`);
270
+ }
271
+ return unsupported("hooks", mode, `${a.provider} hook category projection is not enforced yet.`);
272
+ }
273
+
274
+ function filesystemEntry(profile: AgentProfile): AgentProfileProjectionEntry {
275
+ if (profile.permissions.filesystem === "host") return applied("permissions.filesystem", "host", "No profile-level filesystem boundary requested.");
276
+ return partial("permissions.filesystem", profile.permissions.filesystem, "Relay constrains spawn cwd through the orchestrator; provider-level filesystem sandbox projection is not complete yet.");
277
+ }
278
+
279
+ function configHomeEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
280
+ if (!a.configHome) return applied("provider.configHome", "host", "Provider uses the host's normal configuration home.");
281
+ return applied("provider.configHome", a.base, `${a.provider} is launched with an Agent Relay managed provider config home and linked host auth where available.`);
282
+ }
283
+
284
+ function envEntry(profile: AgentProfile): AgentProfileProjectionEntry {
285
+ const count = Object.keys(profile.env).length;
286
+ return count
287
+ ? applied("env", `${count} vars`, "Profile environment variables are passed through the orchestrator runner environment.")
288
+ : applied("env", "none", "No profile environment variables requested.");
289
+ }
290
+
291
+ function applied(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
292
+ return { capability, requested, result: "applied", detail };
293
+ }
294
+
295
+ function partial(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
296
+ return { capability, requested, result: "partial", detail };
297
+ }
298
+
299
+ function unsupported(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
300
+ return { capability, requested, result: "unsupported", detail };
301
+ }
302
+
303
+ function notApplicable(capability: string, requested: string, detail: string): AgentProfileProjectionEntry {
304
+ return { capability, requested, result: "not-applicable", detail };
305
+ }
@@ -0,0 +1,20 @@
1
+ import type { SpawnProvider } from "agent-relay-sdk";
2
+ import type { ProviderAdapter } from "./adapter";
3
+ import { ClaudeAdapter } from "./adapters/claude";
4
+ import { CodexAdapter } from "./adapters/codex";
5
+
6
+ interface RunnerProviderRegistration {
7
+ provider: SpawnProvider;
8
+ createAdapter: () => ProviderAdapter;
9
+ }
10
+
11
+ const RUNNER_PROVIDERS: Record<string, RunnerProviderRegistration> = {
12
+ claude: { provider: "claude", createAdapter: () => new ClaudeAdapter() },
13
+ codex: { provider: "codex", createAdapter: () => new CodexAdapter() },
14
+ };
15
+
16
+ export function createProviderAdapter(provider: SpawnProvider): ProviderAdapter {
17
+ const registration = RUNNER_PROVIDERS[provider];
18
+ if (!registration) throw new Error(`unsupported provider adapter: ${provider}`);
19
+ return registration.createAdapter();
20
+ }