agent-relay-server 0.122.0 → 0.123.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/docs/openapi.json +1 -1
- package/package.json +7 -8
- package/public/assets/{MessageBubble-DRdUD-kW.js → MessageBubble-BTeFExjj.js} +2 -2
- package/public/assets/{MessageBubble-DRdUD-kW.js.map → MessageBubble-BTeFExjj.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-siKMPCln.js → PlanGraphPanel-jU1DXg7w.js} +2 -2
- package/public/assets/{PlanGraphPanel-siKMPCln.js.map → PlanGraphPanel-jU1DXg7w.js.map} +1 -1
- package/public/assets/{activity-D3MNcSaV.js → activity-DUig2iYW.js} +2 -2
- package/public/assets/{activity-D3MNcSaV.js.map → activity-DUig2iYW.js.map} +1 -1
- package/public/assets/{agent-profiles-CKzttq6G.js → agent-profiles-D-oFmb6S.js} +2 -2
- package/public/assets/{agent-profiles-CKzttq6G.js.map → agent-profiles-D-oFmb6S.js.map} +1 -1
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js → agent-type-icon-Dan2l3Tm.js} +2 -2
- package/public/assets/{agent-type-icon-0ZJRP1Vy.js.map → agent-type-icon-Dan2l3Tm.js.map} +1 -1
- package/public/assets/{agents-CuVj_rfG.js → agents-BkC6mcTh.js} +2 -2
- package/public/assets/{agents-CuVj_rfG.js.map → agents-BkC6mcTh.js.map} +1 -1
- package/public/assets/{analytics-iob6C27d.js → analytics-C3Dlmqe9.js} +2 -2
- package/public/assets/{analytics-iob6C27d.js.map → analytics-C3Dlmqe9.js.map} +1 -1
- package/public/assets/{automation-B2ixxs4q.js → automation-AeMZ70WQ.js} +2 -2
- package/public/assets/{automation-B2ixxs4q.js.map → automation-AeMZ70WQ.js.map} +1 -1
- package/public/assets/{branch-state-badge-CafBJWKo.js → branch-state-badge-CNW10MTC.js} +2 -2
- package/public/assets/{branch-state-badge-CafBJWKo.js.map → branch-state-badge-CNW10MTC.js.map} +1 -1
- package/public/assets/{channels-C-WsbL2H.js → channels-Bc_mEd56.js} +2 -2
- package/public/assets/{channels-C-WsbL2H.js.map → channels-Bc_mEd56.js.map} +1 -1
- package/public/assets/{chat-DeoVTVob.js → chat-BCNCuTgw.js} +2 -2
- package/public/assets/{chat-DeoVTVob.js.map → chat-BCNCuTgw.js.map} +1 -1
- package/public/assets/{connectors-C2Ac03LJ.js → connectors-h0d5Mc6e.js} +2 -2
- package/public/assets/{connectors-C2Ac03LJ.js.map → connectors-h0d5Mc6e.js.map} +1 -1
- package/public/assets/{display-CJzSoTMq.js → display-GFbGUG0O.js} +2 -2
- package/public/assets/{display-CJzSoTMq.js.map → display-GFbGUG0O.js.map} +1 -1
- package/public/assets/{formatted-body-35XBOY5L.js → formatted-body-CVr3Iev9.js} +3 -3
- package/public/assets/{formatted-body-35XBOY5L.js.map → formatted-body-CVr3Iev9.js.map} +1 -1
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js → formatted-body-impl-Cq5SRGpm.js} +2 -2
- package/public/assets/{formatted-body-impl-RmDxI5Ux.js.map → formatted-body-impl-Cq5SRGpm.js.map} +1 -1
- package/public/assets/{index-D0xMQQb_.js → index--9UMzCFf.js} +6 -6
- package/public/assets/{index-D0xMQQb_.js.map → index--9UMzCFf.js.map} +1 -1
- package/public/assets/{insights-B643SFvm.js → insights-C8lIBXg3.js} +2 -2
- package/public/assets/{insights-B643SFvm.js.map → insights-C8lIBXg3.js.map} +1 -1
- package/public/assets/{integrations-hFIHeCF4.js → integrations-7OFsJtY7.js} +2 -2
- package/public/assets/{integrations-hFIHeCF4.js.map → integrations-7OFsJtY7.js.map} +1 -1
- package/public/assets/{maintenance-CURvdp_Z.js → maintenance-HDpg20OY.js} +2 -2
- package/public/assets/{maintenance-CURvdp_Z.js.map → maintenance-HDpg20OY.js.map} +1 -1
- package/public/assets/{managed-agents-DrZtdlSn.js → managed-agents-muCIMxng.js} +2 -2
- package/public/assets/{managed-agents-DrZtdlSn.js.map → managed-agents-muCIMxng.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CVDO--ek.js → markdown-preview-impl-Cx7pCfA6.js} +2 -2
- package/public/assets/{markdown-preview-impl-CVDO--ek.js.map → markdown-preview-impl-Cx7pCfA6.js.map} +1 -1
- package/public/assets/{memory-Bm7dt_Qf.js → memory-CTO1FfJ-.js} +2 -2
- package/public/assets/{memory-Bm7dt_Qf.js.map → memory-CTO1FfJ-.js.map} +1 -1
- package/public/assets/{messages-DTq4i2KZ.js → messages-RWSXG2UM.js} +2 -2
- package/public/assets/{messages-DTq4i2KZ.js.map → messages-RWSXG2UM.js.map} +1 -1
- package/public/assets/{orchestrators-Dra318Ap.js → orchestrators-5aiYw3yx.js} +2 -2
- package/public/assets/{orchestrators-Dra318Ap.js.map → orchestrators-5aiYw3yx.js.map} +1 -1
- package/public/assets/{overview-yr1Vmk64.js → overview-Du6XLt5l.js} +2 -2
- package/public/assets/{overview-yr1Vmk64.js.map → overview-Du6XLt5l.js.map} +1 -1
- package/public/assets/{pairs-mnlKDBGp.js → pairs-BDRnlEta.js} +2 -2
- package/public/assets/{pairs-mnlKDBGp.js.map → pairs-BDRnlEta.js.map} +1 -1
- package/public/assets/{projects-Dzdf-gkK.js → projects-DwsWE9es.js} +2 -2
- package/public/assets/{projects-Dzdf-gkK.js.map → projects-DwsWE9es.js.map} +1 -1
- package/public/assets/{prompt-settings-ByL8SNCi.js → prompt-settings-CaJX_LbA.js} +2 -2
- package/public/assets/{prompt-settings-ByL8SNCi.js.map → prompt-settings-CaJX_LbA.js.map} +1 -1
- package/public/assets/{registry-CXWyOtpr.js → registry-C-uIL0Nl.js} +2 -2
- package/public/assets/{registry-CXWyOtpr.js.map → registry-C-uIL0Nl.js.map} +1 -1
- package/public/assets/{security-0SBsc_4W.js → security-BpVrdYhJ.js} +2 -2
- package/public/assets/{security-0SBsc_4W.js.map → security-BpVrdYhJ.js.map} +1 -1
- package/public/assets/{select-Bw5z2KJ_.js → select-DallbU93.js} +2 -2
- package/public/assets/{select-Bw5z2KJ_.js.map → select-DallbU93.js.map} +1 -1
- package/public/assets/{settings-78Cyq_tA.js → settings-BUQirdyI.js} +2 -2
- package/public/assets/{settings-78Cyq_tA.js.map → settings-BUQirdyI.js.map} +1 -1
- package/public/assets/{store-BPrVuKiv.js → store-DMZSTozC.js} +2 -2
- package/public/assets/{store-BPrVuKiv.js.map → store-DMZSTozC.js.map} +1 -1
- package/public/assets/{tasks-Cg2MMeri.js → tasks-k8mQDfKy.js} +2 -2
- package/public/assets/{tasks-Cg2MMeri.js.map → tasks-k8mQDfKy.js.map} +1 -1
- package/public/assets/{teams-DWv6UwFV.js → teams-DI-Lhhbl.js} +2 -2
- package/public/assets/{teams-DWv6UwFV.js.map → teams-DI-Lhhbl.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js → terminal-viewer-impl-BL853Fpi.js} +2 -2
- package/public/assets/{terminal-viewer-impl-DBLDjbIg.js.map → terminal-viewer-impl-BL853Fpi.js.map} +1 -1
- package/public/assets/types-CRptbSYh.js +2 -0
- package/public/assets/types-CRptbSYh.js.map +1 -0
- package/public/assets/{user-avatar-BsOLyVJj.js → user-avatar-D9xyGi3u.js} +2 -2
- package/public/assets/{user-avatar-BsOLyVJj.js.map → user-avatar-D9xyGi3u.js.map} +1 -1
- package/public/assets/{work-queue-DmwvnF6F.js → work-queue-DrLIdvxB.js} +2 -2
- package/public/assets/{work-queue-DmwvnF6F.js.map → work-queue-DrLIdvxB.js.map} +1 -1
- package/public/assets/{workspaces-dIBivJN8.js → workspaces-CnNL6T8Q.js} +2 -2
- package/public/assets/{workspaces-dIBivJN8.js.map → workspaces-CnNL6T8Q.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/adapters/claude-delivery.ts +127 -0
- package/runner/src/adapters/claude-quota-harvest.ts +92 -0
- package/runner/src/adapters/claude-session-probe.ts +225 -0
- package/runner/src/adapters/claude-status-detectors.ts +147 -0
- package/runner/src/adapters/claude-transcript-tail.ts +128 -0
- package/runner/src/adapters/claude-transcript.ts +414 -0
- package/runner/src/adapters/claude.ts +675 -0
- package/runner/src/adapters/codex-agent-message-capture.ts +139 -0
- package/runner/src/adapters/codex-client.ts +279 -0
- package/runner/src/adapters/codex.ts +1345 -0
- package/runner/src/attachment-cache.ts +189 -0
- package/runner/src/busy-reconciler.ts +102 -0
- package/runner/src/claim-tracker.ts +140 -0
- package/runner/src/claude-prompt-gates.ts +268 -0
- package/runner/src/codex-version.ts +63 -0
- package/runner/src/config.ts +7 -0
- package/runner/src/context-probe-state.ts +6 -0
- package/runner/src/continuation-archive.ts +179 -0
- package/runner/src/control-server.ts +639 -0
- package/runner/src/index.ts +396 -0
- package/runner/src/launch-assembly.ts +674 -0
- package/runner/src/liveness.ts +50 -0
- package/runner/src/logger.ts +99 -0
- package/runner/src/mcp-outbox.ts +120 -0
- package/runner/src/message-body-config/config.ts +3 -0
- package/runner/src/message-body-config.ts +1 -0
- package/runner/src/native-self-resume.ts +202 -0
- package/runner/src/outbox.ts +342 -0
- package/runner/src/process-meta.ts +9 -0
- package/runner/src/profile-home.ts +260 -0
- package/runner/src/profile-projection.ts +324 -0
- package/runner/src/providers.ts +20 -0
- package/runner/src/provisioning.ts +368 -0
- package/runner/src/quota.ts +40 -0
- package/runner/src/rate-limit.ts +189 -0
- package/runner/src/relay-injection-events.ts +105 -0
- package/runner/src/relay-instructions.ts +60 -0
- package/runner/src/relay-mcp-proxy.ts +383 -0
- package/runner/src/relay-mcp.ts +156 -0
- package/runner/src/reply-obligation-cache.ts +136 -0
- package/runner/src/response-capture-report.ts +63 -0
- package/runner/src/runner-core.ts +2409 -0
- package/runner/src/runner-helpers.ts +435 -0
- package/runner/src/runner-insights.ts +105 -0
- package/runner/src/runner.ts +14 -0
- package/runner/src/session-destroy.ts +22 -0
- package/runner/src/session-insights.ts +118 -0
- package/runner/src/session-scratch.ts +271 -0
- package/runner/src/template-resolver.ts +29 -0
- package/runner/src/version.ts +44 -0
- package/src/config-store.ts +2 -0
- package/src/db/provisioning-registry.ts +40 -9
- package/src/db/provisioning-resolve.ts +66 -5
- package/src/gate-resolver.ts +7 -12
- package/src/routes/provisioning-registry.ts +1 -1
- package/src/services/spawn-agent.ts +1 -0
- package/public/assets/types-BeJUSfDj.js +0 -2
- package/public/assets/types-BeJUSfDj.js.map +0 -1
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import type { AgentProfile, AgentProfileBase, ProvisioningHookSet, ProvisioningMcpServer, SpawnProvider } from "agent-relay-sdk";
|
|
4
|
+
import { shellQuote } from "agent-relay-sdk/shell-utils";
|
|
5
|
+
import {
|
|
6
|
+
profileAllowsRelayFeature,
|
|
7
|
+
profileIsVanillaBase,
|
|
8
|
+
type ProviderConfig,
|
|
9
|
+
type RunnerSpawnConfig,
|
|
10
|
+
} from "./adapter";
|
|
11
|
+
import {
|
|
12
|
+
projectConfigProjectionOn,
|
|
13
|
+
hostUserScopeMcpServers,
|
|
14
|
+
profileAllowsHostAssets,
|
|
15
|
+
profileUsesProviderHostGlobals,
|
|
16
|
+
providerHomePathFor,
|
|
17
|
+
providerProvisioningHomePathFor,
|
|
18
|
+
} from "./profile-home";
|
|
19
|
+
import { getManifest } from "agent-relay-providers";
|
|
20
|
+
import { codexHooksSupported } from "./codex-version";
|
|
21
|
+
import { claudeLaunchPromptGateSettings } from "./claude-prompt-gates";
|
|
22
|
+
import {
|
|
23
|
+
materializeResolvedAssets,
|
|
24
|
+
renderProvisionedHookSet,
|
|
25
|
+
resolveClaudeProjectSkills,
|
|
26
|
+
resolveClaudeHostSkills,
|
|
27
|
+
resolveProjectClaudeSettings,
|
|
28
|
+
resolveClaudeProvisionedPlugins,
|
|
29
|
+
resolveClaudeProvisionedSkills,
|
|
30
|
+
resolveCodexProvisionedSkills,
|
|
31
|
+
resolveProvisionedHooks,
|
|
32
|
+
resolveProjectMcpServers,
|
|
33
|
+
profileProvisioning,
|
|
34
|
+
writeCodexHooksJson,
|
|
35
|
+
} from "./provisioning";
|
|
36
|
+
import {
|
|
37
|
+
claudeMcpLaunchArgs,
|
|
38
|
+
codexProvisionedMcpArgs,
|
|
39
|
+
relayMcpCodexConfigArgs,
|
|
40
|
+
resolveSharedMcpUrl,
|
|
41
|
+
sharedMcpBridgeServer,
|
|
42
|
+
tomlString,
|
|
43
|
+
RELAY_MCP_SERVER_NAME,
|
|
44
|
+
SHARED_MCP_SERVER_NAME,
|
|
45
|
+
} from "./relay-mcp";
|
|
46
|
+
|
|
47
|
+
// #557 — THE single launch-assembly contract. Given a resolved AgentProfile (+ its
|
|
48
|
+
// spawn-time ResolvedProvisioning), `assembleLaunch` produces ONE provider-neutral
|
|
49
|
+
// description of the launch: the relay-provisioned skills/plugins/MCP, the bundled relay
|
|
50
|
+
// surface, the instruction-source disposition, and `base:"vanilla"` as the zero-host
|
|
51
|
+
// floor. Both adapters' buildSpawnArgs splice `assembled.args`/`assembled.env`, and
|
|
52
|
+
// profile-projection describes `assembled` directly — so projection and reality come
|
|
53
|
+
// from one source and cannot drift.
|
|
54
|
+
//
|
|
55
|
+
// `assembleLaunch` is PURE (it reads the filesystem to decide which declared dirs
|
|
56
|
+
// resolve, but writes nothing). `materializeLaunchAssembly` performs the disk writes the
|
|
57
|
+
// description implies, driven by the SAME resolvers — so what's written equals what's
|
|
58
|
+
// reported equals what's launched.
|
|
59
|
+
|
|
60
|
+
export interface AssembledAsset {
|
|
61
|
+
name: string;
|
|
62
|
+
/** Materialized/in-place dir feeding the launch (e.g. a --plugin-dir or skills.config path). */
|
|
63
|
+
dir: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type AssembledInstructionDisposition = "allow" | "suppressed" | "isolated" | "unsupported";
|
|
67
|
+
|
|
68
|
+
export interface AssembledInstructions {
|
|
69
|
+
/** Repo CLAUDE.md/AGENTS.md cascade: "allow" left available, "suppressed" by the
|
|
70
|
+
* vanilla launch mechanism, "unsupported" requested-but-not-enforced (non-vanilla isolated). */
|
|
71
|
+
repo: AssembledInstructionDisposition;
|
|
72
|
+
/** Host/global provider instructions: "allow", "isolated" (dropped by the isolated home), or "unsupported". */
|
|
73
|
+
global: AssembledInstructionDisposition;
|
|
74
|
+
/** A composed systemPromptAppend block (project preamble + profile system/append +
|
|
75
|
+
* caller context, composed relay-side by composeSpawnInstructions) flows into this launch. */
|
|
76
|
+
systemPromptAppend: boolean;
|
|
77
|
+
// ── #559 SEAM ──────────────────────────────────────────────────────────────────
|
|
78
|
+
// Scoped {profile|user} memory does not exist yet (#559 generalizes Projects' anchor
|
|
79
|
+
// to project|profile|user). When it lands, the assembled instruction world becomes
|
|
80
|
+
// "instructions + provisioned assets + scoped knowledge": the relay-side composer
|
|
81
|
+
// (composeSpawnInstructions) gains a profile/user-scoped preamble beside the existing
|
|
82
|
+
// project preamble, and THIS field reports which scopes contributed. Until then it is
|
|
83
|
+
// always "none" — a thin add for #559, not a refactor.
|
|
84
|
+
scopedKnowledge: "none";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface AssembledMcp {
|
|
88
|
+
/** The identity-bearing relay HTTP MCP endpoint is injected. */
|
|
89
|
+
relayEndpoint: boolean;
|
|
90
|
+
/** Only the assembled servers may load (Claude --strict-mcp-config). #662: true for ALL
|
|
91
|
+
* managed Claude launches so the project `.mcp.json` enable-modal can't wedge a headless
|
|
92
|
+
* agent; relay's endpoint + provisioned servers (via --mcp-config) are preserved. Always
|
|
93
|
+
* false for Codex (no equivalent discovery modal). */
|
|
94
|
+
strict: boolean;
|
|
95
|
+
/** Relay-provisioned server names (excludes the relay endpoint itself). */
|
|
96
|
+
servers: string[];
|
|
97
|
+
/** #667 — project `.mcp.json` server names projected from the spawn cwd's committed
|
|
98
|
+
* `.mcp.json` (via `--mcp-config`, so they survive strict). Sourced from the project,
|
|
99
|
+
* not ambient host MCP. Empty unless the profile/spawn opted into `projectMcp`, and a
|
|
100
|
+
* name already declared by relay-provisioned `servers` wins (not double-listed here). */
|
|
101
|
+
projectServers: string[];
|
|
102
|
+
/** #662 — host USER-scope MCP server names (`~/.claude.json` `mcpServers`) re-injected
|
|
103
|
+
* into `--mcp-config` for an `mcp.mode:"host"` profile, so `--strict-mcp-config` keeps
|
|
104
|
+
* the ambient host MCP (callmux/qmd/gh) instead of dropping it. Empty for non-host mcp
|
|
105
|
+
* modes, isolated bases (own config home, never had host MCP), and Codex. */
|
|
106
|
+
hostServers: string[];
|
|
107
|
+
/** #672 — when the profile/spawn opted into `sharedMcp`, the agent's NON-IDENTITY MCP
|
|
108
|
+
* (tokenlean, github) is routed through one orchestrator-owned shared `callmux` listener via
|
|
109
|
+
* a per-agent `callmux bridge --cwd "$WORKTREE"` (session-cwd isolation), injected through
|
|
110
|
+
* the same `--mcp-config` / `-c` channel so it survives strict. `url` is the listener the
|
|
111
|
+
* bridge connects to. Undefined when projection is OFF (the default). The bridge is opaque
|
|
112
|
+
* at assembly time — its server SET is owned by the listener config, not enumerated here.
|
|
113
|
+
* The identity-bearing relay endpoint is never on the shared listener (#215). */
|
|
114
|
+
sharedListener?: { url: string };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AssembledLaunch {
|
|
118
|
+
provider: SpawnProvider;
|
|
119
|
+
/** The resolved profile this launch was assembled from; undefined for a profile-less
|
|
120
|
+
* (legacy default) spawn, which behaves as host base with relay features default-on. */
|
|
121
|
+
profile?: AgentProfile;
|
|
122
|
+
base: AgentProfileBase;
|
|
123
|
+
vanilla: boolean;
|
|
124
|
+
/** Isolated provider config home (CLAUDE_CONFIG_DIR / CODEX_HOME), or undefined for host base. */
|
|
125
|
+
configHome?: string;
|
|
126
|
+
relayContext: boolean;
|
|
127
|
+
/** Bundled relay skills are available (Codex skills.config / Claude via the relay plugin). */
|
|
128
|
+
relaySkills: boolean;
|
|
129
|
+
/** Bundled relay plugin dir is passed (Claude only). */
|
|
130
|
+
relayPlugin: boolean;
|
|
131
|
+
/** Relay status-line context probe is injected (Claude only). */
|
|
132
|
+
statusLine: boolean;
|
|
133
|
+
/** Relay-provisioned skills materialized for this launch. */
|
|
134
|
+
skills: AssembledAsset[];
|
|
135
|
+
/** Project `.claude/skills` materialized for this Claude launch (#668). */
|
|
136
|
+
projectSkills: AssembledAsset[];
|
|
137
|
+
/** Host/global Claude skills explicitly inherited without enabling host MCP. */
|
|
138
|
+
hostSkills: AssembledAsset[];
|
|
139
|
+
/** Project `.claude/settings.json` sanitized and injected for this Claude launch (#669). */
|
|
140
|
+
projectSettings: { applied: boolean; keys: string[]; ignoredKeys: string[]; file?: string };
|
|
141
|
+
/** Relay-provisioned hooks materialized for this launch. Empty when hooks were resolved
|
|
142
|
+
* but could not be delivered — see `hooksSuppressed` (the anti-lie invariant, #1096). */
|
|
143
|
+
hooks: AssembledAsset[];
|
|
144
|
+
/** #1096 review — set when this launch RESOLVED provisioned hooks (incl. a baseline safety
|
|
145
|
+
* guard) that could NOT be delivered: caller `--settings` was unmergeable (Claude), a
|
|
146
|
+
* host-base Codex agent has no managed CODEX_HOME, or the installed Codex is too old to
|
|
147
|
+
* consume hooks.json. Drives a LOUD projection warning so a baseline guard is never
|
|
148
|
+
* SILENTLY absent while the report says "applied". */
|
|
149
|
+
hooksSuppressed?: { reason: string; names: string[] };
|
|
150
|
+
/** #1096 review — Claude only: true when the caller's own `--settings` was folded into the
|
|
151
|
+
* relay-emitted `--settings` (relay hooks/projectSettings/statusLine merged in). The adapter
|
|
152
|
+
* must then STRIP the caller's `--settings` from the provider args so exactly one reaches the
|
|
153
|
+
* launch. False when relay emitted its own settings standalone or added none. */
|
|
154
|
+
callerSettingsConsumed: boolean;
|
|
155
|
+
/** Relay-provisioned plugins (Claude --plugin-dir). Empty for Codex (no plugin surface). */
|
|
156
|
+
plugins: AssembledAsset[];
|
|
157
|
+
/** Host/global provider plugin dirs explicitly inherited without enabling host MCP. */
|
|
158
|
+
hostPluginDirs: string[];
|
|
159
|
+
mcp: AssembledMcp;
|
|
160
|
+
instructions: AssembledInstructions;
|
|
161
|
+
/** Provider provisioning/vanilla/instruction launch-arg block, in launch order. */
|
|
162
|
+
args: string[];
|
|
163
|
+
/** Provisioning-derived env additions (the isolated config-home var). */
|
|
164
|
+
env: Record<string, string>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Bundled relay assets (relocated from the adapters so the assembler — not each
|
|
168
|
+
// adapter — owns launch-arg construction; #557). Adapters re-export for test parity. ──
|
|
169
|
+
|
|
170
|
+
const CLAUDE_RELAY_PLUGIN_DIR = resolve(import.meta.dir, "../plugins/claude");
|
|
171
|
+
const CODEX_BUNDLED_SKILLS_DIR = resolve(import.meta.dir, "../plugins/codex/skills");
|
|
172
|
+
|
|
173
|
+
export function bundledCodexSkillDirs(baseDir = CODEX_BUNDLED_SKILLS_DIR): string[] {
|
|
174
|
+
if (!existsSync(baseDir)) return [];
|
|
175
|
+
return readdirSync(baseDir, { withFileTypes: true })
|
|
176
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(baseDir, entry.name, "SKILL.md")))
|
|
177
|
+
.map((entry) => join(baseDir, entry.name))
|
|
178
|
+
.sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function bundledSkillConfigArgs(skillDirs = bundledCodexSkillDirs()): string[] {
|
|
182
|
+
if (skillDirs.length === 0) return [];
|
|
183
|
+
const skillsConfig = skillDirs.map((dir) => `{path=${tomlString(dir)},enabled=true}`).join(",");
|
|
184
|
+
return ["-c", `skills.config=[${skillsConfig}]`];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// #552 vanilla base (Codex): suppress the AGENTS.md project-doc cascade. project_doc_max_bytes
|
|
188
|
+
// (default 32768) caps bytes Codex reads from AGENTS.md walking root→cwd; 0 stops before adding
|
|
189
|
+
// any, so no repo (or fresh-CODEX_HOME global) project doc loads. The isolated CODEX_HOME already
|
|
190
|
+
// drops host config.toml MCP/hooks; this closes the cwd-resident repo instruction channel.
|
|
191
|
+
export function codexProjectDocSuppressionArgs(config: Pick<RunnerSpawnConfig, "agentProfile">): string[] {
|
|
192
|
+
return profileIsVanillaBase(config) ? ["-c", "project_doc_max_bytes=0"] : [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function hasSettingsArg(args: string[]): boolean {
|
|
196
|
+
return args.some((arg) => arg === "--settings" || arg.startsWith("--settings="));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Claude relay status-line + readable-thinking settings, injected only when no caller
|
|
200
|
+
// --settings is already present (caller wins). Relocated from claude.ts (#557).
|
|
201
|
+
export function sessionStatusLineSettingsArgs(...argLists: string[][]): string[] {
|
|
202
|
+
if (argLists.some(hasSettingsArg)) return [];
|
|
203
|
+
return ["--settings", JSON.stringify(relayStatusLineSettings())];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function relayStatusLineSettings(): Record<string, unknown> {
|
|
207
|
+
return {
|
|
208
|
+
statusLine: { type: "command", command: `node ${shellQuote(claudeStatusLineProbePath())}`, refreshInterval: 30 },
|
|
209
|
+
showThinkingSummaries: true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function claudeStatusLineProbePath(): string {
|
|
214
|
+
return resolve(import.meta.dir, "../plugins/claude/claude-statusline-probe.cjs");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface ClaudeSettingsResolution {
|
|
218
|
+
args: string[];
|
|
219
|
+
/** The caller's own --settings was merged in and must be stripped from the provider args. */
|
|
220
|
+
callerSettingsConsumed: boolean;
|
|
221
|
+
/** Provisioned hook names that could NOT be delivered (caller --settings unmergeable). */
|
|
222
|
+
suppressedHookNames: string[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// #1096 review, finding 1 — when the caller (spawn providerArgs / provider defaultArgs) already
|
|
226
|
+
// carries `--settings`, DO NOT silently drop relay's settings (which used to skip the baseline
|
|
227
|
+
// safety hooks + projectSettings + statusLine while projection still claimed "applied"). Merge
|
|
228
|
+
// relay's settings into the caller's INLINE JSON so the guard survives, and signal the adapter to
|
|
229
|
+
// strip the caller's now-redundant `--settings`. If the caller's `--settings` is a FILE PATH or
|
|
230
|
+
// malformed JSON (unmergeable), relay's provisioned hooks are reported as SUPPRESSED (a loud
|
|
231
|
+
// projection warning) rather than a silent absence with an "applied" lie.
|
|
232
|
+
function claudeSettingsArgs(
|
|
233
|
+
projectSettings: Record<string, unknown>,
|
|
234
|
+
provisionedHooks: ProvisioningHookSet,
|
|
235
|
+
hookNames: string[],
|
|
236
|
+
includeStatusLine: boolean,
|
|
237
|
+
...argLists: string[][]
|
|
238
|
+
): ClaudeSettingsResolution {
|
|
239
|
+
const relaySettings: Record<string, unknown> = {
|
|
240
|
+
...projectSettings,
|
|
241
|
+
...(Object.keys(provisionedHooks).length ? { hooks: provisionedHooks } : {}),
|
|
242
|
+
...(includeStatusLine ? relayStatusLineSettings() : {}),
|
|
243
|
+
};
|
|
244
|
+
const hasProvisionedHooks = Object.keys(provisionedHooks).length > 0;
|
|
245
|
+
const caller = extractCallerSettings(argLists);
|
|
246
|
+
if (!caller) {
|
|
247
|
+
return {
|
|
248
|
+
args: Object.keys(relaySettings).length ? ["--settings", JSON.stringify(relaySettings)] : [],
|
|
249
|
+
callerSettingsConsumed: false,
|
|
250
|
+
suppressedHookNames: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (caller.settings) {
|
|
254
|
+
const merged = mergeClaudeSettings(caller.settings, relaySettings);
|
|
255
|
+
return { args: ["--settings", JSON.stringify(merged)], callerSettingsConsumed: true, suppressedHookNames: [] };
|
|
256
|
+
}
|
|
257
|
+
// Unmergeable caller --settings (a file path or malformed JSON): relay cannot fold its
|
|
258
|
+
// settings in without clobbering the caller's file, so the provisioned hooks don't reach
|
|
259
|
+
// the launch. Report them suppressed (loud) — never claim "applied".
|
|
260
|
+
return { args: [], callerSettingsConsumed: false, suppressedHookNames: hasProvisionedHooks ? hookNames : [] };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The caller's `--settings` value, parsed as inline JSON when possible. `settings` is null for a
|
|
264
|
+
// file-path / malformed value (an inline object is required to merge relay settings in).
|
|
265
|
+
function extractCallerSettings(argLists: string[][]): { settings: Record<string, unknown> | null } | null {
|
|
266
|
+
for (const list of argLists) {
|
|
267
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
268
|
+
const arg = list[i];
|
|
269
|
+
if (arg === "--settings") return { settings: parseInlineSettings(list[i + 1]) };
|
|
270
|
+
if (arg?.startsWith("--settings=")) return { settings: parseInlineSettings(arg.slice("--settings=".length)) };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function parseInlineSettings(value: string | undefined): Record<string, unknown> | null {
|
|
277
|
+
if (!value) return null;
|
|
278
|
+
const trimmed = value.trim();
|
|
279
|
+
if (!trimmed.startsWith("{")) return null; // a settings file path, not inline JSON
|
|
280
|
+
try {
|
|
281
|
+
const parsed = JSON.parse(trimmed);
|
|
282
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Merge relay settings into the caller's inline settings. The safety-critical `hooks` map is
|
|
289
|
+
// UNIONED per event (caller entries first, relay's baseline guard appended) so both fire; every
|
|
290
|
+
// other relay key is fill-if-absent (the caller's explicit value wins), preserving the historical
|
|
291
|
+
// "caller --settings wins" spirit for non-safety settings.
|
|
292
|
+
function mergeClaudeSettings(caller: Record<string, unknown>, relay: Record<string, unknown>): Record<string, unknown> {
|
|
293
|
+
const out: Record<string, unknown> = { ...caller };
|
|
294
|
+
for (const [key, value] of Object.entries(relay)) {
|
|
295
|
+
if (key === "hooks") out.hooks = mergeHookSets(caller.hooks, value);
|
|
296
|
+
else if (!(key in out)) out[key] = value;
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function mergeHookSets(caller: unknown, relay: unknown): ProvisioningHookSet {
|
|
302
|
+
const base: ProvisioningHookSet = isHookSet(caller) ? { ...caller } : {};
|
|
303
|
+
if (!isHookSet(relay)) return base;
|
|
304
|
+
for (const [event, entries] of Object.entries(relay)) {
|
|
305
|
+
base[event] = [...(base[event] ?? []), ...entries];
|
|
306
|
+
}
|
|
307
|
+
return base;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isHookSet(value: unknown): value is ProvisioningHookSet {
|
|
311
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Strip every `--settings <json>` / `--settings=<json>` occurrence from a provider arg list. The
|
|
315
|
+
// adapter applies this to the caller args once the assembler has folded them into its own merged
|
|
316
|
+
// `--settings` (callerSettingsConsumed), so exactly one `--settings` reaches the launch (#1096).
|
|
317
|
+
export function stripSettingsArgs(args: string[]): string[] {
|
|
318
|
+
const result: string[] = [];
|
|
319
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
320
|
+
const arg = args[i];
|
|
321
|
+
if (arg === "--settings") { i += 1; continue; }
|
|
322
|
+
if (arg?.startsWith("--settings=")) continue;
|
|
323
|
+
if (arg !== undefined) result.push(arg);
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Instruction disposition (the vanilla floor's effect on the host instruction cascade) ──
|
|
329
|
+
|
|
330
|
+
function repoInstructionDisposition(profile: AgentProfile): AssembledInstructionDisposition {
|
|
331
|
+
if (profile.instructions.repoInstructions === "allow") return "allow";
|
|
332
|
+
if (profile.base === "vanilla") return "suppressed";
|
|
333
|
+
// Non-vanilla isolated bases can request ignore but have no launch-time enforcement yet.
|
|
334
|
+
return "unsupported";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function globalInstructionDisposition(profile: AgentProfile): AssembledInstructionDisposition {
|
|
338
|
+
if (profile.instructions.globalInstructions === "allow") return "allow";
|
|
339
|
+
if (profile.base !== "host") return "isolated";
|
|
340
|
+
return "unsupported";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function assembleInstructions(config: RunnerSpawnConfig): AssembledInstructions {
|
|
344
|
+
const profile = config.agentProfile;
|
|
345
|
+
return {
|
|
346
|
+
// A profile-less spawn is host-base: the full host instruction cascade is left available.
|
|
347
|
+
repo: profile ? repoInstructionDisposition(profile) : "allow",
|
|
348
|
+
global: profile ? globalInstructionDisposition(profile) : "allow",
|
|
349
|
+
systemPromptAppend: Boolean(config.systemPromptAppend?.trim()),
|
|
350
|
+
scopedKnowledge: "none",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Per-provider arg assembly ──────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]): AssembledLaunch {
|
|
357
|
+
const configHome = providerHomePathFor("claude", config);
|
|
358
|
+
const assetHome = providerProvisioningHomePathFor("claude", config);
|
|
359
|
+
const vanilla = profileIsVanillaBase(config);
|
|
360
|
+
const projectConfigProjection = projectConfigProjectionOn(config);
|
|
361
|
+
const relayPlugin = profileAllowsRelayFeature(config, "plugins");
|
|
362
|
+
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
363
|
+
const statusLine = profileAllowsRelayFeature(config, "statusLine");
|
|
364
|
+
|
|
365
|
+
const skills = configHome ? resolveClaudeProvisionedSkills(configHome, config) : [];
|
|
366
|
+
const hostSkills = configHome
|
|
367
|
+
? resolveClaudeHostSkills(configHome, config, new Set(skills.map((s) => s.name)))
|
|
368
|
+
: [];
|
|
369
|
+
const projectSkills = configHome
|
|
370
|
+
? resolveClaudeProjectSkills(configHome, config, new Set([...skills, ...hostSkills].map((s) => s.name)))
|
|
371
|
+
: [];
|
|
372
|
+
const projectSettings = resolveProjectClaudeSettings(config);
|
|
373
|
+
const provisionedPlugins = assetHome ? resolveClaudeProvisionedPlugins(assetHome, config) : [];
|
|
374
|
+
const hookHome = configHome ?? assetHome;
|
|
375
|
+
const provisionedHooks = hookHome ? resolveProvisionedHooks(hookHome, config) : [];
|
|
376
|
+
const provisionedHookSet = renderProvisionedHookSet(provisionedHooks);
|
|
377
|
+
const settings = claudeSettingsArgs(
|
|
378
|
+
projectSettings.settings,
|
|
379
|
+
provisionedHookSet,
|
|
380
|
+
provisionedHooks.map((h) => h.name),
|
|
381
|
+
statusLine,
|
|
382
|
+
hostDefaultArgs,
|
|
383
|
+
config.providerArgs,
|
|
384
|
+
);
|
|
385
|
+
// The anti-lie invariant (#1096): only report hooks as materialized when they actually reach
|
|
386
|
+
// the launch. If the caller's --settings suppressed them, empty the reported set and surface a
|
|
387
|
+
// loud suppression reason instead of an "applied" report.
|
|
388
|
+
const hooksSuppressed = settings.suppressedHookNames.length
|
|
389
|
+
? { reason: "caller-provided --settings could not be merged (file path or malformed JSON); relay hooks were not injected", names: settings.suppressedHookNames }
|
|
390
|
+
: undefined;
|
|
391
|
+
const deliveredHooks = hooksSuppressed ? [] : provisionedHooks;
|
|
392
|
+
|
|
393
|
+
// Plugin dirs (--plugin-dir loads explicitly, independent of --setting-sources):
|
|
394
|
+
// relay-bundled plugin → only when relay.plugins (carries the relay monitor +
|
|
395
|
+
// turn-lifecycle hooks #556, not reproducible via settings.json).
|
|
396
|
+
// relay-provisioned plugins (#556) → materialized into the isolated home.
|
|
397
|
+
// host plugin dirs → ONLY host-base profiles; isolated/vanilla must not pull them back in.
|
|
398
|
+
const relayPluginDirs = !config.agentProfile && relayPlugin ? [CLAUDE_RELAY_PLUGIN_DIR] : [];
|
|
399
|
+
const provisionedPluginDirs = provisionedPlugins.map((p) => p.dir);
|
|
400
|
+
const hostPluginDirs = profileAllowsHostAssets(config, "plugins") ? providerConfig.pluginDirs : [];
|
|
401
|
+
const pluginDirs = [...new Set([...relayPluginDirs, ...provisionedPluginDirs, ...hostPluginDirs])];
|
|
402
|
+
|
|
403
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
404
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
405
|
+
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
406
|
+
|
|
407
|
+
// #667 — project `.mcp.json` projection (opt-in). The spawn cwd's committed servers are
|
|
408
|
+
// injected through the SAME `--mcp-config` channel as relay-provisioned servers, so they
|
|
409
|
+
// survive `--strict-mcp-config` without the ambient project-discovery modal. A relay-
|
|
410
|
+
// provisioned server of the same name wins (the profile is the explicit, relay-owned
|
|
411
|
+
// source); the relay endpoint name is reserved. `declaredMcp` is the union relay injects.
|
|
412
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
413
|
+
const projectMcp = Object.fromEntries(
|
|
414
|
+
Object.entries(projectMcpRaw).filter(
|
|
415
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
const projectServers = Object.keys(projectMcp);
|
|
419
|
+
|
|
420
|
+
// #672 — shared host-listener MCP (opt-in, default OFF). When on, route the agent's
|
|
421
|
+
// NON-IDENTITY MCP (tokenlean, github) through ONE orchestrator-owned shared `callmux`
|
|
422
|
+
// listener via a per-agent `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry:
|
|
423
|
+
// the MCP session survives a listener/downstream restart, and --cwd gives each isolated
|
|
424
|
+
// worktree session-cwd isolation. It's a plain stdio server descriptor, so it rides the same
|
|
425
|
+
// controlled --mcp-config channel as provisioned servers (survives --strict-mcp-config) — no
|
|
426
|
+
// ambient discovery, no modal. Relay-owned infra → authoritative on its name (spread last);
|
|
427
|
+
// the relay endpoint stays per-agent (#215), NEVER on the shared listener.
|
|
428
|
+
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
429
|
+
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
430
|
+
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
431
|
+
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
|
432
|
+
: {};
|
|
433
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
434
|
+
|
|
435
|
+
// #662/#663 — strict controlled MCP is a Claude prompt-gate prevention entry, not a
|
|
436
|
+
// launch-assembly special case. It prevents the blocking "N new MCP servers found in
|
|
437
|
+
// this project" modal while preserving every relay-declared server in --mcp-config.
|
|
438
|
+
const strictMcp = claudeLaunchPromptGateSettings(config).strictMcpConfig;
|
|
439
|
+
|
|
440
|
+
// #662 follow-up — strict drops ALL on-disk config, including the host user-scope
|
|
441
|
+
// `~/.claude.json` MCP that a host-base agent loaded ambiently before strict. That
|
|
442
|
+
// over-reached: it stopped honoring `mcp.mode:"host"`, so default-relay workers/reviewers
|
|
443
|
+
// lost callmux/qmd/gh. Re-inject the host USER-scope servers (NOT the project `.mcp.json`,
|
|
444
|
+
// still excluded) when the launch both uses the host config home AND its mcp mode is host.
|
|
445
|
+
// An isolated/minimal base has its own config home and never had the real host's user MCP,
|
|
446
|
+
// so it stays relay+provisioned only even if a profile sets mode:"host".
|
|
447
|
+
const usesHostMcp = profileUsesProviderHostGlobals(config)
|
|
448
|
+
&& (!config.agentProfile || config.agentProfile.mcp.mode === "host");
|
|
449
|
+
const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
|
|
450
|
+
const hostMcpServers = Object.keys(hostMcp).filter(
|
|
451
|
+
(name) => name !== RELAY_MCP_SERVER_NAME && !(name in declaredMcp),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const instructions = assembleInstructions(config);
|
|
455
|
+
|
|
456
|
+
// Launch order matches the historical claude buildSpawnArgs block exactly so existing
|
|
457
|
+
// arg assertions hold: vanilla scope → plugin dirs → MCP → status-line → append.
|
|
458
|
+
const args = [
|
|
459
|
+
...(vanilla || projectConfigProjection ? ["--setting-sources", "user"] : []),
|
|
460
|
+
...pluginDirs.flatMap((dir) => ["--plugin-dir", dir]),
|
|
461
|
+
...claudeMcpLaunchArgs({
|
|
462
|
+
relayUrl: config.relayUrl,
|
|
463
|
+
...(config.relayMcpEndpoint ? { endpoint: config.relayMcpEndpoint } : {}),
|
|
464
|
+
includeRelay: relayEndpoint,
|
|
465
|
+
servers: declaredMcp,
|
|
466
|
+
hostServers: hostMcp,
|
|
467
|
+
strict: strictMcp,
|
|
468
|
+
}),
|
|
469
|
+
...settings.args,
|
|
470
|
+
...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
provider: "claude",
|
|
475
|
+
...(config.agentProfile ? { profile: config.agentProfile } : {}),
|
|
476
|
+
base: config.agentProfile?.base ?? "host",
|
|
477
|
+
vanilla,
|
|
478
|
+
...(configHome ? { configHome } : {}),
|
|
479
|
+
relayContext: profileAllowsRelayFeature(config, "context"),
|
|
480
|
+
relaySkills,
|
|
481
|
+
relayPlugin,
|
|
482
|
+
statusLine,
|
|
483
|
+
skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
484
|
+
hostSkills: hostSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
485
|
+
projectSkills: projectSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
486
|
+
projectSettings: {
|
|
487
|
+
applied: projectSettings.applied,
|
|
488
|
+
keys: projectSettings.keys,
|
|
489
|
+
ignoredKeys: projectSettings.ignoredKeys,
|
|
490
|
+
...(projectSettings.file ? { file: projectSettings.file } : {}),
|
|
491
|
+
},
|
|
492
|
+
hooks: deliveredHooks.map((h) => ({ name: h.name, dir: h.dir })),
|
|
493
|
+
...(hooksSuppressed ? { hooksSuppressed } : {}),
|
|
494
|
+
callerSettingsConsumed: settings.callerSettingsConsumed,
|
|
495
|
+
plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
|
|
496
|
+
hostPluginDirs,
|
|
497
|
+
mcp: {
|
|
498
|
+
relayEndpoint, strict: strictMcp, servers: mcpServers, projectServers, hostServers: hostMcpServers,
|
|
499
|
+
...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
|
|
500
|
+
},
|
|
501
|
+
instructions,
|
|
502
|
+
args,
|
|
503
|
+
env: configHomeEnv("claude", configHome),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function assembleCodex(config: RunnerSpawnConfig, providerConfig: ProviderConfig): AssembledLaunch {
|
|
508
|
+
const configHome = providerHomePathFor("codex", config);
|
|
509
|
+
const assetHome = providerProvisioningHomePathFor("codex", config);
|
|
510
|
+
const vanilla = profileIsVanillaBase(config);
|
|
511
|
+
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
512
|
+
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
513
|
+
|
|
514
|
+
const provisionedSkills = assetHome ? resolveCodexProvisionedSkills(assetHome, config) : [];
|
|
515
|
+
// #1096 review, findings 3 & 4 — Codex delivers hooks via CODEX_HOME/hooks.json, so a launch
|
|
516
|
+
// can only carry them when it has an isolated CODEX_HOME (host-base profiles reuse the real
|
|
517
|
+
// ~/.codex, which relay must not clobber) AND the installed Codex is new enough to consume the
|
|
518
|
+
// file. Resolve the profile's hook definitions to decide whether there was anything to deliver;
|
|
519
|
+
// any absence is reported LOUDLY (hooksSuppressed) instead of a silent "not-applicable".
|
|
520
|
+
const resolvedHookDefs = profileProvisioning(config).hooks ?? [];
|
|
521
|
+
const hooksSupported = codexHooksSupported(providerConfig.command);
|
|
522
|
+
const provisionedHooks = configHome && hooksSupported ? resolveProvisionedHooks(configHome, config) : [];
|
|
523
|
+
const hooksSuppressed = resolvedHookDefs.length && provisionedHooks.length === 0
|
|
524
|
+
? {
|
|
525
|
+
reason: !configHome
|
|
526
|
+
? "Codex host-base profile has no managed CODEX_HOME; relay will not overwrite the host ~/.codex/hooks.json, so the baseline guard cannot be delivered"
|
|
527
|
+
: "installed Codex is older than 0.124 and ignores CODEX_HOME/hooks.json (set AGENT_RELAY_CODEX_VERSION to override the probe)",
|
|
528
|
+
names: resolvedHookDefs.map((h) => h.name),
|
|
529
|
+
}
|
|
530
|
+
: undefined;
|
|
531
|
+
const bundledSkillDirs = !config.agentProfile && relaySkills ? bundledCodexSkillDirs() : [];
|
|
532
|
+
const skillDirs = [...bundledSkillDirs, ...provisionedSkills.map((s) => s.dir)];
|
|
533
|
+
|
|
534
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
535
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
536
|
+
|
|
537
|
+
// #667 — project `.mcp.json` projection (opt-in), same union rule as Claude: a relay-
|
|
538
|
+
// provisioned server of the same name wins; the relay endpoint name is reserved. Codex has
|
|
539
|
+
// no `.mcp.json` discovery of its own, so this is purely additive (no strict/modal concern).
|
|
540
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
541
|
+
const projectMcp = Object.fromEntries(
|
|
542
|
+
Object.entries(projectMcpRaw).filter(
|
|
543
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
544
|
+
),
|
|
545
|
+
);
|
|
546
|
+
const projectServers = Object.keys(projectMcp);
|
|
547
|
+
|
|
548
|
+
// #672 — shared host-listener MCP (opt-in, default OFF), same as Claude: a per-agent
|
|
549
|
+
// `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry routes the agent's
|
|
550
|
+
// NON-IDENTITY MCP (tokenlean, github) through the shared listener (session survives a
|
|
551
|
+
// restart; --cwd gives session-cwd isolation). Emitted via the same `-c mcp_servers.*`
|
|
552
|
+
// overrides as provisioned servers. The relay endpoint stays per-agent (#215).
|
|
553
|
+
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
554
|
+
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
555
|
+
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
556
|
+
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
|
557
|
+
: {};
|
|
558
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
559
|
+
|
|
560
|
+
const instructions = assembleInstructions(config);
|
|
561
|
+
|
|
562
|
+
// The skills/MCP/project-doc block; core launch config (model/approval/listen) stays
|
|
563
|
+
// in the adapter. -c overrides are order-independent, so the assembled block is contiguous.
|
|
564
|
+
const args = [
|
|
565
|
+
...bundledSkillConfigArgs(skillDirs),
|
|
566
|
+
...(relayEndpoint ? relayMcpCodexConfigArgs(config.relayUrl, config.relayMcpEndpoint) : []),
|
|
567
|
+
...codexProvisionedMcpArgs(declaredMcp),
|
|
568
|
+
...codexProjectDocSuppressionArgs(config),
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
provider: "codex",
|
|
573
|
+
...(config.agentProfile ? { profile: config.agentProfile } : {}),
|
|
574
|
+
base: config.agentProfile?.base ?? "host",
|
|
575
|
+
vanilla,
|
|
576
|
+
...(configHome ? { configHome } : {}),
|
|
577
|
+
relayContext: profileAllowsRelayFeature(config, "context"),
|
|
578
|
+
relaySkills,
|
|
579
|
+
relayPlugin: false,
|
|
580
|
+
statusLine: false,
|
|
581
|
+
skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
582
|
+
hostSkills: [],
|
|
583
|
+
projectSkills: [],
|
|
584
|
+
projectSettings: { applied: false, keys: [], ignoredKeys: [] },
|
|
585
|
+
hooks: provisionedHooks.map((h) => ({ name: h.name, dir: h.dir })),
|
|
586
|
+
...(hooksSuppressed ? { hooksSuppressed } : {}),
|
|
587
|
+
callerSettingsConsumed: false,
|
|
588
|
+
plugins: [],
|
|
589
|
+
hostPluginDirs: [],
|
|
590
|
+
mcp: {
|
|
591
|
+
relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [],
|
|
592
|
+
...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
|
|
593
|
+
},
|
|
594
|
+
instructions,
|
|
595
|
+
args,
|
|
596
|
+
env: configHomeEnv("codex", configHome),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ── Manifest-driven env + dispatch ────────────────────────────────────────────
|
|
601
|
+
|
|
602
|
+
function configHomeEnv(provider: string, configHome: string | undefined): Record<string, string> {
|
|
603
|
+
if (!configHome) return {};
|
|
604
|
+
const manifest = getManifest(provider);
|
|
605
|
+
const envVar = manifest?.home?.configDirEnvVar;
|
|
606
|
+
return envVar ? { [envVar]: configHome } : {};
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
type Assembler = (config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]) => AssembledLaunch;
|
|
610
|
+
type Materializer = (config: RunnerSpawnConfig) => void;
|
|
611
|
+
|
|
612
|
+
const ASSEMBLERS: Record<string, Assembler> = {
|
|
613
|
+
claude: assembleClaude,
|
|
614
|
+
codex: (config, providerConfig) => assembleCodex(config, providerConfig),
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
const MATERIALIZERS: Record<string, Materializer> = {
|
|
618
|
+
claude: materializeClaudeAssets,
|
|
619
|
+
codex: materializeCodexAssets,
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
function materializeClaudeAssets(config: RunnerSpawnConfig): void {
|
|
623
|
+
const configHome = providerHomePathFor("claude", config);
|
|
624
|
+
const assetHome = providerProvisioningHomePathFor("claude", config);
|
|
625
|
+
if (configHome) {
|
|
626
|
+
const skills = resolveClaudeProvisionedSkills(configHome, config);
|
|
627
|
+
materializeResolvedAssets(skills);
|
|
628
|
+
const hostSkills = resolveClaudeHostSkills(configHome, config, new Set(skills.map((s) => s.name)));
|
|
629
|
+
materializeResolvedAssets(hostSkills);
|
|
630
|
+
materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set([...skills, ...hostSkills].map((s) => s.name))));
|
|
631
|
+
materializeResolvedAssets(resolveProvisionedHooks(configHome, config));
|
|
632
|
+
}
|
|
633
|
+
if (assetHome) materializeResolvedAssets(resolveClaudeProvisionedPlugins(assetHome, config));
|
|
634
|
+
if (!configHome && assetHome) materializeResolvedAssets(resolveProvisionedHooks(assetHome, config));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function materializeCodexAssets(config: RunnerSpawnConfig): void {
|
|
638
|
+
const assetHome = providerProvisioningHomePathFor("codex", config);
|
|
639
|
+
if (assetHome) materializeResolvedAssets(resolveCodexProvisionedSkills(assetHome, config));
|
|
640
|
+
const configHome = providerHomePathFor("codex", config);
|
|
641
|
+
// #1096 review, finding 4 — don't write CODEX_HOME/hooks.json for a Codex that would ignore it;
|
|
642
|
+
// keep disk == report (the assembler reports these hooks suppressed for such a launch).
|
|
643
|
+
if (configHome && codexHooksSupported(config.providerConfig.command)) {
|
|
644
|
+
const hooks = resolveProvisionedHooks(configHome, config);
|
|
645
|
+
materializeResolvedAssets(hooks);
|
|
646
|
+
writeCodexHooksJson(configHome, renderProvisionedHookSet(hooks));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Assemble the launch for `provider` from the resolved profile + provisioning. PURE:
|
|
652
|
+
* resolves which declared assets exist (filesystem reads) but writes nothing. A missing
|
|
653
|
+
* `agentProfile` assembles the legacy default (host base, relay features on, no provisioning).
|
|
654
|
+
*/
|
|
655
|
+
export function assembleLaunch(provider: SpawnProvider, config: RunnerSpawnConfig, providerConfig: ProviderConfig): AssembledLaunch {
|
|
656
|
+
const assembler = ASSEMBLERS[provider];
|
|
657
|
+
if (!assembler) throw new Error(`no launch assembler registered for provider: ${provider}`);
|
|
658
|
+
const hostDefaultArgs = profileUsesProviderHostGlobals(config) ? providerConfig.defaultArgs : [];
|
|
659
|
+
return assembler(config, providerConfig, hostDefaultArgs);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Write the provisioned assets the assembly describes (skills/plugins/hooks and Codex
|
|
664
|
+
* hooks.json). Driven by the SAME resolvers `assembleLaunch` uses, so disk == launch ==
|
|
665
|
+
* report. Idempotent; the isolated home must already exist (prepare*ProfileHome). No-op
|
|
666
|
+
* for a host-base profile.
|
|
667
|
+
*/
|
|
668
|
+
export function materializeLaunchAssembly(provider: SpawnProvider, config: RunnerSpawnConfig): void {
|
|
669
|
+
const configHome = providerHomePathFor(provider, config);
|
|
670
|
+
const assetHome = providerProvisioningHomePathFor(provider, config);
|
|
671
|
+
if (!configHome && !assetHome) return;
|
|
672
|
+
const materializer = MATERIALIZERS[provider];
|
|
673
|
+
if (materializer) materializer(config);
|
|
674
|
+
}
|