agent-relay-runner 0.102.2 → 0.103.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +238 -16
- package/src/{claude-quota-harvest.ts → adapters/claude-quota-harvest.ts} +2 -2
- package/src/config.ts +8 -3
- package/src/index.ts +27 -10
- package/src/launch-assembly.ts +52 -20
- package/src/profile-home.ts +40 -32
- package/src/profile-projection.ts +62 -40
- package/src/providers.ts +4 -2
- package/src/relay-injection-events.ts +3 -6
- package/src/runner-core.ts +10 -9
- package/src/runner-helpers.ts +69 -49
- package/src/runner.ts +1 -1
- package/src/version.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.103.4",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-
|
|
23
|
+
"agent-relay-providers": "0.102.2",
|
|
24
|
+
"agent-relay-sdk": "0.2.87"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"@types/bun": "latest",
|
|
@@ -6,10 +6,32 @@ function isRecord(value) {
|
|
|
6
6
|
// sdk/src/types/constants.ts
|
|
7
7
|
var DEFAULT_RELAY_PORT = 4850;
|
|
8
8
|
var DEFAULT_RELAY_URL = `http://127.0.0.1:${DEFAULT_RELAY_PORT}`;
|
|
9
|
-
//
|
|
9
|
+
// providers/registry.ts
|
|
10
|
+
var PROVIDER_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
11
|
+
var manifests = new Map;
|
|
12
|
+
function registerProvider(manifest) {
|
|
13
|
+
if (!PROVIDER_ID_RE.test(manifest.id)) {
|
|
14
|
+
throw new Error(`invalid provider id: ${manifest.id}`);
|
|
15
|
+
}
|
|
16
|
+
if (!manifest.label.trim()) {
|
|
17
|
+
throw new Error(`provider ${manifest.id} is missing a label`);
|
|
18
|
+
}
|
|
19
|
+
if (!manifest.icon.trim()) {
|
|
20
|
+
throw new Error(`provider ${manifest.id} is missing an icon`);
|
|
21
|
+
}
|
|
22
|
+
if (manifests.has(manifest.id)) {
|
|
23
|
+
throw new Error(`provider already registered: ${manifest.id}`);
|
|
24
|
+
}
|
|
25
|
+
manifests.set(manifest.id, manifest);
|
|
26
|
+
}
|
|
27
|
+
function getAllManifests() {
|
|
28
|
+
return [...manifests.values()];
|
|
29
|
+
}
|
|
30
|
+
var accountKeyAliasResolvers = new Map;
|
|
31
|
+
|
|
32
|
+
// providers/claude.ts
|
|
10
33
|
var CLAUDE_LOW_TO_MAX = ["low", "medium", "high", "max"];
|
|
11
34
|
var CLAUDE_LOW_TO_XHIGH_MAX = ["low", "medium", "high", "xhigh", "max"];
|
|
12
|
-
var CODEX_REASONING = ["low", "medium", "high", "xhigh"];
|
|
13
35
|
var CONTEXT_200K = { value: 200000, source: "catalog", confidence: "declared" };
|
|
14
36
|
var CONTEXT_1M = { value: 1e6, source: "catalog", confidence: "declared" };
|
|
15
37
|
var CODE_MODEL_CAPABILITIES = {
|
|
@@ -26,16 +48,26 @@ var CODE_MODEL_CAPABILITIES = {
|
|
|
26
48
|
source: "catalog",
|
|
27
49
|
confidence: "declared"
|
|
28
50
|
};
|
|
51
|
+
var quotaWindows = [
|
|
52
|
+
{ name: "five_hour", unit: "%", reset: true },
|
|
53
|
+
{ name: "seven_day", unit: "%", reset: true }
|
|
54
|
+
];
|
|
29
55
|
function codeModel(limits) {
|
|
30
56
|
return {
|
|
31
57
|
...limits ? { limits } : {},
|
|
32
58
|
capabilities: CODE_MODEL_CAPABILITIES
|
|
33
59
|
};
|
|
34
60
|
}
|
|
35
|
-
var
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
61
|
+
var claudeProviderManifest = {
|
|
62
|
+
id: "claude",
|
|
63
|
+
label: "Claude",
|
|
64
|
+
icon: "\uD83D\uDFE0",
|
|
65
|
+
launch: {
|
|
66
|
+
managedPolicyPrompt: {
|
|
67
|
+
alwaysOn: "append-system-prompt"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
catalog: {
|
|
39
71
|
defaultModel: "sonnet-4.6",
|
|
40
72
|
models: [
|
|
41
73
|
{ alias: "fable-5", label: "Fable 5", providerModel: "claude-fable-5", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", unavailable: "suspended by export-control directive, 2026-06-12", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
@@ -50,20 +82,210 @@ var PROVIDER_CATALOG = {
|
|
|
50
82
|
{ alias: "haiku", label: "Haiku", providerModel: "haiku", efforts: [], ...codeModel() }
|
|
51
83
|
]
|
|
52
84
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
quotaWindows: [...quotaWindows],
|
|
86
|
+
capabilityDefaults: {
|
|
87
|
+
lifecycle: {
|
|
88
|
+
managed: true,
|
|
89
|
+
shutdownHard: { when: "headless", value: true, otherwise: false },
|
|
90
|
+
restartHard: { when: "headless", value: true, otherwise: false },
|
|
91
|
+
semanticStatus: true,
|
|
92
|
+
reconnect: true
|
|
93
|
+
},
|
|
94
|
+
session: {
|
|
95
|
+
fileRead: true,
|
|
96
|
+
fileWrite: { when: "approval:not-read-only", value: true, otherwise: false },
|
|
97
|
+
shell: true,
|
|
98
|
+
shellMode: { open: "unrestricted", guarded: "guarded", readOnly: "read-only-guarded" }
|
|
99
|
+
},
|
|
100
|
+
context: {
|
|
101
|
+
managed: "headless",
|
|
102
|
+
compact: { when: "headless", value: true, otherwise: false },
|
|
103
|
+
clear: { when: "headless", value: true, otherwise: false },
|
|
104
|
+
inject: true,
|
|
105
|
+
resumePreference: ["native", "clear-inject", "none"]
|
|
106
|
+
},
|
|
107
|
+
quota: {
|
|
108
|
+
supported: true,
|
|
109
|
+
source: "probe",
|
|
110
|
+
unit: "%",
|
|
111
|
+
windows: [...quotaWindows]
|
|
112
|
+
},
|
|
113
|
+
terminal: {
|
|
114
|
+
mode: "live",
|
|
115
|
+
condition: "headless",
|
|
116
|
+
live: { read: true, write: true }
|
|
117
|
+
},
|
|
118
|
+
liveSession: {
|
|
119
|
+
capture: true,
|
|
120
|
+
inject: true,
|
|
121
|
+
interrupt: true,
|
|
122
|
+
promptEcho: true,
|
|
123
|
+
reasoning: true,
|
|
124
|
+
slashCommands: true
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
slashCommands: ["/clear", "/compact", "/model", "/help"],
|
|
128
|
+
resume: { supported: true, idFormat: "uuid-v4", idPattern: "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" },
|
|
129
|
+
home: { configDir: ".claude", envPrefix: "CLAUDE", configDirEnvVar: "CLAUDE_CONFIG_DIR", quotaCredentialFile: ".credentials.json", authItems: [".credentials.json", "statsig"] },
|
|
130
|
+
provisioning: {
|
|
131
|
+
pluginManifestPath: ".claude-plugin/plugin.json",
|
|
132
|
+
bundledPlugin: {
|
|
133
|
+
ref: "agent-relay-claude-plugin",
|
|
134
|
+
dir: "runner/plugins/claude",
|
|
135
|
+
title: "Agent Relay Claude plugin",
|
|
136
|
+
description: "Bundled Agent Relay Claude plugin, materialized whole through the provisioning registry.",
|
|
137
|
+
omitFiles: ["monitors/relay-monitor.provisioned.mjs"],
|
|
138
|
+
fileOverrides: [{ path: "monitors/relay-monitor.ts", sourcePath: "monitors/relay-monitor.provisioned.mjs" }]
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
upgrade: {
|
|
142
|
+
packages: ["agent-relay-runner", "agent-relay-plugin"],
|
|
143
|
+
detectPackages: ["agent-relay-runner"],
|
|
144
|
+
detectCommands: ["claude"]
|
|
145
|
+
},
|
|
146
|
+
probe: {
|
|
147
|
+
command: "claude",
|
|
148
|
+
args: ["--version"],
|
|
149
|
+
healthCheck: "claude --version",
|
|
150
|
+
featureChecks: [{ name: "rig", command: "claude-rig", homeDir: ".claude-rig" }]
|
|
151
|
+
},
|
|
152
|
+
quotaPoll: { strategy: "none" },
|
|
153
|
+
shim: { kind: "claude-relay" }
|
|
154
|
+
};
|
|
155
|
+
registerProvider(claudeProviderManifest);
|
|
156
|
+
|
|
157
|
+
// providers/codex.ts
|
|
158
|
+
var CODEX_REASONING = ["low", "medium", "high", "xhigh"];
|
|
159
|
+
var CONTEXT_200K2 = { value: 200000, source: "catalog", confidence: "declared" };
|
|
160
|
+
var CODE_MODEL_CAPABILITIES2 = {
|
|
161
|
+
modalities: {
|
|
162
|
+
input: { text: true, image: true },
|
|
163
|
+
output: { text: true }
|
|
164
|
+
},
|
|
165
|
+
tools: {
|
|
166
|
+
code: true,
|
|
167
|
+
review: true,
|
|
168
|
+
debug: true,
|
|
169
|
+
refactor: true
|
|
170
|
+
},
|
|
171
|
+
source: "catalog",
|
|
172
|
+
confidence: "declared"
|
|
173
|
+
};
|
|
174
|
+
var quotaWindows2 = [
|
|
175
|
+
{ name: "primary", unit: "%", reset: true },
|
|
176
|
+
{ name: "secondary", unit: "%", reset: true }
|
|
177
|
+
];
|
|
178
|
+
function codeModel2(limits) {
|
|
179
|
+
return {
|
|
180
|
+
...limits ? { limits } : {},
|
|
181
|
+
capabilities: CODE_MODEL_CAPABILITIES2
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
var codexProviderManifest = {
|
|
185
|
+
id: "codex",
|
|
186
|
+
label: "Codex",
|
|
187
|
+
icon: "\u2328\uFE0F",
|
|
188
|
+
launch: {
|
|
189
|
+
managedPolicyPrompt: {
|
|
190
|
+
alwaysOn: "prompt"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
catalog: {
|
|
56
194
|
defaultModel: "gpt-5.5",
|
|
57
195
|
models: [
|
|
58
|
-
{ alias: "gpt-5.5", label: "GPT-5.5", providerModel: "gpt-5.5", efforts: CODEX_REASONING, defaultEffort: "medium", ...
|
|
59
|
-
{ alias: "gpt-5.4", label: "GPT-5.4", providerModel: "gpt-5.4", efforts: CODEX_REASONING, defaultEffort: "medium", ...
|
|
60
|
-
{ alias: "gpt-5.4-mini", label: "GPT-5.4 Mini", providerModel: "gpt-5.4-mini", efforts: CODEX_REASONING, defaultEffort: "medium", ...
|
|
61
|
-
{ alias: "gpt-5.3-codex", label: "GPT-5.3 Codex", providerModel: "gpt-5.3-codex", efforts: CODEX_REASONING, defaultEffort: "medium", ...
|
|
62
|
-
{ alias: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark", providerModel: "gpt-5.3-codex-spark", efforts: CODEX_REASONING, defaultEffort: "high", ...
|
|
63
|
-
{ alias: "gpt-5.2", label: "GPT-5.2", providerModel: "gpt-5.2", efforts: CODEX_REASONING, defaultEffort: "medium", ...
|
|
196
|
+
{ alias: "gpt-5.5", label: "GPT-5.5", providerModel: "gpt-5.5", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
197
|
+
{ alias: "gpt-5.4", label: "GPT-5.4", providerModel: "gpt-5.4", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
198
|
+
{ alias: "gpt-5.4-mini", label: "GPT-5.4 Mini", providerModel: "gpt-5.4-mini", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
199
|
+
{ alias: "gpt-5.3-codex", label: "GPT-5.3 Codex", providerModel: "gpt-5.3-codex", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
200
|
+
{ alias: "gpt-5.3-codex-spark", label: "GPT-5.3 Codex Spark", providerModel: "gpt-5.3-codex-spark", efforts: CODEX_REASONING, defaultEffort: "high", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) },
|
|
201
|
+
{ alias: "gpt-5.2", label: "GPT-5.2", providerModel: "gpt-5.2", efforts: CODEX_REASONING, defaultEffort: "medium", ...codeModel2({ contextWindowTokens: CONTEXT_200K2 }) }
|
|
64
202
|
]
|
|
65
|
-
}
|
|
203
|
+
},
|
|
204
|
+
quotaWindows: [...quotaWindows2],
|
|
205
|
+
capabilityDefaults: {
|
|
206
|
+
lifecycle: {
|
|
207
|
+
managed: true,
|
|
208
|
+
shutdownHard: true,
|
|
209
|
+
restartHard: true,
|
|
210
|
+
semanticStatus: true,
|
|
211
|
+
reconnect: true
|
|
212
|
+
},
|
|
213
|
+
session: {
|
|
214
|
+
fileRead: true,
|
|
215
|
+
fileWrite: { when: "approval:not-read-only", value: true, otherwise: false },
|
|
216
|
+
shell: { when: "approval:not-read-only", value: true, otherwise: false },
|
|
217
|
+
shellMode: { open: "unrestricted", guarded: "guarded", readOnly: "none" }
|
|
218
|
+
},
|
|
219
|
+
context: {
|
|
220
|
+
managed: "always",
|
|
221
|
+
compact: true,
|
|
222
|
+
clear: true,
|
|
223
|
+
inject: true,
|
|
224
|
+
resumePreference: ["native", "clear-inject", "none"]
|
|
225
|
+
},
|
|
226
|
+
quota: {
|
|
227
|
+
supported: true,
|
|
228
|
+
source: "probe",
|
|
229
|
+
unit: "%",
|
|
230
|
+
windows: [...quotaWindows2]
|
|
231
|
+
},
|
|
232
|
+
terminal: {
|
|
233
|
+
mode: "attach",
|
|
234
|
+
attach: {
|
|
235
|
+
create: true,
|
|
236
|
+
read: true,
|
|
237
|
+
write: true,
|
|
238
|
+
detach: true
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
liveSession: {
|
|
242
|
+
capture: true,
|
|
243
|
+
inject: true,
|
|
244
|
+
interrupt: true,
|
|
245
|
+
promptEcho: true,
|
|
246
|
+
reasoning: true,
|
|
247
|
+
slashCommands: true
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
slashCommands: ["/compact", "/new"],
|
|
251
|
+
resume: { supported: true, idFormat: "codex-thread-id", idPattern: "[\\w-]+" },
|
|
252
|
+
home: { configDir: ".codex", envPrefix: "CODEX", configDirEnvVar: "CODEX_HOME", quotaCredentialFile: "auth.json", authItems: ["auth.json", "installation_id"] },
|
|
253
|
+
provisioning: {
|
|
254
|
+
pluginManifestPath: ".codex-plugin/plugin.json",
|
|
255
|
+
bundledSkills: {
|
|
256
|
+
refPrefix: "agent-relay-codex-",
|
|
257
|
+
dir: "runner/plugins/codex/skills",
|
|
258
|
+
titlePrefix: "Agent Relay Codex skill: ",
|
|
259
|
+
description: "Bundled Agent Relay Codex skill, materialized through the provisioning registry.",
|
|
260
|
+
entryFile: "SKILL.md"
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
upgrade: {
|
|
264
|
+
packages: ["agent-relay-runner", "agent-relay-codex"],
|
|
265
|
+
detectPackages: ["agent-relay-runner", "agent-relay-codex"],
|
|
266
|
+
detectCommands: ["codex-relay", "agent-relay-runner"]
|
|
267
|
+
},
|
|
268
|
+
probe: {
|
|
269
|
+
command: "codex",
|
|
270
|
+
args: ["--version"],
|
|
271
|
+
healthCheck: "codex --version"
|
|
272
|
+
},
|
|
273
|
+
quotaPoll: { strategy: "codex-app-server" },
|
|
274
|
+
shim: { kind: "codex-relay", extraBinPaths: ["codex/bin/codex"], cleanLegacyHooks: true }
|
|
66
275
|
};
|
|
276
|
+
registerProvider(codexProviderManifest);
|
|
277
|
+
// sdk/src/types/agents.ts
|
|
278
|
+
var SPAWN_PROVIDERS = getAllManifests().map((manifest) => manifest.id);
|
|
279
|
+
// sdk/src/provider-catalog.ts
|
|
280
|
+
function manifestToCatalogEntry(manifest) {
|
|
281
|
+
return {
|
|
282
|
+
provider: manifest.id,
|
|
283
|
+
label: manifest.label,
|
|
284
|
+
defaultModel: manifest.catalog.defaultModel,
|
|
285
|
+
models: manifest.catalog.models
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
var PROVIDER_CATALOG = Object.fromEntries(getAllManifests().map((manifest) => [manifest.id, manifestToCatalogEntry(manifest)]));
|
|
67
289
|
// sdk/src/reconnect.ts
|
|
68
290
|
import { setTimeout as delay } from "timers/promises";
|
|
69
291
|
var DEFAULTS = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QuotaState, RelayHttpClient } from "agent-relay-sdk";
|
|
2
2
|
import { quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
|
|
3
|
-
import { readRunnerContextProbeState } from "
|
|
4
|
-
import { DEFAULT_PROVIDER_QUOTA_CONFIG, normalizeProviderQuotaConfig, resolveClaudeStatuslineQuotaIdentity } from "
|
|
3
|
+
import { readRunnerContextProbeState } from "../context-probe-state";
|
|
4
|
+
import { DEFAULT_PROVIDER_QUOTA_CONFIG, normalizeProviderQuotaConfig, resolveClaudeStatuslineQuotaIdentity } from "../quota";
|
|
5
5
|
|
|
6
6
|
const PASSIVE_QUOTA_CONFIG_REFRESH_MS = 60_000;
|
|
7
7
|
const PASSIVE_QUOTA_SAMPLE_STALE_MS = 10 * 60_000;
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
5
5
|
import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER, errMessage, stringValue } from "agent-relay-sdk";
|
|
6
6
|
import type { SettingEntry, WorkspaceMetadata } from "agent-relay-sdk";
|
|
7
7
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
8
|
+
import { getManifest } from "agent-relay-providers";
|
|
8
9
|
|
|
9
10
|
interface ProviderConfig {
|
|
10
11
|
command: string;
|
|
@@ -40,11 +41,15 @@ function providersDir(home = agentRelayHome()): string {
|
|
|
40
41
|
return join(home, "providers");
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
const DEFAULT_PROVIDER_ARGS: Record<string, string[]> = {
|
|
45
|
+
claude: ["--dangerously-skip-permissions"],
|
|
46
|
+
};
|
|
47
|
+
|
|
43
48
|
export function defaultProviderConfig(provider: string): ProviderConfig {
|
|
44
|
-
const
|
|
49
|
+
const manifest = getManifest(provider);
|
|
45
50
|
return {
|
|
46
|
-
command,
|
|
47
|
-
defaultArgs: provider
|
|
51
|
+
command: manifest?.probe?.command ?? provider,
|
|
52
|
+
defaultArgs: DEFAULT_PROVIDER_ARGS[provider] ?? [],
|
|
48
53
|
env: {},
|
|
49
54
|
pluginDirs: [],
|
|
50
55
|
defaultCapabilities: ["chat", "code", "review"],
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { createProviderAdapter } from "./providers";
|
|
|
26
26
|
import { VERSION } from "./version";
|
|
27
27
|
import type { AgentProfile, RelayInjectionCategory, SpawnProvider, WorkspaceMetadata } from "agent-relay-sdk";
|
|
28
28
|
import { errMessage, isRecord, normalizeAgentLifecycle, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
|
|
29
|
+
import { getAllManifests } from "agent-relay-providers";
|
|
29
30
|
import type { RunnerRelayInjectionEvent } from "./relay-injection-events";
|
|
30
31
|
|
|
31
32
|
interface CliOptions {
|
|
@@ -233,9 +234,20 @@ export function isVersionRequest(argv: string[]): boolean {
|
|
|
233
234
|
return ownArgs.some((arg) => arg === "--version" || arg === "-v");
|
|
234
235
|
}
|
|
235
236
|
|
|
236
|
-
function
|
|
237
|
+
function detectProviderFromBin(argv: string[]): SpawnProvider {
|
|
237
238
|
const bin = [argv[1], process.env._].map((s) => basename(s || "")).join(" ");
|
|
238
|
-
|
|
239
|
+
for (const manifest of getAllManifests()) {
|
|
240
|
+
if (bin.includes(manifest.id)) return manifest.id as SpawnProvider;
|
|
241
|
+
}
|
|
242
|
+
return getAllManifests()[0]?.id as SpawnProvider ?? "claude";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isRegisteredProvider(value: string): value is SpawnProvider {
|
|
246
|
+
return getAllManifests().some((m) => m.id === value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseArgs(argv: string[]): CliOptions {
|
|
250
|
+
let provider = detectProviderFromBin(argv);
|
|
239
251
|
const relayArgs = argv.slice(2);
|
|
240
252
|
const providerSep = relayArgs.indexOf("--");
|
|
241
253
|
const ownArgs = providerSep >= 0 ? relayArgs.slice(0, providerSep) : relayArgs;
|
|
@@ -258,7 +270,7 @@ function parseArgs(argv: string[]): CliOptions {
|
|
|
258
270
|
|
|
259
271
|
for (let i = 0; i < ownArgs.length; i++) {
|
|
260
272
|
const arg = ownArgs[i];
|
|
261
|
-
if (arg
|
|
273
|
+
if (arg && isRegisteredProvider(arg)) provider = arg;
|
|
262
274
|
else if (arg === "--headless") headless = true;
|
|
263
275
|
else if (arg === "--model" && ownArgs[i + 1]) model = ownArgs[++i];
|
|
264
276
|
else if (arg === "--effort" && ownArgs[i + 1]) effort = ownArgs[++i];
|
|
@@ -338,9 +350,10 @@ function signalPromise(): Promise<number> {
|
|
|
338
350
|
});
|
|
339
351
|
}
|
|
340
352
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
353
|
+
type ApprovalModeDetector = (allArgs: string[]) => string | undefined;
|
|
354
|
+
|
|
355
|
+
const APPROVAL_MODE_DETECTORS: Record<string, ApprovalModeDetector> = {
|
|
356
|
+
claude(allArgs) {
|
|
344
357
|
if (allArgs.includes("--dangerously-skip-permissions")) return "open";
|
|
345
358
|
const modeIdx = allArgs.lastIndexOf("--permission-mode");
|
|
346
359
|
if (modeIdx >= 0) {
|
|
@@ -349,8 +362,8 @@ export function detectApprovalModeFromArgs(provider: string, defaultArgs: string
|
|
|
349
362
|
if (mode === "default") return "guarded";
|
|
350
363
|
}
|
|
351
364
|
return undefined;
|
|
352
|
-
}
|
|
353
|
-
|
|
365
|
+
},
|
|
366
|
+
codex(allArgs) {
|
|
354
367
|
if (allArgs.includes("--yolo")) return "open";
|
|
355
368
|
for (let i = 0; i < allArgs.length; i++) {
|
|
356
369
|
if (allArgs[i] === "-c" || allArgs[i] === "--config") {
|
|
@@ -360,8 +373,12 @@ export function detectApprovalModeFromArgs(provider: string, defaultArgs: string
|
|
|
360
373
|
}
|
|
361
374
|
}
|
|
362
375
|
return undefined;
|
|
363
|
-
}
|
|
364
|
-
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
export function detectApprovalModeFromArgs(provider: string, defaultArgs: string[], providerArgs: string[]): string | undefined {
|
|
380
|
+
const detector = APPROVAL_MODE_DETECTORS[provider];
|
|
381
|
+
return detector ? detector([...defaultArgs, ...providerArgs]) : undefined;
|
|
365
382
|
}
|
|
366
383
|
|
|
367
384
|
if (import.meta.main) {
|
package/src/launch-assembly.ts
CHANGED
|
@@ -8,12 +8,13 @@ import {
|
|
|
8
8
|
type RunnerSpawnConfig,
|
|
9
9
|
} from "./adapter";
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
projectConfigProjectionOn,
|
|
12
12
|
hostUserScopeMcpServers,
|
|
13
|
-
|
|
13
|
+
profileUsesProviderHostGlobals,
|
|
14
14
|
providerHomePathFor,
|
|
15
15
|
providerProvisioningHomePathFor,
|
|
16
16
|
} from "./profile-home";
|
|
17
|
+
import { getManifest } from "agent-relay-providers";
|
|
17
18
|
import { claudeLaunchPromptGateSettings } from "./claude-prompt-gates";
|
|
18
19
|
import {
|
|
19
20
|
materializeResolvedAssets,
|
|
@@ -229,7 +230,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
229
230
|
const configHome = providerHomePathFor("claude", config);
|
|
230
231
|
const assetHome = providerProvisioningHomePathFor("claude", config);
|
|
231
232
|
const vanilla = profileIsVanillaBase(config);
|
|
232
|
-
const projectConfigProjection =
|
|
233
|
+
const projectConfigProjection = projectConfigProjectionOn(config);
|
|
233
234
|
const relayPlugin = profileAllowsRelayFeature(config, "plugins");
|
|
234
235
|
const relaySkills = profileAllowsRelayFeature(config, "skills");
|
|
235
236
|
const statusLine = profileAllowsRelayFeature(config, "statusLine");
|
|
@@ -248,7 +249,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
248
249
|
// host plugin dirs → ONLY host-base profiles; isolated/vanilla must not pull them back in.
|
|
249
250
|
const relayPluginDirs = !config.agentProfile && relayPlugin ? [CLAUDE_RELAY_PLUGIN_DIR] : [];
|
|
250
251
|
const provisionedPluginDirs = provisionedPlugins.map((p) => p.dir);
|
|
251
|
-
const hostPluginDirs =
|
|
252
|
+
const hostPluginDirs = profileUsesProviderHostGlobals(config) ? providerConfig.pluginDirs : [];
|
|
252
253
|
const pluginDirs = [...new Set([...relayPluginDirs, ...provisionedPluginDirs, ...hostPluginDirs])];
|
|
253
254
|
|
|
254
255
|
const provisionedMcp = profileProvisioning(config).mcpServers;
|
|
@@ -295,7 +296,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
295
296
|
// still excluded) when the launch both uses the host config home AND its mcp mode is host.
|
|
296
297
|
// An isolated/minimal base has its own config home and never had the real host's user MCP,
|
|
297
298
|
// so it stays relay+provisioned only even if a profile sets mode:"host".
|
|
298
|
-
const usesHostMcp =
|
|
299
|
+
const usesHostMcp = profileUsesProviderHostGlobals(config)
|
|
299
300
|
&& (!config.agentProfile || config.agentProfile.mcp.mode === "host");
|
|
300
301
|
const hostMcp = usesHostMcp ? hostUserScopeMcpServers() : {};
|
|
301
302
|
const hostMcpServers = Object.keys(hostMcp).filter(
|
|
@@ -346,7 +347,7 @@ function assembleClaude(config: RunnerSpawnConfig, providerConfig: ProviderConfi
|
|
|
346
347
|
},
|
|
347
348
|
instructions,
|
|
348
349
|
args,
|
|
349
|
-
env:
|
|
350
|
+
env: configHomeEnv("claude", configHome),
|
|
350
351
|
};
|
|
351
352
|
}
|
|
352
353
|
|
|
@@ -418,19 +419,58 @@ function assembleCodex(config: RunnerSpawnConfig, _providerConfig: ProviderConfi
|
|
|
418
419
|
},
|
|
419
420
|
instructions,
|
|
420
421
|
args,
|
|
421
|
-
env:
|
|
422
|
+
env: configHomeEnv("codex", configHome),
|
|
422
423
|
};
|
|
423
424
|
}
|
|
424
425
|
|
|
426
|
+
// ── Manifest-driven env + dispatch ────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
function configHomeEnv(provider: string, configHome: string | undefined): Record<string, string> {
|
|
429
|
+
if (!configHome) return {};
|
|
430
|
+
const manifest = getManifest(provider);
|
|
431
|
+
const envVar = manifest?.home?.configDirEnvVar;
|
|
432
|
+
return envVar ? { [envVar]: configHome } : {};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
type Assembler = (config: RunnerSpawnConfig, providerConfig: ProviderConfig, hostDefaultArgs: string[]) => AssembledLaunch;
|
|
436
|
+
type Materializer = (config: RunnerSpawnConfig) => void;
|
|
437
|
+
|
|
438
|
+
const ASSEMBLERS: Record<string, Assembler> = {
|
|
439
|
+
claude: assembleClaude,
|
|
440
|
+
codex: (config, providerConfig) => assembleCodex(config, providerConfig),
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const MATERIALIZERS: Record<string, Materializer> = {
|
|
444
|
+
claude: materializeClaudeAssets,
|
|
445
|
+
codex: materializeCodexAssets,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
function materializeClaudeAssets(config: RunnerSpawnConfig): void {
|
|
449
|
+
const configHome = providerHomePathFor("claude", config);
|
|
450
|
+
const assetHome = providerProvisioningHomePathFor("claude", config);
|
|
451
|
+
if (configHome) {
|
|
452
|
+
const skills = resolveClaudeProvisionedSkills(configHome, config);
|
|
453
|
+
materializeResolvedAssets(skills);
|
|
454
|
+
materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set(skills.map((s) => s.name))));
|
|
455
|
+
}
|
|
456
|
+
if (assetHome) materializeResolvedAssets(resolveClaudeProvisionedPlugins(assetHome, config));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function materializeCodexAssets(config: RunnerSpawnConfig): void {
|
|
460
|
+
const assetHome = providerProvisioningHomePathFor("codex", config);
|
|
461
|
+
if (assetHome) materializeResolvedAssets(resolveCodexProvisionedSkills(assetHome, config));
|
|
462
|
+
}
|
|
463
|
+
|
|
425
464
|
/**
|
|
426
465
|
* Assemble the launch for `provider` from the resolved profile + provisioning. PURE:
|
|
427
466
|
* resolves which declared assets exist (filesystem reads) but writes nothing. A missing
|
|
428
467
|
* `agentProfile` assembles the legacy default (host base, relay features on, no provisioning).
|
|
429
468
|
*/
|
|
430
469
|
export function assembleLaunch(provider: SpawnProvider, config: RunnerSpawnConfig, providerConfig: ProviderConfig): AssembledLaunch {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
470
|
+
const assembler = ASSEMBLERS[provider];
|
|
471
|
+
if (!assembler) throw new Error(`no launch assembler registered for provider: ${provider}`);
|
|
472
|
+
const hostDefaultArgs = profileUsesProviderHostGlobals(config) ? providerConfig.defaultArgs : [];
|
|
473
|
+
return assembler(config, providerConfig, hostDefaultArgs);
|
|
434
474
|
}
|
|
435
475
|
|
|
436
476
|
/**
|
|
@@ -443,14 +483,6 @@ export function materializeLaunchAssembly(provider: SpawnProvider, config: Runne
|
|
|
443
483
|
const configHome = providerHomePathFor(provider, config);
|
|
444
484
|
const assetHome = providerProvisioningHomePathFor(provider, config);
|
|
445
485
|
if (!configHome && !assetHome) return;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const skills = resolveClaudeProvisionedSkills(configHome, config);
|
|
449
|
-
materializeResolvedAssets(skills);
|
|
450
|
-
materializeResolvedAssets(resolveClaudeProjectSkills(configHome, config, new Set(skills.map((s) => s.name))));
|
|
451
|
-
}
|
|
452
|
-
if (assetHome) materializeResolvedAssets(resolveClaudeProvisionedPlugins(assetHome, config));
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (assetHome) materializeResolvedAssets(resolveCodexProvisionedSkills(assetHome, config));
|
|
486
|
+
const materializer = MATERIALIZERS[provider];
|
|
487
|
+
if (materializer) materializer(config);
|
|
456
488
|
}
|
package/src/profile-home.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { existsSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
5
|
-
import type { ProvisioningMcpServer } from "agent-relay-sdk";
|
|
5
|
+
import type { ProvisioningMcpServer, SpawnProvider } from "agent-relay-sdk";
|
|
6
|
+
import { getManifest } from "agent-relay-providers";
|
|
6
7
|
import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
|
|
7
8
|
import { CLAUDE_RELAY_MANUAL } from "./relay-instructions";
|
|
8
9
|
import { providerHomeRootFromEnv } from "./config";
|
|
@@ -17,25 +18,31 @@ export function profileUsesHostProviderGlobals(config: { agentProfile?: RunnerSp
|
|
|
17
18
|
return !config.agentProfile || config.agentProfile.base === "host";
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export function
|
|
21
|
+
export function projectConfigProjectionOn(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
|
|
21
22
|
return Boolean(config.agentProfile?.projectSkills || config.agentProfile?.projectSettings);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
/** @deprecated Use {@link projectConfigProjectionOn}. */
|
|
26
|
+
export { projectConfigProjectionOn as claudeProjectConfigProjectionOn };
|
|
27
|
+
|
|
28
|
+
export function profileUsesProviderHostGlobals(config: { agentProfile?: RunnerSpawnConfig["agentProfile"] }): boolean {
|
|
29
|
+
return profileUsesHostProviderGlobals(config) && !projectConfigProjectionOn(config);
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
/** @deprecated Use {@link profileUsesProviderHostGlobals}. */
|
|
33
|
+
export { profileUsesProviderHostGlobals as profileUsesClaudeHostProviderGlobals };
|
|
34
|
+
|
|
35
|
+
function profileRequiresIsolatedHome(_provider: SpawnProvider, config: RunnerSpawnConfig): boolean {
|
|
29
36
|
if (!config.agentProfile) return false;
|
|
30
37
|
if (config.agentProfile.base !== "host") return true;
|
|
31
|
-
return
|
|
38
|
+
return projectConfigProjectionOn(config);
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
// #557 — PURE: the isolated provider config home path a launch WILL use, or undefined
|
|
35
42
|
// for a host-base profile (which uses the provider's normal home). The launch assembler
|
|
36
43
|
// computes the home this way to report it + build CLAUDE_CONFIG_DIR/skill/plugin paths
|
|
37
44
|
// without side effects; prepare*ProfileHome creates it at the same path at spawn time.
|
|
38
|
-
export function providerHomePathFor(provider:
|
|
45
|
+
export function providerHomePathFor(provider: SpawnProvider, config: RunnerSpawnConfig): string | undefined {
|
|
39
46
|
if (!profileRequiresIsolatedHome(provider, config)) return undefined;
|
|
40
47
|
return providerHomePath(provider, config);
|
|
41
48
|
}
|
|
@@ -44,7 +51,7 @@ export function providerHomePathFor(provider: "claude" | "codex", config: Runner
|
|
|
44
51
|
// host-base profiles, where the provider's real config home stays in use. This path is
|
|
45
52
|
// only passed explicitly through launch args such as Codex skills.config or Claude
|
|
46
53
|
// --plugin-dir; it does not imply CODEX_HOME/CLAUDE_CONFIG_DIR isolation.
|
|
47
|
-
export function providerProvisioningHomePathFor(provider:
|
|
54
|
+
export function providerProvisioningHomePathFor(provider: SpawnProvider, config: RunnerSpawnConfig): string | undefined {
|
|
48
55
|
if (!config.agentProfile) return undefined;
|
|
49
56
|
return providerHomePathFor(provider, config) ?? join(providerHomePath(provider, config), "provisioning");
|
|
50
57
|
}
|
|
@@ -67,44 +74,45 @@ export function providerProvisioningHomePathFor(provider: "claude" | "codex", co
|
|
|
67
74
|
// (claude-rig / host-base launches dodge all of this via onboarded host config +
|
|
68
75
|
// --dangerously-skip-permissions; only isolated homes need the bootstrap.)
|
|
69
76
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const CLAUDE_AUTH_ITEMS = [".credentials.json", "statsig"];
|
|
77
|
+
// Provider-specific first-run bootstrap hooks, registered at module load.
|
|
78
|
+
type BootstrapHook = (home: string, config: RunnerSpawnConfig) => void;
|
|
79
|
+
const BOOTSTRAP_HOOKS = new Map<string, BootstrapHook>();
|
|
74
80
|
|
|
75
|
-
// Shared skeleton
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
function prepareProviderHome(provider: "claude" | "codex", config: RunnerSpawnConfig): ProviderHome | undefined {
|
|
81
|
+
// Shared skeleton: gate on isolated-profile, make the instance-keyed home,
|
|
82
|
+
// link auth items from the manifest, and run any registered first-run hook.
|
|
83
|
+
function prepareProviderHome(provider: SpawnProvider, config: RunnerSpawnConfig): ProviderHome | undefined {
|
|
79
84
|
if (!profileRequiresIsolatedHome(provider, config)) return undefined;
|
|
80
85
|
const target = providerHomePath(provider, config);
|
|
81
86
|
mkdirSync(target, { recursive: true });
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
const hook = BOOTSTRAP_HOOKS.get(provider);
|
|
88
|
+
if (hook) hook(target, config);
|
|
89
|
+
const manifest = getManifest(provider);
|
|
90
|
+
const authItems = manifest?.home?.authItems ?? [];
|
|
91
|
+
const sourceHome = providerSourceHome(provider);
|
|
92
|
+
const authLinked = linkExistingAuthItems(sourceHome, target, authItems);
|
|
85
93
|
return { path: target, authLinked };
|
|
86
94
|
}
|
|
87
95
|
|
|
88
96
|
export const prepareCodexProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("codex", config);
|
|
89
97
|
export const prepareClaudeProfileHome = (config: RunnerSpawnConfig) => prepareProviderHome("claude", config);
|
|
90
98
|
|
|
91
|
-
function bootstrapCodexFirstRun(codexHome: string, config: RunnerSpawnConfig):
|
|
99
|
+
function bootstrapCodexFirstRun(codexHome: string, config: RunnerSpawnConfig): void {
|
|
92
100
|
trustWorkspaceForCodex(codexHome, config);
|
|
93
|
-
const sourceHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
94
|
-
return linkExistingAuthItems(sourceHome, codexHome, CODEX_AUTH_ITEMS);
|
|
95
101
|
}
|
|
96
102
|
|
|
97
|
-
function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig):
|
|
98
|
-
// Only inject the Relay usage manual when the profile actually wants a Relay
|
|
99
|
-
// surface. An isolated-research profile (relay.context disabled) must not get
|
|
100
|
-
// agent-relay communication instructions written into its config home.
|
|
103
|
+
function bootstrapClaudeFirstRun(claudeHome: string, config: RunnerSpawnConfig): void {
|
|
101
104
|
if (profileAllowsRelayFeature(config, "context")) writeClaudeRelayManual(claudeHome);
|
|
102
105
|
seedClaudeConfigIfMissing(claudeHome, config);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
BOOTSTRAP_HOOKS.set("codex", bootstrapCodexFirstRun);
|
|
109
|
+
BOOTSTRAP_HOOKS.set("claude", bootstrapClaudeFirstRun);
|
|
110
|
+
|
|
111
|
+
function providerSourceHome(provider: string): string {
|
|
112
|
+
const manifest = getManifest(provider);
|
|
113
|
+
const envVar = manifest?.home?.configDirEnvVar;
|
|
114
|
+
const configDir = manifest?.home?.configDir ?? `.${provider}`;
|
|
115
|
+
return (envVar && process.env[envVar]) || join(homedir(), configDir);
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
function seedClaudeConfigIfMissing(claudeHome: string, config: RunnerSpawnConfig): void {
|
|
@@ -139,7 +147,7 @@ export function hostUserScopeMcpServers(): Record<string, ProvisioningMcpServer>
|
|
|
139
147
|
return servers as Record<string, ProvisioningMcpServer>;
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
function providerHomePath(provider:
|
|
150
|
+
function providerHomePath(provider: SpawnProvider, config: RunnerSpawnConfig): string {
|
|
143
151
|
const root = providerHomeRootFromEnv();
|
|
144
152
|
const profileName = sanitizePathPart(config.agentProfile?.name || config.profile || "profile");
|
|
145
153
|
const instance = sanitizePathPart(config.instanceId || config.runnerId);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentProfile, AgentProfileProjectionEntry, AgentProfileProjectionReport, SpawnProvider } from "agent-relay-sdk";
|
|
2
|
+
import { getManifest } from "agent-relay-providers";
|
|
2
3
|
import type { AssembledLaunch } from "./launch-assembly";
|
|
3
4
|
|
|
4
5
|
// #557 — the projection report DESCRIBES an AssembledLaunch, the same object the adapters
|
|
@@ -68,43 +69,52 @@ function relayContextEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
function relaySkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
71
|
-
|
|
72
|
+
const manifest = getManifest(a.provider);
|
|
73
|
+
const label = manifest?.label ?? a.provider;
|
|
74
|
+
const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
|
|
75
|
+
if (!hasPluginSurface) {
|
|
72
76
|
return a.relaySkills
|
|
73
|
-
? applied("relay.skills", "enabled",
|
|
74
|
-
: applied("relay.skills", "disabled",
|
|
77
|
+
? applied("relay.skills", "enabled", `Bundled Agent Relay ${label} skills are passed through the provider's skill delivery mechanism.`)
|
|
78
|
+
: applied("relay.skills", "disabled", `Bundled Agent Relay ${label} skills are omitted.`);
|
|
75
79
|
}
|
|
76
80
|
return a.relaySkills
|
|
77
|
-
? applied("relay.skills", "enabled",
|
|
81
|
+
? applied("relay.skills", "enabled", `Agent Relay ${label} skills are available through the bundled Relay plugin.`)
|
|
78
82
|
: applied("relay.skills", "disabled", a.relayPlugin
|
|
79
|
-
?
|
|
80
|
-
:
|
|
83
|
+
? `${label} does not expose an independent Relay skill switch; disabling skills requires disabling the Relay plugin.`
|
|
84
|
+
: `Bundled Agent Relay ${label} plugin dirs are omitted, so Relay plugin-provided skills are unavailable.`);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
function relayPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
84
|
-
|
|
88
|
+
const manifest = getManifest(a.provider);
|
|
89
|
+
const label = manifest?.label ?? a.provider;
|
|
90
|
+
const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
|
|
91
|
+
if (hasPluginSurface) {
|
|
85
92
|
return a.relayPlugin
|
|
86
|
-
? applied("relay.plugins", "enabled",
|
|
87
|
-
: applied("relay.plugins", "disabled",
|
|
93
|
+
? applied("relay.plugins", "enabled", `Bundled Agent Relay ${label} plugin dir is passed to ${label}.`)
|
|
94
|
+
: applied("relay.plugins", "disabled", `Bundled Agent Relay ${label} plugin dirs are omitted.`);
|
|
88
95
|
}
|
|
89
|
-
return notApplicable("relay.plugins", a.relaySkills ? "enabled" : "disabled",
|
|
96
|
+
return notApplicable("relay.plugins", a.relaySkills ? "enabled" : "disabled", `${label} Relay integration uses skills/app-server delivery, not a Relay plugin dir.`);
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
function relayStatusLineEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
const manifest = getManifest(a.provider);
|
|
101
|
+
const label = manifest?.label ?? a.provider;
|
|
102
|
+
if (a.statusLine) {
|
|
103
|
+
return applied("relay.statusLine", "enabled", "Agent Relay status-line context probe settings are injected when caller settings allow it.");
|
|
97
104
|
}
|
|
98
|
-
|
|
105
|
+
const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
|
|
106
|
+
if (hasPluginSurface) {
|
|
107
|
+
return applied("relay.statusLine", "disabled", "Agent Relay status-line settings are omitted.");
|
|
108
|
+
}
|
|
109
|
+
return notApplicable("relay.statusLine", "disabled", `${label} reports context through provider-native events instead of a Relay status-line setting.`);
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
function repoInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
102
113
|
const disposition = a.instructions.repo;
|
|
103
114
|
if (disposition === "allow") return applied("instructions.repo", "allow", "Repo-local provider instructions are left available.");
|
|
104
115
|
if (disposition === "suppressed") {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
: "Repo AGENTS.md is suppressed: Codex launches with project_doc_max_bytes=0 (no project doc loaded).");
|
|
116
|
+
const label = getManifest(a.provider)?.label ?? a.provider;
|
|
117
|
+
return applied("instructions.repo", "ignore", `Repo project instructions are suppressed: ${label} launches with the vanilla base mechanism (provider-specific suppression of repo instructions).`);
|
|
108
118
|
}
|
|
109
119
|
return unsupported("instructions.repo", "ignore", "Repo instruction suppression requires the vanilla base; isolated/minimal do not enforce it at launch yet.");
|
|
110
120
|
}
|
|
@@ -113,39 +123,55 @@ function repoInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry
|
|
|
113
123
|
// provider mechanism the assembler emitted (Claude --setting-sources "user"; Codex
|
|
114
124
|
// project_doc_max_bytes=0). OS-level admin/managed policy is beyond any client flag and is
|
|
115
125
|
// called out so it isn't mistaken for a leak.
|
|
126
|
+
const VANILLA_MECHANISM: Record<string, { requested: string; detail: string }> = {
|
|
127
|
+
claude: {
|
|
128
|
+
requested: "--setting-sources user",
|
|
129
|
+
detail: "Claude launches the isolated config home under --setting-sources \"user\": host CLAUDE.md (repo + global), host skills/plugins/hooks, and host MCP (.mcp.json / ~/.claude.json) are all excluded; relay-provisioned assets in the home load. Auth, model, and built-in tools work normally. OS-level admin-managed policy still applies.",
|
|
130
|
+
},
|
|
131
|
+
codex: {
|
|
132
|
+
requested: "project_doc_max_bytes=0",
|
|
133
|
+
detail: "Codex launches with an isolated CODEX_HOME (no host config.toml MCP/hooks) and project_doc_max_bytes=0 (no AGENTS.md project doc, repo or global).",
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
116
137
|
function vanillaLaunchEntry(provider: SpawnProvider): AgentProfileProjectionEntry {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
138
|
+
const label = getManifest(provider)?.label ?? provider;
|
|
139
|
+
const mechanism = VANILLA_MECHANISM[provider];
|
|
140
|
+
return applied(
|
|
141
|
+
"provider.vanilla",
|
|
142
|
+
mechanism?.requested ?? "active",
|
|
143
|
+
mechanism?.detail ?? `${label} launches with the vanilla base: an isolated provider config home and provider-specific repo instruction suppression. Auth, model, and built-in tools work normally.`,
|
|
144
|
+
);
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
function provisioningSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
123
148
|
const count = a.skills.length;
|
|
124
149
|
if (!count) return notApplicable("provisioning.skills", "none", "No relay-provisioned skills materialized for this launch.");
|
|
125
150
|
const names = ` (${a.skills.map((s) => s.name).join(", ")})`;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
: `Relay-provisioned skills added to Codex skills.config from the isolated CODEX_HOME${names}.`);
|
|
151
|
+
const label = getManifest(a.provider)?.label ?? a.provider;
|
|
152
|
+
return applied("provisioning.skills", `${count}`, `Relay-provisioned skills materialized for ${label} from the isolated provider config home${names}.`);
|
|
129
153
|
}
|
|
130
154
|
|
|
131
155
|
function provisioningPluginsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
132
|
-
|
|
156
|
+
const manifest = getManifest(a.provider);
|
|
157
|
+
const label = manifest?.label ?? a.provider;
|
|
158
|
+
const hasPluginSurface = Boolean(manifest?.provisioning?.bundledPlugin);
|
|
159
|
+
if (!hasPluginSurface) {
|
|
133
160
|
const declared = (a.profile?.plugins ?? []).filter((p) => p.enabled).length;
|
|
134
|
-
return notApplicable("provisioning.plugins", `${declared}`,
|
|
161
|
+
return notApplicable("provisioning.plugins", `${declared}`, `${label} has no plugin surface; provisioned plugins apply to plugin-capable providers only.`);
|
|
135
162
|
}
|
|
136
163
|
const count = a.plugins.length;
|
|
137
164
|
if (!count) return notApplicable("provisioning.plugins", "none", "No relay-provisioned plugins materialized for this launch.");
|
|
138
165
|
const names = ` (${a.plugins.map((p) => p.name).join(", ")})`;
|
|
139
|
-
return applied("provisioning.plugins", `${count}`, `Relay-provisioned plugins passed to
|
|
166
|
+
return applied("provisioning.plugins", `${count}`, `Relay-provisioned plugins passed to ${label} via the plugin dir mechanism${names}.`);
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
function provisioningMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
143
170
|
const names = a.mcp.servers;
|
|
144
171
|
if (!names.length) return notApplicable("provisioning.mcp", "none", "No relay-provisioned MCP servers materialized for this launch.");
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: `Relay-provisioned MCP servers emitted as -c mcp_servers.* overrides (${names.join(", ")}).`);
|
|
172
|
+
const count = `${names.length} server${names.length === 1 ? "" : "s"}`;
|
|
173
|
+
const providerLabel = getManifest(a.provider)?.label ?? a.provider;
|
|
174
|
+
return applied("provisioning.mcp", count, `Relay-provisioned MCP servers injected for ${providerLabel} (${names.join(", ")})${a.mcp.strict ? "; strict mode guarantees only declared servers load" : ""}.`);
|
|
149
175
|
}
|
|
150
176
|
|
|
151
177
|
function provisioningSkippedEntries(a: AssembledLaunch): AgentProfileProjectionEntry[] {
|
|
@@ -164,16 +190,14 @@ function projectMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
|
164
190
|
const names = a.mcp.projectServers;
|
|
165
191
|
if (!optedIn) return notApplicable("projectMcp", "off", "Project .mcp.json projection is off (opt in via the projectMcp profile/spawn option).");
|
|
166
192
|
if (!names.length) return applied("projectMcp", "none", "Project .mcp.json projection is on, but the spawn cwd declared no injectable MCP servers.");
|
|
167
|
-
const
|
|
168
|
-
return applied("projectMcp",
|
|
169
|
-
? `Project .mcp.json servers composed into --mcp-config (${names.join(", ")})${a.mcp.strict ? "; survives --strict-mcp-config" : ""}.`
|
|
170
|
-
: `Project .mcp.json servers emitted as -c mcp_servers.* overrides (${names.join(", ")}).`);
|
|
193
|
+
const count = `${names.length} server${names.length === 1 ? "" : "s"}`;
|
|
194
|
+
return applied("projectMcp", count, `Project .mcp.json servers injected for this launch (${names.join(", ")})${a.mcp.strict ? "; survives strict MCP mode" : ""}.`);
|
|
171
195
|
}
|
|
172
196
|
|
|
173
197
|
function projectSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
174
198
|
const optedIn = Boolean(a.profile?.projectSkills);
|
|
175
199
|
if (!optedIn) return notApplicable("projectSkills", "off", "Project .claude/skills projection is off (opt in via the projectSkills profile/spawn option).");
|
|
176
|
-
if (a.provider
|
|
200
|
+
if (!getManifest(a.provider)?.provisioning?.bundledPlugin) return notApplicable("projectSkills", "unsupported", "Project skills projection applies to plugin-capable providers only.");
|
|
177
201
|
if (!a.projectSkills.length) return applied("projectSkills", "none", "Project .claude/skills projection is on, but the spawn cwd declared no valid skill dirs.");
|
|
178
202
|
const names = a.projectSkills.map((s) => s.name).join(", ");
|
|
179
203
|
return applied("projectSkills", `${a.projectSkills.length}`, `Project .claude skills materialized into Relay's managed Claude config home (${names}); host/global skills are excluded.`);
|
|
@@ -182,7 +206,7 @@ function projectSkillsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
|
182
206
|
function projectSettingsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
183
207
|
const optedIn = Boolean(a.profile?.projectSettings);
|
|
184
208
|
if (!optedIn) return notApplicable("projectSettings", "off", "Project .claude/settings.json projection is off (opt in via the projectSettings profile/spawn option).");
|
|
185
|
-
if (a.provider
|
|
209
|
+
if (!getManifest(a.provider)?.provisioning?.bundledPlugin) return notApplicable("projectSettings", "unsupported", "Project settings projection applies to plugin-capable providers only.");
|
|
186
210
|
const ignored = a.projectSettings.ignoredKeys.length ? `; ignored security keys: ${a.projectSettings.ignoredKeys.join(", ")}` : "";
|
|
187
211
|
return applied("projectSettings", a.projectSettings.keys.length ? `${a.projectSettings.keys.length} keys` : "none", `Project .claude/settings.json was sanitized and injected through --settings; Relay approvalMode/permission gates remain authoritative${ignored}.`);
|
|
188
212
|
}
|
|
@@ -194,9 +218,7 @@ function sharedMcpEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
|
|
194
218
|
const optedIn = Boolean(a.profile?.sharedMcp);
|
|
195
219
|
const listener = a.mcp.sharedListener;
|
|
196
220
|
if (!optedIn || !listener) return notApplicable("sharedMcp", "off", "Shared host-listener MCP is off for this resolved profile; non-identity MCP is injected per-agent.");
|
|
197
|
-
return applied("sharedMcp", "on",
|
|
198
|
-
? `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"', injected into --mcp-config (survives --strict-mcp-config); session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent (#215).`
|
|
199
|
-
: `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"' emitted as -c mcp_servers.* overrides; session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent (#215).`);
|
|
221
|
+
return applied("sharedMcp", "on", `Non-identity MCP (e.g. tokenlean, github) routed through the shared callmux listener (${listener.url}) via a per-agent 'callmux bridge --cwd "$WORKTREE"'; session survives a listener restart, session-cwd isolated. Relay endpoint stays per-agent.`);
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
function globalInstructionsEntry(a: AssembledLaunch): AgentProfileProjectionEntry {
|
package/src/providers.ts
CHANGED
|
@@ -8,11 +8,13 @@ interface RunnerProviderRegistration {
|
|
|
8
8
|
createAdapter: () => ProviderAdapter;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const RUNNER_PROVIDERS: Record<
|
|
11
|
+
const RUNNER_PROVIDERS: Record<string, RunnerProviderRegistration> = {
|
|
12
12
|
claude: { provider: "claude", createAdapter: () => new ClaudeAdapter() },
|
|
13
13
|
codex: { provider: "codex", createAdapter: () => new CodexAdapter() },
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
export function createProviderAdapter(provider: SpawnProvider): ProviderAdapter {
|
|
17
|
-
|
|
17
|
+
const registration = RUNNER_PROVIDERS[provider];
|
|
18
|
+
if (!registration) throw new Error(`unsupported provider adapter: ${provider}`);
|
|
19
|
+
return registration.createAdapter();
|
|
18
20
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { MessageSessionMeta, RelayInjectionCategory
|
|
1
|
+
import type { MessageSessionMeta, RelayInjectionCategory } from "agent-relay-sdk";
|
|
2
|
+
import { isSpawnProvider } from "agent-relay-sdk";
|
|
2
3
|
import type { ProviderConfig, RunnerSpawnConfig } from "./adapter";
|
|
3
4
|
import { assembleLaunch } from "./launch-assembly";
|
|
4
5
|
|
|
@@ -30,13 +31,9 @@ interface RunnerLaunchInjectionInput {
|
|
|
30
31
|
timestamp?: number;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function supportsLaunchAssembly(provider: string): provider is SpawnProvider {
|
|
34
|
-
return provider === "claude" || provider === "codex";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
function launchInjectionEvents(input: RunnerLaunchInjectionInput): KeyedRelayInjectionEvent[] {
|
|
38
35
|
const events: KeyedRelayInjectionEvent[] = (input.carried ?? []).map((event, index) => ({ key: `spawn-${index}`, event }));
|
|
39
|
-
if (!
|
|
36
|
+
if (!isSpawnProvider(input.provider)) return events;
|
|
40
37
|
const assembled = assembleLaunch(input.provider, input.config, input.providerConfig);
|
|
41
38
|
const toolCount = assembled.skills.length + assembled.plugins.length + assembled.mcp.servers.length
|
|
42
39
|
+ assembled.projectSkills.length
|
package/src/runner-core.ts
CHANGED
|
@@ -11,13 +11,14 @@ import { computeLivenessSignal, type LivenessInputs } from "./liveness";
|
|
|
11
11
|
import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderStatusUpdate, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
|
|
12
12
|
import { messagesWithCachedAttachments } from "./attachment-cache";
|
|
13
13
|
import { ClaimTracker } from "./claim-tracker";
|
|
14
|
-
import { ClaudeQuotaHarvest } from "./claude-quota-harvest";
|
|
14
|
+
import { ClaudeQuotaHarvest } from "./adapters/claude-quota-harvest";
|
|
15
15
|
import { readRunnerContextProbeState } from "./context-probe-state";
|
|
16
16
|
import { startControlServer, type ControlServer, type ResolvedPermissionPrompt } from "./control-server";
|
|
17
17
|
import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
|
|
18
18
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
19
19
|
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
20
|
-
import {
|
|
20
|
+
import { getManifest } from "agent-relay-providers";
|
|
21
|
+
import { profileUsesProviderHostGlobals } from "./profile-home";
|
|
21
22
|
import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint, resolveSharedMcpUrl } from "./relay-mcp";
|
|
22
23
|
import { RelayMcpProxy } from "./relay-mcp-proxy";
|
|
23
24
|
import { runtimeMetadata } from "./version";
|
|
@@ -530,7 +531,7 @@ export class AgentRunner {
|
|
|
530
531
|
private async spawnProvider(opts: { resumeId?: string; suppressPrompt?: boolean } = {}): Promise<ManagedProcess> {
|
|
531
532
|
this.providerSessionId = crypto.randomUUID();
|
|
532
533
|
this.lastTranscriptPath = undefined;
|
|
533
|
-
const includeProviderGlobals =
|
|
534
|
+
const includeProviderGlobals = profileUsesProviderHostGlobals(this.options);
|
|
534
535
|
const env = {
|
|
535
536
|
...process.env as Record<string, string>,
|
|
536
537
|
...(includeProviderGlobals ? this.options.providerConfig.env : {}),
|
|
@@ -874,7 +875,7 @@ export class AgentRunner {
|
|
|
874
875
|
...(answers ? { answers } : {}),
|
|
875
876
|
};
|
|
876
877
|
if (this.control?.resolvePermissionDecision(input)) {
|
|
877
|
-
return { approvalId, decision, provider:
|
|
878
|
+
return { approvalId, decision, provider: this.options.provider };
|
|
878
879
|
}
|
|
879
880
|
if (!this.process) throw new Error("provider process is unavailable");
|
|
880
881
|
if (!this.options.adapter.respondToPermissionDecision) throw new Error("provider does not support permission decisions");
|
|
@@ -987,7 +988,7 @@ export class AgentRunner {
|
|
|
987
988
|
});
|
|
988
989
|
|
|
989
990
|
const nativeSelfResume = this.nativeSelfResume.active;
|
|
990
|
-
if (nativeSelfResume && this.options.provider
|
|
991
|
+
if (nativeSelfResume && getManifest(this.options.provider)?.resume?.supported) {
|
|
991
992
|
await recoverNativeSelfResumeExit({ handoff: nativeSelfResume, tracker: this.nativeSelfResume, providerSessionId: this.providerSessionId, status, diagnostics, now, publishTimeline: (event) => this.publishRunnerTimelineEvent(event), setTerminalProviderExit: (marker) => { this.terminalProviderExit = marker; }, setProviderStatus: (update) => this.setProviderStatus(update), restartProvider: (resumeId) => this.restartProvider(resumeId ? { resumeId } : { suppressPrompt: true }), publishStatus: () => this.publishStatus(), scheduleDrain: () => this.scheduleDrain() });
|
|
992
993
|
return;
|
|
993
994
|
}
|
|
@@ -1089,7 +1090,7 @@ export class AgentRunner {
|
|
|
1089
1090
|
}
|
|
1090
1091
|
|
|
1091
1092
|
private shouldStopUnexpectedProviderExit(diagnostics: Record<string, unknown>): boolean {
|
|
1092
|
-
return this.options.provider === "
|
|
1093
|
+
return getManifest(this.options.provider)?.launch?.managedPolicyPrompt?.alwaysOn === "append-system-prompt" && diagnostics.exitCommandInProgress !== true;
|
|
1093
1094
|
}
|
|
1094
1095
|
|
|
1095
1096
|
private async shutdownProvider(hard: boolean, timeoutMs = this.options.providerConfig.headless.shutdownTimeoutMs): Promise<void> {
|
|
@@ -1246,9 +1247,9 @@ export class AgentRunner {
|
|
|
1246
1247
|
this.compactionMidTurn = false;
|
|
1247
1248
|
this.busyReconciler.disarm();
|
|
1248
1249
|
this.stopReasoningTail();
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
if (this.options.provider !== "
|
|
1250
|
+
// Providers with append-system-prompt policy use lifecycle hooks for obligation handling;
|
|
1251
|
+
// others need the runner to re-inject pending obligations on idle.
|
|
1252
|
+
if (getManifest(this.options.provider)?.launch?.managedPolicyPrompt?.alwaysOn !== "append-system-prompt") void this.reInjectPendingObligation();
|
|
1252
1253
|
}
|
|
1253
1254
|
// A genuine restart back to forward progress clears the terminal-exit marker so a later offline
|
|
1254
1255
|
// doesn't re-announce a stale provider exit.
|
package/src/runner-helpers.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
2
2
|
import type { AgentProfile, ContextState, Message, ProviderCapabilities, ProviderState, QuotaState } from "agent-relay-sdk";
|
|
3
|
-
import { errMessage } from "agent-relay-sdk";
|
|
3
|
+
import { errMessage, isSpawnProvider } from "agent-relay-sdk";
|
|
4
|
+
import { getManifest } from "agent-relay-providers";
|
|
5
|
+
import type { BooleanCapabilityDefault, ProviderCapabilityCondition } from "agent-relay-providers";
|
|
4
6
|
import { targetMatchesAgentIdentity } from "agent-relay-sdk/agent-target";
|
|
5
7
|
import type { ProviderAdapter, ProviderConfig, RunnerSpawnConfig, SemanticStatus } from "./adapter";
|
|
6
8
|
import { agentProfileProjectionReport } from "./profile-projection";
|
|
@@ -8,7 +10,6 @@ import { assembleLaunch } from "./launch-assembly";
|
|
|
8
10
|
|
|
9
11
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
10
12
|
const LOG_TAIL_BYTES = 128 * 1024;
|
|
11
|
-
const CLAUDE_RESUME_RE = /\bclaude\s+--resume\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b/gi;
|
|
12
13
|
const RUNNER_CLAIMED_GUIDANCE_RE = /\s*Claim this task before working it, then update task status when finished\.\s*$/;
|
|
13
14
|
|
|
14
15
|
interface RuntimeProviderOptions {
|
|
@@ -131,16 +132,24 @@ export function runnerShouldRestartUnexpectedProviderExit(
|
|
|
131
132
|
&& input.hasTerminalSession;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
function resumeLogExtractionRe(provider: string): RegExp | undefined {
|
|
136
|
+
const manifest = getManifest(provider);
|
|
137
|
+
const resume = manifest?.resume as { supported: boolean; idPattern?: string } | undefined;
|
|
138
|
+
if (!resume?.supported || !resume.idPattern || !manifest?.probe?.command) return undefined;
|
|
139
|
+
return new RegExp(`\\b${manifest.probe.command}\\s+--resume\\s+(${resume.idPattern})\\b`, "gi");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function latestResumeIdFromText(provider: string, text: string): string | undefined {
|
|
143
|
+
const re = resumeLogExtractionRe(provider);
|
|
144
|
+
if (!re) return undefined;
|
|
135
145
|
let latest: string | undefined;
|
|
136
|
-
|
|
137
|
-
for (let match = CLAUDE_RESUME_RE.exec(text); match; match = CLAUDE_RESUME_RE.exec(text)) {
|
|
146
|
+
for (let match = re.exec(text); match; match = re.exec(text)) {
|
|
138
147
|
latest = match[1];
|
|
139
148
|
}
|
|
140
149
|
return latest;
|
|
141
150
|
}
|
|
142
151
|
|
|
143
|
-
export function
|
|
152
|
+
export function latestResumeIdFromLogFile(provider: string, path: string): string | undefined {
|
|
144
153
|
let fd: number | undefined;
|
|
145
154
|
try {
|
|
146
155
|
const stat = statSync(path);
|
|
@@ -149,7 +158,7 @@ export function latestClaudeResumeIdFromLogFile(path: string): string | undefine
|
|
|
149
158
|
const buffer = Buffer.alloc(length);
|
|
150
159
|
fd = openSync(path, "r");
|
|
151
160
|
readSync(fd, buffer, 0, length, offset);
|
|
152
|
-
return
|
|
161
|
+
return latestResumeIdFromText(provider, buffer.toString("utf8"));
|
|
153
162
|
} catch {
|
|
154
163
|
return undefined;
|
|
155
164
|
} finally {
|
|
@@ -173,7 +182,7 @@ export function providerExitDiagnostics(input: {
|
|
|
173
182
|
const tmuxSession = typeof input.processMeta?.tmuxSession === "string" ? input.processMeta.tmuxSession : undefined;
|
|
174
183
|
const tmuxSocket = typeof input.processMeta?.tmuxSocket === "string" ? input.processMeta.tmuxSocket : undefined;
|
|
175
184
|
const exitSource = tmuxSession ? "tmux-session-ended" : input.hasProcess ? "process-exit" : "provider-status";
|
|
176
|
-
const claudeResumeId = input.provider
|
|
185
|
+
const claudeResumeId = getManifest(input.provider)?.resume?.supported && input.logFile ? latestResumeIdFromLogFile(input.provider, input.logFile) : undefined;
|
|
177
186
|
return {
|
|
178
187
|
status: input.status,
|
|
179
188
|
runtimeMs: Number.isFinite(input.runtimeMs) ? input.runtimeMs : null,
|
|
@@ -225,18 +234,39 @@ export function lifecycleCapabilities(): Record<string, true> {
|
|
|
225
234
|
};
|
|
226
235
|
}
|
|
227
236
|
|
|
237
|
+
function resolveCondition(condition: ProviderCapabilityCondition, options: RuntimeProviderOptions): boolean {
|
|
238
|
+
switch (condition) {
|
|
239
|
+
case "always": return true;
|
|
240
|
+
case "never": return false;
|
|
241
|
+
case "headless": return options.headless;
|
|
242
|
+
case "interactive": return !options.headless;
|
|
243
|
+
case "approval:read-only": return options.approvalMode === "read-only";
|
|
244
|
+
case "approval:not-read-only": return options.approvalMode !== "read-only";
|
|
245
|
+
case "adapter:compact-supports-instructions": return Boolean(options.adapter.compactSupportsInstructions);
|
|
246
|
+
case "adapter:clear-context": return Boolean(options.adapter.clearContext);
|
|
247
|
+
default: return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveBooleanDefault(def: BooleanCapabilityDefault, options: RuntimeProviderOptions): boolean {
|
|
252
|
+
if (typeof def === "boolean") return def;
|
|
253
|
+
return resolveCondition(def.when, options) ? def.value : def.otherwise;
|
|
254
|
+
}
|
|
255
|
+
|
|
228
256
|
export function runtimeProviderCapabilities(options: RuntimeProviderOptions, contextState?: ContextState, probeModel?: ProbeModelInfo, quotaState?: QuotaState): ProviderCapabilities {
|
|
257
|
+
const manifest = getManifest(options.provider);
|
|
258
|
+
const capDefaults = manifest?.capabilityDefaults;
|
|
229
259
|
const model = options.model ?? probeModel?.model;
|
|
230
260
|
const effort = options.effort ?? probeModel?.effort;
|
|
231
261
|
const modelSource = options.model ? "runtime" as const : probeModel?.model ? "provider" as const : "runtime" as const;
|
|
232
262
|
const shellMode = runtimeShellMode(options);
|
|
233
263
|
return {
|
|
234
264
|
lifecycle: {
|
|
235
|
-
managed: true,
|
|
236
|
-
shutdownHard:
|
|
237
|
-
restartHard:
|
|
238
|
-
semanticStatus: true,
|
|
239
|
-
reconnect: true,
|
|
265
|
+
managed: capDefaults?.lifecycle?.managed ?? true,
|
|
266
|
+
shutdownHard: capDefaults?.lifecycle?.shutdownHard !== undefined ? resolveBooleanDefault(capDefaults.lifecycle.shutdownHard, options) : true,
|
|
267
|
+
restartHard: capDefaults?.lifecycle?.restartHard !== undefined ? resolveBooleanDefault(capDefaults.lifecycle.restartHard, options) : true,
|
|
268
|
+
semanticStatus: capDefaults?.lifecycle?.semanticStatus ?? true,
|
|
269
|
+
reconnect: capDefaults?.lifecycle?.reconnect ?? true,
|
|
240
270
|
},
|
|
241
271
|
model: {
|
|
242
272
|
provider: options.provider,
|
|
@@ -266,7 +296,7 @@ export function runtimeProviderCapabilities(options: RuntimeProviderOptions, con
|
|
|
266
296
|
interrupt: Boolean(options.adapter.interrupt),
|
|
267
297
|
promptEcho: true,
|
|
268
298
|
reasoning: true,
|
|
269
|
-
slashCommands:
|
|
299
|
+
slashCommands: capDefaults?.liveSession?.slashCommands ?? false,
|
|
270
300
|
},
|
|
271
301
|
source: "runtime",
|
|
272
302
|
confidence: "reported",
|
|
@@ -275,34 +305,23 @@ export function runtimeProviderCapabilities(options: RuntimeProviderOptions, con
|
|
|
275
305
|
}
|
|
276
306
|
|
|
277
307
|
function runtimeShellMode(options: RuntimeProviderOptions): NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]> {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
308
|
+
const manifest = getManifest(options.provider);
|
|
309
|
+
const shellModeMap = manifest?.capabilityDefaults?.session?.shellMode;
|
|
310
|
+
if (options.approvalMode === "read-only") return (shellModeMap?.readOnly ?? "none") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
|
|
311
|
+
if (options.approvalMode === "open") return (shellModeMap?.open ?? "unrestricted") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
|
|
312
|
+
return (shellModeMap?.guarded ?? "guarded") as NonNullable<NonNullable<ProviderCapabilities["session"]>["shellMode"]>;
|
|
282
313
|
}
|
|
283
314
|
|
|
284
315
|
function runtimeProviderTerminalCapabilities(options: RuntimeProviderOptions): Pick<ProviderCapabilities, "terminal"> {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
};
|
|
316
|
+
const manifest = getManifest(options.provider);
|
|
317
|
+
const termDefaults = manifest?.capabilityDefaults?.terminal;
|
|
318
|
+
if (!termDefaults || termDefaults.mode === "none") return {};
|
|
319
|
+
if (termDefaults.condition && !resolveCondition(termDefaults.condition, options)) return {};
|
|
320
|
+
if (termDefaults.mode === "live" && termDefaults.live) {
|
|
321
|
+
return { terminal: { live: termDefaults.live } };
|
|
294
322
|
}
|
|
295
|
-
if (
|
|
296
|
-
return {
|
|
297
|
-
terminal: {
|
|
298
|
-
attach: {
|
|
299
|
-
create: true,
|
|
300
|
-
read: true,
|
|
301
|
-
write: true,
|
|
302
|
-
detach: true,
|
|
303
|
-
},
|
|
304
|
-
},
|
|
305
|
-
};
|
|
323
|
+
if (termDefaults.mode === "attach" && termDefaults.attach) {
|
|
324
|
+
return { terminal: { attach: termDefaults.attach } };
|
|
306
325
|
}
|
|
307
326
|
return {};
|
|
308
327
|
}
|
|
@@ -316,7 +335,7 @@ export function appliedAgentProfileMetadata(
|
|
|
316
335
|
config?: RunnerSpawnConfig,
|
|
317
336
|
providerConfig?: ProviderConfig,
|
|
318
337
|
): Record<string, unknown> {
|
|
319
|
-
const projection = (provider
|
|
338
|
+
const projection = isSpawnProvider(provider) && config && providerConfig
|
|
320
339
|
? agentProfileProjectionReport({ assembled: assembleLaunch(provider, config, providerConfig) })
|
|
321
340
|
: undefined;
|
|
322
341
|
return {
|
|
@@ -340,29 +359,30 @@ export function appliedAgentProfileMetadata(
|
|
|
340
359
|
}
|
|
341
360
|
|
|
342
361
|
function runtimeProviderContextCapabilities(options: RuntimeProviderOptions, contextState?: ContextState): Pick<ProviderCapabilities, "context"> {
|
|
362
|
+
const manifest = getManifest(options.provider);
|
|
363
|
+
const ctxDefaults = manifest?.capabilityDefaults?.context;
|
|
343
364
|
const context: NonNullable<ProviderCapabilities["context"]> = { resume: "none" };
|
|
344
|
-
const supportsManagedContext =
|
|
365
|
+
const supportsManagedContext = ctxDefaults?.managed ? resolveCondition(ctxDefaults.managed, options) : false;
|
|
345
366
|
if (contextState) {
|
|
346
367
|
context.stats = { source: contextState.source, confidence: contextState.confidence };
|
|
347
368
|
if (typeof contextState.tokensMax === "number" && Number.isFinite(contextState.tokensMax)) {
|
|
348
369
|
context.windowTokens = contextState.tokensMax;
|
|
349
370
|
}
|
|
350
371
|
}
|
|
351
|
-
if (
|
|
352
|
-
context.compact = true;
|
|
353
|
-
context.clear = true;
|
|
372
|
+
if (ctxDefaults) {
|
|
373
|
+
if (resolveBooleanDefault(ctxDefaults.compact, options)) context.compact = true;
|
|
374
|
+
if (resolveBooleanDefault(ctxDefaults.clear, options)) context.clear = true;
|
|
375
|
+
context.inject = ctxDefaults.inject;
|
|
354
376
|
}
|
|
355
|
-
context.inject = true;
|
|
356
377
|
if (supportsManagedContext && options.adapter.compact && options.adapter.compactSupportsInstructions) context.resume = "native";
|
|
357
378
|
else if (supportsManagedContext && options.adapter.clearContext) context.resume = "clear-inject";
|
|
358
379
|
return Object.keys(context).length ? { context } : {};
|
|
359
380
|
}
|
|
360
381
|
|
|
361
382
|
function runtimeProviderQuotaCapabilities(options: RuntimeProviderOptions, quotaState?: QuotaState): Pick<ProviderCapabilities, "quota"> {
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
: [{ name: "primary", unit: "%" as const, reset: true }, { name: "secondary", unit: "%" as const, reset: true }];
|
|
383
|
+
const manifest = getManifest(options.provider);
|
|
384
|
+
const quotaDefaults = manifest?.capabilityDefaults?.quota;
|
|
385
|
+
if (!quotaDefaults?.supported) return {};
|
|
366
386
|
return {
|
|
367
387
|
quota: {
|
|
368
388
|
supported: true,
|
|
@@ -372,7 +392,7 @@ function runtimeProviderQuotaCapabilities(options: RuntimeProviderOptions, quota
|
|
|
372
392
|
name: window.name,
|
|
373
393
|
unit: window.unit,
|
|
374
394
|
reset: window.resetsAt !== undefined,
|
|
375
|
-
})) ??
|
|
395
|
+
})) ?? manifest!.quotaWindows.map(w => ({ name: w.name, unit: w.unit as "%" | "tokens" | "requests", reset: w.reset })),
|
|
376
396
|
},
|
|
377
397
|
};
|
|
378
398
|
}
|
package/src/runner.ts
CHANGED
package/src/version.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { CONTRACT_VERSIONS } from "agent-relay-sdk";
|
|
5
|
+
import { getManifest } from "agent-relay-providers";
|
|
5
6
|
|
|
6
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8")) as { name?: string; version?: string };
|
|
@@ -26,6 +27,7 @@ export const RUNTIME_CAPABILITIES = {
|
|
|
26
27
|
} as const;
|
|
27
28
|
|
|
28
29
|
export function runtimeMetadata(provider?: string) {
|
|
30
|
+
const manifest = provider ? getManifest(provider) : undefined;
|
|
29
31
|
return {
|
|
30
32
|
package: {
|
|
31
33
|
name: PACKAGE_NAME,
|
|
@@ -34,8 +36,8 @@ export function runtimeMetadata(provider?: string) {
|
|
|
34
36
|
contracts: CONTRACTS,
|
|
35
37
|
capabilities: {
|
|
36
38
|
...RUNTIME_CAPABILITIES,
|
|
37
|
-
...(
|
|
38
|
-
...(
|
|
39
|
+
...(manifest?.provisioning?.bundledPlugin ? { bundledPlugin: true } : {}),
|
|
40
|
+
...(manifest?.provisioning?.bundledSkills ? { bundledSkills: true } : {}),
|
|
39
41
|
},
|
|
40
42
|
};
|
|
41
43
|
}
|