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.
- package/docs/openapi.json +1 -1
- package/package.json +6 -6
- package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
- package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
- package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
- package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
- package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
- package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
- package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
- package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
- package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
- package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
- package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
- package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
- package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
- package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
- package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
- package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
- package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
- package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
- package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
- package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
- package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
- package/public/assets/display-CJzSoTMq.js +3 -0
- package/public/assets/display-CJzSoTMq.js.map +1 -0
- package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
- package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
- package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
- package/public/assets/index-D0xMQQb_.js +25 -0
- package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
- package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
- package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
- package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
- package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
- package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
- package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
- package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
- package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
- package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
- package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
- package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
- package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
- package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
- package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
- package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
- package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
- package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
- package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
- package/public/assets/projects-Dzdf-gkK.js +2 -0
- package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
- package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
- package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
- package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
- package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
- package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
- package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
- package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
- package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
- package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
- package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
- package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
- package/public/assets/store-BPrVuKiv.js.map +1 -0
- package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
- package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
- package/public/assets/teams-DWv6UwFV.js +2 -0
- package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
- package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
- package/public/assets/types-BeJUSfDj.js.map +1 -0
- package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
- package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
- package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
- package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
- package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
- package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
- package/runner/src/adapter.ts +53 -18
- 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 +672 -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/config.ts +1 -4
- 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 +508 -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 +305 -0
- package/runner/src/providers.ts +20 -0
- package/runner/src/provisioning.ts +314 -0
- package/runner/src/quota.ts +40 -0
- package/runner/src/rate-limit.ts +189 -0
- package/runner/src/relay-injection-events.ts +102 -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 +43 -0
- package/src/agent-card-projection.ts +5 -1
- package/src/agent-command-activity.ts +39 -0
- package/src/automations/reconcile.ts +2 -2
- package/src/cli/workspace.ts +3 -1
- package/src/config.ts +10 -1
- package/src/db/agents.ts +118 -37
- package/src/db/message-reads.ts +50 -0
- package/src/db/messages.ts +48 -9
- package/src/db/migrations.ts +54 -0
- package/src/db/provider-quota-burn.ts +96 -67
- package/src/db/provider-quotas.ts +29 -7
- package/src/db/scheduled-timers.ts +21 -0
- package/src/db/schema.ts +1 -0
- package/src/db/tasks.ts +33 -3
- package/src/db/work-evidence.ts +10 -7
- package/src/event-loop-lag.ts +15 -0
- package/src/gate-resolver.ts +19 -1
- package/src/maintenance/jobs.ts +23 -6
- package/src/managed-policy.ts +5 -12
- package/src/mcp-plan-tools.ts +13 -1
- package/src/project-instructions.ts +36 -2
- package/src/quota-advisory.ts +6 -4
- package/src/quota-band-transition.ts +23 -14
- package/src/routes/agent-sessions.ts +39 -7
- package/src/routes/agents.ts +3 -2
- package/src/routes/tasks.ts +16 -4
- package/src/routes/workspaces.ts +35 -10
- package/src/services/plan-graph.ts +27 -9
- package/src/services/register-agent.ts +12 -12
- package/src/services/send-message.ts +43 -15
- package/src/services/spawn-agent.ts +3 -0
- package/src/spawn-command.ts +8 -0
- package/src/spawn-targets.ts +19 -34
- package/src/sse.ts +25 -3
- package/src/workspace-merge.ts +25 -5
- package/src/workspace-orphans.ts +13 -6
- package/public/assets/display-exQDWum3.js +0 -3
- package/public/assets/display-exQDWum3.js.map +0 -1
- package/public/assets/index-D9OogaB5.js +0 -25
- package/public/assets/projects-DdPNPFJI.js +0 -2
- package/public/assets/prompt-settings-BpZse7-X.js +0 -2
- package/public/assets/store-DKTLubBT.js.map +0 -1
- package/public/assets/teams-B8f2pGJe.js +0 -2
- package/public/assets/types-uVOXgvMT.js.map +0 -1
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import type { AgentProfile, AgentProfileBase, 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 { claudeLaunchPromptGateSettings } from "./claude-prompt-gates";
|
|
21
|
+
import {
|
|
22
|
+
materializeResolvedAssets,
|
|
23
|
+
resolveClaudeProjectSkills,
|
|
24
|
+
resolveClaudeHostSkills,
|
|
25
|
+
resolveProjectClaudeSettings,
|
|
26
|
+
resolveClaudeProvisionedPlugins,
|
|
27
|
+
resolveClaudeProvisionedSkills,
|
|
28
|
+
resolveCodexProvisionedSkills,
|
|
29
|
+
resolveProjectMcpServers,
|
|
30
|
+
profileProvisioning,
|
|
31
|
+
} from "./provisioning";
|
|
32
|
+
import {
|
|
33
|
+
claudeMcpLaunchArgs,
|
|
34
|
+
codexProvisionedMcpArgs,
|
|
35
|
+
relayMcpCodexConfigArgs,
|
|
36
|
+
resolveSharedMcpUrl,
|
|
37
|
+
sharedMcpBridgeServer,
|
|
38
|
+
tomlString,
|
|
39
|
+
RELAY_MCP_SERVER_NAME,
|
|
40
|
+
SHARED_MCP_SERVER_NAME,
|
|
41
|
+
} from "./relay-mcp";
|
|
42
|
+
|
|
43
|
+
// #557 — THE single launch-assembly contract. Given a resolved AgentProfile (+ its
|
|
44
|
+
// spawn-time ResolvedProvisioning), `assembleLaunch` produces ONE provider-neutral
|
|
45
|
+
// description of the launch: the relay-provisioned skills/plugins/MCP, the bundled relay
|
|
46
|
+
// surface, the instruction-source disposition, and `base:"vanilla"` as the zero-host
|
|
47
|
+
// floor. Both adapters' buildSpawnArgs splice `assembled.args`/`assembled.env`, and
|
|
48
|
+
// profile-projection describes `assembled` directly — so projection and reality come
|
|
49
|
+
// from one source and cannot drift.
|
|
50
|
+
//
|
|
51
|
+
// `assembleLaunch` is PURE (it reads the filesystem to decide which declared dirs
|
|
52
|
+
// resolve, but writes nothing). `materializeLaunchAssembly` performs the disk writes the
|
|
53
|
+
// description implies, driven by the SAME resolvers — so what's written equals what's
|
|
54
|
+
// reported equals what's launched.
|
|
55
|
+
|
|
56
|
+
export interface AssembledAsset {
|
|
57
|
+
name: string;
|
|
58
|
+
/** Materialized/in-place dir feeding the launch (e.g. a --plugin-dir or skills.config path). */
|
|
59
|
+
dir: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type AssembledInstructionDisposition = "allow" | "suppressed" | "isolated" | "unsupported";
|
|
63
|
+
|
|
64
|
+
export interface AssembledInstructions {
|
|
65
|
+
/** Repo CLAUDE.md/AGENTS.md cascade: "allow" left available, "suppressed" by the
|
|
66
|
+
* vanilla launch mechanism, "unsupported" requested-but-not-enforced (non-vanilla isolated). */
|
|
67
|
+
repo: AssembledInstructionDisposition;
|
|
68
|
+
/** Host/global provider instructions: "allow", "isolated" (dropped by the isolated home), or "unsupported". */
|
|
69
|
+
global: AssembledInstructionDisposition;
|
|
70
|
+
/** A composed systemPromptAppend block (project preamble + profile system/append +
|
|
71
|
+
* caller context, composed relay-side by composeSpawnInstructions) flows into this launch. */
|
|
72
|
+
systemPromptAppend: boolean;
|
|
73
|
+
// ── #559 SEAM ──────────────────────────────────────────────────────────────────
|
|
74
|
+
// Scoped {profile|user} memory does not exist yet (#559 generalizes Projects' anchor
|
|
75
|
+
// to project|profile|user). When it lands, the assembled instruction world becomes
|
|
76
|
+
// "instructions + provisioned assets + scoped knowledge": the relay-side composer
|
|
77
|
+
// (composeSpawnInstructions) gains a profile/user-scoped preamble beside the existing
|
|
78
|
+
// project preamble, and THIS field reports which scopes contributed. Until then it is
|
|
79
|
+
// always "none" — a thin add for #559, not a refactor.
|
|
80
|
+
scopedKnowledge: "none";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface AssembledMcp {
|
|
84
|
+
/** The identity-bearing relay HTTP MCP endpoint is injected. */
|
|
85
|
+
relayEndpoint: boolean;
|
|
86
|
+
/** Only the assembled servers may load (Claude --strict-mcp-config). #662: true for ALL
|
|
87
|
+
* managed Claude launches so the project `.mcp.json` enable-modal can't wedge a headless
|
|
88
|
+
* agent; relay's endpoint + provisioned servers (via --mcp-config) are preserved. Always
|
|
89
|
+
* false for Codex (no equivalent discovery modal). */
|
|
90
|
+
strict: boolean;
|
|
91
|
+
/** Relay-provisioned server names (excludes the relay endpoint itself). */
|
|
92
|
+
servers: string[];
|
|
93
|
+
/** #667 — project `.mcp.json` server names projected from the spawn cwd's committed
|
|
94
|
+
* `.mcp.json` (via `--mcp-config`, so they survive strict). Sourced from the project,
|
|
95
|
+
* not ambient host MCP. Empty unless the profile/spawn opted into `projectMcp`, and a
|
|
96
|
+
* name already declared by relay-provisioned `servers` wins (not double-listed here). */
|
|
97
|
+
projectServers: string[];
|
|
98
|
+
/** #662 — host USER-scope MCP server names (`~/.claude.json` `mcpServers`) re-injected
|
|
99
|
+
* into `--mcp-config` for an `mcp.mode:"host"` profile, so `--strict-mcp-config` keeps
|
|
100
|
+
* the ambient host MCP (callmux/qmd/gh) instead of dropping it. Empty for non-host mcp
|
|
101
|
+
* modes, isolated bases (own config home, never had host MCP), and Codex. */
|
|
102
|
+
hostServers: string[];
|
|
103
|
+
/** #672 — when the profile/spawn opted into `sharedMcp`, the agent's NON-IDENTITY MCP
|
|
104
|
+
* (tokenlean, github) is routed through one orchestrator-owned shared `callmux` listener via
|
|
105
|
+
* a per-agent `callmux bridge --cwd "$WORKTREE"` (session-cwd isolation), injected through
|
|
106
|
+
* the same `--mcp-config` / `-c` channel so it survives strict. `url` is the listener the
|
|
107
|
+
* bridge connects to. Undefined when projection is OFF (the default). The bridge is opaque
|
|
108
|
+
* at assembly time — its server SET is owned by the listener config, not enumerated here.
|
|
109
|
+
* The identity-bearing relay endpoint is never on the shared listener (#215). */
|
|
110
|
+
sharedListener?: { url: string };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface AssembledLaunch {
|
|
114
|
+
provider: SpawnProvider;
|
|
115
|
+
/** The resolved profile this launch was assembled from; undefined for a profile-less
|
|
116
|
+
* (legacy default) spawn, which behaves as host base with relay features default-on. */
|
|
117
|
+
profile?: AgentProfile;
|
|
118
|
+
base: AgentProfileBase;
|
|
119
|
+
vanilla: boolean;
|
|
120
|
+
/** Isolated provider config home (CLAUDE_CONFIG_DIR / CODEX_HOME), or undefined for host base. */
|
|
121
|
+
configHome?: string;
|
|
122
|
+
relayContext: boolean;
|
|
123
|
+
/** Bundled relay skills are available (Codex skills.config / Claude via the relay plugin). */
|
|
124
|
+
relaySkills: boolean;
|
|
125
|
+
/** Bundled relay plugin dir is passed (Claude only). */
|
|
126
|
+
relayPlugin: boolean;
|
|
127
|
+
/** Relay status-line context probe is injected (Claude only). */
|
|
128
|
+
statusLine: boolean;
|
|
129
|
+
/** Relay-provisioned skills materialized for this launch. */
|
|
130
|
+
skills: AssembledAsset[];
|
|
131
|
+
/** Project `.claude/skills` materialized for this Claude launch (#668). */
|
|
132
|
+
projectSkills: AssembledAsset[];
|
|
133
|
+
/** Host/global Claude skills explicitly inherited without enabling host MCP. */
|
|
134
|
+
hostSkills: AssembledAsset[];
|
|
135
|
+
/** Project `.claude/settings.json` sanitized and injected for this Claude launch (#669). */
|
|
136
|
+
projectSettings: { applied: boolean; keys: string[]; ignoredKeys: string[]; file?: string };
|
|
137
|
+
/** Relay-provisioned plugins (Claude --plugin-dir). Empty for Codex (no plugin surface). */
|
|
138
|
+
plugins: AssembledAsset[];
|
|
139
|
+
/** Host/global provider plugin dirs explicitly inherited without enabling host MCP. */
|
|
140
|
+
hostPluginDirs: string[];
|
|
141
|
+
mcp: AssembledMcp;
|
|
142
|
+
instructions: AssembledInstructions;
|
|
143
|
+
/** Provider provisioning/vanilla/instruction launch-arg block, in launch order. */
|
|
144
|
+
args: string[];
|
|
145
|
+
/** Provisioning-derived env additions (the isolated config-home var). */
|
|
146
|
+
env: Record<string, string>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Bundled relay assets (relocated from the adapters so the assembler — not each
|
|
150
|
+
// adapter — owns launch-arg construction; #557). Adapters re-export for test parity. ──
|
|
151
|
+
|
|
152
|
+
const CLAUDE_RELAY_PLUGIN_DIR = resolve(import.meta.dir, "../plugins/claude");
|
|
153
|
+
const CODEX_BUNDLED_SKILLS_DIR = resolve(import.meta.dir, "../plugins/codex/skills");
|
|
154
|
+
|
|
155
|
+
export function bundledCodexSkillDirs(baseDir = CODEX_BUNDLED_SKILLS_DIR): string[] {
|
|
156
|
+
if (!existsSync(baseDir)) return [];
|
|
157
|
+
return readdirSync(baseDir, { withFileTypes: true })
|
|
158
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(baseDir, entry.name, "SKILL.md")))
|
|
159
|
+
.map((entry) => join(baseDir, entry.name))
|
|
160
|
+
.sort();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function bundledSkillConfigArgs(skillDirs = bundledCodexSkillDirs()): string[] {
|
|
164
|
+
if (skillDirs.length === 0) return [];
|
|
165
|
+
const skillsConfig = skillDirs.map((dir) => `{path=${tomlString(dir)},enabled=true}`).join(",");
|
|
166
|
+
return ["-c", `skills.config=[${skillsConfig}]`];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// #552 vanilla base (Codex): suppress the AGENTS.md project-doc cascade. project_doc_max_bytes
|
|
170
|
+
// (default 32768) caps bytes Codex reads from AGENTS.md walking root→cwd; 0 stops before adding
|
|
171
|
+
// any, so no repo (or fresh-CODEX_HOME global) project doc loads. The isolated CODEX_HOME already
|
|
172
|
+
// drops host config.toml MCP/hooks; this closes the cwd-resident repo instruction channel.
|
|
173
|
+
export function codexProjectDocSuppressionArgs(config: Pick<RunnerSpawnConfig, "agentProfile">): string[] {
|
|
174
|
+
return profileIsVanillaBase(config) ? ["-c", "project_doc_max_bytes=0"] : [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function hasSettingsArg(args: string[]): boolean {
|
|
178
|
+
return args.some((arg) => arg === "--settings" || arg.startsWith("--settings="));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Claude relay status-line + readable-thinking settings, injected only when no caller
|
|
182
|
+
// --settings is already present (caller wins). Relocated from claude.ts (#557).
|
|
183
|
+
export function sessionStatusLineSettingsArgs(...argLists: string[][]): string[] {
|
|
184
|
+
if (argLists.some(hasSettingsArg)) return [];
|
|
185
|
+
return ["--settings", JSON.stringify(relayStatusLineSettings())];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function relayStatusLineSettings(): Record<string, unknown> {
|
|
189
|
+
return {
|
|
190
|
+
statusLine: { type: "command", command: `node ${shellQuote(claudeStatusLineProbePath())}`, refreshInterval: 30 },
|
|
191
|
+
showThinkingSummaries: true,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function claudeStatusLineProbePath(): string {
|
|
196
|
+
return resolve(import.meta.dir, "../plugins/claude/claude-statusline-probe.cjs");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function claudeSettingsArgs(
|
|
200
|
+
projectSettings: Record<string, unknown>,
|
|
201
|
+
includeStatusLine: boolean,
|
|
202
|
+
...argLists: string[][]
|
|
203
|
+
): string[] {
|
|
204
|
+
if (argLists.some(hasSettingsArg)) return [];
|
|
205
|
+
const settings = {
|
|
206
|
+
...projectSettings,
|
|
207
|
+
...(includeStatusLine ? relayStatusLineSettings() : {}),
|
|
208
|
+
};
|
|
209
|
+
return Object.keys(settings).length ? ["--settings", JSON.stringify(settings)] : [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Instruction disposition (the vanilla floor's effect on the host instruction cascade) ──
|
|
213
|
+
|
|
214
|
+
function repoInstructionDisposition(profile: AgentProfile): AssembledInstructionDisposition {
|
|
215
|
+
if (profile.instructions.repoInstructions === "allow") return "allow";
|
|
216
|
+
if (profile.base === "vanilla") return "suppressed";
|
|
217
|
+
// Non-vanilla isolated bases can request ignore but have no launch-time enforcement yet.
|
|
218
|
+
return "unsupported";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function globalInstructionDisposition(profile: AgentProfile): AssembledInstructionDisposition {
|
|
222
|
+
if (profile.instructions.globalInstructions === "allow") return "allow";
|
|
223
|
+
if (profile.base !== "host") return "isolated";
|
|
224
|
+
return "unsupported";
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function assembleInstructions(config: RunnerSpawnConfig): AssembledInstructions {
|
|
228
|
+
const profile = config.agentProfile;
|
|
229
|
+
return {
|
|
230
|
+
// A profile-less spawn is host-base: the full host instruction cascade is left available.
|
|
231
|
+
repo: profile ? repoInstructionDisposition(profile) : "allow",
|
|
232
|
+
global: profile ? globalInstructionDisposition(profile) : "allow",
|
|
233
|
+
systemPromptAppend: Boolean(config.systemPromptAppend?.trim()),
|
|
234
|
+
scopedKnowledge: "none",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── Per-provider arg assembly ──────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]): AssembledLaunch {
|
|
241
|
+
const configHome = providerHomePathFor("claude", config);
|
|
242
|
+
const assetHome = providerProvisioningHomePathFor("claude", config);
|
|
243
|
+
const vanilla = profileIsVanillaBase(config);
|
|
244
|
+
const projectConfigProjection = projectConfigProjectionOn(config);
|
|
245
|
+
const relayPlugin = profileAllowsRelayFeature(config, "plugins");
|
|
246
|
+
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
247
|
+
const statusLine = profileAllowsRelayFeature(config, "statusLine");
|
|
248
|
+
|
|
249
|
+
const skills = configHome ? resolveClaudeProvisionedSkills(configHome, config) : [];
|
|
250
|
+
const hostSkills = configHome
|
|
251
|
+
? resolveClaudeHostSkills(configHome, config, new Set(skills.map((s) => s.name)))
|
|
252
|
+
: [];
|
|
253
|
+
const projectSkills = configHome
|
|
254
|
+
? resolveClaudeProjectSkills(configHome, config, new Set([...skills, ...hostSkills].map((s) => s.name)))
|
|
255
|
+
: [];
|
|
256
|
+
const projectSettings = resolveProjectClaudeSettings(config);
|
|
257
|
+
const provisionedPlugins = assetHome ? resolveClaudeProvisionedPlugins(assetHome, config) : [];
|
|
258
|
+
|
|
259
|
+
// Plugin dirs (--plugin-dir loads explicitly, independent of --setting-sources):
|
|
260
|
+
// relay-bundled plugin → only when relay.plugins (carries the relay monitor +
|
|
261
|
+
// turn-lifecycle hooks #556, not reproducible via settings.json).
|
|
262
|
+
// relay-provisioned plugins (#556) → materialized into the isolated home.
|
|
263
|
+
// host plugin dirs → ONLY host-base profiles; isolated/vanilla must not pull them back in.
|
|
264
|
+
const relayPluginDirs = !config.agentProfile && relayPlugin ? [CLAUDE_RELAY_PLUGIN_DIR] : [];
|
|
265
|
+
const provisionedPluginDirs = provisionedPlugins.map((p) => p.dir);
|
|
266
|
+
const hostPluginDirs = profileAllowsHostAssets(config, "plugins") ? providerConfig.pluginDirs : [];
|
|
267
|
+
const pluginDirs = [...new Set([...relayPluginDirs, ...provisionedPluginDirs, ...hostPluginDirs])];
|
|
268
|
+
|
|
269
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
270
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
271
|
+
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
272
|
+
|
|
273
|
+
// #667 — project `.mcp.json` projection (opt-in). The spawn cwd's committed servers are
|
|
274
|
+
// injected through the SAME `--mcp-config` channel as relay-provisioned servers, so they
|
|
275
|
+
// survive `--strict-mcp-config` without the ambient project-discovery modal. A relay-
|
|
276
|
+
// provisioned server of the same name wins (the profile is the explicit, relay-owned
|
|
277
|
+
// source); the relay endpoint name is reserved. `declaredMcp` is the union relay injects.
|
|
278
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
279
|
+
const projectMcp = Object.fromEntries(
|
|
280
|
+
Object.entries(projectMcpRaw).filter(
|
|
281
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
const projectServers = Object.keys(projectMcp);
|
|
285
|
+
|
|
286
|
+
// #672 — shared host-listener MCP (opt-in, default OFF). When on, route the agent's
|
|
287
|
+
// NON-IDENTITY MCP (tokenlean, github) through ONE orchestrator-owned shared `callmux`
|
|
288
|
+
// listener via a per-agent `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry:
|
|
289
|
+
// the MCP session survives a listener/downstream restart, and --cwd gives each isolated
|
|
290
|
+
// worktree session-cwd isolation. It's a plain stdio server descriptor, so it rides the same
|
|
291
|
+
// controlled --mcp-config channel as provisioned servers (survives --strict-mcp-config) — no
|
|
292
|
+
// ambient discovery, no modal. Relay-owned infra → authoritative on its name (spread last);
|
|
293
|
+
// the relay endpoint stays per-agent (#215), NEVER on the shared listener.
|
|
294
|
+
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
295
|
+
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
296
|
+
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
297
|
+
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
|
298
|
+
: {};
|
|
299
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
300
|
+
|
|
301
|
+
// #662/#663 — strict controlled MCP is a Claude prompt-gate prevention entry, not a
|
|
302
|
+
// launch-assembly special case. It prevents the blocking "N new MCP servers found in
|
|
303
|
+
// this project" modal while preserving every relay-declared server in --mcp-config.
|
|
304
|
+
const strictMcp = claudeLaunchPromptGateSettings(config).strictMcpConfig;
|
|
305
|
+
|
|
306
|
+
// #662 follow-up — strict drops ALL on-disk config, including the host user-scope
|
|
307
|
+
// `~/.claude.json` MCP that a host-base agent loaded ambiently before strict. That
|
|
308
|
+
// over-reached: it stopped honoring `mcp.mode:"host"`, so default-relay workers/reviewers
|
|
309
|
+
// lost callmux/qmd/gh. Re-inject the host USER-scope servers (NOT the project `.mcp.json`,
|
|
310
|
+
// still excluded) when the launch both uses the host config home AND its mcp mode is host.
|
|
311
|
+
// An isolated/minimal base has its own config home and never had the real host's user MCP,
|
|
312
|
+
// so it stays relay+provisioned only even if a profile sets mode:"host".
|
|
313
|
+
const usesHostMcp = profileUsesProviderHostGlobals(config)
|
|
314
|
+
&& (!config.agentProfile || config.agentProfile.mcp.mode === "host");
|
|
315
|
+
const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
|
|
316
|
+
const hostMcpServers = Object.keys(hostMcp).filter(
|
|
317
|
+
(name) => name !== RELAY_MCP_SERVER_NAME && !(name in declaredMcp),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const instructions = assembleInstructions(config);
|
|
321
|
+
|
|
322
|
+
// Launch order matches the historical claude buildSpawnArgs block exactly so existing
|
|
323
|
+
// arg assertions hold: vanilla scope → plugin dirs → MCP → status-line → append.
|
|
324
|
+
const args = [
|
|
325
|
+
...(vanilla || projectConfigProjection ? ["--setting-sources", "user"] : []),
|
|
326
|
+
...pluginDirs.flatMap((dir) => ["--plugin-dir", dir]),
|
|
327
|
+
...claudeMcpLaunchArgs({
|
|
328
|
+
relayUrl: config.relayUrl,
|
|
329
|
+
...(config.relayMcpEndpoint ? { endpoint: config.relayMcpEndpoint } : {}),
|
|
330
|
+
includeRelay: relayEndpoint,
|
|
331
|
+
servers: declaredMcp,
|
|
332
|
+
hostServers: hostMcp,
|
|
333
|
+
strict: strictMcp,
|
|
334
|
+
}),
|
|
335
|
+
...claudeSettingsArgs(projectSettings.settings, statusLine, hostDefaultArgs, config.providerArgs),
|
|
336
|
+
...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
provider: "claude",
|
|
341
|
+
...(config.agentProfile ? { profile: config.agentProfile } : {}),
|
|
342
|
+
base: config.agentProfile?.base ?? "host",
|
|
343
|
+
vanilla,
|
|
344
|
+
...(configHome ? { configHome } : {}),
|
|
345
|
+
relayContext: profileAllowsRelayFeature(config, "context"),
|
|
346
|
+
relaySkills,
|
|
347
|
+
relayPlugin,
|
|
348
|
+
statusLine,
|
|
349
|
+
skills: skills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
350
|
+
hostSkills: hostSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
351
|
+
projectSkills: projectSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
352
|
+
projectSettings: {
|
|
353
|
+
applied: projectSettings.applied,
|
|
354
|
+
keys: projectSettings.keys,
|
|
355
|
+
ignoredKeys: projectSettings.ignoredKeys,
|
|
356
|
+
...(projectSettings.file ? { file: projectSettings.file } : {}),
|
|
357
|
+
},
|
|
358
|
+
plugins: provisionedPlugins.map((p) => ({ name: p.name, dir: p.dir })),
|
|
359
|
+
hostPluginDirs,
|
|
360
|
+
mcp: {
|
|
361
|
+
relayEndpoint, strict: strictMcp, servers: mcpServers, projectServers, hostServers: hostMcpServers,
|
|
362
|
+
...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
|
|
363
|
+
},
|
|
364
|
+
instructions,
|
|
365
|
+
args,
|
|
366
|
+
env: configHomeEnv("claude", configHome),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfig): AssembledLaunch {
|
|
371
|
+
const configHome = providerHomePathFor("codex", config);
|
|
372
|
+
const assetHome = providerProvisioningHomePathFor("codex", config);
|
|
373
|
+
const vanilla = profileIsVanillaBase(config);
|
|
374
|
+
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
375
|
+
const relayEndpoint = profileAllowsRelayFeature(config, "mcp");
|
|
376
|
+
|
|
377
|
+
const provisionedSkills = assetHome ? resolveCodexProvisionedSkills(assetHome, config) : [];
|
|
378
|
+
const bundledSkillDirs = !config.agentProfile && relaySkills ? bundledCodexSkillDirs() : [];
|
|
379
|
+
const skillDirs = [...bundledSkillDirs, ...provisionedSkills.map((s) => s.dir)];
|
|
380
|
+
|
|
381
|
+
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
382
|
+
const mcpServers = Object.keys(provisionedMcp).filter((name) => name !== RELAY_MCP_SERVER_NAME);
|
|
383
|
+
|
|
384
|
+
// #667 — project `.mcp.json` projection (opt-in), same union rule as Claude: a relay-
|
|
385
|
+
// provisioned server of the same name wins; the relay endpoint name is reserved. Codex has
|
|
386
|
+
// no `.mcp.json` discovery of its own, so this is purely additive (no strict/modal concern).
|
|
387
|
+
const projectMcpRaw = resolveProjectMcpServers(config);
|
|
388
|
+
const projectMcp = Object.fromEntries(
|
|
389
|
+
Object.entries(projectMcpRaw).filter(
|
|
390
|
+
([name]) => name !== RELAY_MCP_SERVER_NAME && !(name in provisionedMcp),
|
|
391
|
+
),
|
|
392
|
+
);
|
|
393
|
+
const projectServers = Object.keys(projectMcp);
|
|
394
|
+
|
|
395
|
+
// #672 — shared host-listener MCP (opt-in, default OFF), same as Claude: a per-agent
|
|
396
|
+
// `callmux bridge --url <listener> --cwd "$WORKTREE"` stdio entry routes the agent's
|
|
397
|
+
// NON-IDENTITY MCP (tokenlean, github) through the shared listener (session survives a
|
|
398
|
+
// restart; --cwd gives session-cwd isolation). Emitted via the same `-c mcp_servers.*`
|
|
399
|
+
// overrides as provisioned servers. The relay endpoint stays per-agent (#215).
|
|
400
|
+
const sharedListenerOn = Boolean(config.agentProfile?.sharedMcp);
|
|
401
|
+
const sharedListenerUrl = sharedListenerOn ? resolveSharedMcpUrl(config.sharedMcpUrl) : undefined;
|
|
402
|
+
const sharedMcp: Record<string, ProvisioningMcpServer> = sharedListenerUrl
|
|
403
|
+
? { [SHARED_MCP_SERVER_NAME]: sharedMcpBridgeServer(sharedListenerUrl, config.cwd) }
|
|
404
|
+
: {};
|
|
405
|
+
const declaredMcp = { ...projectMcp, ...provisionedMcp, ...sharedMcp };
|
|
406
|
+
|
|
407
|
+
const instructions = assembleInstructions(config);
|
|
408
|
+
|
|
409
|
+
// The skills/MCP/project-doc block; core launch config (model/approval/listen) stays
|
|
410
|
+
// in the adapter. -c overrides are order-independent, so the assembled block is contiguous.
|
|
411
|
+
const args = [
|
|
412
|
+
...bundledSkillConfigArgs(skillDirs),
|
|
413
|
+
...(relayEndpoint ? relayMcpCodexConfigArgs(config.relayUrl, config.relayMcpEndpoint) : []),
|
|
414
|
+
...codexProvisionedMcpArgs(declaredMcp),
|
|
415
|
+
...codexProjectDocSuppressionArgs(config),
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
provider: "codex",
|
|
420
|
+
...(config.agentProfile ? { profile: config.agentProfile } : {}),
|
|
421
|
+
base: config.agentProfile?.base ?? "host",
|
|
422
|
+
vanilla,
|
|
423
|
+
...(configHome ? { configHome } : {}),
|
|
424
|
+
relayContext: profileAllowsRelayFeature(config, "context"),
|
|
425
|
+
relaySkills,
|
|
426
|
+
relayPlugin: false,
|
|
427
|
+
statusLine: false,
|
|
428
|
+
skills: provisionedSkills.map((s) => ({ name: s.name, dir: s.dir })),
|
|
429
|
+
hostSkills: [],
|
|
430
|
+
projectSkills: [],
|
|
431
|
+
projectSettings: { applied: false, keys: [], ignoredKeys: [] },
|
|
432
|
+
plugins: [],
|
|
433
|
+
hostPluginDirs: [],
|
|
434
|
+
mcp: {
|
|
435
|
+
relayEndpoint, strict: false, servers: mcpServers, projectServers, hostServers: [],
|
|
436
|
+
...(sharedListenerUrl ? { sharedListener: { url: sharedListenerUrl } } : {}),
|
|
437
|
+
},
|
|
438
|
+
instructions,
|
|
439
|
+
args,
|
|
440
|
+
env: configHomeEnv("codex", configHome),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Manifest-driven env + dispatch ────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
function configHomeEnv(provider: string, configHome: string | undefined): Record<string, string> {
|
|
447
|
+
if (!configHome) return {};
|
|
448
|
+
const manifest = getManifest(provider);
|
|
449
|
+
const envVar = manifest?.home?.configDirEnvVar;
|
|
450
|
+
return envVar ? { [envVar]: configHome } : {};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
type Assembler = (config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]) => AssembledLaunch;
|
|
454
|
+
type Materializer = (config: RunnerSpawnConfig) => void;
|
|
455
|
+
|
|
456
|
+
const ASSEMBLERS: Record<string, Assembler> = {
|
|
457
|
+
claude: assembleClaude,
|
|
458
|
+
codex: (config, providerConfig) => assembleCodex(config, providerConfig),
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const MATERIALIZERS: Record<string, Materializer> = {
|
|
462
|
+
claude: materializeClaudeAssets,
|
|
463
|
+
codex: materializeCodexAssets,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
function materializeClaudeAssets(config: RunnerSpawnConfig): void {
|
|
467
|
+
const configHome = providerHomePathFor("claude", config);
|
|
468
|
+
const assetHome = providerProvisioningHomePathFor("claude", config);
|
|
469
|
+
if (configHome) {
|
|
470
|
+
const skills = resolveClaudeProvisionedSkills(configHome, config);
|
|
471
|
+
materializeResolvedAssets(skills);
|
|
472
|
+
const hostSkills = resolveClaudeHostSkills(configHome, config, new Set(skills.map((s) => s.name)));
|
|
473
|
+
materializeResolvedAssets(hostSkills);
|
|
474
|
+
materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set([...skills, ...hostSkills].map((s) => s.name))));
|
|
475
|
+
}
|
|
476
|
+
if (assetHome) materializeResolvedAssets(resolveClaudeProvisionedPlugins(assetHome, config));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function materializeCodexAssets(config: RunnerSpawnConfig): void {
|
|
480
|
+
const assetHome = providerProvisioningHomePathFor("codex", config);
|
|
481
|
+
if (assetHome) materializeResolvedAssets(resolveCodexProvisionedSkills(assetHome, config));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Assemble the launch for `provider` from the resolved profile + provisioning. PURE:
|
|
486
|
+
* resolves which declared assets exist (filesystem reads) but writes nothing. A missing
|
|
487
|
+
* `agentProfile` assembles the legacy default (host base, relay features on, no provisioning).
|
|
488
|
+
*/
|
|
489
|
+
export function assembleLaunch(provider: SpawnProvider, config: RunnerSpawnConfig, providerConfig: ProviderConfig): AssembledLaunch {
|
|
490
|
+
const assembler = ASSEMBLERS[provider];
|
|
491
|
+
if (!assembler) throw new Error(`no launch assembler registered for provider: ${provider}`);
|
|
492
|
+
const hostDefaultArgs = profileUsesProviderHostGlobals(config) ? providerConfig.defaultArgs : [];
|
|
493
|
+
return assembler(config, providerConfig, hostDefaultArgs);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Write the provisioned assets the assembly describes (Claude skills + plugins, Codex
|
|
498
|
+
* skills). Driven by the SAME resolvers `assembleLaunch` uses, so disk == launch ==
|
|
499
|
+
* report. Idempotent; the isolated home must already exist (prepare*ProfileHome). No-op
|
|
500
|
+
* for a host-base profile.
|
|
501
|
+
*/
|
|
502
|
+
export function materializeLaunchAssembly(provider: SpawnProvider, config: RunnerSpawnConfig): void {
|
|
503
|
+
const configHome = providerHomePathFor(provider, config);
|
|
504
|
+
const assetHome = providerProvisioningHomePathFor(provider, config);
|
|
505
|
+
if (!configHome && !assetHome) return;
|
|
506
|
+
const materializer = MATERIALIZERS[provider];
|
|
507
|
+
if (materializer) materializer(config);
|
|
508
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LivenessSignal } from "agent-relay-sdk";
|
|
2
|
+
|
|
3
|
+
// The raw runner-tracked signals an adapter folds into a normalized LivenessSignal.
|
|
4
|
+
// This is the INPUT to the contract computation (#746): the runner owns this state
|
|
5
|
+
// (transport frames, the last provider event, the pending timeline transition, the
|
|
6
|
+
// active-work set), passes it to the adapter's `liveness()` verdict, and ships the
|
|
7
|
+
// resulting LivenessSignal on the agent card. Provider-raw interpretation stops here —
|
|
8
|
+
// nothing above the runner ever reads these fields again.
|
|
9
|
+
export interface LivenessInputs {
|
|
10
|
+
/** Runner→server transport frame/ack timestamps (sdk BusClient.transportState). */
|
|
11
|
+
transport?: { lastServerFrameAt?: number; lastHeartbeatAckAt?: number };
|
|
12
|
+
/** Epoch-ms of the most recent provider status/token event the runner observed. */
|
|
13
|
+
lastProviderEventAt?: number;
|
|
14
|
+
/** The most recent lifecycle/timeline transition the runner published (status + ts). */
|
|
15
|
+
timelineEvent?: { status?: string; timestamp?: number };
|
|
16
|
+
/** Active work records. The adapter classifies its own kinds; `provider-turn` is the
|
|
17
|
+
* bare turn, anything else (subagent, background-script, tool, subprocess) is live work. */
|
|
18
|
+
activeWork: Array<{ kind?: string; startedAt?: number }>;
|
|
19
|
+
/** Fallback progress floor when no other progress evidence exists yet (e.g. work start). */
|
|
20
|
+
fallbackProgressAt?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function num(value: unknown): number {
|
|
24
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The shared liveness fold (#725 / #746). Both reference adapters (Claude #713, Codex #724)
|
|
29
|
+
* route through this so the verdict is computed in ONE place; each adapter's `liveness()`
|
|
30
|
+
* decides WHICH raw signals it feeds in (Claude surfaces a `compacting` timeline transition
|
|
31
|
+
* as progress; Codex surfaces live subprocess work-kinds). The fold itself is provider-
|
|
32
|
+
* agnostic, which is the point of the contract: a new provider only has to feed its signals.
|
|
33
|
+
*/
|
|
34
|
+
export function computeLivenessSignal(inputs: LivenessInputs): LivenessSignal {
|
|
35
|
+
const transportLiveAt = Math.max(
|
|
36
|
+
num(inputs.transport?.lastServerFrameAt),
|
|
37
|
+
num(inputs.transport?.lastHeartbeatAckAt),
|
|
38
|
+
);
|
|
39
|
+
// Progress = provider tokens OR a lifecycle/timeline transition (e.g. native compaction,
|
|
40
|
+
// #713) — whichever is latest, with the caller-supplied floor as the baseline.
|
|
41
|
+
const lastProgressAt = Math.max(
|
|
42
|
+
num(inputs.lastProviderEventAt),
|
|
43
|
+
num(inputs.timelineEvent?.timestamp),
|
|
44
|
+
num(inputs.fallbackProgressAt),
|
|
45
|
+
);
|
|
46
|
+
// Any work-kind that is NOT the bare provider turn is live tool/subprocess work (#724):
|
|
47
|
+
// a wedged provider turn with a live `bun test` child must read as progressing.
|
|
48
|
+
const hasLiveWork = inputs.activeWork.some((work) => Boolean(work.kind && work.kind !== "provider-turn"));
|
|
49
|
+
return { transportLiveAt, lastProgressAt, hasLiveWork };
|
|
50
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
4
|
+
import { logLevelFromEnv } from "./config";
|
|
5
|
+
|
|
6
|
+
// Phase 1 observability (#198): one leveled, runtime-togglable logger for the
|
|
7
|
+
// Runner and the provider adapters below it. Replaces the ad-hoc scatter of
|
|
8
|
+
// `console.error`, `logRunnerDiagnostic` (-> runner-<agent>.log) and
|
|
9
|
+
// `sessionLog`/`sessionDebug` (-> session-mirror-<agent>.log) with a single
|
|
10
|
+
// switch and a single greppable, ANSI-free sink.
|
|
11
|
+
//
|
|
12
|
+
// Sink: the per-agent `session-mirror-<agent>.log` — the file the orchestrator
|
|
13
|
+
// already surfaces to the dashboard log-viewer (captureSessionMirror). One place
|
|
14
|
+
// to look when anything in the Runner misbehaves.
|
|
15
|
+
//
|
|
16
|
+
// Level is read once from AGENT_RELAY_LOG_LEVEL (default "info") and can be
|
|
17
|
+
// flipped at runtime via the control port (no restart) — so a phase refactor can
|
|
18
|
+
// be watched at debug without bouncing the agent.
|
|
19
|
+
|
|
20
|
+
type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
|
|
21
|
+
|
|
22
|
+
const ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40, fatal: 50 };
|
|
23
|
+
export const LOG_LEVELS = Object.keys(ORDER) as LogLevel[];
|
|
24
|
+
|
|
25
|
+
export function parseLogLevel(value: string | undefined | null): LogLevel | undefined {
|
|
26
|
+
if (!value) return undefined;
|
|
27
|
+
const v = value.trim().toLowerCase();
|
|
28
|
+
return (LOG_LEVELS as string[]).includes(v) ? (v as LogLevel) : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Shared with the orchestrator (session-mirror reader) + runner outbox via the
|
|
32
|
+
// SDK helper, so all sites resolve the identical filename for a given agent id.
|
|
33
|
+
function safeLogName(value: string): string {
|
|
34
|
+
return sanitizeFsName(value, { replacement: "_", maxLen: 180 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface LoggerConfig {
|
|
38
|
+
agentId?: string;
|
|
39
|
+
level?: LogLevel;
|
|
40
|
+
headless?: boolean;
|
|
41
|
+
logDir?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class Logger {
|
|
45
|
+
private level: LogLevel;
|
|
46
|
+
private agentId: string;
|
|
47
|
+
private headless: boolean;
|
|
48
|
+
private logDir: string;
|
|
49
|
+
|
|
50
|
+
constructor(config: LoggerConfig = {}) {
|
|
51
|
+
this.level = config.level ?? parseLogLevel(logLevelFromEnv()) ?? "info";
|
|
52
|
+
this.agentId = config.agentId ?? "runner";
|
|
53
|
+
this.headless = config.headless ?? false;
|
|
54
|
+
this.logDir = config.logDir ?? join(process.env.HOME || ".", ".agent-relay", "logs");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Bind the logger to a concrete agent once the runner knows its id. Preserves a
|
|
58
|
+
// level already set via env/runtime unless an explicit level is passed.
|
|
59
|
+
configure(config: LoggerConfig): void {
|
|
60
|
+
if (config.agentId !== undefined) this.agentId = config.agentId;
|
|
61
|
+
if (config.headless !== undefined) this.headless = config.headless;
|
|
62
|
+
if (config.logDir !== undefined) this.logDir = config.logDir;
|
|
63
|
+
if (config.level !== undefined) this.level = config.level;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setLevel(level: LogLevel): void { this.level = level; }
|
|
67
|
+
getLevel(): LogLevel { return this.level; }
|
|
68
|
+
isEnabled(level: LogLevel): boolean { return ORDER[level] >= ORDER[this.level]; }
|
|
69
|
+
|
|
70
|
+
debug(component: string, message: string): void { this.log("debug", component, message); }
|
|
71
|
+
info(component: string, message: string): void { this.log("info", component, message); }
|
|
72
|
+
warn(component: string, message: string): void { this.log("warn", component, message); }
|
|
73
|
+
error(component: string, message: string): void { this.log("error", component, message); }
|
|
74
|
+
fatal(component: string, message: string): void { this.log("fatal", component, message); }
|
|
75
|
+
|
|
76
|
+
log(level: LogLevel, component: string, message: string): void {
|
|
77
|
+
if (!this.isEnabled(level)) return;
|
|
78
|
+
const line = `[${new Date().toISOString()}] ${level.toUpperCase().padEnd(5)} [${component}] ${oneLine(message)}\n`;
|
|
79
|
+
try {
|
|
80
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
81
|
+
appendFileSync(join(this.logDir, `session-mirror-${safeLogName(this.agentId)}.log`), line);
|
|
82
|
+
} catch {
|
|
83
|
+
// Best-effort. If the per-agent file can't be written, surface error/fatal to
|
|
84
|
+
// stderr so it is not lost entirely (headless: lands in the orchestrator log).
|
|
85
|
+
if (ORDER[level] >= ORDER.error) { try { console.error(line.trimEnd()); } catch { /* give up */ } }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Newlines would split one record across several log lines and break greppability;
|
|
91
|
+
// collapse them so a multi-line message stays one line.
|
|
92
|
+
function oneLine(message: string): string {
|
|
93
|
+
return message.replace(/\r?\n/g, " ⏎ ");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Process-global logger. A runner process serves exactly one agent, so a singleton
|
|
97
|
+
// is the right scope; the runner calls configure() once it knows its id, and
|
|
98
|
+
// adapters import this instance directly (no constructor threading).
|
|
99
|
+
export const logger = new Logger();
|