@byok-sdk/client 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -5
- package/dist/adapters/claude/claude-adapter.d.ts +6 -19
- package/dist/adapters/claude/events.d.ts +3 -0
- package/dist/adapters/claude/process-client.d.ts +9 -1
- package/dist/adapters/codex/codex-adapter.d.ts +4 -15
- package/dist/adapters/codex/process-runner.d.ts +4 -1
- package/dist/adapters/index.d.ts +4 -2
- package/dist/adapters/index.js +1081 -258
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +24 -15
- package/dist/adapters/pi/rpc-client.d.ts +9 -1
- package/dist/adapters/process-tree.d.ts +19 -0
- package/dist/adapters/provider-credential-environment.d.ts +18 -0
- package/dist/bin/audit-log.d.ts +12 -0
- package/dist/bin/byok-agent.js +2686 -912
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js +2 -2
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/workspaces.d.ts +11 -0
- package/dist/bin/format.d.ts +13 -0
- package/dist/bin/runtime-probe.d.ts +1 -1
- package/dist/bin/tasks-view.d.ts +13 -0
- package/dist/daemon/approvals.d.ts +2 -2
- package/dist/daemon/assertion-client.d.ts +68 -0
- package/dist/daemon/capabilities-client.d.ts +48 -0
- package/dist/daemon/connection-manager.d.ts +4 -2
- package/dist/daemon/control-protocol.d.ts +81 -4
- package/dist/daemon/control-server.d.ts +18 -1
- package/dist/daemon/create-daemon.d.ts +171 -3
- package/dist/daemon/daemon-owner.d.ts +37 -0
- package/dist/daemon/device-assertion-signer.d.ts +41 -0
- package/dist/daemon/device-keys.d.ts +15 -13
- package/dist/daemon/environment.d.ts +9 -9
- package/dist/daemon/git-workspace.d.ts +21 -0
- package/dist/daemon/observer.d.ts +81 -3
- package/dist/daemon/presence-publisher.d.ts +98 -0
- package/dist/daemon/runtime-capabilities.d.ts +1 -1
- package/dist/daemon/skill-pack-installer.d.ts +116 -0
- package/dist/daemon/task-runner.d.ts +156 -37
- package/dist/daemon/ws-transport.d.ts +3 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.js +2972 -597
- package/dist/index.js.map +1 -1
- package/dist/runtime-failure.d.ts +64 -0
- package/dist/types.d.ts +114 -58
- package/package.json +4 -4
package/dist/bin/byok-agent.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile, spawn, spawnSync } from 'child_process';
|
|
3
|
-
import { randomUUID, createHash, randomBytes, timingSafeEqual, createPrivateKey, generateKeyPairSync, sign
|
|
3
|
+
import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, createPrivateKey, generateKeyPairSync, sign } from 'crypto';
|
|
4
4
|
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
|
|
5
|
-
import path20, {
|
|
5
|
+
import path20, { isAbsolute, join } from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
|
-
import {
|
|
7
|
+
import { DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
8
|
+
import { TASK_STATES, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
8
9
|
import { promisify } from 'util';
|
|
9
10
|
import { fileURLToPath } from 'url';
|
|
10
11
|
import 'readline';
|
|
@@ -14,6 +15,52 @@ import { createRequire } from 'module';
|
|
|
14
15
|
import { createInterface } from 'readline/promises';
|
|
15
16
|
|
|
16
17
|
// src/types.ts
|
|
18
|
+
function frozenStrings(values) {
|
|
19
|
+
return values === void 0 ? void 0 : Object.freeze([...values]);
|
|
20
|
+
}
|
|
21
|
+
function frozenPolicy(policy) {
|
|
22
|
+
const allowTools = policy.allowTools === void 0 ? void 0 : Object.freeze([...policy.allowTools]);
|
|
23
|
+
const denyTools = policy.denyTools === void 0 ? void 0 : Object.freeze([...policy.denyTools]);
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
mode: policy.mode,
|
|
26
|
+
...allowTools === void 0 ? {} : { allowTools },
|
|
27
|
+
...denyTools === void 0 ? {} : { denyTools },
|
|
28
|
+
...policy.workspaceRoot === void 0 ? {} : { workspaceRoot: policy.workspaceRoot },
|
|
29
|
+
...policy.network === void 0 ? {} : { network: policy.network }
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
33
|
+
const baseNames = frozenStrings(descriptor.environmentRequirements.baseNames);
|
|
34
|
+
const credentialNames = frozenStrings(descriptor.environmentRequirements.credentialNames);
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
id: descriptor.id,
|
|
37
|
+
supportsDispatchSelection: descriptor.supportsDispatchSelection === true,
|
|
38
|
+
capabilities: Object.freeze({
|
|
39
|
+
steer: descriptor.capabilities.steer === true,
|
|
40
|
+
resume: descriptor.capabilities.resume === true,
|
|
41
|
+
approvalInteractive: descriptor.capabilities.approvalInteractive === true,
|
|
42
|
+
...descriptor.capabilities.mcpToolsets === void 0 ? {} : { mcpToolsets: descriptor.capabilities.mcpToolsets === true },
|
|
43
|
+
permissionModes: Object.freeze([...descriptor.capabilities.permissionModes])
|
|
44
|
+
}),
|
|
45
|
+
environmentRequirements: Object.freeze({
|
|
46
|
+
...baseNames === void 0 ? {} : { baseNames },
|
|
47
|
+
...credentialNames === void 0 ? {} : { credentialNames }
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function sealRuntimeOperationManifest(manifest) {
|
|
52
|
+
return Object.freeze({
|
|
53
|
+
taskId: manifest.taskId,
|
|
54
|
+
runtimeId: manifest.runtimeId,
|
|
55
|
+
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
56
|
+
policy: frozenPolicy(manifest.policy),
|
|
57
|
+
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
58
|
+
...manifest.dispatchSelection === void 0 ? {} : { dispatchSelection: Object.freeze({ ...manifest.dispatchSelection }) },
|
|
59
|
+
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
60
|
+
workspace: Object.freeze({ ...manifest.workspace }),
|
|
61
|
+
forwardedEnvironmentNames: Object.freeze([...manifest.forwardedEnvironmentNames])
|
|
62
|
+
});
|
|
63
|
+
}
|
|
17
64
|
var PolicyUnsupportedError = class extends Error {
|
|
18
65
|
constructor(message) {
|
|
19
66
|
super(message);
|
|
@@ -21,7 +68,7 @@ var PolicyUnsupportedError = class extends Error {
|
|
|
21
68
|
}
|
|
22
69
|
};
|
|
23
70
|
var SteerUnsupportedError = class extends Error {
|
|
24
|
-
/** The `RuntimeAdapter.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
71
|
+
/** The `RuntimeAdapter.descriptor.id` that cannot steer (e.g. `claude`, `codex`). */
|
|
25
72
|
runtimeId;
|
|
26
73
|
constructor(runtimeId, message) {
|
|
27
74
|
super(message);
|
|
@@ -29,6 +76,112 @@ var SteerUnsupportedError = class extends Error {
|
|
|
29
76
|
this.runtimeId = runtimeId;
|
|
30
77
|
}
|
|
31
78
|
};
|
|
79
|
+
|
|
80
|
+
// src/runtime-failure.ts
|
|
81
|
+
var RUNTIME_EXECUTION_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeExecutionFailure/v1");
|
|
82
|
+
var RUNTIME_DISPOSAL_FAILURE_BRAND = /* @__PURE__ */ Symbol.for("@byok-sdk/client/RuntimeDisposalFailure/v1");
|
|
83
|
+
var RuntimeDisposalFailure = class extends Error {
|
|
84
|
+
stage;
|
|
85
|
+
constructor(input, options) {
|
|
86
|
+
if (!isRuntimeDisposalStage(input.stage) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
87
|
+
throw new TypeError("invalid RuntimeDisposalFailure input");
|
|
88
|
+
}
|
|
89
|
+
super(input.reason, options);
|
|
90
|
+
this.name = "RuntimeDisposalFailure";
|
|
91
|
+
this.stage = input.stage;
|
|
92
|
+
Object.defineProperty(this, RUNTIME_DISPOSAL_FAILURE_BRAND, { value: true });
|
|
93
|
+
Object.freeze(this);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function isRuntimeDisposalStage(value) {
|
|
97
|
+
return value === "signal" || value === "quiescence" || value === "cleanup";
|
|
98
|
+
}
|
|
99
|
+
function isRuntimeDisposalFailure(value) {
|
|
100
|
+
if (typeof value !== "object" || value === null) return false;
|
|
101
|
+
const candidate = value;
|
|
102
|
+
return candidate[RUNTIME_DISPOSAL_FAILURE_BRAND] === true && isRuntimeDisposalStage(candidate.stage) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
103
|
+
}
|
|
104
|
+
var RuntimeExecutionFailure = class extends Error {
|
|
105
|
+
phase;
|
|
106
|
+
category;
|
|
107
|
+
retry;
|
|
108
|
+
constructor(input, options) {
|
|
109
|
+
if (!isRuntimeFailurePhase(input.phase) || !isRuntimeFailureCategory(input.category) || !isRuntimeRetryDisposition(input.retry) || typeof input.reason !== "string" || input.reason.length === 0) {
|
|
110
|
+
throw new TypeError("invalid RuntimeExecutionFailure input");
|
|
111
|
+
}
|
|
112
|
+
super(input.reason, options);
|
|
113
|
+
this.name = "RuntimeExecutionFailure";
|
|
114
|
+
this.phase = input.phase;
|
|
115
|
+
this.category = input.category;
|
|
116
|
+
this.retry = input.retry;
|
|
117
|
+
Object.defineProperty(this, RUNTIME_EXECUTION_FAILURE_BRAND, { value: true });
|
|
118
|
+
Object.freeze(this);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
function isRuntimeFailurePhase(value) {
|
|
122
|
+
return value === "start" || value === "run";
|
|
123
|
+
}
|
|
124
|
+
function isRuntimeFailureCategory(value) {
|
|
125
|
+
return value === "semantic" || value === "infrastructure" || value === "authority";
|
|
126
|
+
}
|
|
127
|
+
function isRuntimeRetryDisposition(value) {
|
|
128
|
+
return value === "retryable" || value === "non-retryable";
|
|
129
|
+
}
|
|
130
|
+
function isRuntimeExecutionFailure(value) {
|
|
131
|
+
if (typeof value !== "object" || value === null) return false;
|
|
132
|
+
const candidate = value;
|
|
133
|
+
return candidate[RUNTIME_EXECUTION_FAILURE_BRAND] === true && isRuntimeFailurePhase(candidate.phase) && isRuntimeFailureCategory(candidate.category) && isRuntimeRetryDisposition(candidate.retry) && typeof candidate.message === "string" && candidate.message.length > 0;
|
|
134
|
+
}
|
|
135
|
+
function retryableFromDisposition(disposition) {
|
|
136
|
+
switch (disposition) {
|
|
137
|
+
case "retryable":
|
|
138
|
+
return true;
|
|
139
|
+
case "non-retryable":
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function projectRuntimeExecutionFailure(failure) {
|
|
144
|
+
return {
|
|
145
|
+
reason: failure.message,
|
|
146
|
+
retryable: retryableFromDisposition(failure.retry)
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
var RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON = Object.freeze({
|
|
150
|
+
start: "runtime adapter contract violation during start",
|
|
151
|
+
run: "runtime adapter contract violation during run"
|
|
152
|
+
});
|
|
153
|
+
function projectRuntimeBoundaryFailure(value, expectedPhase) {
|
|
154
|
+
if (isRuntimeExecutionFailure(value) && value.phase === expectedPhase) {
|
|
155
|
+
return { ...projectRuntimeExecutionFailure(value), contractViolation: false };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
reason: RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON[expectedPhase],
|
|
159
|
+
retryable: false,
|
|
160
|
+
contractViolation: true
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
var GIT_ERROR_CATEGORIES = [
|
|
164
|
+
"git-unavailable",
|
|
165
|
+
"git-timeout",
|
|
166
|
+
"git-output-limit",
|
|
167
|
+
"git-command-failed",
|
|
168
|
+
"workspace-root-invalid",
|
|
169
|
+
"workspace-root-conflict",
|
|
170
|
+
"workspace-not-owned",
|
|
171
|
+
"repository-root-mismatch",
|
|
172
|
+
"repository-invalid",
|
|
173
|
+
"lease-busy",
|
|
174
|
+
"ledger-invalid"
|
|
175
|
+
];
|
|
176
|
+
var GIT_WORKSPACE_PHASES = [
|
|
177
|
+
"preparing",
|
|
178
|
+
"active",
|
|
179
|
+
"completed",
|
|
180
|
+
"failed",
|
|
181
|
+
"cancelled",
|
|
182
|
+
"interrupted",
|
|
183
|
+
"salvage"
|
|
184
|
+
];
|
|
32
185
|
var GitWorkspaceError = class extends Error {
|
|
33
186
|
constructor(category, message = category) {
|
|
34
187
|
super(message);
|
|
@@ -944,6 +1097,155 @@ var AsyncQueue = class {
|
|
|
944
1097
|
};
|
|
945
1098
|
}
|
|
946
1099
|
};
|
|
1100
|
+
var DEFAULT_TERM_GRACE_MS = 750;
|
|
1101
|
+
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
1102
|
+
var POLL_MS = 20;
|
|
1103
|
+
var terminationRequested = /* @__PURE__ */ new WeakSet();
|
|
1104
|
+
var terminationRequestFailed = /* @__PURE__ */ new WeakSet();
|
|
1105
|
+
function withOwnedProcessTree(options) {
|
|
1106
|
+
return {
|
|
1107
|
+
...options,
|
|
1108
|
+
...process.platform === "win32" ? { windowsHide: true } : { detached: true }
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
function positivePid(child, label) {
|
|
1112
|
+
const pid = child.pid;
|
|
1113
|
+
if (pid === void 0) return void 0;
|
|
1114
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
|
|
1115
|
+
throw new RuntimeDisposalFailure({
|
|
1116
|
+
stage: "signal",
|
|
1117
|
+
reason: `${label} runtime process has an unsafe owned pid`
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
return pid;
|
|
1121
|
+
}
|
|
1122
|
+
function groupExists(pid, label) {
|
|
1123
|
+
try {
|
|
1124
|
+
process.kill(-pid, 0);
|
|
1125
|
+
return true;
|
|
1126
|
+
} catch (cause) {
|
|
1127
|
+
const code = cause.code;
|
|
1128
|
+
if (code === "ESRCH") return false;
|
|
1129
|
+
if (code === "EPERM") return true;
|
|
1130
|
+
throw new RuntimeDisposalFailure({
|
|
1131
|
+
stage: "quiescence",
|
|
1132
|
+
reason: `${label} runtime process-group state could not be verified`
|
|
1133
|
+
}, { cause });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function signalGroup(pid, signal, label) {
|
|
1137
|
+
try {
|
|
1138
|
+
process.kill(-pid, signal);
|
|
1139
|
+
} catch (cause) {
|
|
1140
|
+
const code = cause.code;
|
|
1141
|
+
if (code === "ESRCH" || code === "EPERM") return;
|
|
1142
|
+
throw new RuntimeDisposalFailure({
|
|
1143
|
+
stage: "signal",
|
|
1144
|
+
reason: `${label} runtime process group ${pid} could not receive ${signal} (${code ?? "unknown"})`
|
|
1145
|
+
}, { cause });
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
async function waitUntil(predicate, timeoutMs) {
|
|
1149
|
+
const deadline = Date.now() + timeoutMs;
|
|
1150
|
+
while (predicate()) {
|
|
1151
|
+
if (Date.now() >= deadline) return false;
|
|
1152
|
+
await new Promise((resolve) => {
|
|
1153
|
+
setTimeout(resolve, POLL_MS);
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
1158
|
+
async function waitWithDeadline(promise, timeoutMs) {
|
|
1159
|
+
return new Promise((resolve) => {
|
|
1160
|
+
let settled = false;
|
|
1161
|
+
const timer = setTimeout(() => {
|
|
1162
|
+
if (!settled) {
|
|
1163
|
+
settled = true;
|
|
1164
|
+
resolve(false);
|
|
1165
|
+
}
|
|
1166
|
+
}, timeoutMs);
|
|
1167
|
+
void promise.then(
|
|
1168
|
+
() => {
|
|
1169
|
+
if (!settled) {
|
|
1170
|
+
settled = true;
|
|
1171
|
+
clearTimeout(timer);
|
|
1172
|
+
resolve(true);
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
() => {
|
|
1176
|
+
if (!settled) {
|
|
1177
|
+
settled = true;
|
|
1178
|
+
clearTimeout(timer);
|
|
1179
|
+
resolve(false);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
);
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
function requestOwnedProcessTreeTermination(options) {
|
|
1186
|
+
if (options.isClosed()) return;
|
|
1187
|
+
const pid = positivePid(options.child, options.label);
|
|
1188
|
+
if (pid === void 0) return;
|
|
1189
|
+
if (process.platform === "win32") {
|
|
1190
|
+
const result = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true });
|
|
1191
|
+
if (result.error) {
|
|
1192
|
+
throw new RuntimeDisposalFailure({
|
|
1193
|
+
stage: "signal",
|
|
1194
|
+
reason: `${options.label} runtime process tree could not be terminated`
|
|
1195
|
+
}, { cause: result.error });
|
|
1196
|
+
}
|
|
1197
|
+
terminationRequested.add(options.child);
|
|
1198
|
+
if (result.status !== 0) terminationRequestFailed.add(options.child);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
signalGroup(pid, "SIGTERM", options.label);
|
|
1202
|
+
terminationRequested.add(options.child);
|
|
1203
|
+
}
|
|
1204
|
+
async function disposeOwnedProcessTree(options) {
|
|
1205
|
+
const pid = positivePid(options.child, options.label);
|
|
1206
|
+
const termGraceMs = options.termGraceMs ?? DEFAULT_TERM_GRACE_MS;
|
|
1207
|
+
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
1208
|
+
if (pid === void 0) {
|
|
1209
|
+
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
1210
|
+
throw new RuntimeDisposalFailure({
|
|
1211
|
+
stage: "quiescence",
|
|
1212
|
+
reason: `${options.label} runtime process did not settle after spawn failure`
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
if (process.platform === "win32") {
|
|
1216
|
+
if (!options.isClosed() && !terminationRequested.has(options.child)) requestOwnedProcessTreeTermination(options);
|
|
1217
|
+
if (await waitWithDeadline(options.waitClosed(), killGraceMs)) return;
|
|
1218
|
+
if (terminationRequestFailed.has(options.child)) {
|
|
1219
|
+
throw new RuntimeDisposalFailure({
|
|
1220
|
+
stage: "signal",
|
|
1221
|
+
reason: `${options.label} runtime process tree could not be terminated`
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
throw new RuntimeDisposalFailure({
|
|
1225
|
+
stage: "quiescence",
|
|
1226
|
+
reason: `${options.label} runtime process tree did not close before the disposal deadline`
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
if (groupExists(pid, options.label) && !terminationRequested.has(options.child)) {
|
|
1230
|
+
signalGroup(pid, "SIGTERM", options.label);
|
|
1231
|
+
terminationRequested.add(options.child);
|
|
1232
|
+
}
|
|
1233
|
+
if (!await waitUntil(() => groupExists(pid, options.label), termGraceMs)) {
|
|
1234
|
+
signalGroup(pid, "SIGKILL", options.label);
|
|
1235
|
+
if (!await waitUntil(() => groupExists(pid, options.label), killGraceMs)) {
|
|
1236
|
+
throw new RuntimeDisposalFailure({
|
|
1237
|
+
stage: "quiescence",
|
|
1238
|
+
reason: `${options.label} runtime process group remained live after SIGKILL`
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (!await waitWithDeadline(options.waitClosed(), killGraceMs)) {
|
|
1243
|
+
throw new RuntimeDisposalFailure({
|
|
1244
|
+
stage: "quiescence",
|
|
1245
|
+
reason: `${options.label} runtime root did not emit close after its process group exited`
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
947
1249
|
|
|
948
1250
|
// src/adapters/pi/rpc-client.ts
|
|
949
1251
|
var STDERR_RING_CAPACITY = 20;
|
|
@@ -956,16 +1258,22 @@ var PiRpcClient = class {
|
|
|
956
1258
|
eventQueue = new AsyncQueue();
|
|
957
1259
|
closed = false;
|
|
958
1260
|
exitError;
|
|
1261
|
+
closedPromise;
|
|
1262
|
+
resolveClosed;
|
|
1263
|
+
disposalAttempt;
|
|
959
1264
|
/** Bounded tail of recent stderr lines — pi discarded this entirely before (nothing ever read `child.stderr`), which is exactly why finding #1 (`Error: Unknown option: --session-id`, exit 1) had to be root-caused by hand instead of reading it off a thrown error. See `buildExitError`. */
|
|
960
1265
|
stderrRing = [];
|
|
961
1266
|
/** Count of pi RPC message types `PiSession` (pi-adapter.ts) has told us have no `AgentEvent` mapping and aren't routine bookkeeping — see `recordUnmappedFrame`. */
|
|
962
1267
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
963
1268
|
constructor(options) {
|
|
964
1269
|
const spawnFn = options.spawnFn ?? spawn;
|
|
965
|
-
this.child = spawnFn(options.command, options.args, {
|
|
1270
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
966
1271
|
cwd: options.cwd,
|
|
967
1272
|
env: options.env,
|
|
968
1273
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1274
|
+
}));
|
|
1275
|
+
this.closedPromise = new Promise((resolve) => {
|
|
1276
|
+
this.resolveClosed = resolve;
|
|
969
1277
|
});
|
|
970
1278
|
this.child.stdout.setEncoding("utf8");
|
|
971
1279
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -1000,6 +1308,10 @@ var PiRpcClient = class {
|
|
|
1000
1308
|
get events() {
|
|
1001
1309
|
return this.eventQueue;
|
|
1002
1310
|
}
|
|
1311
|
+
/** Local transport diagnostic retained when the process closes; consumers must classify it explicitly. */
|
|
1312
|
+
get terminalError() {
|
|
1313
|
+
return this.exitError;
|
|
1314
|
+
}
|
|
1003
1315
|
/**
|
|
1004
1316
|
* Record a pi RPC message `type` that `PiSession` (pi-adapter.ts) decided
|
|
1005
1317
|
* has no `AgentEvent` mapping and isn't routine bookkeeping (see
|
|
@@ -1018,15 +1330,30 @@ var PiRpcClient = class {
|
|
|
1018
1330
|
);
|
|
1019
1331
|
}
|
|
1020
1332
|
}
|
|
1021
|
-
/**
|
|
1333
|
+
/** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
|
|
1022
1334
|
kill() {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1335
|
+
requestOwnedProcessTreeTermination(this.processTreeOptions());
|
|
1336
|
+
}
|
|
1337
|
+
waitClosed() {
|
|
1338
|
+
return this.closedPromise;
|
|
1339
|
+
}
|
|
1340
|
+
dispose() {
|
|
1341
|
+
if (!this.disposalAttempt) {
|
|
1342
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
1343
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
1344
|
+
this.disposalAttempt = void 0;
|
|
1345
|
+
throw error;
|
|
1346
|
+
});
|
|
1029
1347
|
}
|
|
1348
|
+
return this.disposalAttempt;
|
|
1349
|
+
}
|
|
1350
|
+
processTreeOptions() {
|
|
1351
|
+
return {
|
|
1352
|
+
child: this.child,
|
|
1353
|
+
waitClosed: () => this.closedPromise,
|
|
1354
|
+
isClosed: () => this.closed,
|
|
1355
|
+
label: "pi"
|
|
1356
|
+
};
|
|
1030
1357
|
}
|
|
1031
1358
|
onData(chunk) {
|
|
1032
1359
|
this.buffer += chunk;
|
|
@@ -1112,19 +1439,15 @@ var PiRpcClient = class {
|
|
|
1112
1439
|
if (this.closed) return;
|
|
1113
1440
|
this.closed = true;
|
|
1114
1441
|
this.exitError = err;
|
|
1442
|
+
this.resolveClosed();
|
|
1115
1443
|
for (const [, waiter] of this.pending) waiter.reject(err);
|
|
1116
1444
|
this.pending.clear();
|
|
1117
1445
|
this.eventQueue.end();
|
|
1118
1446
|
}
|
|
1119
1447
|
};
|
|
1120
1448
|
|
|
1121
|
-
// src/adapters/
|
|
1122
|
-
var
|
|
1123
|
-
var DETECT_TIMEOUT_MS = 5e3;
|
|
1124
|
-
function errorMessage(err) {
|
|
1125
|
-
return err instanceof Error ? err.message : String(err);
|
|
1126
|
-
}
|
|
1127
|
-
var KNOWN_PROVIDER_ENV_VARS = [
|
|
1449
|
+
// src/adapters/provider-credential-environment.ts
|
|
1450
|
+
var PROVIDER_CREDENTIAL_ENV_NAMES = [
|
|
1128
1451
|
"ANTHROPIC_API_KEY",
|
|
1129
1452
|
"ANTHROPIC_OAUTH_TOKEN",
|
|
1130
1453
|
"OPENAI_API_KEY",
|
|
@@ -1135,123 +1458,288 @@ var KNOWN_PROVIDER_ENV_VARS = [
|
|
|
1135
1458
|
"MISTRAL_API_KEY",
|
|
1136
1459
|
"OPENROUTER_API_KEY",
|
|
1137
1460
|
"XAI_API_KEY",
|
|
1138
|
-
// Confirmed against the installed pi's own docs/providers.md ("ZAI |
|
|
1139
|
-
// `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
|
|
1140
|
-
// during this task's acceptance run — omitting it made `authPresent`
|
|
1141
|
-
// silently false for a perfectly valid, working z.ai/GLM setup.
|
|
1142
1461
|
"ZAI_API_KEY"
|
|
1143
1462
|
];
|
|
1463
|
+
var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
|
|
1464
|
+
...PROVIDER_CREDENTIAL_ENV_NAMES,
|
|
1465
|
+
"ANT_LING_API_KEY",
|
|
1466
|
+
"NVIDIA_API_KEY",
|
|
1467
|
+
"CEREBRAS_API_KEY",
|
|
1468
|
+
"CLOUDFLARE_API_KEY",
|
|
1469
|
+
"AI_GATEWAY_API_KEY",
|
|
1470
|
+
"ZAI_CODING_CN_API_KEY",
|
|
1471
|
+
"OPENCODE_API_KEY",
|
|
1472
|
+
"RADIUS_API_KEY",
|
|
1473
|
+
"FIREWORKS_API_KEY",
|
|
1474
|
+
"TOGETHER_API_KEY",
|
|
1475
|
+
"BASETEN_API_KEY",
|
|
1476
|
+
"KIMI_API_KEY",
|
|
1477
|
+
"MINIMAX_API_KEY",
|
|
1478
|
+
"MINIMAX_CN_API_KEY",
|
|
1479
|
+
"QWEN_TOKEN_PLAN_API_KEY",
|
|
1480
|
+
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
1481
|
+
"XIAOMI_API_KEY",
|
|
1482
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
1483
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
1484
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
1485
|
+
"AWS_ACCESS_KEY_ID",
|
|
1486
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1487
|
+
"AWS_SESSION_TOKEN",
|
|
1488
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
1489
|
+
// Reserved by the keys-owned Pi projection. It must never be inherited
|
|
1490
|
+
// from the daemon; the launcher deletes any ambient copy and injects only
|
|
1491
|
+
// the exact credential it just resolved from OS custody.
|
|
1492
|
+
"PI_PROVIDER_API_KEY"
|
|
1493
|
+
];
|
|
1494
|
+
function withoutProviderCredentials(env) {
|
|
1495
|
+
const sanitized = { ...env };
|
|
1496
|
+
for (const name of PROVIDER_CREDENTIAL_ENV_DENY_NAMES) {
|
|
1497
|
+
delete sanitized[name];
|
|
1498
|
+
}
|
|
1499
|
+
return sanitized;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// src/adapters/pi/pi-adapter.ts
|
|
1503
|
+
var execFileAsync = promisify(execFile);
|
|
1504
|
+
var DETECT_TIMEOUT_MS = 5e3;
|
|
1505
|
+
function errorMessage(err) {
|
|
1506
|
+
return err instanceof Error ? err.message : String(err);
|
|
1507
|
+
}
|
|
1144
1508
|
var PiAdapter = class {
|
|
1145
1509
|
constructor(options = {}) {
|
|
1146
1510
|
this.options = options;
|
|
1147
1511
|
}
|
|
1148
1512
|
options;
|
|
1149
|
-
|
|
1513
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
1514
|
+
id: "pi",
|
|
1515
|
+
supportsDispatchSelection: true,
|
|
1516
|
+
capabilities: {
|
|
1517
|
+
steer: true,
|
|
1518
|
+
resume: true,
|
|
1519
|
+
approvalInteractive: false,
|
|
1520
|
+
permissionModes: ["auto", "readonly"]
|
|
1521
|
+
},
|
|
1522
|
+
environmentRequirements: { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES }
|
|
1523
|
+
});
|
|
1150
1524
|
async detect() {
|
|
1151
1525
|
try {
|
|
1152
1526
|
const bin = this.resolveBin();
|
|
1153
1527
|
const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
|
|
1154
1528
|
const version = stdout.trim() || stderr.trim();
|
|
1155
|
-
const authPresent =
|
|
1529
|
+
const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
|
|
1156
1530
|
return { present: true, version, authPresent };
|
|
1157
1531
|
} catch {
|
|
1158
1532
|
return { present: false };
|
|
1159
1533
|
}
|
|
1160
1534
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}
|
|
1164
|
-
/**
|
|
1165
|
-
* M5: pi authenticates to its ~30 supported providers via env-var API
|
|
1166
|
-
* keys — `detect()`'s own `authPresent` probe above checks this identical
|
|
1167
|
-
* list — so these MUST keep flowing into pi's spawned process or pi auth
|
|
1168
|
-
* breaks entirely. `KNOWN_PROVIDER_ENV_VARS` above is the single source
|
|
1169
|
-
* of truth, reused here rather than duplicated. No `baseNames`: nothing
|
|
1170
|
-
* in this adapter or `rpc-client.ts` reads a pi-specific config-discovery
|
|
1171
|
-
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
1172
|
-
*/
|
|
1173
|
-
environmentRequirements() {
|
|
1174
|
-
return { credentialNames: KNOWN_PROVIDER_ENV_VARS };
|
|
1175
|
-
}
|
|
1176
|
-
async start(task, ctx) {
|
|
1177
|
-
if (typeof task.instruction !== "string") {
|
|
1178
|
-
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1179
|
-
}
|
|
1180
|
-
const mapping = mapPermissionPolicyToPiArgs(ctx.policy);
|
|
1535
|
+
async prepare(input) {
|
|
1536
|
+
const mapping = mapPermissionPolicyToPiArgs(input.policy);
|
|
1181
1537
|
if (!mapping.ok) {
|
|
1182
|
-
|
|
1538
|
+
return { kind: "reject", reason: mapping.reason ?? "policy rejected by pi adapter", retryable: false };
|
|
1183
1539
|
}
|
|
1184
1540
|
const bin = this.resolveBin();
|
|
1185
|
-
const
|
|
1186
|
-
const
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1541
|
+
const selection = input.offer.dispatchSelection;
|
|
1542
|
+
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({ ...selection });
|
|
1543
|
+
let command = bin.command;
|
|
1544
|
+
let launcherArgs;
|
|
1545
|
+
if (pinnedSelection !== void 0) {
|
|
1546
|
+
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
1547
|
+
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
1548
|
+
}
|
|
1549
|
+
const launcher = this.options.byokLauncher;
|
|
1550
|
+
if (launcher === void 0) {
|
|
1551
|
+
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
1552
|
+
}
|
|
1553
|
+
command = launcher.command;
|
|
1554
|
+
launcherArgs = [
|
|
1555
|
+
...launcher.args ?? [],
|
|
1556
|
+
"--pi-bin",
|
|
1557
|
+
bin.command,
|
|
1558
|
+
"--profile-db",
|
|
1559
|
+
launcher.profileDbPath,
|
|
1560
|
+
"--session-dir",
|
|
1561
|
+
launcher.sessionDir,
|
|
1562
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
1563
|
+
"--provider",
|
|
1564
|
+
pinnedSelection.providerId,
|
|
1565
|
+
"--model",
|
|
1566
|
+
pinnedSelection.modelId
|
|
1567
|
+
];
|
|
1198
1568
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1569
|
+
return {
|
|
1570
|
+
kind: "prepared",
|
|
1571
|
+
operation: {
|
|
1572
|
+
start: async (startInput) => {
|
|
1573
|
+
const manifestSelection = startInput.manifest.dispatchSelection;
|
|
1574
|
+
if (!sameDispatchSelection(manifestSelection, pinnedSelection)) {
|
|
1575
|
+
throw new RuntimeExecutionFailure({
|
|
1576
|
+
phase: "start",
|
|
1577
|
+
category: "authority",
|
|
1578
|
+
retry: "non-retryable",
|
|
1579
|
+
reason: "prepared pi operation received a manifest with different runtime selection"
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
if (typeof startInput.instruction !== "string") {
|
|
1583
|
+
throw new RuntimeExecutionFailure({
|
|
1584
|
+
phase: "start",
|
|
1585
|
+
category: "authority",
|
|
1586
|
+
retry: "non-retryable",
|
|
1587
|
+
reason: "prepared pi operation requires a resolved string instruction"
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
1591
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
1592
|
+
const args = launcherArgs === void 0 ? piArgs : [...launcherArgs, "--", ...piArgs];
|
|
1593
|
+
let rpc;
|
|
1594
|
+
try {
|
|
1595
|
+
rpc = new PiRpcClient({
|
|
1596
|
+
command,
|
|
1597
|
+
args,
|
|
1598
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
1599
|
+
env: manifestSelection === void 0 ? startInput.env : withoutProviderCredentials(startInput.env),
|
|
1600
|
+
spawnFn: this.options.spawnFn
|
|
1601
|
+
});
|
|
1602
|
+
} catch (cause) {
|
|
1603
|
+
throw new RuntimeExecutionFailure({
|
|
1604
|
+
phase: "start",
|
|
1605
|
+
category: "infrastructure",
|
|
1606
|
+
retry: "retryable",
|
|
1607
|
+
reason: "pi runtime process could not be spawned"
|
|
1608
|
+
}, { cause });
|
|
1609
|
+
}
|
|
1610
|
+
let response;
|
|
1611
|
+
try {
|
|
1612
|
+
response = await rpc.send({ type: "prompt", message: startInput.instruction });
|
|
1613
|
+
} catch (cause) {
|
|
1614
|
+
rpc.kill();
|
|
1615
|
+
throw new RuntimeExecutionFailure({
|
|
1616
|
+
phase: "start",
|
|
1617
|
+
category: "infrastructure",
|
|
1618
|
+
retry: "retryable",
|
|
1619
|
+
reason: `pi initial prompt transport failed: ${errorMessage(cause)}`
|
|
1620
|
+
}, { cause });
|
|
1621
|
+
}
|
|
1622
|
+
if (response.success === false) {
|
|
1623
|
+
rpc.kill();
|
|
1624
|
+
throw new RuntimeExecutionFailure({
|
|
1625
|
+
phase: "start",
|
|
1626
|
+
category: "semantic",
|
|
1627
|
+
retry: "non-retryable",
|
|
1628
|
+
reason: typeof response.error === "string" ? response.error : "pi rejected the initial prompt"
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
let sessionRef;
|
|
1632
|
+
try {
|
|
1633
|
+
sessionRef = await resolveAuthoritativeSessionId(rpc);
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
rpc.kill();
|
|
1636
|
+
throw err;
|
|
1637
|
+
}
|
|
1638
|
+
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1639
|
+
rpc.kill();
|
|
1640
|
+
throw new RuntimeExecutionFailure({
|
|
1641
|
+
phase: "start",
|
|
1642
|
+
category: "authority",
|
|
1643
|
+
retry: "non-retryable",
|
|
1644
|
+
reason: "pi resumed a different authoritative session than requested"
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
return new PiSession(sessionRef, rpc, manifestSelection);
|
|
1648
|
+
}
|
|
1208
1649
|
}
|
|
1209
|
-
}
|
|
1210
|
-
return new PiSession(sessionRef, rpc);
|
|
1650
|
+
};
|
|
1211
1651
|
}
|
|
1212
1652
|
resolveBin() {
|
|
1213
1653
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
1214
1654
|
}
|
|
1215
1655
|
};
|
|
1216
|
-
|
|
1656
|
+
function sameDispatchSelection(left, right) {
|
|
1657
|
+
if (left === void 0 || right === void 0) return left === right;
|
|
1658
|
+
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
1659
|
+
}
|
|
1660
|
+
async function resolveAuthoritativeSessionId(rpc) {
|
|
1217
1661
|
let state;
|
|
1218
1662
|
try {
|
|
1219
1663
|
state = await rpc.send({ type: "get_state" });
|
|
1220
1664
|
} catch (err) {
|
|
1221
|
-
|
|
1665
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
1666
|
+
throw new RuntimeExecutionFailure({
|
|
1667
|
+
phase: "start",
|
|
1668
|
+
category: "infrastructure",
|
|
1669
|
+
retry: "retryable",
|
|
1670
|
+
reason: `pi transport ended before yielding an authoritative session id: ${errorMessage(err)}`
|
|
1671
|
+
}, {
|
|
1222
1672
|
cause: err
|
|
1223
1673
|
});
|
|
1224
1674
|
}
|
|
1225
1675
|
if (state.success === false) {
|
|
1226
1676
|
const reason = typeof state.error === "string" ? state.error : "get_state reported failure";
|
|
1227
|
-
throw new
|
|
1677
|
+
throw new RuntimeExecutionFailure({
|
|
1678
|
+
phase: "start",
|
|
1679
|
+
category: "authority",
|
|
1680
|
+
retry: "non-retryable",
|
|
1681
|
+
reason: `pi did not yield an authoritative session id: ${reason}`
|
|
1682
|
+
});
|
|
1228
1683
|
}
|
|
1229
1684
|
const data = state.data;
|
|
1230
1685
|
if (typeof data?.sessionId === "string" && data.sessionId.length > 0) {
|
|
1231
1686
|
return data.sessionId;
|
|
1232
1687
|
}
|
|
1233
|
-
throw new
|
|
1234
|
-
|
|
1235
|
-
|
|
1688
|
+
throw new RuntimeExecutionFailure({
|
|
1689
|
+
phase: "start",
|
|
1690
|
+
category: "authority",
|
|
1691
|
+
retry: "non-retryable",
|
|
1692
|
+
reason: "pi get_state reported no authoritative session id"
|
|
1693
|
+
});
|
|
1236
1694
|
}
|
|
1237
1695
|
var PiSession = class {
|
|
1238
|
-
constructor(sessionRef, rpc) {
|
|
1696
|
+
constructor(sessionRef, rpc, selection) {
|
|
1239
1697
|
this.sessionRef = sessionRef;
|
|
1240
1698
|
this.rpc = rpc;
|
|
1699
|
+
this.selection = selection;
|
|
1241
1700
|
}
|
|
1242
1701
|
sessionRef;
|
|
1243
1702
|
rpc;
|
|
1703
|
+
selection;
|
|
1244
1704
|
get events() {
|
|
1245
1705
|
const rpc = this.rpc;
|
|
1246
1706
|
return {
|
|
1247
1707
|
[Symbol.asyncIterator]() {
|
|
1248
1708
|
const inner = rpc.events[Symbol.asyncIterator]();
|
|
1709
|
+
let terminalFailure;
|
|
1249
1710
|
return {
|
|
1250
1711
|
async next() {
|
|
1251
1712
|
for (; ; ) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1713
|
+
if (terminalFailure) throw terminalFailure;
|
|
1714
|
+
let result;
|
|
1715
|
+
try {
|
|
1716
|
+
result = await inner.next();
|
|
1717
|
+
} catch (cause) {
|
|
1718
|
+
throw new RuntimeExecutionFailure({
|
|
1719
|
+
phase: "run",
|
|
1720
|
+
category: "infrastructure",
|
|
1721
|
+
retry: "retryable",
|
|
1722
|
+
reason: "pi runtime event transport failed"
|
|
1723
|
+
}, { cause });
|
|
1724
|
+
}
|
|
1725
|
+
const { value, done } = result;
|
|
1726
|
+
if (done) {
|
|
1727
|
+
throw new RuntimeExecutionFailure({
|
|
1728
|
+
phase: "run",
|
|
1729
|
+
category: "infrastructure",
|
|
1730
|
+
retry: "retryable",
|
|
1731
|
+
reason: "pi runtime process ended before agent_settled"
|
|
1732
|
+
}, { cause: rpc.terminalError });
|
|
1733
|
+
}
|
|
1254
1734
|
const mapped = mapPiMessageToAgentEvent(value);
|
|
1735
|
+
if (value.type === "auto_retry_end" && value.success === false) {
|
|
1736
|
+
terminalFailure = new RuntimeExecutionFailure({
|
|
1737
|
+
phase: "run",
|
|
1738
|
+
category: "semantic",
|
|
1739
|
+
retry: "non-retryable",
|
|
1740
|
+
reason: "pi exhausted its native retry policy"
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1255
1743
|
if (mapped) return { value: mapped, done: false };
|
|
1256
1744
|
if (!ROUTINE_PI_EVENT_TYPES.has(value.type)) {
|
|
1257
1745
|
rpc.recordUnmappedFrame(value.type);
|
|
@@ -1269,13 +1757,19 @@ var PiSession = class {
|
|
|
1269
1757
|
if (typeof task.instruction !== "string") {
|
|
1270
1758
|
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1271
1759
|
}
|
|
1760
|
+
const requestedSelection = task.dispatchSelection;
|
|
1761
|
+
if (requestedSelection !== void 0 && (this.selection?.lane !== "byok" || requestedSelection.lane !== "byok" || requestedSelection.runtimeId !== "pi" || requestedSelection.providerId !== this.selection.providerId || requestedSelection.modelId !== this.selection.modelId)) {
|
|
1762
|
+
throw new PolicyUnsupportedError(
|
|
1763
|
+
"pi persistent session cannot change its authoritative BYOK provider/model selection"
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1272
1766
|
await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
|
|
1273
1767
|
}
|
|
1274
1768
|
async interrupt() {
|
|
1275
1769
|
await this.rpc.send({ type: "abort" });
|
|
1276
1770
|
}
|
|
1277
1771
|
async close() {
|
|
1278
|
-
this.rpc.
|
|
1772
|
+
await this.rpc.dispose();
|
|
1279
1773
|
}
|
|
1280
1774
|
async resolveApproval() {
|
|
1281
1775
|
throw new Error("pi adapter does not support approval resume: pi never emits needs_approval in M0/M1");
|
|
@@ -1487,7 +1981,15 @@ function mapResult(msg) {
|
|
|
1487
1981
|
diagnostic ? `claude result frame had a missing/invalid is_error flag (got ${JSON.stringify(msg.is_error)}) \u2014 treating as failure, fail-closed; diagnostic content on the frame: ${truncateResultDiagnostic(diagnostic)}` : `claude result frame had a missing/invalid is_error flag (got ${JSON.stringify(msg.is_error)}) \u2014 treating as failure, fail-closed`
|
|
1488
1982
|
);
|
|
1489
1983
|
const events = usageEvent ? [usageEvent, { type: "error", message }] : [{ type: "error", message }];
|
|
1490
|
-
return {
|
|
1984
|
+
return {
|
|
1985
|
+
events,
|
|
1986
|
+
terminalFailure: new RuntimeExecutionFailure({
|
|
1987
|
+
phase: "run",
|
|
1988
|
+
category: msg.is_error === true ? "semantic" : "authority",
|
|
1989
|
+
retry: "non-retryable",
|
|
1990
|
+
reason: msg.is_error === true ? "claude reported terminal task failure" : "claude emitted a malformed terminal result frame"
|
|
1991
|
+
})
|
|
1992
|
+
};
|
|
1491
1993
|
}
|
|
1492
1994
|
var RESULT_DIAGNOSTIC_MAX_CHARS = 2e3;
|
|
1493
1995
|
function truncateResultDiagnostic(text) {
|
|
@@ -1539,16 +2041,22 @@ var ClaudeProcessClient = class {
|
|
|
1539
2041
|
eventQueue = new AsyncQueue();
|
|
1540
2042
|
closed = false;
|
|
1541
2043
|
exitError;
|
|
2044
|
+
closedPromise;
|
|
2045
|
+
resolveClosed;
|
|
2046
|
+
disposalAttempt;
|
|
1542
2047
|
stderrRing = [];
|
|
1543
2048
|
unmappedFrameCounts = /* @__PURE__ */ new Map();
|
|
1544
2049
|
sessionId;
|
|
1545
2050
|
initWaiter;
|
|
1546
2051
|
constructor(options) {
|
|
1547
2052
|
const spawnFn = options.spawnFn ?? spawn;
|
|
1548
|
-
this.child = spawnFn(options.command, options.args, {
|
|
2053
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
1549
2054
|
cwd: options.cwd,
|
|
1550
2055
|
env: options.env,
|
|
1551
2056
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2057
|
+
}));
|
|
2058
|
+
this.closedPromise = new Promise((resolve) => {
|
|
2059
|
+
this.resolveClosed = resolve;
|
|
1552
2060
|
});
|
|
1553
2061
|
this.child.stdout.setEncoding("utf8");
|
|
1554
2062
|
this.child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
@@ -1604,6 +2112,10 @@ var ClaudeProcessClient = class {
|
|
|
1604
2112
|
get events() {
|
|
1605
2113
|
return this.eventQueue;
|
|
1606
2114
|
}
|
|
2115
|
+
/** Local transport diagnostic retained when the process closes; consumers classify it at the session boundary. */
|
|
2116
|
+
get terminalError() {
|
|
2117
|
+
return this.exitError;
|
|
2118
|
+
}
|
|
1607
2119
|
/**
|
|
1608
2120
|
* Record a claude stream-json frame/subtype/content-block label that
|
|
1609
2121
|
* `ClaudeSession`'s event iterator (`../claude-adapter.ts`) decided has
|
|
@@ -1623,15 +2135,30 @@ var ClaudeProcessClient = class {
|
|
|
1623
2135
|
);
|
|
1624
2136
|
}
|
|
1625
2137
|
}
|
|
1626
|
-
/**
|
|
2138
|
+
/** Immediate process-tree termination request. `dispose()` is the settlement receipt. */
|
|
1627
2139
|
kill() {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
2140
|
+
requestOwnedProcessTreeTermination(this.processTreeOptions());
|
|
2141
|
+
}
|
|
2142
|
+
waitClosed() {
|
|
2143
|
+
return this.closedPromise;
|
|
2144
|
+
}
|
|
2145
|
+
dispose() {
|
|
2146
|
+
if (!this.disposalAttempt) {
|
|
2147
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
2148
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
2149
|
+
this.disposalAttempt = void 0;
|
|
2150
|
+
throw error;
|
|
2151
|
+
});
|
|
1634
2152
|
}
|
|
2153
|
+
return this.disposalAttempt;
|
|
2154
|
+
}
|
|
2155
|
+
processTreeOptions() {
|
|
2156
|
+
return {
|
|
2157
|
+
child: this.child,
|
|
2158
|
+
waitClosed: () => this.closedPromise,
|
|
2159
|
+
isClosed: () => this.closed,
|
|
2160
|
+
label: "claude"
|
|
2161
|
+
};
|
|
1635
2162
|
}
|
|
1636
2163
|
onData(chunk) {
|
|
1637
2164
|
this.buffer += chunk;
|
|
@@ -1682,6 +2209,7 @@ var ClaudeProcessClient = class {
|
|
|
1682
2209
|
if (this.closed) return;
|
|
1683
2210
|
this.closed = true;
|
|
1684
2211
|
this.exitError = err;
|
|
2212
|
+
this.resolveClosed();
|
|
1685
2213
|
this.initWaiter?.reject(err);
|
|
1686
2214
|
this.initWaiter = void 0;
|
|
1687
2215
|
this.eventQueue.end();
|
|
@@ -1693,17 +2221,37 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1693
2221
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1694
2222
|
var execFileAsync2 = promisify(execFile);
|
|
1695
2223
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
1696
|
-
|
|
2224
|
+
function errorMessage2(err) {
|
|
2225
|
+
return err instanceof Error ? err.message : String(err);
|
|
2226
|
+
}
|
|
2227
|
+
async function cleanupMcpConfigDir(dir) {
|
|
1697
2228
|
if (!dir) return;
|
|
1698
|
-
|
|
1699
|
-
|
|
2229
|
+
try {
|
|
2230
|
+
await promises.rm(dir, { recursive: true, force: true });
|
|
2231
|
+
} catch (cause) {
|
|
2232
|
+
throw new RuntimeDisposalFailure({
|
|
2233
|
+
stage: "cleanup",
|
|
2234
|
+
reason: "claude task-scoped MCP configuration could not be removed"
|
|
2235
|
+
}, { cause });
|
|
2236
|
+
}
|
|
1700
2237
|
}
|
|
1701
2238
|
var ClaudeAdapter = class {
|
|
1702
2239
|
constructor(options = {}) {
|
|
1703
2240
|
this.options = options;
|
|
1704
2241
|
}
|
|
1705
2242
|
options;
|
|
1706
|
-
|
|
2243
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
2244
|
+
id: "claude",
|
|
2245
|
+
supportsDispatchSelection: true,
|
|
2246
|
+
capabilities: {
|
|
2247
|
+
steer: false,
|
|
2248
|
+
resume: true,
|
|
2249
|
+
approvalInteractive: true,
|
|
2250
|
+
mcpToolsets: true,
|
|
2251
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
2252
|
+
},
|
|
2253
|
+
environmentRequirements: { credentialNames: [] }
|
|
2254
|
+
});
|
|
1707
2255
|
async detect() {
|
|
1708
2256
|
const bin = this.resolveBin();
|
|
1709
2257
|
try {
|
|
@@ -1715,75 +2263,138 @@ var ClaudeAdapter = class {
|
|
|
1715
2263
|
return { present: false };
|
|
1716
2264
|
}
|
|
1717
2265
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
* passthrough for claude is a separate, still-pending product decision.
|
|
1727
|
-
* A product that genuinely needs it can opt in locally per-device via
|
|
1728
|
-
* `DaemonConfig.runtimeEnvironment.claude.allow` (`create-daemon.ts`).
|
|
1729
|
-
* `baseNames` is empty too: nothing in this adapter reads a
|
|
1730
|
-
* claude-specific config-discovery variable (e.g. `CLAUDE_CONFIG_DIR`)
|
|
1731
|
-
* today — if a future version of this adapter starts reading one, it
|
|
1732
|
-
* belongs here, not left to rely on the platform baseline alone.
|
|
1733
|
-
*/
|
|
1734
|
-
environmentRequirements() {
|
|
1735
|
-
return { credentialNames: [] };
|
|
1736
|
-
}
|
|
1737
|
-
async start(task, ctx) {
|
|
1738
|
-
if (typeof task.instruction !== "string") {
|
|
1739
|
-
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2266
|
+
async prepare(input) {
|
|
2267
|
+
const mapping = mapPermissionPolicyToClaudeArgs(input.policy);
|
|
2268
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by claude adapter", retryable: false };
|
|
2269
|
+
let modelId;
|
|
2270
|
+
try {
|
|
2271
|
+
modelId = subscriptionModel(input.offer.dispatchSelection, "claude");
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
1740
2274
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
2275
|
+
if (mapping.needsApprovalMcp && Object.prototype.hasOwnProperty.call(input.mcpServers ?? {}, APPROVAL_MCP_SERVER_NAME)) {
|
|
2276
|
+
return { kind: "reject", reason: `MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`, retryable: false };
|
|
1744
2277
|
}
|
|
1745
|
-
let
|
|
2278
|
+
let bin;
|
|
2279
|
+
try {
|
|
2280
|
+
bin = this.resolveBin();
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
2283
|
+
}
|
|
2284
|
+
let approvalMcpBin;
|
|
1746
2285
|
if (mapping.needsApprovalMcp) {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
);
|
|
2286
|
+
try {
|
|
2287
|
+
approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
2288
|
+
} catch (error) {
|
|
2289
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return {
|
|
2293
|
+
kind: "prepared",
|
|
2294
|
+
operation: {
|
|
2295
|
+
start: (startInput) => this.startPrepared(startInput, mapping, modelId, bin, approvalMcpBin)
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
async startPrepared(startInput, initialMapping, modelId, bin, approvalMcpBin) {
|
|
2300
|
+
if (!initialMapping.ok) throw new RuntimeExecutionFailure({
|
|
2301
|
+
phase: "start",
|
|
2302
|
+
category: "authority",
|
|
2303
|
+
retry: "non-retryable",
|
|
2304
|
+
reason: "prepared claude permission mapping was invalid"
|
|
2305
|
+
});
|
|
2306
|
+
if (typeof startInput.instruction !== "string") {
|
|
2307
|
+
throw new RuntimeExecutionFailure({
|
|
2308
|
+
phase: "start",
|
|
2309
|
+
category: "authority",
|
|
2310
|
+
retry: "non-retryable",
|
|
2311
|
+
reason: "prepared claude operation requires a resolved string instruction"
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
const mapping = { ...initialMapping, args: [...initialMapping.args] };
|
|
2315
|
+
let mcpConfigDir;
|
|
2316
|
+
const taskMcpServers = startInput.mcpServers ?? {};
|
|
2317
|
+
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
2318
|
+
if (mapping.needsApprovalMcp) {
|
|
2319
|
+
if (!startInput.approvalChannel) {
|
|
2320
|
+
throw new RuntimeExecutionFailure({
|
|
2321
|
+
phase: "start",
|
|
2322
|
+
category: "authority",
|
|
2323
|
+
retry: "non-retryable",
|
|
2324
|
+
reason: 'claude adapter requires policy.mode "confirm" to be started with an approval channel'
|
|
2325
|
+
});
|
|
1751
2326
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
2327
|
+
if (!approvalMcpBin) throw new RuntimeExecutionFailure({
|
|
2328
|
+
phase: "start",
|
|
2329
|
+
category: "authority",
|
|
2330
|
+
retry: "non-retryable",
|
|
2331
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
1755
2332
|
});
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2333
|
+
}
|
|
2334
|
+
if (needsMcpConfig) {
|
|
2335
|
+
mcpConfigDir = await promises.mkdtemp(path20.join(os.tmpdir(), "byok-mcp-"));
|
|
2336
|
+
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
2337
|
+
});
|
|
2338
|
+
const mcpConfigPath = path20.join(mcpConfigDir, "mcp-config.json");
|
|
2339
|
+
const mcpServers = { ...taskMcpServers };
|
|
2340
|
+
if (mapping.needsApprovalMcp) {
|
|
2341
|
+
const approvalChannel = startInput.approvalChannel;
|
|
2342
|
+
if (!approvalChannel) throw new RuntimeExecutionFailure({
|
|
2343
|
+
phase: "start",
|
|
2344
|
+
category: "authority",
|
|
2345
|
+
retry: "non-retryable",
|
|
2346
|
+
reason: "prepared claude approval channel was not available"
|
|
2347
|
+
});
|
|
2348
|
+
const preparedApprovalMcpBin = approvalMcpBin;
|
|
2349
|
+
if (!preparedApprovalMcpBin) throw new RuntimeExecutionFailure({
|
|
2350
|
+
phase: "start",
|
|
2351
|
+
category: "authority",
|
|
2352
|
+
retry: "non-retryable",
|
|
2353
|
+
reason: "prepared claude approval MCP binary was not resolved"
|
|
2354
|
+
});
|
|
2355
|
+
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
2356
|
+
command: preparedApprovalMcpBin.command,
|
|
2357
|
+
args: preparedApprovalMcpBin.args,
|
|
2358
|
+
env: {
|
|
2359
|
+
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
2360
|
+
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
2361
|
+
BYOK_TASK_ID: approvalChannel.taskId,
|
|
2362
|
+
BYOK_APPROVAL_TIMEOUT_MS: String(approvalChannel.timeoutMs)
|
|
1768
2363
|
}
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
await promises.writeFile(mcpConfigPath, JSON.stringify(
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
1772
2367
|
mapping.args = [
|
|
1773
2368
|
...mapping.args,
|
|
1774
|
-
"--permission-prompt-tool",
|
|
1775
|
-
`mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
|
|
2369
|
+
...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
|
|
1776
2370
|
"--mcp-config",
|
|
1777
2371
|
mcpConfigPath,
|
|
1778
|
-
//
|
|
1779
|
-
//
|
|
1780
|
-
// the only MCP server this invocation should ever see.
|
|
2372
|
+
// The generated file is the complete task-scoped MCP authority.
|
|
2373
|
+
// Never merge ambient user/project MCP configuration into it.
|
|
1781
2374
|
"--strict-mcp-config"
|
|
1782
2375
|
];
|
|
1783
2376
|
}
|
|
1784
|
-
const
|
|
1785
|
-
|
|
1786
|
-
|
|
2377
|
+
const resumeSessionId = startInput.manifest.sessionRef;
|
|
2378
|
+
let manifestModelId;
|
|
2379
|
+
try {
|
|
2380
|
+
manifestModelId = subscriptionModel(startInput.manifest.dispatchSelection, "claude");
|
|
2381
|
+
} catch (cause) {
|
|
2382
|
+
throw new RuntimeExecutionFailure({
|
|
2383
|
+
phase: "start",
|
|
2384
|
+
category: "authority",
|
|
2385
|
+
retry: "non-retryable",
|
|
2386
|
+
reason: "prepared claude operation received an invalid runtime selection manifest"
|
|
2387
|
+
}, { cause });
|
|
2388
|
+
}
|
|
2389
|
+
if (manifestModelId !== modelId) {
|
|
2390
|
+
throw new RuntimeExecutionFailure({
|
|
2391
|
+
phase: "start",
|
|
2392
|
+
category: "authority",
|
|
2393
|
+
retry: "non-retryable",
|
|
2394
|
+
reason: "prepared claude operation received a manifest with different runtime selection"
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
const args = [
|
|
1787
2398
|
"-p",
|
|
1788
2399
|
"--input-format",
|
|
1789
2400
|
"stream-json",
|
|
@@ -1794,33 +2405,72 @@ var ClaudeAdapter = class {
|
|
|
1794
2405
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1795
2406
|
// --verbose", before spawning any model call.
|
|
1796
2407
|
"--verbose",
|
|
2408
|
+
...manifestModelId ? ["--model", manifestModelId] : [],
|
|
1797
2409
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1798
2410
|
...mapping.args
|
|
1799
2411
|
];
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2412
|
+
let client;
|
|
2413
|
+
try {
|
|
2414
|
+
client = new ClaudeProcessClient({
|
|
2415
|
+
command: bin.command,
|
|
2416
|
+
args,
|
|
2417
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
2418
|
+
env: withoutProviderCredentials(startInput.env),
|
|
2419
|
+
spawnFn: this.options.spawnFn
|
|
2420
|
+
});
|
|
2421
|
+
} catch (cause) {
|
|
2422
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2423
|
+
throw new RuntimeExecutionFailure({
|
|
2424
|
+
phase: "start",
|
|
2425
|
+
category: "infrastructure",
|
|
2426
|
+
retry: "retryable",
|
|
2427
|
+
reason: "claude runtime process could not be spawned"
|
|
2428
|
+
}, { cause });
|
|
2429
|
+
}
|
|
2430
|
+
try {
|
|
2431
|
+
client.writeUserMessage(startInput.instruction);
|
|
2432
|
+
} catch (cause) {
|
|
2433
|
+
client.kill();
|
|
2434
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2435
|
+
throw new RuntimeExecutionFailure({
|
|
2436
|
+
phase: "start",
|
|
2437
|
+
category: "infrastructure",
|
|
2438
|
+
retry: "retryable",
|
|
2439
|
+
reason: "claude initial instruction transport failed"
|
|
2440
|
+
}, { cause });
|
|
2441
|
+
}
|
|
1808
2442
|
let sessionRef;
|
|
1809
2443
|
try {
|
|
1810
2444
|
sessionRef = await client.waitForInit();
|
|
1811
2445
|
} catch (err) {
|
|
1812
2446
|
client.kill();
|
|
1813
|
-
await
|
|
1814
|
-
throw err;
|
|
2447
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2448
|
+
if (isRuntimeExecutionFailure(err)) throw err;
|
|
2449
|
+
throw new RuntimeExecutionFailure({
|
|
2450
|
+
phase: "start",
|
|
2451
|
+
category: "infrastructure",
|
|
2452
|
+
retry: "retryable",
|
|
2453
|
+
reason: `claude exited before yielding an authoritative session id: ${errorMessage2(err)}`
|
|
2454
|
+
}, { cause: err });
|
|
1815
2455
|
}
|
|
1816
2456
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1817
2457
|
client.kill();
|
|
1818
|
-
await
|
|
1819
|
-
throw new
|
|
1820
|
-
|
|
1821
|
-
|
|
2458
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
2459
|
+
throw new RuntimeExecutionFailure({
|
|
2460
|
+
phase: "start",
|
|
2461
|
+
category: "authority",
|
|
2462
|
+
retry: "non-retryable",
|
|
2463
|
+
reason: `claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef})`
|
|
2464
|
+
});
|
|
1822
2465
|
}
|
|
1823
|
-
return new ClaudeSession(
|
|
2466
|
+
return new ClaudeSession(
|
|
2467
|
+
sessionRef,
|
|
2468
|
+
client,
|
|
2469
|
+
startInput.manifest.workspace.workspaceDir,
|
|
2470
|
+
startInput.approvalChannel,
|
|
2471
|
+
mcpConfigDir,
|
|
2472
|
+
manifestModelId
|
|
2473
|
+
);
|
|
1824
2474
|
}
|
|
1825
2475
|
/**
|
|
1826
2476
|
* `claude auth status --json` is claude's OWN non-secret login-state
|
|
@@ -1854,20 +2504,32 @@ var ClaudeAdapter = class {
|
|
|
1854
2504
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1855
2505
|
}
|
|
1856
2506
|
};
|
|
2507
|
+
function subscriptionModel(selection, runtimeId) {
|
|
2508
|
+
if (selection === void 0) return void 0;
|
|
2509
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
2510
|
+
throw new PolicyUnsupportedError(
|
|
2511
|
+
`claude adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
return selection.modelId;
|
|
2515
|
+
}
|
|
1857
2516
|
var ClaudeSession = class {
|
|
1858
|
-
constructor(sessionRef, client, workspaceDir, approvalChannel,
|
|
2517
|
+
constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
|
|
1859
2518
|
this.sessionRef = sessionRef;
|
|
1860
2519
|
this.client = client;
|
|
1861
2520
|
this.workspaceDir = workspaceDir;
|
|
1862
2521
|
this.approvalChannel = approvalChannel;
|
|
1863
|
-
this.
|
|
2522
|
+
this.mcpConfigDir = mcpConfigDir;
|
|
2523
|
+
this.modelId = modelId;
|
|
1864
2524
|
}
|
|
1865
2525
|
sessionRef;
|
|
1866
2526
|
client;
|
|
1867
2527
|
workspaceDir;
|
|
1868
2528
|
approvalChannel;
|
|
1869
|
-
|
|
2529
|
+
mcpConfigDir;
|
|
2530
|
+
modelId;
|
|
1870
2531
|
correlation = createToolUseCorrelation();
|
|
2532
|
+
closeAttempt;
|
|
1871
2533
|
get events() {
|
|
1872
2534
|
const client = this.client;
|
|
1873
2535
|
const correlation = this.correlation;
|
|
@@ -1876,17 +2538,40 @@ var ClaudeSession = class {
|
|
|
1876
2538
|
[Symbol.asyncIterator]() {
|
|
1877
2539
|
const inner = client.events[Symbol.asyncIterator]();
|
|
1878
2540
|
let pending = [];
|
|
2541
|
+
let terminalFailure;
|
|
1879
2542
|
let turnSettled = false;
|
|
1880
2543
|
return {
|
|
1881
2544
|
async next() {
|
|
1882
2545
|
for (; ; ) {
|
|
1883
2546
|
const buffered = pending.shift();
|
|
1884
2547
|
if (buffered) return { value: buffered, done: false };
|
|
1885
|
-
if (turnSettled)
|
|
1886
|
-
|
|
1887
|
-
|
|
2548
|
+
if (turnSettled) {
|
|
2549
|
+
if (terminalFailure) throw terminalFailure;
|
|
2550
|
+
return { value: void 0, done: true };
|
|
2551
|
+
}
|
|
2552
|
+
let raw;
|
|
2553
|
+
try {
|
|
2554
|
+
raw = await inner.next();
|
|
2555
|
+
} catch (cause) {
|
|
2556
|
+
throw new RuntimeExecutionFailure({
|
|
2557
|
+
phase: "run",
|
|
2558
|
+
category: "infrastructure",
|
|
2559
|
+
retry: "retryable",
|
|
2560
|
+
reason: "claude runtime event transport failed"
|
|
2561
|
+
}, { cause });
|
|
2562
|
+
}
|
|
2563
|
+
const { value, done } = raw;
|
|
2564
|
+
if (done) {
|
|
2565
|
+
throw new RuntimeExecutionFailure({
|
|
2566
|
+
phase: "run",
|
|
2567
|
+
category: "infrastructure",
|
|
2568
|
+
retry: "retryable",
|
|
2569
|
+
reason: "claude runtime process ended before a terminal result frame"
|
|
2570
|
+
}, { cause: client.terminalError });
|
|
2571
|
+
}
|
|
1888
2572
|
if (value.type === "result") turnSettled = true;
|
|
1889
2573
|
const mapped = mapClaudeMessageToAgentEvents(value, correlation, { workspaceDir });
|
|
2574
|
+
terminalFailure = mapped.terminalFailure ?? terminalFailure;
|
|
1890
2575
|
if (mapped.unmappedLabel) {
|
|
1891
2576
|
client.recordUnmappedFrame(mapped.unmappedLabel);
|
|
1892
2577
|
}
|
|
@@ -1917,6 +2602,12 @@ var ClaudeSession = class {
|
|
|
1917
2602
|
if (typeof task.instruction !== "string") {
|
|
1918
2603
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1919
2604
|
}
|
|
2605
|
+
const requestedModel = subscriptionModel(task.dispatchSelection, "claude");
|
|
2606
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2607
|
+
throw new PolicyUnsupportedError(
|
|
2608
|
+
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2609
|
+
);
|
|
2610
|
+
}
|
|
1920
2611
|
this.client.writeUserMessage(task.instruction);
|
|
1921
2612
|
}
|
|
1922
2613
|
/**
|
|
@@ -1935,8 +2626,17 @@ var ClaudeSession = class {
|
|
|
1935
2626
|
this.client.kill();
|
|
1936
2627
|
}
|
|
1937
2628
|
async close() {
|
|
1938
|
-
this.
|
|
1939
|
-
|
|
2629
|
+
if (!this.closeAttempt) {
|
|
2630
|
+
const attempt = (async () => {
|
|
2631
|
+
await this.client.dispose();
|
|
2632
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
2633
|
+
})();
|
|
2634
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
2635
|
+
this.closeAttempt = void 0;
|
|
2636
|
+
throw error;
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
await this.closeAttempt;
|
|
1940
2640
|
}
|
|
1941
2641
|
/**
|
|
1942
2642
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -2153,14 +2853,15 @@ var CodexProcessRunner = class {
|
|
|
2153
2853
|
exitSignal = null;
|
|
2154
2854
|
closedPromise;
|
|
2155
2855
|
resolveClosed;
|
|
2856
|
+
disposalAttempt;
|
|
2156
2857
|
constructor(options) {
|
|
2157
2858
|
this.onEvent = options.onEvent;
|
|
2158
2859
|
const spawnFn = options.spawnFn ?? spawn;
|
|
2159
|
-
this.child = spawnFn(options.command, options.args, {
|
|
2860
|
+
this.child = spawnFn(options.command, options.args, withOwnedProcessTree({
|
|
2160
2861
|
cwd: options.cwd,
|
|
2161
2862
|
env: options.env,
|
|
2162
2863
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2163
|
-
});
|
|
2864
|
+
}));
|
|
2164
2865
|
this.closedPromise = new Promise((resolve) => {
|
|
2165
2866
|
this.resolveClosed = resolve;
|
|
2166
2867
|
});
|
|
@@ -2190,7 +2891,7 @@ var CodexProcessRunner = class {
|
|
|
2190
2891
|
return this.closed;
|
|
2191
2892
|
}
|
|
2192
2893
|
/**
|
|
2193
|
-
*
|
|
2894
|
+
* Immediate tree termination request. SIGTERM on POSIX: SIGINT was empirically confirmed
|
|
2194
2895
|
* to be silently ignored by `codex exec` (a real, direct test — a 60s
|
|
2195
2896
|
* shell `sleep` ran to full, unaffected completion despite SIGINT sent at
|
|
2196
2897
|
* t=4s) — a genuine, evidence-based correction to this task's own initial
|
|
@@ -2203,13 +2904,25 @@ var CodexProcessRunner = class {
|
|
|
2203
2904
|
* `../pi/rpc-client.ts`'s own cross-platform convention.
|
|
2204
2905
|
*/
|
|
2205
2906
|
kill() {
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
this.
|
|
2907
|
+
requestOwnedProcessTreeTermination(this.processTreeOptions());
|
|
2908
|
+
}
|
|
2909
|
+
dispose() {
|
|
2910
|
+
if (!this.disposalAttempt) {
|
|
2911
|
+
const attempt = disposeOwnedProcessTree(this.processTreeOptions());
|
|
2912
|
+
this.disposalAttempt = attempt.catch((error) => {
|
|
2913
|
+
this.disposalAttempt = void 0;
|
|
2914
|
+
throw error;
|
|
2915
|
+
});
|
|
2212
2916
|
}
|
|
2917
|
+
return this.disposalAttempt;
|
|
2918
|
+
}
|
|
2919
|
+
processTreeOptions() {
|
|
2920
|
+
return {
|
|
2921
|
+
child: this.child,
|
|
2922
|
+
waitClosed: () => this.closedPromise,
|
|
2923
|
+
isClosed: () => this.closed,
|
|
2924
|
+
label: "codex"
|
|
2925
|
+
};
|
|
2213
2926
|
}
|
|
2214
2927
|
/** Builds a descriptive error folding in the exit code/signal and the stderr tail — mirrors `PiRpcClient.buildExitError`'s reasoning: a post-mortem on a failed start/resume should never need separately re-running codex by hand with a raw JSONL logger to learn why. */
|
|
2215
2928
|
buildExitError(context) {
|
|
@@ -2260,7 +2973,17 @@ var CodexAdapter = class {
|
|
|
2260
2973
|
this.options = options;
|
|
2261
2974
|
}
|
|
2262
2975
|
options;
|
|
2263
|
-
|
|
2976
|
+
descriptor = freezeRuntimeAdapterDescriptor({
|
|
2977
|
+
id: "codex",
|
|
2978
|
+
supportsDispatchSelection: true,
|
|
2979
|
+
capabilities: {
|
|
2980
|
+
steer: false,
|
|
2981
|
+
resume: true,
|
|
2982
|
+
approvalInteractive: false,
|
|
2983
|
+
permissionModes: ["auto", "readonly"]
|
|
2984
|
+
},
|
|
2985
|
+
environmentRequirements: { credentialNames: [] }
|
|
2986
|
+
});
|
|
2264
2987
|
async detect() {
|
|
2265
2988
|
const bin = this.resolveBin();
|
|
2266
2989
|
try {
|
|
@@ -2309,57 +3032,100 @@ ${result.stderr}`);
|
|
|
2309
3032
|
${withStreams.stderr ?? ""}`);
|
|
2310
3033
|
}
|
|
2311
3034
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
* API-key passthrough remains a separate, pending product decision. No
|
|
2321
|
-
* `baseNames` either: nothing in this adapter reads a codex-specific
|
|
2322
|
-
* config-discovery variable (e.g. `CODEX_HOME`) today.
|
|
2323
|
-
*/
|
|
2324
|
-
environmentRequirements() {
|
|
2325
|
-
return { credentialNames: [] };
|
|
2326
|
-
}
|
|
2327
|
-
async start(task, ctx) {
|
|
2328
|
-
if (typeof task.instruction !== "string") {
|
|
2329
|
-
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
3035
|
+
async prepare(input) {
|
|
3036
|
+
const mapping = mapPermissionPolicyToCodexArgs(input.policy);
|
|
3037
|
+
if (!mapping.ok) return { kind: "reject", reason: mapping.reason ?? "policy rejected by codex adapter", retryable: false };
|
|
3038
|
+
let modelId;
|
|
3039
|
+
try {
|
|
3040
|
+
modelId = subscriptionModel2(input.offer.dispatchSelection);
|
|
3041
|
+
} catch (error) {
|
|
3042
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: false };
|
|
2330
3043
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
3044
|
+
let command;
|
|
3045
|
+
try {
|
|
3046
|
+
command = this.resolveBin().command;
|
|
3047
|
+
} catch (error) {
|
|
3048
|
+
return { kind: "reject", reason: error instanceof Error ? error.message : String(error), retryable: true };
|
|
3049
|
+
}
|
|
3050
|
+
return {
|
|
3051
|
+
kind: "prepared",
|
|
3052
|
+
operation: {
|
|
3053
|
+
start: (startInput) => this.startPrepared(startInput, mapping.args, modelId, command)
|
|
3054
|
+
}
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
async startPrepared(startInput, policyArgs, modelId, command) {
|
|
3058
|
+
if (typeof startInput.instruction !== "string") {
|
|
3059
|
+
throw new RuntimeExecutionFailure({
|
|
3060
|
+
phase: "start",
|
|
3061
|
+
category: "authority",
|
|
3062
|
+
retry: "non-retryable",
|
|
3063
|
+
reason: "prepared codex operation requires a resolved string instruction"
|
|
3064
|
+
});
|
|
2334
3065
|
}
|
|
2335
|
-
const bin = this.resolveBin();
|
|
2336
3066
|
const queue = new AsyncQueue();
|
|
3067
|
+
const terminal = {};
|
|
2337
3068
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
2338
|
-
|
|
3069
|
+
let workspaceDir;
|
|
3070
|
+
try {
|
|
3071
|
+
workspaceDir = await resolveRealWorkspaceDir(startInput.manifest.workspace.workspaceDir);
|
|
3072
|
+
} catch (cause) {
|
|
3073
|
+
throw new RuntimeExecutionFailure({
|
|
3074
|
+
phase: "start",
|
|
3075
|
+
category: "infrastructure",
|
|
3076
|
+
retry: "retryable",
|
|
3077
|
+
reason: "codex runtime workspace could not be resolved"
|
|
3078
|
+
}, { cause });
|
|
3079
|
+
}
|
|
3080
|
+
const runtimeEnv = withoutProviderCredentials(startInput.env);
|
|
3081
|
+
let manifestModelId;
|
|
3082
|
+
try {
|
|
3083
|
+
manifestModelId = subscriptionModel2(startInput.manifest.dispatchSelection);
|
|
3084
|
+
} catch (cause) {
|
|
3085
|
+
throw new RuntimeExecutionFailure({
|
|
3086
|
+
phase: "start",
|
|
3087
|
+
category: "authority",
|
|
3088
|
+
retry: "non-retryable",
|
|
3089
|
+
reason: "prepared codex operation received an invalid runtime selection manifest"
|
|
3090
|
+
}, { cause });
|
|
3091
|
+
}
|
|
3092
|
+
if (manifestModelId !== modelId) {
|
|
3093
|
+
throw new RuntimeExecutionFailure({
|
|
3094
|
+
phase: "start",
|
|
3095
|
+
category: "authority",
|
|
3096
|
+
retry: "non-retryable",
|
|
3097
|
+
reason: "prepared codex operation received a manifest with different runtime selection"
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
2339
3100
|
const { sessionRef, runner } = await runCodexTurn({
|
|
2340
|
-
command
|
|
2341
|
-
resumeRef:
|
|
2342
|
-
instruction:
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
3101
|
+
command,
|
|
3102
|
+
resumeRef: startInput.manifest.sessionRef,
|
|
3103
|
+
instruction: startInput.instruction,
|
|
3104
|
+
modelId: manifestModelId,
|
|
3105
|
+
policyArgs: [...policyArgs],
|
|
3106
|
+
cwd: startInput.manifest.workspace.workspaceDir,
|
|
3107
|
+
env: runtimeEnv,
|
|
2346
3108
|
spawnFn: this.options.spawnFn,
|
|
2347
3109
|
workspaceDir,
|
|
2348
3110
|
queue,
|
|
2349
3111
|
recordUnmapped,
|
|
2350
|
-
expectedSessionRef:
|
|
2351
|
-
preparedGit:
|
|
3112
|
+
expectedSessionRef: startInput.manifest.sessionRef,
|
|
3113
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
3114
|
+
failurePhase: "start",
|
|
3115
|
+
terminal
|
|
2352
3116
|
});
|
|
2353
3117
|
return new CodexSession({
|
|
2354
3118
|
sessionRef,
|
|
2355
|
-
command
|
|
3119
|
+
command,
|
|
2356
3120
|
workspaceDir,
|
|
2357
|
-
env:
|
|
3121
|
+
env: startInput.env,
|
|
2358
3122
|
spawnFn: this.options.spawnFn,
|
|
2359
3123
|
queue,
|
|
2360
3124
|
recordUnmapped,
|
|
2361
3125
|
initialRunner: runner,
|
|
2362
|
-
preparedGit:
|
|
3126
|
+
preparedGit: startInput.manifest.workspace.workspaceId !== void 0,
|
|
3127
|
+
modelId: manifestModelId,
|
|
3128
|
+
terminal
|
|
2363
3129
|
});
|
|
2364
3130
|
}
|
|
2365
3131
|
resolveBin() {
|
|
@@ -2380,12 +3146,25 @@ function makeUnmappedFrameRecorder(counts) {
|
|
|
2380
3146
|
}
|
|
2381
3147
|
};
|
|
2382
3148
|
}
|
|
2383
|
-
function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
|
|
3149
|
+
function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
|
|
2384
3150
|
const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
|
|
2385
|
-
return [
|
|
3151
|
+
return [
|
|
3152
|
+
...base,
|
|
3153
|
+
"--json",
|
|
3154
|
+
...modelId ? ["--model", modelId] : [],
|
|
3155
|
+
...preparedGit ? [] : ["--skip-git-repo-check"],
|
|
3156
|
+
...policyArgs,
|
|
3157
|
+
instruction
|
|
3158
|
+
];
|
|
2386
3159
|
}
|
|
2387
3160
|
async function runCodexTurn(params) {
|
|
2388
|
-
const argv = buildArgv(
|
|
3161
|
+
const argv = buildArgv(
|
|
3162
|
+
params.resumeRef,
|
|
3163
|
+
params.policyArgs,
|
|
3164
|
+
params.instruction,
|
|
3165
|
+
params.modelId,
|
|
3166
|
+
params.preparedGit
|
|
3167
|
+
);
|
|
2389
3168
|
let firstLineSettled = false;
|
|
2390
3169
|
let resolveFirstLine;
|
|
2391
3170
|
let rejectFirstLine;
|
|
@@ -2394,37 +3173,69 @@ async function runCodexTurn(params) {
|
|
|
2394
3173
|
rejectFirstLine = reject;
|
|
2395
3174
|
});
|
|
2396
3175
|
let turnEnded = false;
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
if (
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
3176
|
+
let runner;
|
|
3177
|
+
try {
|
|
3178
|
+
runner = new CodexProcessRunner({
|
|
3179
|
+
command: params.command,
|
|
3180
|
+
args: argv,
|
|
3181
|
+
cwd: params.cwd,
|
|
3182
|
+
env: params.env,
|
|
3183
|
+
spawnFn: params.spawnFn,
|
|
3184
|
+
onEvent: (evt) => {
|
|
3185
|
+
if (!firstLineSettled) {
|
|
3186
|
+
firstLineSettled = true;
|
|
3187
|
+
if (evt.type === "thread.started" && typeof evt.thread_id === "string" && evt.thread_id.length > 0) {
|
|
3188
|
+
resolveFirstLine(evt.thread_id);
|
|
3189
|
+
} else {
|
|
3190
|
+
rejectFirstLine(
|
|
3191
|
+
new RuntimeExecutionFailure({
|
|
3192
|
+
phase: params.failurePhase,
|
|
3193
|
+
category: "authority",
|
|
3194
|
+
retry: "non-retryable",
|
|
3195
|
+
reason: `codex did not yield thread.started as its first event (got ${JSON.stringify(evt).slice(0, 200)})`
|
|
3196
|
+
})
|
|
3197
|
+
);
|
|
3198
|
+
}
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
3202
|
+
for (const agentEvent of mapped) {
|
|
3203
|
+
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
3204
|
+
params.queue.push(agentEvent);
|
|
3205
|
+
}
|
|
3206
|
+
if (evt.type === "turn.failed") {
|
|
3207
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
3208
|
+
phase: "run",
|
|
3209
|
+
category: "semantic",
|
|
3210
|
+
retry: "non-retryable",
|
|
3211
|
+
reason: "codex reported terminal task failure"
|
|
3212
|
+
});
|
|
3213
|
+
params.queue.end();
|
|
3214
|
+
}
|
|
3215
|
+
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
3216
|
+
params.recordUnmapped(unmappedFrameKey(evt));
|
|
2412
3217
|
}
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
const mapped = mapCodexEventToAgentEvents(evt, params.workspaceDir);
|
|
2416
|
-
for (const agentEvent of mapped) {
|
|
2417
|
-
if (agentEvent.type === "turn_end") turnEnded = true;
|
|
2418
|
-
params.queue.push(agentEvent);
|
|
2419
|
-
}
|
|
2420
|
-
if (mapped.length === 0 && !isRoutineCodexEvent(evt)) {
|
|
2421
|
-
params.recordUnmapped(unmappedFrameKey(evt));
|
|
2422
3218
|
}
|
|
2423
|
-
}
|
|
2424
|
-
})
|
|
3219
|
+
});
|
|
3220
|
+
} catch (cause) {
|
|
3221
|
+
throw new RuntimeExecutionFailure({
|
|
3222
|
+
phase: params.failurePhase,
|
|
3223
|
+
category: "infrastructure",
|
|
3224
|
+
retry: "retryable",
|
|
3225
|
+
reason: "codex runtime process could not be spawned"
|
|
3226
|
+
}, { cause });
|
|
3227
|
+
}
|
|
3228
|
+
params.onRunnerCreated?.(runner);
|
|
2425
3229
|
void runner.waitClosed().then(() => {
|
|
2426
|
-
if (turnEnded) return;
|
|
2427
|
-
|
|
3230
|
+
if (turnEnded || params.terminal.failure) return;
|
|
3231
|
+
const cause = runner.buildExitError("codex exited without completing the turn");
|
|
3232
|
+
params.terminal.failure = new RuntimeExecutionFailure({
|
|
3233
|
+
phase: "run",
|
|
3234
|
+
category: "infrastructure",
|
|
3235
|
+
retry: "retryable",
|
|
3236
|
+
reason: cause.message
|
|
3237
|
+
}, { cause });
|
|
3238
|
+
params.queue.push({ type: "error", message: cause.message });
|
|
2428
3239
|
params.queue.end();
|
|
2429
3240
|
});
|
|
2430
3241
|
let sessionRef;
|
|
@@ -2448,7 +3259,13 @@ async function runCodexTurn(params) {
|
|
|
2448
3259
|
void runner.waitClosed().then(() => {
|
|
2449
3260
|
if (!settled) {
|
|
2450
3261
|
settled = true;
|
|
2451
|
-
|
|
3262
|
+
const cause = runner.buildExitError("codex exited before yielding an authoritative thread id");
|
|
3263
|
+
reject(new RuntimeExecutionFailure({
|
|
3264
|
+
phase: params.failurePhase,
|
|
3265
|
+
category: "infrastructure",
|
|
3266
|
+
retry: "retryable",
|
|
3267
|
+
reason: cause.message
|
|
3268
|
+
}, { cause }));
|
|
2452
3269
|
}
|
|
2453
3270
|
});
|
|
2454
3271
|
});
|
|
@@ -2458,9 +3275,12 @@ async function runCodexTurn(params) {
|
|
|
2458
3275
|
}
|
|
2459
3276
|
if (params.expectedSessionRef !== void 0 && sessionRef !== params.expectedSessionRef) {
|
|
2460
3277
|
runner.kill();
|
|
2461
|
-
throw new
|
|
2462
|
-
|
|
2463
|
-
|
|
3278
|
+
throw new RuntimeExecutionFailure({
|
|
3279
|
+
phase: params.failurePhase,
|
|
3280
|
+
category: "authority",
|
|
3281
|
+
retry: "non-retryable",
|
|
3282
|
+
reason: `codex exec resume echoed a different thread id than requested (requested ${params.expectedSessionRef}, got ${sessionRef})`
|
|
3283
|
+
});
|
|
2464
3284
|
}
|
|
2465
3285
|
return { sessionRef, runner };
|
|
2466
3286
|
}
|
|
@@ -2486,8 +3306,13 @@ var CodexSession = class {
|
|
|
2486
3306
|
queue;
|
|
2487
3307
|
recordUnmapped;
|
|
2488
3308
|
preparedGit;
|
|
3309
|
+
modelId;
|
|
3310
|
+
terminal;
|
|
2489
3311
|
currentRunner;
|
|
3312
|
+
ownedRunners = /* @__PURE__ */ new Set();
|
|
3313
|
+
followUpAttempts = /* @__PURE__ */ new Set();
|
|
2490
3314
|
closed = false;
|
|
3315
|
+
closeAttempt;
|
|
2491
3316
|
constructor(options) {
|
|
2492
3317
|
this.sessionRef = options.sessionRef;
|
|
2493
3318
|
this.command = options.command;
|
|
@@ -2497,11 +3322,37 @@ var CodexSession = class {
|
|
|
2497
3322
|
this.queue = options.queue;
|
|
2498
3323
|
this.recordUnmapped = options.recordUnmapped;
|
|
2499
3324
|
this.preparedGit = options.preparedGit;
|
|
3325
|
+
this.modelId = options.modelId;
|
|
3326
|
+
this.terminal = options.terminal;
|
|
2500
3327
|
this.currentRunner = options.initialRunner;
|
|
3328
|
+
this.ownedRunners.add(options.initialRunner);
|
|
2501
3329
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
2502
3330
|
}
|
|
2503
3331
|
get events() {
|
|
2504
|
-
|
|
3332
|
+
const queue = this.queue;
|
|
3333
|
+
const session = this;
|
|
3334
|
+
return {
|
|
3335
|
+
[Symbol.asyncIterator]() {
|
|
3336
|
+
const inner = queue[Symbol.asyncIterator]();
|
|
3337
|
+
return {
|
|
3338
|
+
async next() {
|
|
3339
|
+
let result;
|
|
3340
|
+
try {
|
|
3341
|
+
result = await inner.next();
|
|
3342
|
+
} catch (cause) {
|
|
3343
|
+
throw new RuntimeExecutionFailure({
|
|
3344
|
+
phase: "run",
|
|
3345
|
+
category: "infrastructure",
|
|
3346
|
+
retry: "retryable",
|
|
3347
|
+
reason: "codex runtime event transport failed"
|
|
3348
|
+
}, { cause });
|
|
3349
|
+
}
|
|
3350
|
+
if (result.done && session.terminal.failure) throw session.terminal.failure;
|
|
3351
|
+
return result;
|
|
3352
|
+
}
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
};
|
|
2505
3356
|
}
|
|
2506
3357
|
async forgetRunnerOnceClosed(runner) {
|
|
2507
3358
|
await runner.waitClosed();
|
|
@@ -2546,7 +3397,14 @@ var CodexSession = class {
|
|
|
2546
3397
|
* stale id even after codex had moved on) — it just can now only ever be
|
|
2547
3398
|
* the SAME id this call asked to resume, never a silently-different one.
|
|
2548
3399
|
*/
|
|
2549
|
-
|
|
3400
|
+
followUp(task) {
|
|
3401
|
+
const attempt = this.runFollowUp(task);
|
|
3402
|
+
this.followUpAttempts.add(attempt);
|
|
3403
|
+
void attempt.finally(() => this.followUpAttempts.delete(attempt)).catch(() => {
|
|
3404
|
+
});
|
|
3405
|
+
return attempt;
|
|
3406
|
+
}
|
|
3407
|
+
async runFollowUp(task) {
|
|
2550
3408
|
if (typeof task.instruction !== "string") {
|
|
2551
3409
|
throw new PolicyUnsupportedError("codex adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
2552
3410
|
}
|
|
@@ -2557,41 +3415,79 @@ var CodexSession = class {
|
|
|
2557
3415
|
if (!mapping.ok) {
|
|
2558
3416
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2559
3417
|
}
|
|
3418
|
+
const requestedModel = subscriptionModel2(task.dispatchSelection);
|
|
3419
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
3420
|
+
throw new PolicyUnsupportedError(
|
|
3421
|
+
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
const modelId = this.modelId;
|
|
2560
3425
|
const resumeRef = this.sessionRef;
|
|
2561
3426
|
let sessionRef;
|
|
2562
3427
|
let runner;
|
|
3428
|
+
const terminal = {};
|
|
2563
3429
|
try {
|
|
2564
3430
|
({ sessionRef, runner } = await runCodexTurn({
|
|
2565
3431
|
command: this.command,
|
|
2566
3432
|
resumeRef,
|
|
2567
3433
|
instruction: task.instruction,
|
|
3434
|
+
modelId,
|
|
2568
3435
|
policyArgs: mapping.args,
|
|
2569
3436
|
cwd: this.workspaceDir,
|
|
2570
|
-
env: this.env,
|
|
3437
|
+
env: withoutProviderCredentials(this.env),
|
|
2571
3438
|
spawnFn: this.spawnFn,
|
|
2572
3439
|
workspaceDir: this.workspaceDir,
|
|
2573
3440
|
queue: this.queue,
|
|
2574
3441
|
recordUnmapped: this.recordUnmapped,
|
|
2575
3442
|
expectedSessionRef: resumeRef,
|
|
2576
|
-
preparedGit: this.preparedGit
|
|
3443
|
+
preparedGit: this.preparedGit,
|
|
3444
|
+
failurePhase: "run",
|
|
3445
|
+
terminal,
|
|
3446
|
+
onRunnerCreated: (created) => {
|
|
3447
|
+
this.ownedRunners.add(created);
|
|
3448
|
+
void this.forgetRunnerOnceClosed(created);
|
|
3449
|
+
}
|
|
2577
3450
|
}));
|
|
2578
3451
|
} catch (err) {
|
|
2579
3452
|
this.queue.end();
|
|
3453
|
+
this.terminal = {
|
|
3454
|
+
failure: isRuntimeExecutionFailure(err) ? err : new RuntimeExecutionFailure({
|
|
3455
|
+
phase: "run",
|
|
3456
|
+
category: "authority",
|
|
3457
|
+
retry: "non-retryable",
|
|
3458
|
+
reason: "codex follow-up violated the runtime adapter contract"
|
|
3459
|
+
}, { cause: err })
|
|
3460
|
+
};
|
|
2580
3461
|
throw err;
|
|
2581
3462
|
}
|
|
3463
|
+
if (this.closed) {
|
|
3464
|
+
await runner.dispose();
|
|
3465
|
+
throw new Error("codex session closed while follow-up was starting");
|
|
3466
|
+
}
|
|
3467
|
+
this.terminal = terminal;
|
|
2582
3468
|
this.sessionRef = sessionRef;
|
|
2583
3469
|
this.currentRunner = runner;
|
|
2584
|
-
void this.forgetRunnerOnceClosed(runner);
|
|
2585
3470
|
}
|
|
2586
3471
|
/** Best-effort abort of the current turn. SIGTERM's the currently-running child, if any — see `process-runner.ts`'s `kill()` doc comment for why SIGTERM (not SIGINT) and why this is safe: the underlying codex thread survives and stays resumable, confirmed empirically. A no-op when no turn is currently in flight. */
|
|
2587
3472
|
async interrupt() {
|
|
2588
3473
|
this.currentRunner?.kill();
|
|
2589
3474
|
}
|
|
2590
3475
|
async close() {
|
|
2591
|
-
if (this.
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
3476
|
+
if (!this.closeAttempt) {
|
|
3477
|
+
this.closed = true;
|
|
3478
|
+
this.queue.end();
|
|
3479
|
+
const attempt = (async () => {
|
|
3480
|
+
const runners = [...this.ownedRunners];
|
|
3481
|
+
await Promise.all(runners.map((runner) => runner.dispose()));
|
|
3482
|
+
for (const runner of runners) this.ownedRunners.delete(runner);
|
|
3483
|
+
await Promise.allSettled([...this.followUpAttempts]);
|
|
3484
|
+
})();
|
|
3485
|
+
this.closeAttempt = attempt.catch((error) => {
|
|
3486
|
+
this.closeAttempt = void 0;
|
|
3487
|
+
throw error;
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
await this.closeAttempt;
|
|
2595
3491
|
}
|
|
2596
3492
|
/**
|
|
2597
3493
|
* `codex exec` has no in-band channel to inject text into an already-
|
|
@@ -2627,6 +3523,15 @@ var CodexSession = class {
|
|
|
2627
3523
|
);
|
|
2628
3524
|
}
|
|
2629
3525
|
};
|
|
3526
|
+
function subscriptionModel2(selection) {
|
|
3527
|
+
if (selection === void 0) return void 0;
|
|
3528
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
3529
|
+
throw new PolicyUnsupportedError(
|
|
3530
|
+
`codex adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
return selection.modelId;
|
|
3534
|
+
}
|
|
2630
3535
|
|
|
2631
3536
|
// src/daemon/approvals.ts
|
|
2632
3537
|
var ApprovalNotFoundError = class extends Error {
|
|
@@ -2683,7 +3588,7 @@ var ApprovalRegistry = class {
|
|
|
2683
3588
|
* `requestApproval` timeout and `finish()` fail-closed cleanup
|
|
2684
3589
|
* (`task-runner.ts`) — resolves a decision this device made on its own.
|
|
2685
3590
|
* The one exception, a server-sent wire `task.approve`/`task.reject`
|
|
2686
|
-
* relayed through `
|
|
3591
|
+
* relayed through `RuntimeOperationStartInput.approvalChannel.resolve`
|
|
2687
3592
|
* (`task-runner.ts`'s `handleOffer`), passes `'wire'` explicitly.
|
|
2688
3593
|
*/
|
|
2689
3594
|
resolve(approvalId, decision, reason, origin = "local") {
|
|
@@ -2851,13 +3756,10 @@ function exportPrivateKeyPem(privateKey) {
|
|
|
2851
3756
|
function importPrivateKeyPem(pem) {
|
|
2852
3757
|
return createPrivateKey(pem);
|
|
2853
3758
|
}
|
|
2854
|
-
var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
|
|
2855
3759
|
function signNonce(privateKey, nonce) {
|
|
2856
|
-
const signature = sign(null,
|
|
3760
|
+
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
2857
3761
|
return signature.toString("base64url");
|
|
2858
3762
|
}
|
|
2859
|
-
|
|
2860
|
-
// src/daemon/url.ts
|
|
2861
3763
|
function toHttpBase(serverUrl) {
|
|
2862
3764
|
const url = new URL(serverUrl);
|
|
2863
3765
|
if (url.protocol === "ws:") url.protocol = "http:";
|
|
@@ -2867,7 +3769,7 @@ function toHttpBase(serverUrl) {
|
|
|
2867
3769
|
return url.toString();
|
|
2868
3770
|
}
|
|
2869
3771
|
function toWsUrl(serverUrl) {
|
|
2870
|
-
const url = new URL(
|
|
3772
|
+
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
2871
3773
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2872
3774
|
return url.toString();
|
|
2873
3775
|
}
|
|
@@ -2955,7 +3857,7 @@ var AuthManager = class {
|
|
|
2955
3857
|
return await this.runCredentialMutation(async () => {
|
|
2956
3858
|
const existing = this.record ?? await this.opts.store.load();
|
|
2957
3859
|
const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
|
|
2958
|
-
const url = new URL(
|
|
3860
|
+
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
2959
3861
|
const res = await fetch(url, {
|
|
2960
3862
|
method: "POST",
|
|
2961
3863
|
headers: { "content-type": "application/json" },
|
|
@@ -2989,6 +3891,7 @@ var AuthManager = class {
|
|
|
2989
3891
|
async getValidAccessToken() {
|
|
2990
3892
|
if (!this.record) throw new Error("device is not paired yet; call pair(pairingCode) first");
|
|
2991
3893
|
if (this.revoked) throw new DeviceRevokedError();
|
|
3894
|
+
if (this.renewing) return this.renewing;
|
|
2992
3895
|
if (msUntilExpiry(this.record.expiresAt) > RENEW_MARGIN_MS) return this.record.accessToken;
|
|
2993
3896
|
return this.renew();
|
|
2994
3897
|
}
|
|
@@ -3017,7 +3920,7 @@ var AuthManager = class {
|
|
|
3017
3920
|
const record = this.record;
|
|
3018
3921
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3019
3922
|
const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
|
|
3020
|
-
const challengeRes = await fetch(new URL(
|
|
3923
|
+
const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
|
|
3021
3924
|
method: "POST",
|
|
3022
3925
|
headers: { "content-type": "application/json" },
|
|
3023
3926
|
body: JSON.stringify({ deviceId: record.deviceId })
|
|
@@ -3030,7 +3933,7 @@ var AuthManager = class {
|
|
|
3030
3933
|
}
|
|
3031
3934
|
const { nonce } = await challengeRes.json();
|
|
3032
3935
|
const signature = signNonce(privateKey, nonce);
|
|
3033
|
-
const tokenRes = await fetch(new URL(
|
|
3936
|
+
const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
|
|
3034
3937
|
method: "POST",
|
|
3035
3938
|
headers: { "content-type": "application/json" },
|
|
3036
3939
|
body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
|
|
@@ -3128,7 +4031,7 @@ var BlobClient = class {
|
|
|
3128
4031
|
async resolveInstruction(blobRef) {
|
|
3129
4032
|
const base = toHttpBase(this.serverUrl);
|
|
3130
4033
|
const urlRes = await authedFetch(
|
|
3131
|
-
new URL(
|
|
4034
|
+
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
3132
4035
|
{ method: "GET" },
|
|
3133
4036
|
this.auth
|
|
3134
4037
|
);
|
|
@@ -3156,7 +4059,7 @@ var BlobClient = class {
|
|
|
3156
4059
|
const base = toHttpBase(this.serverUrl);
|
|
3157
4060
|
const reservationId = `blob_${randomUUID()}`;
|
|
3158
4061
|
const createRes = await authedFetch(
|
|
3159
|
-
new URL(
|
|
4062
|
+
new URL(BYOK_BLOBS_PATH, base),
|
|
3160
4063
|
{
|
|
3161
4064
|
method: "POST",
|
|
3162
4065
|
headers: {
|
|
@@ -3188,7 +4091,7 @@ var BlobClient = class {
|
|
|
3188
4091
|
let response;
|
|
3189
4092
|
try {
|
|
3190
4093
|
response = await authedFetch(
|
|
3191
|
-
new URL(
|
|
4094
|
+
new URL(byokBlobFinalizePath(blobId), base),
|
|
3192
4095
|
{
|
|
3193
4096
|
method: "POST",
|
|
3194
4097
|
headers: { "idempotency-key": reservationId }
|
|
@@ -3211,16 +4114,180 @@ var BlobClient = class {
|
|
|
3211
4114
|
throw lastFailure;
|
|
3212
4115
|
}
|
|
3213
4116
|
};
|
|
4117
|
+
var PRESENCE_HINTS_CAPABILITY = "presence.hints";
|
|
4118
|
+
var CapabilityDiscoveryError = class extends Error {
|
|
4119
|
+
constructor(message, options) {
|
|
4120
|
+
super(message, options);
|
|
4121
|
+
this.name = "CapabilityDiscoveryError";
|
|
4122
|
+
}
|
|
4123
|
+
};
|
|
4124
|
+
async function fetchCapabilityDeclaration(serverUrl, options = {}) {
|
|
4125
|
+
const url = new URL(BYOK_CAPABILITIES_PATH, toHttpBase(serverUrl));
|
|
4126
|
+
let response;
|
|
4127
|
+
try {
|
|
4128
|
+
response = await fetch(url, {
|
|
4129
|
+
method: "GET",
|
|
4130
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
4131
|
+
});
|
|
4132
|
+
} catch (err) {
|
|
4133
|
+
throw new CapabilityDiscoveryError(
|
|
4134
|
+
`failed to read the capability declaration from ${url.toString()}: ${err instanceof Error ? err.message : String(err)}`,
|
|
4135
|
+
{ cause: err }
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4138
|
+
if (!response.ok) {
|
|
4139
|
+
throw new CapabilityDiscoveryError(
|
|
4140
|
+
`failed to read the capability declaration from ${url.toString()}: HTTP ${response.status}`
|
|
4141
|
+
);
|
|
4142
|
+
}
|
|
4143
|
+
let body;
|
|
4144
|
+
try {
|
|
4145
|
+
body = await response.json();
|
|
4146
|
+
} catch (err) {
|
|
4147
|
+
throw new CapabilityDiscoveryError(
|
|
4148
|
+
`the capability declaration at ${url.toString()} is not JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
4149
|
+
{ cause: err }
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
4152
|
+
const parsed = CapabilityDeclarationSchema.safeParse(body);
|
|
4153
|
+
if (!parsed.success) {
|
|
4154
|
+
throw new CapabilityDiscoveryError(
|
|
4155
|
+
`the capability declaration at ${url.toString()} is not a valid ADR-010 declaration: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`,
|
|
4156
|
+
{ cause: parsed.error }
|
|
4157
|
+
);
|
|
4158
|
+
}
|
|
4159
|
+
return parsed.data;
|
|
4160
|
+
}
|
|
4161
|
+
function declares(declaration, capability) {
|
|
4162
|
+
return hasCapability(declaration, capability);
|
|
4163
|
+
}
|
|
4164
|
+
var DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
4165
|
+
var DEFAULT_PRESENCE_TTL_MS = 9e4;
|
|
4166
|
+
var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
|
|
4167
|
+
function assertPresenceHeartbeatCadence(cadence) {
|
|
4168
|
+
const { intervalMs, ttlMs, minimumIntervalMs } = cadence;
|
|
4169
|
+
if (!(minimumIntervalMs < intervalMs && intervalMs < ttlMs)) {
|
|
4170
|
+
throw new Error(
|
|
4171
|
+
`presence heartbeat interval must satisfy minimumIntervalMs < intervalMs < ttlMs \u2014 got ${minimumIntervalMs} < ${intervalMs} < ${ttlMs}`
|
|
4172
|
+
);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
var PresencePublisher = class {
|
|
4176
|
+
constructor(opts) {
|
|
4177
|
+
this.opts = opts;
|
|
4178
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS;
|
|
4179
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_PRESENCE_TTL_MS;
|
|
4180
|
+
const minimumIntervalMs = opts.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS;
|
|
4181
|
+
assertPresenceHeartbeatCadence({ intervalMs, ttlMs, minimumIntervalMs });
|
|
4182
|
+
this.intervalMs = intervalMs;
|
|
4183
|
+
this.url = new URL(BYOK_PRESENCE_PATH, toHttpBase(opts.serverUrl));
|
|
4184
|
+
}
|
|
4185
|
+
opts;
|
|
4186
|
+
url;
|
|
4187
|
+
intervalMs;
|
|
4188
|
+
timer;
|
|
4189
|
+
running = false;
|
|
4190
|
+
/** Set once a revoked device is observed. Terminal: `start()` will not restart this instance. */
|
|
4191
|
+
stoppedPermanently = false;
|
|
4192
|
+
/** Publishes immediately, then every `intervalMs`. Idempotent; a no-op after a permanent stop. */
|
|
4193
|
+
start() {
|
|
4194
|
+
if (this.running || this.stoppedPermanently) return;
|
|
4195
|
+
this.running = true;
|
|
4196
|
+
void this.beat();
|
|
4197
|
+
}
|
|
4198
|
+
/** Stops the cadence. Idempotent, and the only "offline" signal this producer emits — the hint's TTL does the rest. */
|
|
4199
|
+
stop() {
|
|
4200
|
+
this.running = false;
|
|
4201
|
+
if (this.timer !== void 0) {
|
|
4202
|
+
clearTimeout(this.timer);
|
|
4203
|
+
this.timer = void 0;
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
schedule() {
|
|
4207
|
+
if (!this.running) return;
|
|
4208
|
+
this.timer = setTimeout(() => {
|
|
4209
|
+
this.timer = void 0;
|
|
4210
|
+
void this.beat();
|
|
4211
|
+
}, this.intervalMs);
|
|
4212
|
+
this.timer.unref?.();
|
|
4213
|
+
}
|
|
4214
|
+
async beat() {
|
|
4215
|
+
if (!this.running) return;
|
|
4216
|
+
try {
|
|
4217
|
+
const response = await authedFetch(
|
|
4218
|
+
this.url,
|
|
4219
|
+
{
|
|
4220
|
+
method: "PUT",
|
|
4221
|
+
headers: { "content-type": "application/json" },
|
|
4222
|
+
body: JSON.stringify({
|
|
4223
|
+
level: "online",
|
|
4224
|
+
...this.opts.configuredToolsets === void 0 ? {} : { configuredToolsets: this.opts.configuredToolsets }
|
|
4225
|
+
})
|
|
4226
|
+
},
|
|
4227
|
+
this.opts.auth
|
|
4228
|
+
);
|
|
4229
|
+
if (!response.ok) {
|
|
4230
|
+
if (response.status === 401) {
|
|
4231
|
+
this.stopPermanently(`presence heartbeat unauthorized after token renewal (HTTP 401)`);
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4234
|
+
this.opts.onDegraded?.(`presence heartbeat failed: HTTP ${response.status}`);
|
|
4235
|
+
}
|
|
4236
|
+
} catch (err) {
|
|
4237
|
+
if (err instanceof DeviceRevokedError) {
|
|
4238
|
+
this.stopPermanently("presence heartbeat stopped: device has been revoked; re-pair required");
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
this.opts.onDegraded?.(`presence heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
4242
|
+
}
|
|
4243
|
+
this.schedule();
|
|
4244
|
+
}
|
|
4245
|
+
stopPermanently(reason) {
|
|
4246
|
+
this.stoppedPermanently = true;
|
|
4247
|
+
this.stop();
|
|
4248
|
+
this.opts.onDegraded?.(reason);
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
function freshJti() {
|
|
4252
|
+
return randomBytes(16).toString("base64url");
|
|
4253
|
+
}
|
|
4254
|
+
function mintDeviceAssertion(input) {
|
|
4255
|
+
const issuedAtMs = input.now.getTime();
|
|
4256
|
+
const expiresAt = new Date(issuedAtMs + input.ttlMs).toISOString();
|
|
4257
|
+
const claims = DeviceAssertionClaimsSchema.parse({
|
|
4258
|
+
version: 1,
|
|
4259
|
+
issuer: input.issuer,
|
|
4260
|
+
productId: input.productId,
|
|
4261
|
+
deviceId: input.record.deviceId,
|
|
4262
|
+
audience: input.audience,
|
|
4263
|
+
jti: freshJti(),
|
|
4264
|
+
issuedAt: new Date(issuedAtMs).toISOString(),
|
|
4265
|
+
expiresAt
|
|
4266
|
+
});
|
|
4267
|
+
const privateKey = importPrivateKeyPem(input.record.devicePrivateKeyPem);
|
|
4268
|
+
const signature = sign(null, deviceAssertionSigningInput(claims), privateKey).toString("base64url");
|
|
4269
|
+
return {
|
|
4270
|
+
envelope: {
|
|
4271
|
+
schema: DEVICE_ASSERTION_SCHEMA_ID,
|
|
4272
|
+
algorithm: "ed25519",
|
|
4273
|
+
protected: claims,
|
|
4274
|
+
signature
|
|
4275
|
+
},
|
|
4276
|
+
claims,
|
|
4277
|
+
expiresAt
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
3214
4280
|
var CONTROL_PROTOCOL_VERSION = 1;
|
|
3215
4281
|
var HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
3216
4282
|
var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
|
|
4283
|
+
var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
|
|
3217
4284
|
function shortHash(input) {
|
|
3218
4285
|
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
|
|
3219
4286
|
}
|
|
3220
4287
|
function controlSocketPath(storeDir) {
|
|
3221
4288
|
const candidate = path20.join(storeDir, "control.sock");
|
|
3222
4289
|
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
|
|
3223
|
-
return path20.join(
|
|
4290
|
+
return path20.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
|
|
3224
4291
|
}
|
|
3225
4292
|
function controlPipeName(productId, storeDir) {
|
|
3226
4293
|
const id = shortHash(`${productId}|${path20.resolve(storeDir)}`);
|
|
@@ -3326,6 +4393,16 @@ function parseApprovalsRequestParams(value) {
|
|
|
3326
4393
|
if (typeof value.summary !== "string") return void 0;
|
|
3327
4394
|
return { taskId: value.taskId, summary: value.summary };
|
|
3328
4395
|
}
|
|
4396
|
+
var ASSERTION_AUDIENCE_MAX_BYTES = 256;
|
|
4397
|
+
function parseAssertionIssueParams(value) {
|
|
4398
|
+
if (!isRecord2(value)) return void 0;
|
|
4399
|
+
const keys = Object.keys(value);
|
|
4400
|
+
if (keys.length !== 1 || keys[0] !== "audience") return void 0;
|
|
4401
|
+
const { audience } = value;
|
|
4402
|
+
if (typeof audience !== "string" || audience.length === 0) return void 0;
|
|
4403
|
+
if (Buffer.byteLength(audience, "utf8") > ASSERTION_AUDIENCE_MAX_BYTES) return void 0;
|
|
4404
|
+
return { audience };
|
|
4405
|
+
}
|
|
3329
4406
|
function parseShutdownParams(value) {
|
|
3330
4407
|
if (!isRecord2(value)) return {};
|
|
3331
4408
|
return value.reason === "unpair" || value.reason === "operator" ? { reason: value.reason } : {};
|
|
@@ -3339,12 +4416,12 @@ var AnotherControlServerRunningError = class extends Error {
|
|
|
3339
4416
|
}
|
|
3340
4417
|
};
|
|
3341
4418
|
var MAX_HALF_OPEN_CONNECTIONS = 8;
|
|
3342
|
-
function
|
|
4419
|
+
function errorMessage3(err) {
|
|
3343
4420
|
return err instanceof Error ? err.message : String(err);
|
|
3344
4421
|
}
|
|
3345
4422
|
function toControlErrorShape(err) {
|
|
3346
4423
|
if (err instanceof ControlError) return { code: err.code, message: err.message };
|
|
3347
|
-
return { code: "internal_error", message:
|
|
4424
|
+
return { code: "internal_error", message: errorMessage3(err) };
|
|
3348
4425
|
}
|
|
3349
4426
|
function probeUnixSocketAlive(socketPath) {
|
|
3350
4427
|
return new Promise((resolve) => {
|
|
@@ -3551,12 +4628,19 @@ async function startControlServer(opts) {
|
|
|
3551
4628
|
}
|
|
3552
4629
|
throw err;
|
|
3553
4630
|
}
|
|
4631
|
+
let stopServingPromise;
|
|
4632
|
+
async function stopServing() {
|
|
4633
|
+
stopServingPromise ??= (async () => {
|
|
4634
|
+
for (const socket of sockets) socket.destroy();
|
|
4635
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
4636
|
+
})();
|
|
4637
|
+
await stopServingPromise;
|
|
4638
|
+
}
|
|
3554
4639
|
let closed = false;
|
|
3555
4640
|
async function close() {
|
|
3556
4641
|
if (closed) return;
|
|
3557
4642
|
closed = true;
|
|
3558
|
-
|
|
3559
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
4643
|
+
await stopServing();
|
|
3560
4644
|
if (process.platform !== "win32") {
|
|
3561
4645
|
await promises.rm(endpoint, { force: true }).catch(() => {
|
|
3562
4646
|
});
|
|
@@ -3564,7 +4648,7 @@ async function startControlServer(opts) {
|
|
|
3564
4648
|
await promises.rm(tokenPath, { force: true }).catch(() => {
|
|
3565
4649
|
});
|
|
3566
4650
|
}
|
|
3567
|
-
return { endpoint, close };
|
|
4651
|
+
return { endpoint, stopServing, close };
|
|
3568
4652
|
}
|
|
3569
4653
|
function deterministicJitterMs(input) {
|
|
3570
4654
|
const ratio = input.ratio ?? 0.2;
|
|
@@ -3658,7 +4742,7 @@ var LongPollClient = class {
|
|
|
3658
4742
|
try {
|
|
3659
4743
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3660
4744
|
const res = await authedFetch(
|
|
3661
|
-
new URL(
|
|
4745
|
+
new URL(BYOK_MESSAGES_PATH, base),
|
|
3662
4746
|
{
|
|
3663
4747
|
method: "POST",
|
|
3664
4748
|
headers: { "content-type": "application/json" },
|
|
@@ -3682,7 +4766,7 @@ var LongPollClient = class {
|
|
|
3682
4766
|
while (this.running) {
|
|
3683
4767
|
try {
|
|
3684
4768
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3685
|
-
const url = new URL(
|
|
4769
|
+
const url = new URL(BYOK_EVENTS_PATH, base);
|
|
3686
4770
|
const cursor = this.opts.getCursor();
|
|
3687
4771
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
3688
4772
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
@@ -3858,6 +4942,7 @@ var WsTransport = class {
|
|
|
3858
4942
|
deviceId: this.opts.deviceId,
|
|
3859
4943
|
productId: this.opts.productId,
|
|
3860
4944
|
runtimes: this.opts.runtimes,
|
|
4945
|
+
configuredToolsets: this.opts.configuredToolsets === void 0 ? void 0 : [...this.opts.configuredToolsets],
|
|
3861
4946
|
cursor: this.opts.getCursor?.()
|
|
3862
4947
|
});
|
|
3863
4948
|
socket.send(encodeEnvelope(hello));
|
|
@@ -3947,6 +5032,7 @@ var ConnectionManager = class {
|
|
|
3947
5032
|
productId: opts.productId,
|
|
3948
5033
|
capabilities: opts.capabilities,
|
|
3949
5034
|
runtimes: opts.runtimes,
|
|
5035
|
+
configuredToolsets: opts.configuredToolsets,
|
|
3950
5036
|
getCursor: () => this.cursor,
|
|
3951
5037
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
3952
5038
|
onStateChange: (state) => {
|
|
@@ -4069,7 +5155,7 @@ var ConnectionManager = class {
|
|
|
4069
5155
|
* already-delivered seqs (see its own doc comment) so the failed seq's own
|
|
4070
5156
|
* redelivery can get through — but that same frozen watermark also means
|
|
4071
5157
|
* every OTHER seq above it rides along on every re-poll too. Without this,
|
|
4072
|
-
* a seq already mid-flight (e.g. a `task.offer` whose
|
|
5158
|
+
* a seq already mid-flight (e.g. a `task.offer` whose prepared operation start()
|
|
4073
5159
|
* hasn't resolved yet) would be re-enqueued into `processingChain` on
|
|
4074
5160
|
* every such re-poll, piling up duplicate copies that — once the first
|
|
4075
5161
|
* finally resolves and the chain unwinds through them — run its handler
|
|
@@ -4945,19 +6031,25 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
|
4945
6031
|
var MAX_OWNER_BYTES = 4096;
|
|
4946
6032
|
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
4947
6033
|
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
6034
|
+
function endOwnershipProbe(socket, response) {
|
|
6035
|
+
socket.on("error", () => {
|
|
6036
|
+
});
|
|
6037
|
+
if (response === void 0) socket.end();
|
|
6038
|
+
else socket.end(response);
|
|
6039
|
+
}
|
|
4951
6040
|
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
4952
6041
|
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
6042
|
+
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
6043
|
+
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
6044
|
+
var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
|
|
4953
6045
|
function storeMutexIdentity(canonicalStoreDir) {
|
|
4954
6046
|
return createHash("sha256").update(canonicalStoreDir).digest("hex");
|
|
4955
6047
|
}
|
|
4956
|
-
function
|
|
4957
|
-
|
|
4958
|
-
const
|
|
4959
|
-
|
|
4960
|
-
return
|
|
6048
|
+
function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
|
|
6049
|
+
if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
|
|
6050
|
+
const candidate = path20.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
|
|
6051
|
+
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
|
|
6052
|
+
return path20.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
|
|
4961
6053
|
}
|
|
4962
6054
|
var DaemonOwnerActiveError = class extends Error {
|
|
4963
6055
|
constructor(role) {
|
|
@@ -5051,7 +6143,7 @@ async function portIsBound(port) {
|
|
|
5051
6143
|
});
|
|
5052
6144
|
}
|
|
5053
6145
|
async function createLivenessListener() {
|
|
5054
|
-
const server = createServer((socket) => socket
|
|
6146
|
+
const server = createServer((socket) => endOwnershipProbe(socket));
|
|
5055
6147
|
const port = await new Promise((resolve, reject) => {
|
|
5056
6148
|
server.once("error", reject);
|
|
5057
6149
|
server.listen({ host: "127.0.0.1", port: 0, exclusive: true }, () => {
|
|
@@ -5078,70 +6170,90 @@ async function createLivenessListener() {
|
|
|
5078
6170
|
})
|
|
5079
6171
|
};
|
|
5080
6172
|
}
|
|
5081
|
-
async function probeStoreMutex(
|
|
6173
|
+
async function probeStoreMutex(endpoint, identity) {
|
|
5082
6174
|
return new Promise((resolve) => {
|
|
5083
|
-
const socket = createConnection(
|
|
6175
|
+
const socket = createConnection(endpoint);
|
|
5084
6176
|
let settled = false;
|
|
5085
6177
|
let raw = "";
|
|
5086
6178
|
const finish = (result) => {
|
|
5087
6179
|
if (settled) return;
|
|
5088
6180
|
settled = true;
|
|
6181
|
+
clearTimeout(timer);
|
|
6182
|
+
socket.removeAllListeners();
|
|
5089
6183
|
socket.destroy();
|
|
5090
6184
|
resolve(result);
|
|
5091
6185
|
};
|
|
6186
|
+
const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
|
|
5092
6187
|
socket.setEncoding("utf8");
|
|
5093
|
-
socket.setTimeout(STORE_MUTEX_PROBE_TIMEOUT_MS, () => finish({ kind: "uncertain" }));
|
|
5094
6188
|
socket.on("data", (chunk) => {
|
|
5095
6189
|
raw += chunk;
|
|
5096
|
-
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "
|
|
5097
|
-
});
|
|
5098
|
-
socket.once("end", () => {
|
|
5099
|
-
const line = raw.trimEnd();
|
|
5100
|
-
const identity = line.startsWith(STORE_MUTEX_ID_PREFIX) ? line.slice(STORE_MUTEX_ID_PREFIX.length) : void 0;
|
|
5101
|
-
finish(
|
|
5102
|
-
identity && /^[a-f0-9]{64}$/.test(identity) ? { kind: "identity", identity } : { kind: "foreign-or-gone" }
|
|
5103
|
-
);
|
|
6190
|
+
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
|
|
5104
6191
|
});
|
|
5105
|
-
socket.once("
|
|
6192
|
+
socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
|
|
6193
|
+
socket.once(
|
|
6194
|
+
"error",
|
|
6195
|
+
(err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
|
|
6196
|
+
);
|
|
5106
6197
|
});
|
|
5107
6198
|
}
|
|
6199
|
+
async function clearStaleStoreMutexSocket(endpoint, identity) {
|
|
6200
|
+
let stat;
|
|
6201
|
+
try {
|
|
6202
|
+
stat = await promises.lstat(endpoint);
|
|
6203
|
+
} catch (err) {
|
|
6204
|
+
if (err.code === "ENOENT") return;
|
|
6205
|
+
throw err;
|
|
6206
|
+
}
|
|
6207
|
+
if (!stat.isSocket()) throw new Error("store mutation lock path exists but is not a socket");
|
|
6208
|
+
if ((await probeStoreMutex(endpoint, identity)).kind !== "unbound") throw new DaemonOwnerActiveError("unknown");
|
|
6209
|
+
await promises.rm(endpoint, { force: true });
|
|
6210
|
+
}
|
|
6211
|
+
async function assertOwnedPrivateDir2(dir) {
|
|
6212
|
+
const uid = process.getuid?.();
|
|
6213
|
+
if (uid === void 0) return;
|
|
6214
|
+
const stat = await promises.lstat(dir);
|
|
6215
|
+
if (stat.isSymbolicLink() || stat.uid !== uid) {
|
|
6216
|
+
throw new Error(`refusing to bind the store mutation lock under "${dir}": not a real directory owned by this process's own uid`);
|
|
6217
|
+
}
|
|
6218
|
+
}
|
|
5108
6219
|
async function acquireStoreMutex(canonicalStoreDir) {
|
|
5109
6220
|
const identity = storeMutexIdentity(canonicalStoreDir);
|
|
5110
|
-
|
|
5111
|
-
|
|
6221
|
+
const endpoint = storeMutexEndpoint(canonicalStoreDir, identity);
|
|
6222
|
+
const isPipe = process.platform === "win32";
|
|
6223
|
+
if (!isPipe) {
|
|
6224
|
+
const endpointDir = path20.dirname(endpoint);
|
|
6225
|
+
if (endpointDir !== canonicalStoreDir) {
|
|
6226
|
+
await ensureSecureDir(endpointDir);
|
|
6227
|
+
await assertOwnedPrivateDir2(endpointDir);
|
|
6228
|
+
}
|
|
6229
|
+
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
6230
|
+
}
|
|
6231
|
+
const server = createServer((socket) => endOwnershipProbe(socket, `${STORE_MUTEX_ID_PREFIX}${identity}
|
|
5112
6232
|
`));
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
server.
|
|
5118
|
-
|
|
5119
|
-
resolve();
|
|
5120
|
-
});
|
|
6233
|
+
try {
|
|
6234
|
+
await new Promise((resolve, reject) => {
|
|
6235
|
+
server.once("error", reject);
|
|
6236
|
+
server.listen(endpoint, () => {
|
|
6237
|
+
server.removeListener("error", reject);
|
|
6238
|
+
resolve();
|
|
5121
6239
|
});
|
|
5122
|
-
}
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
throw new DaemonOwnerActiveError("unknown");
|
|
5127
|
-
}
|
|
5128
|
-
continue;
|
|
5129
|
-
}
|
|
5130
|
-
server.unref();
|
|
5131
|
-
let closed = false;
|
|
5132
|
-
return {
|
|
5133
|
-
port,
|
|
5134
|
-
close: () => new Promise((resolve, reject) => {
|
|
5135
|
-
if (closed) {
|
|
5136
|
-
resolve();
|
|
5137
|
-
return;
|
|
5138
|
-
}
|
|
5139
|
-
closed = true;
|
|
5140
|
-
server.close((err) => err ? reject(err) : resolve());
|
|
5141
|
-
})
|
|
5142
|
-
};
|
|
6240
|
+
});
|
|
6241
|
+
} catch (err) {
|
|
6242
|
+
if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
|
|
6243
|
+
throw err;
|
|
5143
6244
|
}
|
|
5144
|
-
|
|
6245
|
+
if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
6246
|
+
server.unref();
|
|
6247
|
+
let closed = false;
|
|
6248
|
+
return {
|
|
6249
|
+
endpoint,
|
|
6250
|
+
close: async () => {
|
|
6251
|
+
if (closed) return;
|
|
6252
|
+
closed = true;
|
|
6253
|
+
await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
|
|
6254
|
+
if (!isPipe) await promises.rm(endpoint, { force: true }).catch(() => void 0);
|
|
6255
|
+
}
|
|
6256
|
+
};
|
|
5145
6257
|
}
|
|
5146
6258
|
async function reclaimExistsAndIsActive(reclaimPath) {
|
|
5147
6259
|
let stat;
|
|
@@ -5257,7 +6369,8 @@ function toRuntimeInfoCapabilities(caps) {
|
|
|
5257
6369
|
steer: caps.steer,
|
|
5258
6370
|
resume: caps.resume,
|
|
5259
6371
|
approvalInteractive: caps.approvalInteractive,
|
|
5260
|
-
|
|
6372
|
+
...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
|
|
6373
|
+
permissionModes: [...caps.permissionModes]
|
|
5261
6374
|
};
|
|
5262
6375
|
}
|
|
5263
6376
|
var CursorStore = class {
|
|
@@ -5330,7 +6443,7 @@ var DaemonObserver = class {
|
|
|
5330
6443
|
}
|
|
5331
6444
|
/**
|
|
5332
6445
|
* Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
|
|
5333
|
-
*
|
|
6446
|
+
* either offer variant produces a local event here — every other inbound type
|
|
5334
6447
|
* (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
|
|
5335
6448
|
* best-effort notification whose OWN observable effect already surfaces
|
|
5336
6449
|
* through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
|
|
@@ -5338,7 +6451,7 @@ var DaemonObserver = class {
|
|
|
5338
6451
|
* where those are actually reported from.
|
|
5339
6452
|
*/
|
|
5340
6453
|
handleInboundEnvelope(envelope) {
|
|
5341
|
-
if (envelope.type !== "task.offer") return;
|
|
6454
|
+
if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets") return;
|
|
5342
6455
|
const taskId = envelope.task_id;
|
|
5343
6456
|
if (this.taskInfo.has(taskId)) return;
|
|
5344
6457
|
this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
|
|
@@ -5459,6 +6572,37 @@ var DaemonObserver = class {
|
|
|
5459
6572
|
noteShutdownComplete(reason, undeliveredOutboxCount) {
|
|
5460
6573
|
this.emit({ kind: "shutdown-complete", ts: nowIso(), reason, undeliveredOutboxCount });
|
|
5461
6574
|
}
|
|
6575
|
+
/**
|
|
6576
|
+
* Plan `device-assertion-broker`: see the `device-assertion` `DaemonEvent`
|
|
6577
|
+
* variant's own doc comment. The parameter type is what keeps the signature
|
|
6578
|
+
* out — there is no field to pass one through.
|
|
6579
|
+
*
|
|
6580
|
+
* codex round-2 F4: the DENIED caller can pass its raw `audience` here, but
|
|
6581
|
+
* it is converted to a byte SIZE the instant the event is constructed and the
|
|
6582
|
+
* raw string is dropped — it is never placed on the emitted `DaemonEvent`, so
|
|
6583
|
+
* it cannot reach a subscriber, `format.ts`, stdout, or the audit file. The
|
|
6584
|
+
* ISSUED `audience` came from the allowlist and is kept verbatim.
|
|
6585
|
+
*/
|
|
6586
|
+
noteDeviceAssertion(event) {
|
|
6587
|
+
if (event.result === "issued") {
|
|
6588
|
+
this.emit({
|
|
6589
|
+
kind: "device-assertion",
|
|
6590
|
+
ts: nowIso(),
|
|
6591
|
+
result: "issued",
|
|
6592
|
+
audience: event.audience,
|
|
6593
|
+
jti: event.jti,
|
|
6594
|
+
expiresAt: event.expiresAt
|
|
6595
|
+
});
|
|
6596
|
+
return;
|
|
6597
|
+
}
|
|
6598
|
+
this.emit({
|
|
6599
|
+
kind: "device-assertion",
|
|
6600
|
+
ts: nowIso(),
|
|
6601
|
+
result: "denied",
|
|
6602
|
+
reason: event.reason,
|
|
6603
|
+
audienceSize: event.audience === void 0 ? void 0 : Buffer.byteLength(event.audience, "utf8")
|
|
6604
|
+
});
|
|
6605
|
+
}
|
|
5462
6606
|
/** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
|
|
5463
6607
|
noteStaleApprovalDecision(taskId, decision, reason) {
|
|
5464
6608
|
this.emit({ kind: "stale-approval-decision", ts: nowIso(), taskId, decision, reason });
|
|
@@ -5466,6 +6610,9 @@ var DaemonObserver = class {
|
|
|
5466
6610
|
noteGitWorkspace(event) {
|
|
5467
6611
|
this.emit({ kind: "git-workspace", ts: nowIso(), ...event });
|
|
5468
6612
|
}
|
|
6613
|
+
noteRuntimeDisposalFailure(event) {
|
|
6614
|
+
this.emit({ kind: "runtime-disposal-failed", ts: nowIso(), ...event });
|
|
6615
|
+
}
|
|
5469
6616
|
/**
|
|
5470
6617
|
* Finding F4: wired from `TaskRunnerDeps.onApprovalDispatched`, called
|
|
5471
6618
|
* synchronously by `TaskRunner.dispatchApproval` BEFORE its own
|
|
@@ -7097,6 +8244,21 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
|
|
|
7097
8244
|
var MAX_TRACKED_TASK_IDS = 2e3;
|
|
7098
8245
|
var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
|
|
7099
8246
|
var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
|
|
8247
|
+
var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
|
|
8248
|
+
function resultDocumentRejectionDetail(check) {
|
|
8249
|
+
switch (check.reason) {
|
|
8250
|
+
case "over-cap":
|
|
8251
|
+
return `${check.bytes} bytes as canonical JSON, over the ${RESULT_DOCUMENT_MAX_BYTES}-byte limit (it is never truncated \u2014 a truncated JSON document is not valid JSON; use artifactRefs for a result this size)`;
|
|
8252
|
+
case "not-serializable":
|
|
8253
|
+
return "not JSON-serializable (JSON.stringify threw, or produced no output at all)";
|
|
8254
|
+
case "not-plain-json":
|
|
8255
|
+
return "not plain JSON data: it does not equal its own JSON round trip, so serializing it would silently change it (an undefined-valued key, NaN, a function or symbol value, a Date, a toJSON that rewrites the value, or a getter that answers differently on a second read)";
|
|
8256
|
+
default: {
|
|
8257
|
+
const exhaustive = check;
|
|
8258
|
+
throw new Error(`unhandled result document rejection: ${JSON.stringify(exhaustive)}`);
|
|
8259
|
+
}
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
7100
8262
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
7101
8263
|
function isKnownRuntimeId(id) {
|
|
7102
8264
|
return RuntimeIdSchema.safeParse(id).success;
|
|
@@ -7104,12 +8266,20 @@ function isKnownRuntimeId(id) {
|
|
|
7104
8266
|
var DEFAULT_RUNTIME_PREFERENCE = ["claude", "codex", "pi"];
|
|
7105
8267
|
function orderByPreference(candidates, preference) {
|
|
7106
8268
|
const rank = new Map(preference.map((id, index) => [id, index]));
|
|
7107
|
-
return [...candidates].sort((a, b) => (rank.get(a.id) ?? preference.length) - (rank.get(b.id) ?? preference.length));
|
|
8269
|
+
return [...candidates].sort((a, b) => (rank.get(a.descriptor.id) ?? preference.length) - (rank.get(b.descriptor.id) ?? preference.length));
|
|
7108
8270
|
}
|
|
7109
|
-
function adapterSupportsMode(
|
|
7110
|
-
return
|
|
8271
|
+
function adapterSupportsMode(descriptor, mode) {
|
|
8272
|
+
return descriptor.capabilities.permissionModes.includes(mode);
|
|
7111
8273
|
}
|
|
7112
|
-
function
|
|
8274
|
+
function adapterSupportsMcpToolsets(descriptor) {
|
|
8275
|
+
return descriptor.capabilities.mcpToolsets === true;
|
|
8276
|
+
}
|
|
8277
|
+
function withoutRequiredToolsets(payload) {
|
|
8278
|
+
if (!("requiredToolsets" in payload)) return payload;
|
|
8279
|
+
const { requiredToolsets, ...offer } = payload;
|
|
8280
|
+
return offer;
|
|
8281
|
+
}
|
|
8282
|
+
function errorMessage4(err) {
|
|
7113
8283
|
return err instanceof Error ? err.message : String(err);
|
|
7114
8284
|
}
|
|
7115
8285
|
function raceSettleFirst(fn, timeoutMs) {
|
|
@@ -7162,7 +8332,7 @@ async function openArtifact(workspaceDir, name) {
|
|
|
7162
8332
|
try {
|
|
7163
8333
|
handle = await promises.open(candidate, constants.O_RDONLY | O_NOFOLLOW);
|
|
7164
8334
|
} catch (err) {
|
|
7165
|
-
return { ok: false, reason: `artifact "${name}" could not be opened: ${
|
|
8335
|
+
return { ok: false, reason: `artifact "${name}" could not be opened: ${errorMessage4(err)}` };
|
|
7166
8336
|
}
|
|
7167
8337
|
try {
|
|
7168
8338
|
const st = await handle.stat();
|
|
@@ -7174,7 +8344,7 @@ async function openArtifact(workspaceDir, name) {
|
|
|
7174
8344
|
} catch (err) {
|
|
7175
8345
|
await handle.close().catch(() => {
|
|
7176
8346
|
});
|
|
7177
|
-
return { ok: false, reason: `artifact "${name}" could not be verified: ${
|
|
8347
|
+
return { ok: false, reason: `artifact "${name}" could not be verified: ${errorMessage4(err)}` };
|
|
7178
8348
|
}
|
|
7179
8349
|
return { ok: true, handle };
|
|
7180
8350
|
}
|
|
@@ -7188,13 +8358,13 @@ var TaskRunner = class {
|
|
|
7188
8358
|
* Finding F4 (cancel lost during the offer-processing window): a
|
|
7189
8359
|
* `task.cancel` for a taskId that hasn't finished `handleOffer` yet (still
|
|
7190
8360
|
* awaiting adapter detection / instruction resolution / workspace setup /
|
|
7191
|
-
* `
|
|
8361
|
+
* prepared operation `start()`) has no `this.tasks` entry to land on — it used to be
|
|
7192
8362
|
* silently dropped, and the runtime session `handleOffer` was about to
|
|
7193
8363
|
* register would then run an unsupervised ("zombie") turn nobody asked
|
|
7194
8364
|
* for anymore. Recording the taskId here lets `handleOffer` consult it at
|
|
7195
8365
|
* the two points where it can still safely react (see its body): before
|
|
7196
8366
|
* claiming at all (decline instead of ever starting a session), and right
|
|
7197
|
-
* after
|
|
8367
|
+
* after the prepared operation resolves but before this task is registered as
|
|
7198
8368
|
* active (tear the just-started session down immediately, before its
|
|
7199
8369
|
* event loop ever pumps a single event). Consumed (deleted) at whichever
|
|
7200
8370
|
* checkpoint handles it; a cancel for a taskId that's already active,
|
|
@@ -7220,12 +8390,12 @@ var TaskRunner = class {
|
|
|
7220
8390
|
* checkpoint-2 cancel-teardown, or successful registration into
|
|
7221
8391
|
* `this.tasks`). Bounded eviction on `pendingCancelled` (below) must never
|
|
7222
8392
|
* remove an entry for a taskId in this set: doing so is exactly the bug —
|
|
7223
|
-
* block task A in `
|
|
8393
|
+
* block task A in prepared-operation `start()`, deliver A's own `task.cancel` (so
|
|
7224
8394
|
* `pendingCancelled` gets an entry for A while A is still in-flight),
|
|
7225
8395
|
* then deliver `MAX_TRACKED_TASK_IDS` more cancels for unrelated taskIds
|
|
7226
8396
|
* nobody ever offered — under naive oldest-wins eviction, A's entry (the
|
|
7227
8397
|
* single oldest) gets evicted purely because of unrelated churn, so when
|
|
7228
|
-
*
|
|
8398
|
+
* the prepared operation finally resolves, checkpoint 2 finds no cancel marker
|
|
7229
8399
|
* and the already-cancelled task starts a real session. See
|
|
7230
8400
|
* `evictPendingCancelled` below for the fix, and
|
|
7231
8401
|
* `task-runner-bounded-collections.test.ts` for a test mirroring this
|
|
@@ -7245,7 +8415,7 @@ var TaskRunner = class {
|
|
|
7245
8415
|
* explicitly relies on redelivered handlers being idempotent for exactly
|
|
7246
8416
|
* this reason). `handleOffer` must treat a redelivered offer for a taskId
|
|
7247
8417
|
* that's already active (`this.tasks`) or already finished (this set) as
|
|
7248
|
-
* a no-op — never a second `
|
|
8418
|
+
* a no-op — never a second prepared-operation `start()` call, which would orphan the
|
|
7249
8419
|
* first session.
|
|
7250
8420
|
*
|
|
7251
8421
|
* M3-B: unbounded otherwise — a long-lived daemon that's finished many
|
|
@@ -7314,10 +8484,9 @@ var TaskRunner = class {
|
|
|
7314
8484
|
this.stoppingOffers = true;
|
|
7315
8485
|
}
|
|
7316
8486
|
/**
|
|
7317
|
-
*
|
|
7318
|
-
*
|
|
7319
|
-
*
|
|
7320
|
-
* terminal message is sent either way) but reports `task.fail` rather than
|
|
8487
|
+
* Shutdown of every currently ACTIVE task for the control socket's
|
|
8488
|
+
* `shutdown` RPC. Soft interrupt remains bounded, but each task's
|
|
8489
|
+
* authoritative close receipt must settle successfully. Reports `task.fail` rather than
|
|
7321
8490
|
* `task.cancelled` — these tasks aren't ending because the SERVER
|
|
7322
8491
|
* cancelled them, they're ending because this device is shutting down.
|
|
7323
8492
|
* `retryable: true` throughout: nothing about the task/policy itself was
|
|
@@ -7371,28 +8540,13 @@ var TaskRunner = class {
|
|
|
7371
8540
|
* unconditionally, so a hung `interrupt()` (a misbehaving adapter) can
|
|
7372
8541
|
* never block `task.fail` from being sent at all.
|
|
7373
8542
|
*
|
|
7374
|
-
*
|
|
7375
|
-
*
|
|
7376
|
-
*
|
|
7377
|
-
*
|
|
7378
|
-
* far more here than it used to for the pre-existing graceful-shutdown-only
|
|
7379
|
-
* caller, since THAT path is additionally bounded by an outer deadline
|
|
7380
|
-
* (`SHUTDOWN_TASK_TEARDOWN_DEADLINE_MS`/`DaemonConfig.shutdownGraceMs`,
|
|
7381
|
-
* `create-daemon.ts`), while resource-limit enforcement fires during
|
|
7382
|
-
* ordinary operation with no such outer bound watching it). `close()` is
|
|
7383
|
-
* every adapter's harder teardown primitive — an actual process-level kill
|
|
7384
|
-
* (SIGTERM, or `taskkill /F` on Windows — see e.g.
|
|
7385
|
-
* `ClaudeProcessClient.kill()`/`PiRpcClient.kill()`) as opposed to pi's own
|
|
7386
|
-
* soft in-band `interrupt()` (an RPC `abort` message that leaves the
|
|
7387
|
-
* process alive and resumable) — so escalating to it is the closest thing
|
|
7388
|
-
* to a "hard kill" the `Session` interface exposes. `finish()` below calls
|
|
7389
|
-
* `session.close()` again regardless (documented idempotent) — this isn't
|
|
7390
|
-
* a substitute for that, only an earlier, bounded attempt at actually
|
|
7391
|
-
* stopping a stuck runtime before this method gives up and reports failure
|
|
7392
|
-
* anyway.
|
|
8543
|
+
* After the bounded soft interrupt, `finish()` always awaits the authoritative
|
|
8544
|
+
* `Session.close()` receipt. A failed receipt retains active/Git ownership;
|
|
8545
|
+
* shutdown surfaces the rejection while resource enforcement leaves local
|
|
8546
|
+
* evidence for a later retry.
|
|
7393
8547
|
*
|
|
7394
8548
|
* Re-checks task identity (`this.tasks.get(...) === active`) immediately
|
|
7395
|
-
* before sending `task.fail`: the interrupt
|
|
8549
|
+
* before sending `task.fail`: the interrupt race above has await
|
|
7396
8550
|
* points during which a DIFFERENT path (a racing `task.cancel`/
|
|
7397
8551
|
* `task.reject`, or the session completing normally on its own) may have
|
|
7398
8552
|
* already finished this exact task and sent its own terminal message.
|
|
@@ -7401,20 +8555,25 @@ var TaskRunner = class {
|
|
|
7401
8555
|
* identity-check guard for the same class of race.
|
|
7402
8556
|
*/
|
|
7403
8557
|
async teardownActiveTask(active, reason, retryable) {
|
|
8558
|
+
if (active.finalizationStarted) return this.finish(active.taskId);
|
|
8559
|
+
if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
|
|
7404
8560
|
active.beingTornDown = true;
|
|
7405
8561
|
await this.observeGit(active, "salvage");
|
|
7406
8562
|
const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
|
|
7407
|
-
|
|
7408
|
-
if (
|
|
7409
|
-
await raceSettleFirst(() => active.session.close(), timeoutMs);
|
|
7410
|
-
}
|
|
7411
|
-
if (this.tasks.get(active.taskId) !== active) return;
|
|
8563
|
+
await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
|
|
8564
|
+
if (this.tasks.get(active.taskId) !== active) return true;
|
|
7412
8565
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId: active.taskId }));
|
|
7413
|
-
|
|
8566
|
+
return this.finish(active.taskId);
|
|
7414
8567
|
}
|
|
7415
8568
|
/** Graceful-shutdown caller of {@link teardownActiveTask} — see `shutdownActiveTasks`'s own doc comment. `retryable: true`: nothing about the task/policy itself was ever at fault, only this device's own availability right now. */
|
|
7416
8569
|
async shutdownTask(active, reason) {
|
|
7417
|
-
await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
|
|
8570
|
+
const disposed = await this.teardownActiveTask(active, `daemon shutting down: ${reason}`, true);
|
|
8571
|
+
if (!disposed) {
|
|
8572
|
+
throw new RuntimeDisposalFailure({
|
|
8573
|
+
stage: "quiescence",
|
|
8574
|
+
reason: `${active.adapter.descriptor.id} runtime ownership remains active after shutdown disposal failed`
|
|
8575
|
+
});
|
|
8576
|
+
}
|
|
7418
8577
|
}
|
|
7419
8578
|
/**
|
|
7420
8579
|
* M5 batch-3 (workstream 2): shared entry point for both resource-limit
|
|
@@ -7464,6 +8623,9 @@ var TaskRunner = class {
|
|
|
7464
8623
|
case "task.offer":
|
|
7465
8624
|
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7466
8625
|
return;
|
|
8626
|
+
case "task.offer_with_toolsets":
|
|
8627
|
+
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
8628
|
+
return;
|
|
7467
8629
|
case "task.cancel":
|
|
7468
8630
|
await this.handleCancel(envelope.task_id, envelope.payload.reason);
|
|
7469
8631
|
return;
|
|
@@ -7517,16 +8679,49 @@ var TaskRunner = class {
|
|
|
7517
8679
|
);
|
|
7518
8680
|
return;
|
|
7519
8681
|
}
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
8682
|
+
if (payload.dispatchSelection !== void 0 && payload.runtime !== void 0 && payload.runtime !== payload.dispatchSelection.runtimeId) {
|
|
8683
|
+
this.decline(
|
|
8684
|
+
taskId,
|
|
8685
|
+
`offer runtime ${payload.runtime} does not match dispatchSelection.runtimeId ${payload.dispatchSelection.runtimeId}`,
|
|
8686
|
+
false
|
|
8687
|
+
);
|
|
8688
|
+
return;
|
|
8689
|
+
}
|
|
8690
|
+
const requiredToolsets = "requiredToolsets" in payload ? payload.requiredToolsets : void 0;
|
|
8691
|
+
const resolvedMcp = requiredToolsets ? this.resolveMcpServers(requiredToolsets) : void 0;
|
|
8692
|
+
if (resolvedMcp && !resolvedMcp.ok) {
|
|
8693
|
+
this.decline(taskId, resolvedMcp.reason, true);
|
|
8694
|
+
return;
|
|
8695
|
+
}
|
|
7525
8696
|
const decision = computeEffectivePolicy(payload.policy, this.deps.permissionDefaults);
|
|
7526
8697
|
if (!decision.ok) {
|
|
7527
8698
|
this.decline(taskId, decision.reason ?? "policy rejected", false);
|
|
7528
8699
|
return;
|
|
7529
8700
|
}
|
|
8701
|
+
const offered = withoutRequiredToolsets(payload);
|
|
8702
|
+
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
8703
|
+
const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
|
|
8704
|
+
if (!pick.ok) {
|
|
8705
|
+
this.decline(taskId, pick.reason, pick.retryable);
|
|
8706
|
+
return;
|
|
8707
|
+
}
|
|
8708
|
+
let prepared;
|
|
8709
|
+
try {
|
|
8710
|
+
prepared = await pick.adapter.prepare({
|
|
8711
|
+
offer: offered,
|
|
8712
|
+
policy: decision.policy,
|
|
8713
|
+
descriptor: pick.descriptor,
|
|
8714
|
+
requiredToolsetIds: requiredToolsets ?? [],
|
|
8715
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {}
|
|
8716
|
+
});
|
|
8717
|
+
} catch (error) {
|
|
8718
|
+
this.decline(taskId, `runtime preparation failed: ${errorMessage4(error)}`, true);
|
|
8719
|
+
return;
|
|
8720
|
+
}
|
|
8721
|
+
if (prepared.kind === "reject") {
|
|
8722
|
+
this.decline(taskId, prepared.reason, prepared.retryable);
|
|
8723
|
+
return;
|
|
8724
|
+
}
|
|
7530
8725
|
let known = void 0;
|
|
7531
8726
|
let workspaceDir;
|
|
7532
8727
|
let gitLease;
|
|
@@ -7559,6 +8754,7 @@ var TaskRunner = class {
|
|
|
7559
8754
|
}
|
|
7560
8755
|
} else {
|
|
7561
8756
|
workspaceDir = path20.join(this.deps.workspaceRoot, taskId);
|
|
8757
|
+
gitWorkspaceId = randomUUID();
|
|
7562
8758
|
}
|
|
7563
8759
|
try {
|
|
7564
8760
|
gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
|
|
@@ -7574,6 +8770,26 @@ var TaskRunner = class {
|
|
|
7574
8770
|
this.decline(taskId, "workspace mode is unavailable", true);
|
|
7575
8771
|
return;
|
|
7576
8772
|
}
|
|
8773
|
+
const env = buildRuntimeEnv({
|
|
8774
|
+
ambient: process.env,
|
|
8775
|
+
requirements: pick.descriptor.environmentRequirements,
|
|
8776
|
+
locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.descriptor.id]?.allow
|
|
8777
|
+
});
|
|
8778
|
+
const manifest = sealRuntimeOperationManifest({
|
|
8779
|
+
taskId,
|
|
8780
|
+
runtimeId: pick.descriptor.id,
|
|
8781
|
+
descriptor: pick.descriptor,
|
|
8782
|
+
policy: decision.policy,
|
|
8783
|
+
requiredToolsetIds: requiredToolsets ?? [],
|
|
8784
|
+
...offered.dispatchSelection === void 0 ? {} : { dispatchSelection: offered.dispatchSelection },
|
|
8785
|
+
...known === void 0 || payload.sessionRef === void 0 ? {} : { sessionRef: payload.sessionRef },
|
|
8786
|
+
workspace: {
|
|
8787
|
+
workspaceDir,
|
|
8788
|
+
...gitWorkspaceId === void 0 ? {} : { workspaceId: gitWorkspaceId },
|
|
8789
|
+
...gitBaseline === void 0 ? {} : { baseline: gitBaseline }
|
|
8790
|
+
},
|
|
8791
|
+
forwardedEnvironmentNames: Object.freeze(Object.keys(env).sort())
|
|
8792
|
+
});
|
|
7577
8793
|
this.deps.send(
|
|
7578
8794
|
createEnvelope(
|
|
7579
8795
|
"task.claim",
|
|
@@ -7589,7 +8805,7 @@ var TaskRunner = class {
|
|
|
7589
8805
|
// (the merely REQUESTED runtime): this is what closes the gap
|
|
7590
8806
|
// where an auto-selected task left the server never learning
|
|
7591
8807
|
// which runtime actually ran.
|
|
7592
|
-
runtime: isKnownRuntimeId(
|
|
8808
|
+
runtime: isKnownRuntimeId(manifest.descriptor.id) ? manifest.descriptor.id : void 0,
|
|
7593
8809
|
// S0/D-4 (`task.claim.capabilities`, docs/protocol.md §2.4): the
|
|
7594
8810
|
// selected adapter's own capability self-report, carried on the
|
|
7595
8811
|
// same message that establishes the task↔runtime binding. The
|
|
@@ -7606,7 +8822,7 @@ var TaskRunner = class {
|
|
|
7606
8822
|
// Gating them would silently strip a custom steer-capable
|
|
7607
8823
|
// adapter's own truth and leave the server fail-closing on it
|
|
7608
8824
|
// forever.
|
|
7609
|
-
capabilities: toRuntimeInfoCapabilities(
|
|
8825
|
+
capabilities: toRuntimeInfoCapabilities(manifest.descriptor.capabilities)
|
|
7610
8826
|
},
|
|
7611
8827
|
{ taskId }
|
|
7612
8828
|
)
|
|
@@ -7617,7 +8833,7 @@ var TaskRunner = class {
|
|
|
7617
8833
|
if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
|
|
7618
8834
|
} catch (err) {
|
|
7619
8835
|
gitLease?.release();
|
|
7620
|
-
await this.fail(taskId, `failed to resolve instruction blob: ${
|
|
8836
|
+
await this.fail(taskId, `failed to resolve instruction blob: ${errorMessage4(err)}`, true);
|
|
7621
8837
|
return;
|
|
7622
8838
|
}
|
|
7623
8839
|
if (this.deps.gitWorkspaceManager && gitLease) {
|
|
@@ -7626,7 +8842,7 @@ var TaskRunner = class {
|
|
|
7626
8842
|
if (gitExisting) {
|
|
7627
8843
|
observation = await this.deps.gitWorkspaceManager.validateExisting(workspaceDir);
|
|
7628
8844
|
} else {
|
|
7629
|
-
const workspaceId2 = randomUUID();
|
|
8845
|
+
const workspaceId2 = gitWorkspaceId ?? randomUUID();
|
|
7630
8846
|
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
7631
8847
|
gitWorkspaceId = workspaceId2;
|
|
7632
8848
|
await this.deps.gitWorkspaceStore?.upsert({
|
|
@@ -7677,29 +8893,11 @@ var TaskRunner = class {
|
|
|
7677
8893
|
return;
|
|
7678
8894
|
}
|
|
7679
8895
|
}
|
|
7680
|
-
const
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
// module doc comment for the credential-leak gap that closed) —
|
|
7686
|
-
// built fresh per task from the SPECIFIC adapter `pickAdapter`
|
|
7687
|
-
// above already selected, so this always runs after adapter
|
|
7688
|
-
// selection: `pick.adapter.environmentRequirements?.()` (undefined
|
|
7689
|
-
// ⇒ platform baseline only, fail-closed) plus this device's own
|
|
7690
|
-
// `runtimeEnvironment` override, keyed by that same adapter's `id`.
|
|
7691
|
-
env: buildRuntimeEnv({
|
|
7692
|
-
ambient: process.env,
|
|
7693
|
-
requirements: pick.adapter.environmentRequirements?.(),
|
|
7694
|
-
locallyAllowedNames: this.deps.runtimeEnvironment?.[pick.adapter.id]?.allow
|
|
7695
|
-
}),
|
|
7696
|
-
// M4 Phase 3: adapter-agnostic and cheap to always populate — only an
|
|
7697
|
-
// adapter whose runtime genuinely supports an out-of-band approval
|
|
7698
|
-
// pause (claude, today) ever reads this. `resolve` is a closure over
|
|
7699
|
-
// `taskId` (not a pre-bound approvalId): it looks up whichever
|
|
7700
|
-
// approval is CURRENTLY pending for this task at call time, since one
|
|
7701
|
-
// task/session can face several approval requests, one at a time,
|
|
7702
|
-
// over its life. See `types.ts`'s `ApprovalChannel` doc comment.
|
|
8896
|
+
const startInput = {
|
|
8897
|
+
manifest,
|
|
8898
|
+
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
8899
|
+
env,
|
|
8900
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
|
|
7703
8901
|
approvalChannel: {
|
|
7704
8902
|
taskId,
|
|
7705
8903
|
storeDir: this.deps.storeDir,
|
|
@@ -7715,45 +8913,19 @@ var TaskRunner = class {
|
|
|
7715
8913
|
}
|
|
7716
8914
|
}
|
|
7717
8915
|
};
|
|
7718
|
-
const effectiveOffer = {
|
|
7719
|
-
...payload,
|
|
7720
|
-
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
7721
|
-
// Never forward a sessionRef this device has no recorded workspace
|
|
7722
|
-
// for (stale, from another device, or simply made up) — an adapter
|
|
7723
|
-
// that tries to resume an id it never minted fails outright (pi:
|
|
7724
|
-
// "No session found matching '<id>'", exit 1, empirically confirmed)
|
|
7725
|
-
// instead of silently starting fresh, so an unresolvable sessionRef
|
|
7726
|
-
// must look identical to "none supplied" by the time it reaches the
|
|
7727
|
-
// adapter, not get forwarded as a resume attempt doomed to fail.
|
|
7728
|
-
sessionRef: known ? payload.sessionRef : void 0
|
|
7729
|
-
};
|
|
7730
8916
|
let session;
|
|
7731
8917
|
try {
|
|
7732
|
-
session = await
|
|
8918
|
+
session = await prepared.operation.start(startInput);
|
|
7733
8919
|
} catch (err) {
|
|
7734
|
-
const
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
await this.fail(taskId, `adapter failed to start: ${errorMessage3(err)}`, retryable);
|
|
7738
|
-
return;
|
|
7739
|
-
}
|
|
7740
|
-
if (this.pendingCancelled.has(taskId)) {
|
|
7741
|
-
const reason = this.pendingCancelled.get(taskId);
|
|
7742
|
-
this.pendingCancelled.delete(taskId);
|
|
7743
|
-
try {
|
|
7744
|
-
await session.interrupt();
|
|
7745
|
-
} catch {
|
|
7746
|
-
}
|
|
7747
|
-
try {
|
|
7748
|
-
await session.close();
|
|
7749
|
-
} catch {
|
|
8920
|
+
const failure = projectRuntimeBoundaryFailure(err, "start");
|
|
8921
|
+
if (failure.contractViolation) {
|
|
8922
|
+
console.error("[byok/client] runtime adapter start() returned an untyped failure", err);
|
|
7750
8923
|
}
|
|
8924
|
+
await this.updateGitPhaseBestEffort(gitWorkspaceId, "failed", "repository-invalid");
|
|
7751
8925
|
gitLease?.release();
|
|
7752
|
-
await this.
|
|
7753
|
-
this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
|
|
8926
|
+
await this.fail(taskId, failure.reason, failure.retryable);
|
|
7754
8927
|
return;
|
|
7755
8928
|
}
|
|
7756
|
-
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
7757
8929
|
const active = {
|
|
7758
8930
|
taskId,
|
|
7759
8931
|
adapter: pick.adapter,
|
|
@@ -7770,6 +8942,21 @@ var TaskRunner = class {
|
|
|
7770
8942
|
approvalQueue: [],
|
|
7771
8943
|
outputBytesSoFar: 0
|
|
7772
8944
|
};
|
|
8945
|
+
if (this.pendingCancelled.has(taskId)) {
|
|
8946
|
+
const reason = this.pendingCancelled.get(taskId);
|
|
8947
|
+
this.pendingCancelled.delete(taskId);
|
|
8948
|
+
this.tasks.set(taskId, active);
|
|
8949
|
+
this.reserveSemanticTerminal(active);
|
|
8950
|
+
try {
|
|
8951
|
+
await session.interrupt();
|
|
8952
|
+
} catch {
|
|
8953
|
+
}
|
|
8954
|
+
await this.updateGitPhaseBestEffort(gitWorkspaceId, "cancelled");
|
|
8955
|
+
this.deps.send(createEnvelope("task.cancelled", { reason }, { taskId }));
|
|
8956
|
+
await this.finish(taskId);
|
|
8957
|
+
return;
|
|
8958
|
+
}
|
|
8959
|
+
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
7773
8960
|
this.tasks.set(taskId, active);
|
|
7774
8961
|
if (payload.limits?.maxDurationMs !== void 0) {
|
|
7775
8962
|
this.armMaxDurationTimer(active, payload.limits.maxDurationMs);
|
|
@@ -7794,9 +8981,40 @@ var TaskRunner = class {
|
|
|
7794
8981
|
if (typeof instruction === "string") return instruction;
|
|
7795
8982
|
return this.deps.blobClient.resolveInstruction(instruction.blobRef);
|
|
7796
8983
|
}
|
|
8984
|
+
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
8985
|
+
resolveMcpServers(requiredToolsets) {
|
|
8986
|
+
const registry = this.deps.mcpToolsets;
|
|
8987
|
+
if (!registry) {
|
|
8988
|
+
return { ok: false, reason: "offer requires MCP toolsets, but this device has no local mcpToolsets registry" };
|
|
8989
|
+
}
|
|
8990
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
8991
|
+
for (const toolsetId of requiredToolsets) {
|
|
8992
|
+
const toolset = registry.get(toolsetId);
|
|
8993
|
+
if (!toolset) {
|
|
8994
|
+
return { ok: false, reason: `required MCP toolset "${toolsetId}" is not configured on this device` };
|
|
8995
|
+
}
|
|
8996
|
+
for (const [serverName, server] of Object.entries(toolset.mcpServers)) {
|
|
8997
|
+
if (Object.prototype.hasOwnProperty.call(servers, serverName)) {
|
|
8998
|
+
return {
|
|
8999
|
+
ok: false,
|
|
9000
|
+
reason: `required MCP toolsets collide on server name "${serverName}"; refusing ambiguous projection`
|
|
9001
|
+
};
|
|
9002
|
+
}
|
|
9003
|
+
servers[serverName] = Object.freeze({
|
|
9004
|
+
command: server.command,
|
|
9005
|
+
...server.args ? { args: Object.freeze([...server.args]) } : {}
|
|
9006
|
+
});
|
|
9007
|
+
}
|
|
9008
|
+
}
|
|
9009
|
+
if (Object.keys(servers).length === 0) {
|
|
9010
|
+
return { ok: false, reason: "required MCP toolsets resolved to no servers; refusing to run without tools" };
|
|
9011
|
+
}
|
|
9012
|
+
return { ok: true, servers: Object.freeze(servers) };
|
|
9013
|
+
}
|
|
7797
9014
|
async pump(active) {
|
|
7798
9015
|
try {
|
|
7799
9016
|
for await (const event of active.session.events) {
|
|
9017
|
+
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
7800
9018
|
if (this.tasks.get(active.taskId) !== active) return;
|
|
7801
9019
|
active.outputBytesSoFar += estimateEventBytes(event);
|
|
7802
9020
|
if (active.outputBytesSoFar > this.maxTaskOutputBytes) {
|
|
@@ -7819,7 +9037,7 @@ var TaskRunner = class {
|
|
|
7819
9037
|
try {
|
|
7820
9038
|
await active.session.resolveApproval(approved, reason);
|
|
7821
9039
|
} catch (err) {
|
|
7822
|
-
await this.fail(taskId, `failed to resume session after approval decision: ${
|
|
9040
|
+
await this.fail(taskId, `failed to resume session after approval decision: ${errorMessage4(err)}`, false);
|
|
7823
9041
|
}
|
|
7824
9042
|
});
|
|
7825
9043
|
continue;
|
|
@@ -7827,11 +9045,40 @@ var TaskRunner = class {
|
|
|
7827
9045
|
if (event.type === "turn_end") {
|
|
7828
9046
|
active.batcher.push(event);
|
|
7829
9047
|
active.batcher.flush();
|
|
9048
|
+
const finalOutput = active.summaryParts.join("");
|
|
9049
|
+
const outcome = await this.resolveResultDocument(active, finalOutput);
|
|
9050
|
+
if (!outcome.deliver) return;
|
|
7830
9051
|
await this.observeGit(active, "completed");
|
|
9052
|
+
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
9053
|
+
if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
|
|
9054
|
+
await this.fail(
|
|
9055
|
+
active.taskId,
|
|
9056
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server stopped advertising the result-document capability before this completion could be sent (a reconnect to an older server), so it would silently discard this document`,
|
|
9057
|
+
false
|
|
9058
|
+
);
|
|
9059
|
+
return;
|
|
9060
|
+
}
|
|
9061
|
+
if (!this.reserveSemanticTerminal(active)) return;
|
|
7831
9062
|
this.deps.send(
|
|
7832
9063
|
createEnvelope(
|
|
7833
9064
|
"task.complete",
|
|
7834
|
-
{
|
|
9065
|
+
{
|
|
9066
|
+
summary: finalOutput,
|
|
9067
|
+
sessionRef: active.session.sessionRef,
|
|
9068
|
+
// Spread rather than `document: outcome.document`, so a
|
|
9069
|
+
// completion with no document is the exact same payload it
|
|
9070
|
+
// was before this field existed — not one carrying an
|
|
9071
|
+
// explicit `document: undefined` key.
|
|
9072
|
+
//
|
|
9073
|
+
// `outcome.document` is the protocol's CANONICAL SNAPSHOT
|
|
9074
|
+
// (`checkResultDocument`), never the object the extractor
|
|
9075
|
+
// returned: pure data serializes identically at the root
|
|
9076
|
+
// (where it was measured) and nested inside this payload
|
|
9077
|
+
// (where the codec actually serializes it), so a contextual
|
|
9078
|
+
// `toJSON(key)` or an unstable getter cannot make the wire
|
|
9079
|
+
// bytes differ from what the cap gate approved.
|
|
9080
|
+
...outcome.document !== void 0 ? { document: outcome.document } : {}
|
|
9081
|
+
},
|
|
7835
9082
|
{ taskId: active.taskId, sessionRef: active.session.sessionRef }
|
|
7836
9083
|
)
|
|
7837
9084
|
);
|
|
@@ -7847,11 +9094,17 @@ var TaskRunner = class {
|
|
|
7847
9094
|
active.batcher.push(event);
|
|
7848
9095
|
}
|
|
7849
9096
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
7850
|
-
|
|
9097
|
+
const failure = projectRuntimeBoundaryFailure(void 0, "run");
|
|
9098
|
+
console.error("[byok/client] runtime adapter events iterable ended without terminal authority");
|
|
9099
|
+
await this.fail(active.taskId, failure.reason, failure.retryable);
|
|
7851
9100
|
} catch (err) {
|
|
7852
9101
|
if (this.tasks.get(active.taskId) !== active || active.beingTornDown) return;
|
|
7853
9102
|
active.batcher.flush();
|
|
7854
|
-
|
|
9103
|
+
const failure = projectRuntimeBoundaryFailure(err, "run");
|
|
9104
|
+
if (failure.contractViolation) {
|
|
9105
|
+
console.error("[byok/client] runtime adapter events iterable returned an untyped failure", err);
|
|
9106
|
+
}
|
|
9107
|
+
await this.fail(active.taskId, failure.reason, failure.retryable);
|
|
7855
9108
|
}
|
|
7856
9109
|
}
|
|
7857
9110
|
/**
|
|
@@ -7890,7 +9143,7 @@ var TaskRunner = class {
|
|
|
7890
9143
|
try {
|
|
7891
9144
|
bytes = await opened.handle.readFile();
|
|
7892
9145
|
} catch (err) {
|
|
7893
|
-
this.reportArtifactError(active, name, `failed to read artifact "${name}": ${
|
|
9146
|
+
this.reportArtifactError(active, name, `failed to read artifact "${name}": ${errorMessage4(err)}`);
|
|
7894
9147
|
return;
|
|
7895
9148
|
} finally {
|
|
7896
9149
|
await opened.handle.close().catch(() => {
|
|
@@ -7907,7 +9160,7 @@ var TaskRunner = class {
|
|
|
7907
9160
|
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType);
|
|
7908
9161
|
this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
|
|
7909
9162
|
} catch (err) {
|
|
7910
|
-
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${
|
|
9163
|
+
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage4(err)}`);
|
|
7911
9164
|
}
|
|
7912
9165
|
}
|
|
7913
9166
|
/** Loud, non-silent artifact failure (finding F7): logged, and folded into this task's own progress stream as an `error` AgentEvent rather than swallowed — the task itself can still complete normally, but the omission is now visible. */
|
|
@@ -7921,6 +9174,14 @@ var TaskRunner = class {
|
|
|
7921
9174
|
this.setPendingCancelled(taskId, reason);
|
|
7922
9175
|
return;
|
|
7923
9176
|
}
|
|
9177
|
+
if (active.finalizationStarted) {
|
|
9178
|
+
await this.finish(taskId);
|
|
9179
|
+
return;
|
|
9180
|
+
}
|
|
9181
|
+
if (!this.reserveSemanticTerminal(active)) {
|
|
9182
|
+
await active.semanticTerminalSettled;
|
|
9183
|
+
return;
|
|
9184
|
+
}
|
|
7924
9185
|
try {
|
|
7925
9186
|
await active.session.interrupt();
|
|
7926
9187
|
} catch {
|
|
@@ -7949,7 +9210,7 @@ var TaskRunner = class {
|
|
|
7949
9210
|
*
|
|
7950
9211
|
* `inFlightOffers` is naturally tiny (bounded by this device's real
|
|
7951
9212
|
* concurrent-offer-processing count — normally single digits, driven by
|
|
7952
|
-
* how many `task.offer`s are simultaneously mid
|
|
9213
|
+
* how many `task.offer`s are simultaneously mid-prepared-operation start() — nowhere
|
|
7953
9214
|
* near `MAX_TRACKED_TASK_IDS`), so this scan is cheap in practice: it
|
|
7954
9215
|
* finds a safe entry at or near the front almost always. The only case
|
|
7955
9216
|
* where NO entry is safe to evict is every single tracked cancel
|
|
@@ -8302,7 +9563,7 @@ var TaskRunner = class {
|
|
|
8302
9563
|
this.deps.onStaleApprovalDecision?.(taskId, "approve");
|
|
8303
9564
|
return;
|
|
8304
9565
|
}
|
|
8305
|
-
await this.fail(taskId, `failed to resume session after approval: ${
|
|
9566
|
+
await this.fail(taskId, `failed to resume session after approval: ${errorMessage4(err)}`, false);
|
|
8306
9567
|
return;
|
|
8307
9568
|
}
|
|
8308
9569
|
this.clearPendingApproval(resolvedId, "approve", void 0);
|
|
@@ -8333,6 +9594,10 @@ var TaskRunner = class {
|
|
|
8333
9594
|
async handleReject(taskId, reason, approvalId) {
|
|
8334
9595
|
const active = this.tasks.get(taskId);
|
|
8335
9596
|
if (!active) return;
|
|
9597
|
+
if (active.finalizationStarted) {
|
|
9598
|
+
await this.finish(taskId);
|
|
9599
|
+
return;
|
|
9600
|
+
}
|
|
8336
9601
|
if (approvalId !== void 0 && approvalId !== active.pendingApprovalId) {
|
|
8337
9602
|
this.deps.onStaleApprovalDecision?.(
|
|
8338
9603
|
taskId,
|
|
@@ -8351,6 +9616,10 @@ var TaskRunner = class {
|
|
|
8351
9616
|
}
|
|
8352
9617
|
}
|
|
8353
9618
|
this.clearPendingApproval(resolvedId, "reject", reason);
|
|
9619
|
+
if (!this.reserveSemanticTerminal(active)) {
|
|
9620
|
+
await active.semanticTerminalSettled;
|
|
9621
|
+
return;
|
|
9622
|
+
}
|
|
8354
9623
|
try {
|
|
8355
9624
|
await active.session.interrupt();
|
|
8356
9625
|
} catch {
|
|
@@ -8365,10 +9634,114 @@ var TaskRunner = class {
|
|
|
8365
9634
|
}
|
|
8366
9635
|
async fail(taskId, reason, retryable) {
|
|
8367
9636
|
const active = this.tasks.get(taskId);
|
|
9637
|
+
if (active?.finalizationStarted) {
|
|
9638
|
+
await this.finish(taskId);
|
|
9639
|
+
return;
|
|
9640
|
+
}
|
|
9641
|
+
if (active && !this.reserveSemanticTerminal(active)) {
|
|
9642
|
+
await active.semanticTerminalSettled;
|
|
9643
|
+
return;
|
|
9644
|
+
}
|
|
8368
9645
|
if (active) await this.observeGit(active, "salvage");
|
|
8369
9646
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
|
|
8370
9647
|
await this.finish(taskId);
|
|
8371
9648
|
}
|
|
9649
|
+
/**
|
|
9650
|
+
* additive-minor (`task.complete.document`): the whole daemon-side gate
|
|
9651
|
+
* between a configured {@link ResultDocumentExtractor} and the wire —
|
|
9652
|
+
* called once, from the `turn_end` completion path, immediately before
|
|
9653
|
+
* `task.complete` is built.
|
|
9654
|
+
*
|
|
9655
|
+
* `{deliver: true}` means "go on and send `task.complete`", carrying the
|
|
9656
|
+
* document when there is one. `{deliver: false}` means this method has
|
|
9657
|
+
* ALREADY reported `task.fail` and finished the task; the caller must
|
|
9658
|
+
* return without sending anything further.
|
|
9659
|
+
*
|
|
9660
|
+
* Four fail-closed branches, all `retryable: false` (see
|
|
9661
|
+
* {@link RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX} for why none of them
|
|
9662
|
+
* can succeed on a retry):
|
|
9663
|
+
*
|
|
9664
|
+
* 1. The extractor threw — its error is surfaced, never swallowed.
|
|
9665
|
+
* 2. The extractor returned a thenable, violating the synchronous
|
|
9666
|
+
* contract in the one way that would otherwise ship a wrong answer.
|
|
9667
|
+
* 3. The document is over the cap, not JSON-serializable, or not plain
|
|
9668
|
+
* JSON data, per `checkResultDocument` — the protocol's OWN check,
|
|
9669
|
+
* imported rather than reimplemented, so this gate and the server's
|
|
9670
|
+
* schema validation can never disagree about what is legal.
|
|
9671
|
+
* 4. The connected server never advertised `result-document`. Its
|
|
9672
|
+
* tolerant `z.object()` would silently strip the field on arrival
|
|
9673
|
+
* (`version.ts`'s own flag doc comment), so "send anyway" is not a
|
|
9674
|
+
* degraded-but-working path — it is the task's primary structured
|
|
9675
|
+
* result being deleted in transit with nothing reported anywhere.
|
|
9676
|
+
*
|
|
9677
|
+
* The capability is checked LAST, deliberately: a document that is itself
|
|
9678
|
+
* invalid is the host's own bug and is worth reporting as such even when
|
|
9679
|
+
* the connected server could not have accepted any document at all. It is
|
|
9680
|
+
* then re-checked once more by the caller after its own last await, since
|
|
9681
|
+
* a reconnect can invalidate this answer in between (F3).
|
|
9682
|
+
*
|
|
9683
|
+
* **Residual window (bounded, deliberately not hacked around).** Even the
|
|
9684
|
+
* caller's re-check happens before `ConnectionManager.send` hands the
|
|
9685
|
+
* envelope to a transport, and a queued envelope can outlive the
|
|
9686
|
+
* connection it was queued for: a reconnect between `send()` and the
|
|
9687
|
+
* outbox actually draining could still deliver this `task.complete` to a
|
|
9688
|
+
* rolled-back N-1 server that strips the document. Closing that would
|
|
9689
|
+
* mean teaching the transport outbox to inspect payload semantics and
|
|
9690
|
+
* mint a substitute `task.fail` for a task this runner already finished —
|
|
9691
|
+
* a second authority over terminal outcomes living in the queue, which is
|
|
9692
|
+
* worse than the window it closes. Documented instead, here and in
|
|
9693
|
+
* docs/protocol.md §7.2.
|
|
9694
|
+
*/
|
|
9695
|
+
async resolveResultDocument(active, finalOutput) {
|
|
9696
|
+
const extract = this.deps.resultDocument?.extract;
|
|
9697
|
+
if (!extract) return { deliver: true };
|
|
9698
|
+
let document;
|
|
9699
|
+
try {
|
|
9700
|
+
document = extract(finalOutput, { taskId: active.taskId, sessionRef: active.session.sessionRef });
|
|
9701
|
+
} catch (err) {
|
|
9702
|
+
await this.fail(
|
|
9703
|
+
active.taskId,
|
|
9704
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage4(err)}`,
|
|
9705
|
+
false
|
|
9706
|
+
);
|
|
9707
|
+
return { deliver: false };
|
|
9708
|
+
}
|
|
9709
|
+
if (typeof document?.then === "function") {
|
|
9710
|
+
await this.fail(
|
|
9711
|
+
active.taskId,
|
|
9712
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract returned a promise; the contract is synchronous (an awaited value is never read, and a promise encodes to an empty document)`,
|
|
9713
|
+
false
|
|
9714
|
+
);
|
|
9715
|
+
return { deliver: false };
|
|
9716
|
+
}
|
|
9717
|
+
if (document === void 0) return { deliver: true };
|
|
9718
|
+
const check = checkResultDocument(document);
|
|
9719
|
+
if (!check.ok) {
|
|
9720
|
+
const detail = resultDocumentRejectionDetail(check);
|
|
9721
|
+
await this.fail(active.taskId, `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: ${detail}`, false);
|
|
9722
|
+
return { deliver: false };
|
|
9723
|
+
}
|
|
9724
|
+
if (!this.hasResultDocumentCapability()) {
|
|
9725
|
+
await this.fail(
|
|
9726
|
+
active.taskId,
|
|
9727
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server did not advertise the result-document capability, so it would silently discard this ${check.bytes}-byte document`,
|
|
9728
|
+
false
|
|
9729
|
+
);
|
|
9730
|
+
return { deliver: false };
|
|
9731
|
+
}
|
|
9732
|
+
return { deliver: true, document: check.canonical };
|
|
9733
|
+
}
|
|
9734
|
+
/**
|
|
9735
|
+
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
9736
|
+
* read fresh on every call, never captured, because the answer changes
|
|
9737
|
+
* across a reconnect (`ConnectionManager.getServerCapabilities` returns
|
|
9738
|
+
* `[]` from the moment an acked connection closes until a fresh
|
|
9739
|
+
* `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
|
|
9740
|
+
* "no capabilities", the fail-closed reading.
|
|
9741
|
+
*/
|
|
9742
|
+
hasResultDocumentCapability() {
|
|
9743
|
+
return (this.deps.getServerCapabilities?.() ?? []).includes("result-document");
|
|
9744
|
+
}
|
|
8372
9745
|
async observeGit(active, phase) {
|
|
8373
9746
|
if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
|
|
8374
9747
|
try {
|
|
@@ -8403,32 +9776,64 @@ var TaskRunner = class {
|
|
|
8403
9776
|
}
|
|
8404
9777
|
async finish(taskId) {
|
|
8405
9778
|
const active = this.tasks.get(taskId);
|
|
8406
|
-
if (!active) return;
|
|
8407
|
-
if (active.
|
|
8408
|
-
|
|
8409
|
-
active.
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
9779
|
+
if (!active) return true;
|
|
9780
|
+
if (!active.finalizationStarted) {
|
|
9781
|
+
active.finalizationStarted = true;
|
|
9782
|
+
active.beingTornDown = true;
|
|
9783
|
+
if (active.maxDurationTimer) {
|
|
9784
|
+
clearTimeout(active.maxDurationTimer);
|
|
9785
|
+
active.maxDurationTimer = void 0;
|
|
9786
|
+
}
|
|
9787
|
+
active.batcher.stop();
|
|
9788
|
+
this.addFinishedTaskId(taskId);
|
|
9789
|
+
const queued = active.approvalQueue.splice(0);
|
|
9790
|
+
for (const request of queued) {
|
|
9791
|
+
request.resolve({
|
|
9792
|
+
approved: false,
|
|
9793
|
+
reason: `task ${taskId} finished before this queued approval request could be dispatched`
|
|
9794
|
+
});
|
|
9795
|
+
}
|
|
9796
|
+
if (active.pendingApprovalId !== void 0) {
|
|
9797
|
+
try {
|
|
9798
|
+
this.deps.approvalRegistry.resolve(active.pendingApprovalId, "reject", `task ${taskId} finished`);
|
|
9799
|
+
} catch {
|
|
9800
|
+
}
|
|
8425
9801
|
}
|
|
8426
9802
|
}
|
|
8427
|
-
|
|
9803
|
+
const attempt = active.disposalAttempt ?? active.session.close();
|
|
9804
|
+
active.disposalAttempt = attempt;
|
|
8428
9805
|
try {
|
|
8429
|
-
await
|
|
8430
|
-
} catch {
|
|
9806
|
+
await attempt;
|
|
9807
|
+
} catch (caught) {
|
|
9808
|
+
if (active.disposalAttempt === attempt) active.disposalAttempt = void 0;
|
|
9809
|
+
const failure = isRuntimeDisposalFailure(caught) ? caught : new RuntimeDisposalFailure({
|
|
9810
|
+
stage: "quiescence",
|
|
9811
|
+
reason: `${active.adapter.descriptor.id} session.close() returned an untyped disposal failure`
|
|
9812
|
+
}, { cause: caught });
|
|
9813
|
+
console.error(`[byok/client] runtime disposal failed for task ${taskId}: ${failure.message}`);
|
|
9814
|
+
this.deps.onRuntimeDisposalFailure?.({
|
|
9815
|
+
taskId,
|
|
9816
|
+
runtimeId: active.adapter.descriptor.id,
|
|
9817
|
+
stage: failure.stage,
|
|
9818
|
+
reason: failure.message
|
|
9819
|
+
});
|
|
9820
|
+
active.resolveSemanticTerminalSettled?.(false);
|
|
9821
|
+
return false;
|
|
8431
9822
|
}
|
|
9823
|
+
if (this.tasks.get(taskId) !== active) return true;
|
|
9824
|
+
active.gitLease?.release();
|
|
9825
|
+
this.tasks.delete(taskId);
|
|
9826
|
+
active.resolveSemanticTerminalSettled?.(true);
|
|
9827
|
+
return true;
|
|
9828
|
+
}
|
|
9829
|
+
reserveSemanticTerminal(active) {
|
|
9830
|
+
if (active.semanticTerminalReserved || active.finalizationStarted) return false;
|
|
9831
|
+
active.semanticTerminalReserved = true;
|
|
9832
|
+
active.beingTornDown = true;
|
|
9833
|
+
active.semanticTerminalSettled = new Promise((resolve) => {
|
|
9834
|
+
active.resolveSemanticTerminalSettled = resolve;
|
|
9835
|
+
});
|
|
9836
|
+
return true;
|
|
8432
9837
|
}
|
|
8433
9838
|
/** M3-B: bounded insert for `finishedTaskIds` — see its class-level doc comment and `MAX_TRACKED_TASK_IDS`. Evicts the oldest (first-inserted) entry once over cap, same idiom as `ConnectionHub.checkAndRecordDuplicate` (packages/server/src/hub.ts). */
|
|
8434
9839
|
addFinishedTaskId(taskId) {
|
|
@@ -8471,7 +9876,7 @@ var TaskRunner = class {
|
|
|
8471
9876
|
* is device-specific (which runtimes happen to be installed here), so a
|
|
8472
9877
|
* different device's installed runtime set might satisfy it.
|
|
8473
9878
|
*/
|
|
8474
|
-
async pickAdapter(requestedRuntime, policyMode) {
|
|
9879
|
+
async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
|
|
8475
9880
|
const allowlist = this.deps.runtimeAllowlist;
|
|
8476
9881
|
if (requestedRuntime) {
|
|
8477
9882
|
if (allowlist && !allowlist.includes(requestedRuntime)) {
|
|
@@ -8481,17 +9886,25 @@ var TaskRunner = class {
|
|
|
8481
9886
|
retryable: false
|
|
8482
9887
|
};
|
|
8483
9888
|
}
|
|
8484
|
-
const adapter = this.deps.adapters.find((a) => a.id === requestedRuntime);
|
|
9889
|
+
const adapter = this.deps.adapters.find((a) => a.descriptor.id === requestedRuntime);
|
|
8485
9890
|
if (!adapter) {
|
|
8486
9891
|
return { ok: false, reason: `unknown runtime "${requestedRuntime}"`, retryable: false };
|
|
8487
9892
|
}
|
|
8488
|
-
|
|
9893
|
+
const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
|
|
9894
|
+
if (!adapterSupportsMode(descriptor, policyMode)) {
|
|
8489
9895
|
return {
|
|
8490
9896
|
ok: false,
|
|
8491
9897
|
reason: `runtime "${requestedRuntime}" cannot express permission mode "${policyMode}"`,
|
|
8492
9898
|
retryable: false
|
|
8493
9899
|
};
|
|
8494
9900
|
}
|
|
9901
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) {
|
|
9902
|
+
return {
|
|
9903
|
+
ok: false,
|
|
9904
|
+
reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
|
|
9905
|
+
retryable: false
|
|
9906
|
+
};
|
|
9907
|
+
}
|
|
8495
9908
|
const detected = await adapter.detect();
|
|
8496
9909
|
if (!detected.present) {
|
|
8497
9910
|
return {
|
|
@@ -8500,18 +9913,20 @@ var TaskRunner = class {
|
|
|
8500
9913
|
retryable: true
|
|
8501
9914
|
};
|
|
8502
9915
|
}
|
|
8503
|
-
return { ok: true, adapter };
|
|
9916
|
+
return { ok: true, adapter, descriptor };
|
|
8504
9917
|
}
|
|
8505
|
-
const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.id)) : this.deps.adapters;
|
|
9918
|
+
const eligible = allowlist ? this.deps.adapters.filter((a) => allowlist.includes(a.descriptor.id)) : this.deps.adapters;
|
|
8506
9919
|
const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
|
|
8507
9920
|
for (const adapter of candidates) {
|
|
8508
|
-
|
|
9921
|
+
const descriptor = freezeRuntimeAdapterDescriptor(adapter.descriptor);
|
|
9922
|
+
if (!adapterSupportsMode(descriptor, policyMode)) continue;
|
|
9923
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(descriptor)) continue;
|
|
8509
9924
|
const detected = await adapter.detect();
|
|
8510
|
-
if (detected.present) return { ok: true, adapter };
|
|
9925
|
+
if (detected.present) return { ok: true, adapter, descriptor };
|
|
8511
9926
|
}
|
|
8512
9927
|
return {
|
|
8513
9928
|
ok: false,
|
|
8514
|
-
reason: `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
9929
|
+
reason: requiresMcpToolsets ? `no available runtime on this device can express permission mode "${policyMode}" with required MCP toolsets` : `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
8515
9930
|
retryable: true
|
|
8516
9931
|
};
|
|
8517
9932
|
}
|
|
@@ -8542,7 +9957,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
|
|
|
8542
9957
|
bytes,
|
|
8543
9958
|
bytesHash: journalHash(bytes),
|
|
8544
9959
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8545
|
-
opensTask: envelope.type === "task.offer"
|
|
9960
|
+
opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets"
|
|
8546
9961
|
};
|
|
8547
9962
|
}
|
|
8548
9963
|
function isRuntimeId(id) {
|
|
@@ -8552,43 +9967,234 @@ async function detectRuntimes(adapters) {
|
|
|
8552
9967
|
const detections = await Promise.all(adapters.map(async (adapter) => ({ adapter, detected: await adapter.detect() })));
|
|
8553
9968
|
const runtimes = [];
|
|
8554
9969
|
for (const { adapter, detected } of detections) {
|
|
8555
|
-
if (!detected.present || !isRuntimeId(adapter.id)) continue;
|
|
8556
|
-
const info = { id: adapter.id };
|
|
9970
|
+
if (!detected.present || !isRuntimeId(adapter.descriptor.id)) continue;
|
|
9971
|
+
const info = { id: adapter.descriptor.id };
|
|
8557
9972
|
if (detected.version !== void 0) info.version = detected.version;
|
|
8558
9973
|
if (detected.authPresent !== void 0) info.authPresent = detected.authPresent;
|
|
8559
|
-
info.capabilities = toRuntimeInfoCapabilities(adapter.capabilities
|
|
9974
|
+
info.capabilities = toRuntimeInfoCapabilities(adapter.descriptor.capabilities);
|
|
8560
9975
|
runtimes.push(info);
|
|
8561
9976
|
}
|
|
8562
9977
|
return runtimes;
|
|
8563
9978
|
}
|
|
8564
9979
|
function computeCapabilities(adapters) {
|
|
8565
9980
|
const flags = [];
|
|
8566
|
-
if (adapters.some((adapter) => adapter.capabilities
|
|
9981
|
+
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
8567
9982
|
flags.push("blob-upload");
|
|
8568
9983
|
flags.push("approval-targeting");
|
|
9984
|
+
const selectionAdapters = adapters.filter(
|
|
9985
|
+
(adapter) => ALL_RUNTIME_IDS.includes(adapter.descriptor.id)
|
|
9986
|
+
);
|
|
9987
|
+
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
9988
|
+
flags.push("dispatch-selection");
|
|
9989
|
+
}
|
|
9990
|
+
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
9991
|
+
flags.push("toolset-selection");
|
|
9992
|
+
}
|
|
8569
9993
|
return flags;
|
|
8570
9994
|
}
|
|
8571
9995
|
var ALL_RUNTIME_IDS = ["pi", "claude", "codex"];
|
|
8572
|
-
function buildAdapter(id) {
|
|
9996
|
+
function buildAdapter(id, config) {
|
|
8573
9997
|
switch (id) {
|
|
8574
9998
|
case "pi":
|
|
8575
|
-
return new PiAdapter();
|
|
9999
|
+
return new PiAdapter({ byokLauncher: config.piByokLauncher });
|
|
8576
10000
|
case "claude":
|
|
8577
10001
|
return new ClaudeAdapter();
|
|
8578
10002
|
case "codex":
|
|
8579
10003
|
return new CodexAdapter();
|
|
8580
10004
|
}
|
|
8581
10005
|
}
|
|
8582
|
-
function buildDefaultAdapters(
|
|
8583
|
-
const ids = runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => runtimeAllowlist
|
|
8584
|
-
return ids.map(buildAdapter);
|
|
10006
|
+
function buildDefaultAdapters(config) {
|
|
10007
|
+
const ids = config.runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => config.runtimeAllowlist?.includes(id)) : ALL_RUNTIME_IDS;
|
|
10008
|
+
return ids.map((id) => buildAdapter(id, config));
|
|
10009
|
+
}
|
|
10010
|
+
function validatePiByokLauncherConfig(launcher) {
|
|
10011
|
+
for (const [field, value] of [
|
|
10012
|
+
["command", launcher.command],
|
|
10013
|
+
["profileDbPath", launcher.profileDbPath],
|
|
10014
|
+
["sessionDir", launcher.sessionDir]
|
|
10015
|
+
]) {
|
|
10016
|
+
if (value.trim().length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
10017
|
+
throw new Error(`DaemonConfig.piByokLauncher.${field} must be a non-empty single-line string`);
|
|
10018
|
+
}
|
|
10019
|
+
}
|
|
10020
|
+
if (!isAbsolute(launcher.profileDbPath) || !isAbsolute(launcher.sessionDir)) {
|
|
10021
|
+
throw new Error(
|
|
10022
|
+
"DaemonConfig.piByokLauncher profileDbPath and sessionDir must be absolute paths"
|
|
10023
|
+
);
|
|
10024
|
+
}
|
|
10025
|
+
if (launcher.secretServicePrefix !== void 0 && (launcher.secretServicePrefix.trim().length === 0 || /[\u0000\r\n]/u.test(launcher.secretServicePrefix))) {
|
|
10026
|
+
throw new Error(
|
|
10027
|
+
"DaemonConfig.piByokLauncher.secretServicePrefix must be a non-empty single-line string"
|
|
10028
|
+
);
|
|
10029
|
+
}
|
|
10030
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
10031
|
+
"--",
|
|
10032
|
+
"--pi-bin",
|
|
10033
|
+
"--profile-db",
|
|
10034
|
+
"--session-dir",
|
|
10035
|
+
"--secret-service-prefix",
|
|
10036
|
+
"--provider",
|
|
10037
|
+
"--model"
|
|
10038
|
+
]);
|
|
10039
|
+
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
10040
|
+
if (conflicting !== void 0) {
|
|
10041
|
+
throw new Error(
|
|
10042
|
+
`DaemonConfig.piByokLauncher.args must not override reserved launcher argument ${conflicting}`
|
|
10043
|
+
);
|
|
10044
|
+
}
|
|
10045
|
+
const invalidArg = launcher.args?.find((arg) => arg.length === 0 || /[\u0000\r\n]/u.test(arg));
|
|
10046
|
+
if (invalidArg !== void 0) {
|
|
10047
|
+
throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
|
|
10048
|
+
}
|
|
10049
|
+
}
|
|
10050
|
+
var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
|
|
10051
|
+
var MAX_LOCAL_MCP_ARGS = 64;
|
|
10052
|
+
var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
|
|
10053
|
+
function isNonEmptySingleLine(value) {
|
|
10054
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_LOCAL_MCP_TOKEN_CHARS && !/[\u0000\r\n]/u.test(value);
|
|
10055
|
+
}
|
|
10056
|
+
function resolveMcpToolsets(configured) {
|
|
10057
|
+
if (configured === void 0) return void 0;
|
|
10058
|
+
if (configured === null || typeof configured !== "object" || Array.isArray(configured)) {
|
|
10059
|
+
throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
|
|
10060
|
+
}
|
|
10061
|
+
const toolsetEntries = Object.entries(configured);
|
|
10062
|
+
if (toolsetEntries.length > CONFIGURED_TOOLSETS_MAX_ITEMS) {
|
|
10063
|
+
throw new Error(
|
|
10064
|
+
`DaemonConfig.mcpToolsets may contain at most ${CONFIGURED_TOOLSETS_MAX_ITEMS} toolsets`
|
|
10065
|
+
);
|
|
10066
|
+
}
|
|
10067
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
10068
|
+
for (const [toolsetId, rawToolset] of toolsetEntries) {
|
|
10069
|
+
const parsedId = ToolsetIdSchema.safeParse(toolsetId);
|
|
10070
|
+
if (!parsedId.success) {
|
|
10071
|
+
throw new Error(`DaemonConfig.mcpToolsets contains invalid toolset id ${JSON.stringify(toolsetId)}`);
|
|
10072
|
+
}
|
|
10073
|
+
if (rawToolset === null || typeof rawToolset !== "object" || Array.isArray(rawToolset)) {
|
|
10074
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} must be an object`);
|
|
10075
|
+
}
|
|
10076
|
+
const toolsetKeys = Object.keys(rawToolset);
|
|
10077
|
+
if (toolsetKeys.some((key) => key !== "mcpServers")) {
|
|
10078
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} accepts only the mcpServers field`);
|
|
10079
|
+
}
|
|
10080
|
+
const rawServers = rawToolset.mcpServers;
|
|
10081
|
+
if (rawServers === null || typeof rawServers !== "object" || Array.isArray(rawServers)) {
|
|
10082
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must be an object`);
|
|
10083
|
+
}
|
|
10084
|
+
const serverEntries = Object.entries(rawServers);
|
|
10085
|
+
if (serverEntries.length === 0 || serverEntries.length > MAX_LOCAL_MCP_SERVERS_PER_TOOLSET) {
|
|
10086
|
+
throw new Error(
|
|
10087
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must contain 1-${MAX_LOCAL_MCP_SERVERS_PER_TOOLSET} servers`
|
|
10088
|
+
);
|
|
10089
|
+
}
|
|
10090
|
+
const servers = {};
|
|
10091
|
+
for (const [serverName, rawServer] of serverEntries) {
|
|
10092
|
+
if (!ToolsetIdSchema.safeParse(serverName).success) {
|
|
10093
|
+
throw new Error(
|
|
10094
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers contains invalid server name ${JSON.stringify(serverName)}`
|
|
10095
|
+
);
|
|
10096
|
+
}
|
|
10097
|
+
if (serverName === APPROVAL_MCP_SERVER_NAME) {
|
|
10098
|
+
throw new Error(
|
|
10099
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} uses a server name reserved by the daemon`
|
|
10100
|
+
);
|
|
10101
|
+
}
|
|
10102
|
+
if (rawServer === null || typeof rawServer !== "object" || Array.isArray(rawServer)) {
|
|
10103
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} must be an object`);
|
|
10104
|
+
}
|
|
10105
|
+
const serverKeys = Object.keys(rawServer);
|
|
10106
|
+
if (serverKeys.some((key) => key !== "command" && key !== "args")) {
|
|
10107
|
+
throw new Error(
|
|
10108
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} accepts only command and args; env, headers, and remote task data are not supported`
|
|
10109
|
+
);
|
|
10110
|
+
}
|
|
10111
|
+
const server = rawServer;
|
|
10112
|
+
if (!isNonEmptySingleLine(server.command)) {
|
|
10113
|
+
throw new Error(
|
|
10114
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.command must be a non-empty single-line string no longer than ${MAX_LOCAL_MCP_TOKEN_CHARS} characters`
|
|
10115
|
+
);
|
|
10116
|
+
}
|
|
10117
|
+
if (server.args !== void 0 && !Array.isArray(server.args)) {
|
|
10118
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must be an array`);
|
|
10119
|
+
}
|
|
10120
|
+
const args = server.args ?? [];
|
|
10121
|
+
if (args.length > MAX_LOCAL_MCP_ARGS || args.some((arg) => !isNonEmptySingleLine(arg))) {
|
|
10122
|
+
throw new Error(
|
|
10123
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must contain at most ${MAX_LOCAL_MCP_ARGS} non-empty single-line strings`
|
|
10124
|
+
);
|
|
10125
|
+
}
|
|
10126
|
+
servers[serverName] = Object.freeze({
|
|
10127
|
+
command: server.command,
|
|
10128
|
+
...args.length > 0 ? { args: Object.freeze([...args]) } : {}
|
|
10129
|
+
});
|
|
10130
|
+
}
|
|
10131
|
+
resolved.set(toolsetId, Object.freeze({ mcpServers: Object.freeze(servers) }));
|
|
10132
|
+
}
|
|
10133
|
+
return resolved;
|
|
10134
|
+
}
|
|
10135
|
+
function resolveDeviceAssertionAudiences(config) {
|
|
10136
|
+
if (config === void 0) return void 0;
|
|
10137
|
+
if (!Array.isArray(config.audiences)) {
|
|
10138
|
+
throw new Error(
|
|
10139
|
+
`DaemonConfig.deviceAssertion.audiences must be an array of exact audience strings \u2014 got ${JSON.stringify(config.audiences)}. Omit the deviceAssertion section (or pass an empty array) to leave the assertion broker disabled.`
|
|
10140
|
+
);
|
|
10141
|
+
}
|
|
10142
|
+
if (config.audiences.length === 0) return void 0;
|
|
10143
|
+
const audiences = /* @__PURE__ */ new Set();
|
|
10144
|
+
for (const audience of config.audiences) {
|
|
10145
|
+
if (typeof audience !== "string" || audience.length === 0) {
|
|
10146
|
+
throw new Error(
|
|
10147
|
+
`DaemonConfig.deviceAssertion.audiences entries must be non-empty strings \u2014 got ${JSON.stringify(audience)}`
|
|
10148
|
+
);
|
|
10149
|
+
}
|
|
10150
|
+
if (Buffer.byteLength(audience, "utf8") > DEVICE_ASSERTION_AUDIENCE_MAX_BYTES) {
|
|
10151
|
+
throw new Error(
|
|
10152
|
+
`DaemonConfig.deviceAssertion.audiences entry ${JSON.stringify(audience)} exceeds ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
10153
|
+
);
|
|
10154
|
+
}
|
|
10155
|
+
if (audiences.has(audience)) {
|
|
10156
|
+
throw new Error(
|
|
10157
|
+
`DaemonConfig.deviceAssertion.audiences contains ${JSON.stringify(audience)} twice \u2014 rejected rather than de-duplicated, because a duplicate is usually a copy-paste that hid a typo in the entry that was meant to be different`
|
|
10158
|
+
);
|
|
10159
|
+
}
|
|
10160
|
+
audiences.add(audience);
|
|
10161
|
+
}
|
|
10162
|
+
return audiences;
|
|
10163
|
+
}
|
|
10164
|
+
function resolveDeviceAssertionTtlMs(config) {
|
|
10165
|
+
const ttlMs = config?.ttlMs ?? DEVICE_ASSERTION_DEFAULT_TTL_MS;
|
|
10166
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_ASSERTION_MAX_TTL_MS) {
|
|
10167
|
+
throw new Error(
|
|
10168
|
+
`DaemonConfig.deviceAssertion.ttlMs must be a positive integer no greater than ${DEVICE_ASSERTION_MAX_TTL_MS} ms \u2014 got ${JSON.stringify(config?.ttlMs)}`
|
|
10169
|
+
);
|
|
10170
|
+
}
|
|
10171
|
+
return ttlMs;
|
|
8585
10172
|
}
|
|
8586
10173
|
function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
10174
|
+
return buildDaemonWithAdapters(config, adapters, overrides);
|
|
10175
|
+
}
|
|
10176
|
+
function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
|
|
10177
|
+
const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
|
|
10178
|
+
const configuredToolsets = Object.freeze(
|
|
10179
|
+
[...mcpToolsets?.keys() ?? []].sort()
|
|
10180
|
+
);
|
|
10181
|
+
if (config.piByokLauncher !== void 0) {
|
|
10182
|
+
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
10183
|
+
}
|
|
8587
10184
|
if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
|
|
8588
10185
|
throw new Error(
|
|
8589
10186
|
`DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
|
|
8590
10187
|
);
|
|
8591
10188
|
}
|
|
10189
|
+
const presenceCadence = {
|
|
10190
|
+
intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
|
|
10191
|
+
ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
|
|
10192
|
+
minimumIntervalMs: config.presence?.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS
|
|
10193
|
+
};
|
|
10194
|
+
assertPresenceHeartbeatCadence(presenceCadence);
|
|
10195
|
+
const deviceAssertionAudiences = resolveDeviceAssertionAudiences(config.deviceAssertion);
|
|
10196
|
+
const deviceAssertionTtlMs = resolveDeviceAssertionTtlMs(config.deviceAssertion);
|
|
10197
|
+
let shuttingDown = false;
|
|
8592
10198
|
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
8593
10199
|
const store = new DeviceStore(storeDir);
|
|
8594
10200
|
const operationalHealth = new OperationalHealthTracker(storeDir);
|
|
@@ -8679,6 +10285,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8679
10285
|
let runner;
|
|
8680
10286
|
let controlServerHandle;
|
|
8681
10287
|
let daemonOwnerLease;
|
|
10288
|
+
let presencePublisher;
|
|
10289
|
+
let presenceDiscovery;
|
|
10290
|
+
let presenceDiscoveryInFlight = false;
|
|
8682
10291
|
let shutdownPromise;
|
|
8683
10292
|
const pendingLateMutationBarriers = /* @__PURE__ */ new Set();
|
|
8684
10293
|
let startedAt;
|
|
@@ -8851,6 +10460,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8851
10460
|
deviceId: record.deviceId,
|
|
8852
10461
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
8853
10462
|
runtimeEnvironment: config.runtimeEnvironment,
|
|
10463
|
+
...mcpToolsets ? { mcpToolsets } : {},
|
|
8854
10464
|
// M3-2a: `send` is already this file's OWN closure (not something
|
|
8855
10465
|
// `TaskRunner` builds) — every `task.claim`/`task.started`/
|
|
8856
10466
|
// `task.progress`/`task.artifact`/`task.await_approval`/
|
|
@@ -8874,12 +10484,13 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8874
10484
|
dirty: event.observation ? { staged: event.observation.staged, unstaged: event.observation.unstaged, untracked: event.observation.untracked, conflicted: event.observation.conflicted } : void 0,
|
|
8875
10485
|
errorCategory: event.errorCategory
|
|
8876
10486
|
}),
|
|
10487
|
+
onRuntimeDisposalFailure: (event) => observer.noteRuntimeDisposalFailure(event),
|
|
8877
10488
|
// M4 Phase 3: the SAME `ApprovalRegistry` instance the control
|
|
8878
10489
|
// socket's own `approvals.list`/`approvals.resolve` methods already
|
|
8879
10490
|
// share (see that field's own construction above) — `TaskRunner
|
|
8880
10491
|
// .requestApproval` registers into it directly, so a decision arriving
|
|
8881
10492
|
// via either the server wire or the local CLI resolves the identical
|
|
8882
|
-
// entry. `storeDir`/`productId` let
|
|
10493
|
+
// entry. `storeDir`/`productId` let the prepared operation approval channel
|
|
8883
10494
|
// (populated per-task by `TaskRunner`) tell an out-of-process helper
|
|
8884
10495
|
// (`bin/byok-approval-mcp.ts`) exactly which control socket to dial.
|
|
8885
10496
|
approvalRegistry,
|
|
@@ -8890,6 +10501,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8890
10501
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
8891
10502
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
8892
10503
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
10504
|
+
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
10505
|
+
// absent when unconfigured — see `DaemonConfig.resultDocument`'s own
|
|
10506
|
+
// doc comment. Spread rather than assigned so an unconfigured daemon
|
|
10507
|
+
// builds the exact `deps` object it did before this seam existed.
|
|
10508
|
+
...config.resultDocument ? { resultDocument: config.resultDocument } : {},
|
|
8893
10509
|
// M4 Phase 3 hardening: bridges TaskRunner's stale-approval-race
|
|
8894
10510
|
// finding out to the SAME local observability seam every other
|
|
8895
10511
|
// daemon-local event already uses (see observer.ts's own module doc
|
|
@@ -8926,6 +10542,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8926
10542
|
productId: config.productId,
|
|
8927
10543
|
capabilities,
|
|
8928
10544
|
runtimes,
|
|
10545
|
+
configuredToolsets,
|
|
8929
10546
|
auth,
|
|
8930
10547
|
cursorStore,
|
|
8931
10548
|
// Finding F3: return (not void-and-forget) so ConnectionManager can
|
|
@@ -8965,8 +10582,10 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8965
10582
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
8966
10583
|
},
|
|
8967
10584
|
onStateChange: (state) => {
|
|
10585
|
+
const wasSettled = connectionState === "open" || connectionState === "degraded";
|
|
8968
10586
|
connectionState = state;
|
|
8969
10587
|
observer.noteConnectionState(state);
|
|
10588
|
+
if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
|
|
8970
10589
|
},
|
|
8971
10590
|
backoff: overrides.backoff,
|
|
8972
10591
|
liveness: overrides.liveness,
|
|
@@ -8984,6 +10603,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8984
10603
|
});
|
|
8985
10604
|
await connection.start();
|
|
8986
10605
|
await connection.waitForAck();
|
|
10606
|
+
startPresenceProducer();
|
|
8987
10607
|
} catch (err) {
|
|
8988
10608
|
try {
|
|
8989
10609
|
await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
|
|
@@ -8993,7 +10613,42 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8993
10613
|
throw err;
|
|
8994
10614
|
}
|
|
8995
10615
|
}
|
|
10616
|
+
function startPresenceProducer() {
|
|
10617
|
+
presenceDiscovery = new AbortController();
|
|
10618
|
+
runPresenceDiscovery();
|
|
10619
|
+
}
|
|
10620
|
+
function runPresenceDiscovery() {
|
|
10621
|
+
const discovery = presenceDiscovery;
|
|
10622
|
+
if (!discovery || presenceDiscoveryInFlight) return;
|
|
10623
|
+
presenceDiscoveryInFlight = true;
|
|
10624
|
+
void (async () => {
|
|
10625
|
+
try {
|
|
10626
|
+
const declaration = await fetchCapabilityDeclaration(config.serverUrl, { signal: discovery.signal });
|
|
10627
|
+
if (discovery.signal.aborted) return;
|
|
10628
|
+
if (declares(declaration, PRESENCE_HINTS_CAPABILITY)) {
|
|
10629
|
+
presencePublisher ??= new PresencePublisher({
|
|
10630
|
+
serverUrl: config.serverUrl,
|
|
10631
|
+
auth,
|
|
10632
|
+
configuredToolsets,
|
|
10633
|
+
...presenceCadence,
|
|
10634
|
+
onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
|
|
10635
|
+
});
|
|
10636
|
+
presencePublisher.start();
|
|
10637
|
+
} else {
|
|
10638
|
+
presencePublisher?.stop();
|
|
10639
|
+
}
|
|
10640
|
+
} catch (err) {
|
|
10641
|
+
if (discovery.signal.aborted) return;
|
|
10642
|
+
console.warn(
|
|
10643
|
+
`[byok/client] capability discovery failed; presence publishing stays off until the next reconnect: ${err instanceof Error ? err.message : String(err)}`
|
|
10644
|
+
);
|
|
10645
|
+
} finally {
|
|
10646
|
+
presenceDiscoveryInFlight = false;
|
|
10647
|
+
}
|
|
10648
|
+
})();
|
|
10649
|
+
}
|
|
8996
10650
|
async function runShutdownSequence(reason, opts = {}) {
|
|
10651
|
+
shuttingDown = true;
|
|
8997
10652
|
const errors = [];
|
|
8998
10653
|
let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
|
|
8999
10654
|
if (!hostedStorageInitializationBarrierComplete) {
|
|
@@ -9022,6 +10677,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9022
10677
|
errors.push(new Error("a prior active task teardown remains unsettled; ownership lease retained"));
|
|
9023
10678
|
}
|
|
9024
10679
|
}
|
|
10680
|
+
presenceDiscovery?.abort();
|
|
10681
|
+
presenceDiscovery = void 0;
|
|
10682
|
+
presenceDiscoveryInFlight = false;
|
|
10683
|
+
presencePublisher?.stop();
|
|
10684
|
+
presencePublisher = void 0;
|
|
9025
10685
|
const stoppingOwnedPressureEngine = ownedPressureEngine;
|
|
9026
10686
|
const stoppingOwnedJournal = ownedJournal;
|
|
9027
10687
|
const maintenanceStopped = stoppingOwnedPressureEngine?.stop() ?? Promise.resolve();
|
|
@@ -9063,8 +10723,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9063
10723
|
await attempt(() => auth.stop(), true);
|
|
9064
10724
|
connectionState = "closed";
|
|
9065
10725
|
await attempt(async () => {
|
|
9066
|
-
await controlServerHandle?.
|
|
9067
|
-
controlServerHandle = void 0;
|
|
10726
|
+
await controlServerHandle?.stopServing();
|
|
9068
10727
|
}, true);
|
|
9069
10728
|
if (mutationBarrierComplete && daemonOwnerLease) {
|
|
9070
10729
|
await attempt(() => operationalHealth.markCleanStop(), false);
|
|
@@ -9073,6 +10732,12 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9073
10732
|
daemonOwnerLease = void 0;
|
|
9074
10733
|
}, false);
|
|
9075
10734
|
}
|
|
10735
|
+
if (daemonOwnerLease === void 0) {
|
|
10736
|
+
await attempt(async () => {
|
|
10737
|
+
await controlServerHandle?.close();
|
|
10738
|
+
controlServerHandle = void 0;
|
|
10739
|
+
}, false);
|
|
10740
|
+
}
|
|
9076
10741
|
if (errors.length === 1) throw errors[0];
|
|
9077
10742
|
if (errors.length > 1) {
|
|
9078
10743
|
throw new AggregateError(errors, "daemon shutdown completed with errors");
|
|
@@ -9082,6 +10747,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9082
10747
|
}
|
|
9083
10748
|
}
|
|
9084
10749
|
async function stop(opts = {}) {
|
|
10750
|
+
shuttingDown = true;
|
|
9085
10751
|
await requestShutdown(opts.reason ?? "operator", { drainTimeoutMs: opts.drainTimeoutMs });
|
|
9086
10752
|
}
|
|
9087
10753
|
function requestShutdown(reason, opts = {}) {
|
|
@@ -9094,6 +10760,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9094
10760
|
return current;
|
|
9095
10761
|
}
|
|
9096
10762
|
async function unpair() {
|
|
10763
|
+
shuttingDown = true;
|
|
9097
10764
|
await runLifecycleMutation(unpairUnderLease);
|
|
9098
10765
|
}
|
|
9099
10766
|
async function unpairUnderLease() {
|
|
@@ -9150,7 +10817,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9150
10817
|
deviceId: auth.deviceId,
|
|
9151
10818
|
transport: connectionState,
|
|
9152
10819
|
activeTasks,
|
|
9153
|
-
runtimeIds: adapters.map((adapter) => adapter.id),
|
|
10820
|
+
runtimeIds: adapters.map((adapter) => adapter.descriptor.id),
|
|
9154
10821
|
// M4 Phase 4 (part B.3): queue watermarks come from TaskRunner's own
|
|
9155
10822
|
// active-task map (distinct from `observer.tasks()` above, which is
|
|
9156
10823
|
// derived from the envelope feed) — see `TaskRunner.getQueueWatermarks`'s
|
|
@@ -9172,6 +10839,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9172
10839
|
}
|
|
9173
10840
|
async function performControlShutdown(reason) {
|
|
9174
10841
|
const effectiveReason = reason ?? "operator";
|
|
10842
|
+
shuttingDown = true;
|
|
9175
10843
|
observer.noteShutdownRequested(effectiveReason);
|
|
9176
10844
|
try {
|
|
9177
10845
|
await requestShutdown(`control socket shutdown (${effectiveReason})`);
|
|
@@ -9209,7 +10877,107 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9209
10877
|
if (!runner) throw new ControlError("not_found", "daemon is not started");
|
|
9210
10878
|
return runner.requestApproval(parsed.taskId, parsed.summary);
|
|
9211
10879
|
},
|
|
10880
|
+
/**
|
|
10881
|
+
* Plan `device-assertion-broker`: mint one short-lived, audience-scoped
|
|
10882
|
+
* device assertion for a sibling local process.
|
|
10883
|
+
*
|
|
10884
|
+
* SIX fail-closed gates, in this exact order, none of which signs
|
|
10885
|
+
* anything on the way out. The order is part of the contract, not an
|
|
10886
|
+
* implementation detail:
|
|
10887
|
+
*
|
|
10888
|
+
* 1. `assertion_disabled` — before anything else, because a daemon that
|
|
10889
|
+
* was never configured for this must not reveal, by answering
|
|
10890
|
+
* differently for different inputs, that it even validates params.
|
|
10891
|
+
* 2. `bad_request` — shape/length, checked before the allowlist so a
|
|
10892
|
+
* malformed request cannot be used to probe membership.
|
|
10893
|
+
* 3. `audience_denied` — EXACT `Set.has`, never a prefix/suffix rule
|
|
10894
|
+
* (`salesko-api.evil.com` and `salesko-ap` both fail against an entry
|
|
10895
|
+
* of `salesko-api`). The message deliberately does not echo the
|
|
10896
|
+
* allowlist: a refusal must not be an enumeration oracle.
|
|
10897
|
+
* 4. `shutting_down` — see `performControlShutdown`'s own comment for
|
|
10898
|
+
* the minting window this closes.
|
|
10899
|
+
* 5. `revoked` — the server-side revocation this daemon already knows
|
|
10900
|
+
* about.
|
|
10901
|
+
* 6. `not_paired` — the on-disk record, re-read on EVERY call (never
|
|
10902
|
+
* cached), so clearing `device.json` removes local signing authority
|
|
10903
|
+
* immediately.
|
|
10904
|
+
*
|
|
10905
|
+
* Only after all six does the private key get imported, used once, and
|
|
10906
|
+
* dropped (`device-assertion-signer.ts`).
|
|
10907
|
+
*
|
|
10908
|
+
* Honest limit, and it must stay in the docs as well as here: gates 4-6
|
|
10909
|
+
* are only HALF of revocation. They make this daemon stop minting
|
|
10910
|
+
* promptly, but an assertion already in a caller's hands is not recalled
|
|
10911
|
+
* by any of them. The other half is the host's own recheck at exchange
|
|
10912
|
+
* time, which is why core's `verifyDeviceAssertion` makes the device
|
|
10913
|
+
* row's `revoked` state a REQUIRED parameter. Nothing here entitles
|
|
10914
|
+
* anyone to claim this daemon delivers synchronous invalidation on its
|
|
10915
|
+
* own.
|
|
10916
|
+
*/
|
|
10917
|
+
"assertion.issue": async (params) => {
|
|
10918
|
+
if (deviceAssertionAudiences === void 0) {
|
|
10919
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "assertion_disabled" });
|
|
10920
|
+
throw new ControlError(
|
|
10921
|
+
"assertion_disabled",
|
|
10922
|
+
"this daemon is not configured to issue device assertions (DaemonConfig.deviceAssertion.audiences is absent or empty)"
|
|
10923
|
+
);
|
|
10924
|
+
}
|
|
10925
|
+
const parsed = parseAssertionIssueParams(params);
|
|
10926
|
+
if (!parsed) {
|
|
10927
|
+
const rawAudience = typeof params === "object" && params !== null && typeof params.audience === "string" ? params.audience : void 0;
|
|
10928
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "bad_request", audience: rawAudience });
|
|
10929
|
+
throw new ControlError(
|
|
10930
|
+
"bad_request",
|
|
10931
|
+
`assertion.issue requires exactly {audience} where audience is a non-empty string of at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
10932
|
+
);
|
|
10933
|
+
}
|
|
10934
|
+
if (!deviceAssertionAudiences.has(parsed.audience)) {
|
|
10935
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "audience_denied", audience: parsed.audience });
|
|
10936
|
+
throw new ControlError("audience_denied", "the requested audience is not allowed by this daemon");
|
|
10937
|
+
}
|
|
10938
|
+
if (shuttingDown) {
|
|
10939
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10940
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10941
|
+
}
|
|
10942
|
+
if (auth.isRevoked()) {
|
|
10943
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10944
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10945
|
+
}
|
|
10946
|
+
const record = await store.load();
|
|
10947
|
+
if (record === void 0) {
|
|
10948
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "not_paired", audience: parsed.audience });
|
|
10949
|
+
throw new ControlError("not_paired", "this device is not paired; nothing can be asserted about it");
|
|
10950
|
+
}
|
|
10951
|
+
if (shuttingDown) {
|
|
10952
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10953
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10954
|
+
}
|
|
10955
|
+
if (auth.isRevoked()) {
|
|
10956
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10957
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10958
|
+
}
|
|
10959
|
+
const minted = mintDeviceAssertion({
|
|
10960
|
+
record,
|
|
10961
|
+
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
10962
|
+
// (ws:->http:, wss:->https:, path stripped), so an operator who
|
|
10963
|
+
// configured the websocket spelling and one who configured the HTTP
|
|
10964
|
+
// spelling of the same deployment produce the same issuer.
|
|
10965
|
+
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
10966
|
+
productId: config.productId,
|
|
10967
|
+
audience: parsed.audience,
|
|
10968
|
+
ttlMs: deviceAssertionTtlMs,
|
|
10969
|
+
now: /* @__PURE__ */ new Date()
|
|
10970
|
+
});
|
|
10971
|
+
observer.noteDeviceAssertion({
|
|
10972
|
+
result: "issued",
|
|
10973
|
+
audience: minted.claims.audience,
|
|
10974
|
+
jti: minted.claims.jti,
|
|
10975
|
+
expiresAt: minted.expiresAt
|
|
10976
|
+
});
|
|
10977
|
+
return { assertion: minted.envelope, expiresAt: minted.expiresAt };
|
|
10978
|
+
},
|
|
9212
10979
|
shutdown: (params) => {
|
|
10980
|
+
shuttingDown = true;
|
|
9213
10981
|
const { reason } = parseShutdownParams(params);
|
|
9214
10982
|
setImmediate(() => {
|
|
9215
10983
|
void performControlShutdown(reason).catch((err) => {
|
|
@@ -9254,28 +11022,285 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9254
11022
|
return { pair, start, stop, status, subscribe, tasks, unpair, approve, reject };
|
|
9255
11023
|
}
|
|
9256
11024
|
function createDaemon(config) {
|
|
9257
|
-
return createDaemonWithAdapters(config, buildDefaultAdapters(config
|
|
11025
|
+
return createDaemonWithAdapters(config, buildDefaultAdapters(config));
|
|
9258
11026
|
}
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
const program = {
|
|
9263
|
-
command: opts.nodeBin ?? process.execPath,
|
|
9264
|
-
args: [opts.agentBin, "start", "--config", opts.configPath]
|
|
9265
|
-
};
|
|
9266
|
-
if (opts.cwd !== void 0) program.cwd = opts.cwd;
|
|
9267
|
-
return program;
|
|
11027
|
+
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
11028
|
+
function errorMessage5(err) {
|
|
11029
|
+
return err instanceof Error ? err.message : String(err);
|
|
9268
11030
|
}
|
|
9269
|
-
function
|
|
9270
|
-
|
|
9271
|
-
const safe = cleaned.replace(/^-+/, "");
|
|
9272
|
-
if (!safe) {
|
|
9273
|
-
throw new Error(`service name "${name}" has no valid characters left after sanitizing (allowed: letters, digits, ".", "-", "_"; cannot consist only of leading "-")`);
|
|
9274
|
-
}
|
|
9275
|
-
return safe;
|
|
11031
|
+
function sameFileState3(left, right) {
|
|
11032
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
9276
11033
|
}
|
|
9277
|
-
|
|
9278
|
-
|
|
11034
|
+
async function readControlToken(tokenPath) {
|
|
11035
|
+
let namedBefore;
|
|
11036
|
+
try {
|
|
11037
|
+
namedBefore = await promises.lstat(tokenPath, { bigint: true });
|
|
11038
|
+
} catch (err) {
|
|
11039
|
+
if (err.code === "ENOENT") return void 0;
|
|
11040
|
+
throw err;
|
|
11041
|
+
}
|
|
11042
|
+
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
11043
|
+
throw new Error("control token is not a real regular file");
|
|
11044
|
+
}
|
|
11045
|
+
const handle = await promises.open(
|
|
11046
|
+
tokenPath,
|
|
11047
|
+
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
11048
|
+
);
|
|
11049
|
+
try {
|
|
11050
|
+
const opened = await handle.stat({ bigint: true });
|
|
11051
|
+
const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
|
|
11052
|
+
if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
|
|
11053
|
+
throw new Error("control token pathname changed before safe open");
|
|
11054
|
+
}
|
|
11055
|
+
if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
|
|
11056
|
+
throw new Error("control token exceeds the bounded read limit");
|
|
11057
|
+
}
|
|
11058
|
+
const size = Number(opened.size);
|
|
11059
|
+
const bytes = Buffer.alloc(size);
|
|
11060
|
+
const { bytesRead } = await handle.read(bytes, 0, size, 0);
|
|
11061
|
+
const afterRead = await handle.stat({ bigint: true });
|
|
11062
|
+
const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
|
|
11063
|
+
if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
|
|
11064
|
+
throw new Error("control token changed during bounded read");
|
|
11065
|
+
}
|
|
11066
|
+
return bytes.toString("utf8").trim();
|
|
11067
|
+
} finally {
|
|
11068
|
+
await handle.close();
|
|
11069
|
+
}
|
|
11070
|
+
}
|
|
11071
|
+
async function connectControlClient(opts) {
|
|
11072
|
+
const tokenPath = controlTokenPath(opts.storeDir);
|
|
11073
|
+
let token;
|
|
11074
|
+
try {
|
|
11075
|
+
const read = await readControlToken(tokenPath);
|
|
11076
|
+
if (read === void 0) {
|
|
11077
|
+
return { ok: false, reason: "daemon is not running (no control.token found)" };
|
|
11078
|
+
}
|
|
11079
|
+
token = read;
|
|
11080
|
+
} catch (err) {
|
|
11081
|
+
return { ok: false, reason: `could not read the control token: ${errorMessage5(err)}` };
|
|
11082
|
+
}
|
|
11083
|
+
if (!token) {
|
|
11084
|
+
return { ok: false, reason: "control token file is empty" };
|
|
11085
|
+
}
|
|
11086
|
+
const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
|
|
11087
|
+
try {
|
|
11088
|
+
const client = await connectAndHandshake(endpoint, token, opts);
|
|
11089
|
+
return { ok: true, client };
|
|
11090
|
+
} catch (err) {
|
|
11091
|
+
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage5(err)}` };
|
|
11092
|
+
}
|
|
11093
|
+
}
|
|
11094
|
+
function connectAndHandshake(endpoint, token, opts) {
|
|
11095
|
+
return new Promise((resolve, reject) => {
|
|
11096
|
+
const socket = net.createConnection(endpoint);
|
|
11097
|
+
const reader = new NdjsonLineReader();
|
|
11098
|
+
let phase = "server-hello";
|
|
11099
|
+
let settled = false;
|
|
11100
|
+
const clientNonce = randomNonceHex();
|
|
11101
|
+
const timer = setTimeout(() => {
|
|
11102
|
+
fail(new Error("handshake timed out"));
|
|
11103
|
+
}, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
|
|
11104
|
+
timer.unref?.();
|
|
11105
|
+
function fail(err) {
|
|
11106
|
+
if (settled) return;
|
|
11107
|
+
settled = true;
|
|
11108
|
+
clearTimeout(timer);
|
|
11109
|
+
socket.removeAllListeners();
|
|
11110
|
+
socket.destroy();
|
|
11111
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
11112
|
+
}
|
|
11113
|
+
function succeed() {
|
|
11114
|
+
settled = true;
|
|
11115
|
+
clearTimeout(timer);
|
|
11116
|
+
socket.removeListener("error", onError);
|
|
11117
|
+
socket.removeListener("data", onData);
|
|
11118
|
+
resolve(createControlClient(socket, reader, opts));
|
|
11119
|
+
}
|
|
11120
|
+
function onData(chunk) {
|
|
11121
|
+
let lines;
|
|
11122
|
+
try {
|
|
11123
|
+
lines = reader.push(chunk);
|
|
11124
|
+
} catch (err) {
|
|
11125
|
+
fail(err);
|
|
11126
|
+
return;
|
|
11127
|
+
}
|
|
11128
|
+
for (const line of lines) {
|
|
11129
|
+
let parsed;
|
|
11130
|
+
try {
|
|
11131
|
+
parsed = JSON.parse(line);
|
|
11132
|
+
} catch {
|
|
11133
|
+
fail(new Error("malformed handshake frame"));
|
|
11134
|
+
return;
|
|
11135
|
+
}
|
|
11136
|
+
if (phase === "server-hello") {
|
|
11137
|
+
const hello = parseServerHello(parsed);
|
|
11138
|
+
if (!hello) {
|
|
11139
|
+
fail(new Error("malformed or unexpected server hello"));
|
|
11140
|
+
return;
|
|
11141
|
+
}
|
|
11142
|
+
if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
|
|
11143
|
+
fail(new Error("server failed to prove it holds the control token"));
|
|
11144
|
+
return;
|
|
11145
|
+
}
|
|
11146
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
|
|
11147
|
+
phase = "ready";
|
|
11148
|
+
continue;
|
|
11149
|
+
}
|
|
11150
|
+
if (!parseServerReady(parsed)) {
|
|
11151
|
+
fail(new Error("server did not confirm readiness"));
|
|
11152
|
+
return;
|
|
11153
|
+
}
|
|
11154
|
+
succeed();
|
|
11155
|
+
return;
|
|
11156
|
+
}
|
|
11157
|
+
}
|
|
11158
|
+
function onError(err) {
|
|
11159
|
+
fail(err);
|
|
11160
|
+
}
|
|
11161
|
+
socket.once("error", onError);
|
|
11162
|
+
socket.once("connect", () => {
|
|
11163
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
|
|
11164
|
+
socket.on("data", onData);
|
|
11165
|
+
});
|
|
11166
|
+
});
|
|
11167
|
+
}
|
|
11168
|
+
function withTimeout(promise, ms, message) {
|
|
11169
|
+
return new Promise((resolve, reject) => {
|
|
11170
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
11171
|
+
timer.unref?.();
|
|
11172
|
+
promise.then(
|
|
11173
|
+
(value) => {
|
|
11174
|
+
clearTimeout(timer);
|
|
11175
|
+
resolve(value);
|
|
11176
|
+
},
|
|
11177
|
+
(err) => {
|
|
11178
|
+
clearTimeout(timer);
|
|
11179
|
+
reject(err);
|
|
11180
|
+
}
|
|
11181
|
+
);
|
|
11182
|
+
});
|
|
11183
|
+
}
|
|
11184
|
+
function createControlClient(socket, reader, opts) {
|
|
11185
|
+
const pending = /* @__PURE__ */ new Map();
|
|
11186
|
+
let idSeq = 0;
|
|
11187
|
+
let closed = false;
|
|
11188
|
+
function handleFrame(parsed) {
|
|
11189
|
+
if (!isRecord2(parsed) || typeof parsed.id !== "string") return;
|
|
11190
|
+
const entry = pending.get(parsed.id);
|
|
11191
|
+
if (!entry) return;
|
|
11192
|
+
if ("event" in parsed) {
|
|
11193
|
+
entry.onEvent?.(parsed.event);
|
|
11194
|
+
return;
|
|
11195
|
+
}
|
|
11196
|
+
if (parsed.ok === true) {
|
|
11197
|
+
pending.delete(parsed.id);
|
|
11198
|
+
entry.resolve(parsed.done === true ? void 0 : parsed.result);
|
|
11199
|
+
return;
|
|
11200
|
+
}
|
|
11201
|
+
pending.delete(parsed.id);
|
|
11202
|
+
const shape = parsed.error;
|
|
11203
|
+
entry.reject(
|
|
11204
|
+
new ControlError(
|
|
11205
|
+
typeof shape?.code === "string" ? shape.code : "internal_error",
|
|
11206
|
+
typeof shape?.message === "string" ? shape.message : "unknown control error"
|
|
11207
|
+
)
|
|
11208
|
+
);
|
|
11209
|
+
}
|
|
11210
|
+
socket.on("data", (chunk) => {
|
|
11211
|
+
let lines;
|
|
11212
|
+
try {
|
|
11213
|
+
lines = reader.push(chunk);
|
|
11214
|
+
} catch {
|
|
11215
|
+
socket.destroy();
|
|
11216
|
+
return;
|
|
11217
|
+
}
|
|
11218
|
+
for (const line of lines) {
|
|
11219
|
+
let parsed;
|
|
11220
|
+
try {
|
|
11221
|
+
parsed = JSON.parse(line);
|
|
11222
|
+
} catch {
|
|
11223
|
+
continue;
|
|
11224
|
+
}
|
|
11225
|
+
handleFrame(parsed);
|
|
11226
|
+
}
|
|
11227
|
+
});
|
|
11228
|
+
socket.on("close", () => {
|
|
11229
|
+
closed = true;
|
|
11230
|
+
for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
|
|
11231
|
+
pending.clear();
|
|
11232
|
+
});
|
|
11233
|
+
socket.on("error", () => {
|
|
11234
|
+
});
|
|
11235
|
+
function send(method, params, onEvent) {
|
|
11236
|
+
const id = `c${++idSeq}`;
|
|
11237
|
+
const promise = new Promise((resolve, reject) => {
|
|
11238
|
+
pending.set(id, { resolve, reject, onEvent });
|
|
11239
|
+
});
|
|
11240
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
|
|
11241
|
+
return { id, promise };
|
|
11242
|
+
}
|
|
11243
|
+
return {
|
|
11244
|
+
async request(method, params) {
|
|
11245
|
+
if (closed) throw new Error("control connection is closed");
|
|
11246
|
+
const { promise } = send(method, params);
|
|
11247
|
+
const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
|
|
11248
|
+
return result;
|
|
11249
|
+
},
|
|
11250
|
+
subscribe(method, params, onEvent) {
|
|
11251
|
+
const { id, promise } = send(method, params, onEvent);
|
|
11252
|
+
promise.catch(() => {
|
|
11253
|
+
});
|
|
11254
|
+
return {
|
|
11255
|
+
close: () => {
|
|
11256
|
+
pending.delete(id);
|
|
11257
|
+
socket.destroy();
|
|
11258
|
+
}
|
|
11259
|
+
};
|
|
11260
|
+
},
|
|
11261
|
+
close() {
|
|
11262
|
+
socket.destroy();
|
|
11263
|
+
}
|
|
11264
|
+
};
|
|
11265
|
+
}
|
|
11266
|
+
async function isControlDaemonGone(storeDir, productId) {
|
|
11267
|
+
const tokenGone = await promises.stat(controlTokenPath(storeDir)).then(
|
|
11268
|
+
() => false,
|
|
11269
|
+
(err) => err.code === "ENOENT"
|
|
11270
|
+
);
|
|
11271
|
+
if (!tokenGone) return false;
|
|
11272
|
+
const endpoint = controlEndpointPath(productId, storeDir);
|
|
11273
|
+
return new Promise((resolve) => {
|
|
11274
|
+
const socket = net.createConnection(endpoint);
|
|
11275
|
+
const finish = (gone) => {
|
|
11276
|
+
socket.removeAllListeners();
|
|
11277
|
+
socket.destroy();
|
|
11278
|
+
resolve(gone);
|
|
11279
|
+
};
|
|
11280
|
+
socket.once("connect", () => finish(false));
|
|
11281
|
+
socket.once("error", (err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT"));
|
|
11282
|
+
});
|
|
11283
|
+
}
|
|
11284
|
+
|
|
11285
|
+
// src/lifecycle/service-types.ts
|
|
11286
|
+
function nodeAgentProgram(opts) {
|
|
11287
|
+
const program = {
|
|
11288
|
+
command: opts.nodeBin ?? process.execPath,
|
|
11289
|
+
args: [opts.agentBin, "start", "--config", opts.configPath]
|
|
11290
|
+
};
|
|
11291
|
+
if (opts.cwd !== void 0) program.cwd = opts.cwd;
|
|
11292
|
+
return program;
|
|
11293
|
+
}
|
|
11294
|
+
function sanitizeServiceName(name) {
|
|
11295
|
+
const cleaned = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
11296
|
+
const safe = cleaned.replace(/^-+/, "");
|
|
11297
|
+
if (!safe) {
|
|
11298
|
+
throw new Error(`service name "${name}" has no valid characters left after sanitizing (allowed: letters, digits, ".", "-", "_"; cannot consist only of leading "-")`);
|
|
11299
|
+
}
|
|
11300
|
+
return safe;
|
|
11301
|
+
}
|
|
11302
|
+
|
|
11303
|
+
// src/lifecycle/launchd.ts
|
|
9279
11304
|
var LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
9280
11305
|
/operation not permitted/i,
|
|
9281
11306
|
/could not find domain/i,
|
|
@@ -9577,359 +11602,102 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
9577
11602
|
if (!await fileExists(xmlPath)) {
|
|
9578
11603
|
throw new Error(`service "${id}" is not installed (no config at ${xmlPath}) \u2014 call install() first`);
|
|
9579
11604
|
}
|
|
9580
|
-
await runOrThrow(run, exePath, ["start"], "winsw start");
|
|
9581
|
-
}
|
|
9582
|
-
async function stop() {
|
|
9583
|
-
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_ALREADY_STOPPED);
|
|
9584
|
-
}
|
|
9585
|
-
async function status() {
|
|
9586
|
-
const installed = await fileExists(xmlPath);
|
|
9587
|
-
const result = await run("sc.exe", ["query", id]);
|
|
9588
|
-
const detail = (result.stdout || result.stderr).trim();
|
|
9589
|
-
const running = result.code === 0 && /\bSTATE\b.*\bRUNNING\b/i.test(detail);
|
|
9590
|
-
const determinate = running || !WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
9591
|
-
return { installed, running, determinate, detail };
|
|
9592
|
-
}
|
|
9593
|
-
return { install, uninstall, start, stop, status };
|
|
9594
|
-
}
|
|
9595
|
-
|
|
9596
|
-
// src/lifecycle/create-service-lifecycle.ts
|
|
9597
|
-
var UnsupportedServicePlatformError = class extends Error {
|
|
9598
|
-
constructor(platform) {
|
|
9599
|
-
super(`no OS service lifecycle for platform "${platform}" \u2014 supported: darwin (launchd), linux (systemd --user), win32 (WinSW)`);
|
|
9600
|
-
this.name = "UnsupportedServicePlatformError";
|
|
9601
|
-
}
|
|
9602
|
-
};
|
|
9603
|
-
function createServiceLifecycle(def, opts = {}) {
|
|
9604
|
-
const platform = opts.platform ?? process.platform;
|
|
9605
|
-
switch (platform) {
|
|
9606
|
-
case "darwin":
|
|
9607
|
-
return createLaunchdLifecycle(def, opts.deps);
|
|
9608
|
-
case "linux":
|
|
9609
|
-
return createSystemdLifecycle(def, opts.deps);
|
|
9610
|
-
case "win32":
|
|
9611
|
-
return createWinswLifecycle(def, opts.deps);
|
|
9612
|
-
default:
|
|
9613
|
-
throw new UnsupportedServicePlatformError(platform);
|
|
9614
|
-
}
|
|
9615
|
-
}
|
|
9616
|
-
var REQUIRED_FIELDS = ["productName", "productId", "serverUrl", "workspaceRoot"];
|
|
9617
|
-
var ConfigError = class extends Error {
|
|
9618
|
-
constructor(message) {
|
|
9619
|
-
super(message);
|
|
9620
|
-
this.name = "ConfigError";
|
|
9621
|
-
}
|
|
9622
|
-
};
|
|
9623
|
-
function loadConfig(configPath, overrides = {}) {
|
|
9624
|
-
let base = {};
|
|
9625
|
-
if (configPath) {
|
|
9626
|
-
let raw;
|
|
9627
|
-
try {
|
|
9628
|
-
raw = readFileSync(configPath, "utf8");
|
|
9629
|
-
} catch (err) {
|
|
9630
|
-
throw new ConfigError(`could not read config at "${configPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
9631
|
-
}
|
|
9632
|
-
try {
|
|
9633
|
-
base = JSON.parse(raw);
|
|
9634
|
-
} catch (err) {
|
|
9635
|
-
throw new ConfigError(`config at "${configPath}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
9636
|
-
}
|
|
9637
|
-
}
|
|
9638
|
-
const merged = { ...base, ...overrides };
|
|
9639
|
-
if (merged.gitWorkspace !== void 0) {
|
|
9640
|
-
try {
|
|
9641
|
-
GitWorkspaceManager.validateConfig(merged.gitWorkspace);
|
|
9642
|
-
} catch (error) {
|
|
9643
|
-
throw new ConfigError(error instanceof Error ? error.message : "invalid gitWorkspace configuration");
|
|
9644
|
-
}
|
|
9645
|
-
}
|
|
9646
|
-
for (const field of REQUIRED_FIELDS) {
|
|
9647
|
-
if (!merged[field]) {
|
|
9648
|
-
throw new ConfigError(`config is missing required field "${field}"`);
|
|
9649
|
-
}
|
|
9650
|
-
}
|
|
9651
|
-
return merged;
|
|
9652
|
-
}
|
|
9653
|
-
function resolveStoreDir(config) {
|
|
9654
|
-
return DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
9655
|
-
}
|
|
9656
|
-
function argValue(args, flag) {
|
|
9657
|
-
const idx = args.indexOf(flag);
|
|
9658
|
-
const value = idx >= 0 ? args[idx + 1] : void 0;
|
|
9659
|
-
return value !== void 0 && !value.startsWith("--") ? value : void 0;
|
|
9660
|
-
}
|
|
9661
|
-
function hasFlag(args, flag) {
|
|
9662
|
-
return args.includes(flag);
|
|
9663
|
-
}
|
|
9664
|
-
function positionalArgs(args, valueFlags = []) {
|
|
9665
|
-
const result = [];
|
|
9666
|
-
for (let i = 0; i < args.length; i++) {
|
|
9667
|
-
const arg = args[i];
|
|
9668
|
-
if (arg === void 0) continue;
|
|
9669
|
-
if (valueFlags.includes(arg)) {
|
|
9670
|
-
i++;
|
|
9671
|
-
continue;
|
|
9672
|
-
}
|
|
9673
|
-
result.push(arg);
|
|
9674
|
-
}
|
|
9675
|
-
return result;
|
|
9676
|
-
}
|
|
9677
|
-
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
9678
|
-
function errorMessage4(err) {
|
|
9679
|
-
return err instanceof Error ? err.message : String(err);
|
|
9680
|
-
}
|
|
9681
|
-
function sameFileState3(left, right) {
|
|
9682
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
9683
|
-
}
|
|
9684
|
-
async function readControlToken(tokenPath) {
|
|
9685
|
-
let namedBefore;
|
|
9686
|
-
try {
|
|
9687
|
-
namedBefore = await promises.lstat(tokenPath, { bigint: true });
|
|
9688
|
-
} catch (err) {
|
|
9689
|
-
if (err.code === "ENOENT") return void 0;
|
|
9690
|
-
throw err;
|
|
9691
|
-
}
|
|
9692
|
-
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
9693
|
-
throw new Error("control token is not a real regular file");
|
|
9694
|
-
}
|
|
9695
|
-
const handle = await promises.open(
|
|
9696
|
-
tokenPath,
|
|
9697
|
-
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
9698
|
-
);
|
|
9699
|
-
try {
|
|
9700
|
-
const opened = await handle.stat({ bigint: true });
|
|
9701
|
-
const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
|
|
9702
|
-
if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
|
|
9703
|
-
throw new Error("control token pathname changed before safe open");
|
|
9704
|
-
}
|
|
9705
|
-
if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
|
|
9706
|
-
throw new Error("control token exceeds the bounded read limit");
|
|
9707
|
-
}
|
|
9708
|
-
const size = Number(opened.size);
|
|
9709
|
-
const bytes = Buffer.alloc(size);
|
|
9710
|
-
const { bytesRead } = await handle.read(bytes, 0, size, 0);
|
|
9711
|
-
const afterRead = await handle.stat({ bigint: true });
|
|
9712
|
-
const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
|
|
9713
|
-
if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
|
|
9714
|
-
throw new Error("control token changed during bounded read");
|
|
9715
|
-
}
|
|
9716
|
-
return bytes.toString("utf8").trim();
|
|
9717
|
-
} finally {
|
|
9718
|
-
await handle.close();
|
|
9719
|
-
}
|
|
9720
|
-
}
|
|
9721
|
-
async function connectControlClient(opts) {
|
|
9722
|
-
const tokenPath = controlTokenPath(opts.storeDir);
|
|
9723
|
-
let token;
|
|
9724
|
-
try {
|
|
9725
|
-
const read = await readControlToken(tokenPath);
|
|
9726
|
-
if (read === void 0) {
|
|
9727
|
-
return { ok: false, reason: "daemon is not running (no control.token found)" };
|
|
9728
|
-
}
|
|
9729
|
-
token = read;
|
|
9730
|
-
} catch (err) {
|
|
9731
|
-
return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
|
|
9732
|
-
}
|
|
9733
|
-
if (!token) {
|
|
9734
|
-
return { ok: false, reason: "control token file is empty" };
|
|
9735
|
-
}
|
|
9736
|
-
const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
|
|
9737
|
-
try {
|
|
9738
|
-
const client = await connectAndHandshake(endpoint, token, opts);
|
|
9739
|
-
return { ok: true, client };
|
|
9740
|
-
} catch (err) {
|
|
9741
|
-
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage4(err)}` };
|
|
9742
|
-
}
|
|
9743
|
-
}
|
|
9744
|
-
function connectAndHandshake(endpoint, token, opts) {
|
|
9745
|
-
return new Promise((resolve, reject) => {
|
|
9746
|
-
const socket = net.createConnection(endpoint);
|
|
9747
|
-
const reader = new NdjsonLineReader();
|
|
9748
|
-
let phase = "server-hello";
|
|
9749
|
-
let settled = false;
|
|
9750
|
-
const clientNonce = randomNonceHex();
|
|
9751
|
-
const timer = setTimeout(() => {
|
|
9752
|
-
fail(new Error("handshake timed out"));
|
|
9753
|
-
}, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
|
|
9754
|
-
timer.unref?.();
|
|
9755
|
-
function fail(err) {
|
|
9756
|
-
if (settled) return;
|
|
9757
|
-
settled = true;
|
|
9758
|
-
clearTimeout(timer);
|
|
9759
|
-
socket.removeAllListeners();
|
|
9760
|
-
socket.destroy();
|
|
9761
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
9762
|
-
}
|
|
9763
|
-
function succeed() {
|
|
9764
|
-
settled = true;
|
|
9765
|
-
clearTimeout(timer);
|
|
9766
|
-
socket.removeListener("error", onError);
|
|
9767
|
-
socket.removeListener("data", onData);
|
|
9768
|
-
resolve(createControlClient(socket, reader, opts));
|
|
9769
|
-
}
|
|
9770
|
-
function onData(chunk) {
|
|
9771
|
-
let lines;
|
|
9772
|
-
try {
|
|
9773
|
-
lines = reader.push(chunk);
|
|
9774
|
-
} catch (err) {
|
|
9775
|
-
fail(err);
|
|
9776
|
-
return;
|
|
9777
|
-
}
|
|
9778
|
-
for (const line of lines) {
|
|
9779
|
-
let parsed;
|
|
9780
|
-
try {
|
|
9781
|
-
parsed = JSON.parse(line);
|
|
9782
|
-
} catch {
|
|
9783
|
-
fail(new Error("malformed handshake frame"));
|
|
9784
|
-
return;
|
|
9785
|
-
}
|
|
9786
|
-
if (phase === "server-hello") {
|
|
9787
|
-
const hello = parseServerHello(parsed);
|
|
9788
|
-
if (!hello) {
|
|
9789
|
-
fail(new Error("malformed or unexpected server hello"));
|
|
9790
|
-
return;
|
|
9791
|
-
}
|
|
9792
|
-
if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
|
|
9793
|
-
fail(new Error("server failed to prove it holds the control token"));
|
|
9794
|
-
return;
|
|
9795
|
-
}
|
|
9796
|
-
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
|
|
9797
|
-
phase = "ready";
|
|
9798
|
-
continue;
|
|
9799
|
-
}
|
|
9800
|
-
if (!parseServerReady(parsed)) {
|
|
9801
|
-
fail(new Error("server did not confirm readiness"));
|
|
9802
|
-
return;
|
|
9803
|
-
}
|
|
9804
|
-
succeed();
|
|
9805
|
-
return;
|
|
9806
|
-
}
|
|
9807
|
-
}
|
|
9808
|
-
function onError(err) {
|
|
9809
|
-
fail(err);
|
|
9810
|
-
}
|
|
9811
|
-
socket.once("error", onError);
|
|
9812
|
-
socket.once("connect", () => {
|
|
9813
|
-
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
|
|
9814
|
-
socket.on("data", onData);
|
|
9815
|
-
});
|
|
9816
|
-
});
|
|
11605
|
+
await runOrThrow(run, exePath, ["start"], "winsw start");
|
|
11606
|
+
}
|
|
11607
|
+
async function stop() {
|
|
11608
|
+
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_ALREADY_STOPPED);
|
|
11609
|
+
}
|
|
11610
|
+
async function status() {
|
|
11611
|
+
const installed = await fileExists(xmlPath);
|
|
11612
|
+
const result = await run("sc.exe", ["query", id]);
|
|
11613
|
+
const detail = (result.stdout || result.stderr).trim();
|
|
11614
|
+
const running = result.code === 0 && /\bSTATE\b.*\bRUNNING\b/i.test(detail);
|
|
11615
|
+
const determinate = running || !WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
11616
|
+
return { installed, running, determinate, detail };
|
|
11617
|
+
}
|
|
11618
|
+
return { install, uninstall, start, stop, status };
|
|
9817
11619
|
}
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
11620
|
+
|
|
11621
|
+
// src/lifecycle/create-service-lifecycle.ts
|
|
11622
|
+
var UnsupportedServicePlatformError = class extends Error {
|
|
11623
|
+
constructor(platform) {
|
|
11624
|
+
super(`no OS service lifecycle for platform "${platform}" \u2014 supported: darwin (launchd), linux (systemd --user), win32 (WinSW)`);
|
|
11625
|
+
this.name = "UnsupportedServicePlatformError";
|
|
11626
|
+
}
|
|
11627
|
+
};
|
|
11628
|
+
function createServiceLifecycle(def, opts = {}) {
|
|
11629
|
+
const platform = opts.platform ?? process.platform;
|
|
11630
|
+
switch (platform) {
|
|
11631
|
+
case "darwin":
|
|
11632
|
+
return createLaunchdLifecycle(def, opts.deps);
|
|
11633
|
+
case "linux":
|
|
11634
|
+
return createSystemdLifecycle(def, opts.deps);
|
|
11635
|
+
case "win32":
|
|
11636
|
+
return createWinswLifecycle(def, opts.deps);
|
|
11637
|
+
default:
|
|
11638
|
+
throw new UnsupportedServicePlatformError(platform);
|
|
11639
|
+
}
|
|
9833
11640
|
}
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
11641
|
+
var REQUIRED_FIELDS = ["productName", "productId", "serverUrl", "workspaceRoot"];
|
|
11642
|
+
var ConfigError = class extends Error {
|
|
11643
|
+
constructor(message) {
|
|
11644
|
+
super(message);
|
|
11645
|
+
this.name = "ConfigError";
|
|
11646
|
+
}
|
|
11647
|
+
};
|
|
11648
|
+
function loadConfig(configPath, overrides = {}) {
|
|
11649
|
+
let base = {};
|
|
11650
|
+
if (configPath) {
|
|
11651
|
+
let raw;
|
|
11652
|
+
try {
|
|
11653
|
+
raw = readFileSync(configPath, "utf8");
|
|
11654
|
+
} catch (err) {
|
|
11655
|
+
throw new ConfigError(`could not read config at "${configPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
9845
11656
|
}
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
11657
|
+
try {
|
|
11658
|
+
base = JSON.parse(raw);
|
|
11659
|
+
} catch (err) {
|
|
11660
|
+
throw new ConfigError(`config at "${configPath}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
9850
11661
|
}
|
|
9851
|
-
pending.delete(parsed.id);
|
|
9852
|
-
const shape = parsed.error;
|
|
9853
|
-
entry.reject(
|
|
9854
|
-
new ControlError(
|
|
9855
|
-
typeof shape?.code === "string" ? shape.code : "internal_error",
|
|
9856
|
-
typeof shape?.message === "string" ? shape.message : "unknown control error"
|
|
9857
|
-
)
|
|
9858
|
-
);
|
|
9859
11662
|
}
|
|
9860
|
-
|
|
9861
|
-
|
|
11663
|
+
const merged = { ...base, ...overrides };
|
|
11664
|
+
if (merged.gitWorkspace !== void 0) {
|
|
9862
11665
|
try {
|
|
9863
|
-
|
|
9864
|
-
} catch {
|
|
9865
|
-
|
|
9866
|
-
return;
|
|
9867
|
-
}
|
|
9868
|
-
for (const line of lines) {
|
|
9869
|
-
let parsed;
|
|
9870
|
-
try {
|
|
9871
|
-
parsed = JSON.parse(line);
|
|
9872
|
-
} catch {
|
|
9873
|
-
continue;
|
|
9874
|
-
}
|
|
9875
|
-
handleFrame(parsed);
|
|
11666
|
+
GitWorkspaceManager.validateConfig(merged.gitWorkspace);
|
|
11667
|
+
} catch (error) {
|
|
11668
|
+
throw new ConfigError(error instanceof Error ? error.message : "invalid gitWorkspace configuration");
|
|
9876
11669
|
}
|
|
9877
|
-
});
|
|
9878
|
-
socket.on("close", () => {
|
|
9879
|
-
closed = true;
|
|
9880
|
-
for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
|
|
9881
|
-
pending.clear();
|
|
9882
|
-
});
|
|
9883
|
-
socket.on("error", () => {
|
|
9884
|
-
});
|
|
9885
|
-
function send(method, params, onEvent) {
|
|
9886
|
-
const id = `c${++idSeq}`;
|
|
9887
|
-
const promise = new Promise((resolve, reject) => {
|
|
9888
|
-
pending.set(id, { resolve, reject, onEvent });
|
|
9889
|
-
});
|
|
9890
|
-
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
|
|
9891
|
-
return { id, promise };
|
|
9892
11670
|
}
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
const { promise } = send(method, params);
|
|
9897
|
-
const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
|
|
9898
|
-
return result;
|
|
9899
|
-
},
|
|
9900
|
-
subscribe(method, params, onEvent) {
|
|
9901
|
-
const { id, promise } = send(method, params, onEvent);
|
|
9902
|
-
promise.catch(() => {
|
|
9903
|
-
});
|
|
9904
|
-
return {
|
|
9905
|
-
close: () => {
|
|
9906
|
-
pending.delete(id);
|
|
9907
|
-
socket.destroy();
|
|
9908
|
-
}
|
|
9909
|
-
};
|
|
9910
|
-
},
|
|
9911
|
-
close() {
|
|
9912
|
-
socket.destroy();
|
|
11671
|
+
for (const field of REQUIRED_FIELDS) {
|
|
11672
|
+
if (!merged[field]) {
|
|
11673
|
+
throw new ConfigError(`config is missing required field "${field}"`);
|
|
9913
11674
|
}
|
|
9914
|
-
}
|
|
11675
|
+
}
|
|
11676
|
+
return merged;
|
|
9915
11677
|
}
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
);
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
11678
|
+
function resolveStoreDir(config) {
|
|
11679
|
+
return DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
11680
|
+
}
|
|
11681
|
+
function argValue(args, flag) {
|
|
11682
|
+
const idx = args.indexOf(flag);
|
|
11683
|
+
const value = idx >= 0 ? args[idx + 1] : void 0;
|
|
11684
|
+
return value !== void 0 && !value.startsWith("--") ? value : void 0;
|
|
11685
|
+
}
|
|
11686
|
+
function hasFlag(args, flag) {
|
|
11687
|
+
return args.includes(flag);
|
|
11688
|
+
}
|
|
11689
|
+
function positionalArgs(args, valueFlags = []) {
|
|
11690
|
+
const result = [];
|
|
11691
|
+
for (let i = 0; i < args.length; i++) {
|
|
11692
|
+
const arg = args[i];
|
|
11693
|
+
if (arg === void 0) continue;
|
|
11694
|
+
if (valueFlags.includes(arg)) {
|
|
11695
|
+
i++;
|
|
11696
|
+
continue;
|
|
11697
|
+
}
|
|
11698
|
+
result.push(arg);
|
|
11699
|
+
}
|
|
11700
|
+
return result;
|
|
9933
11701
|
}
|
|
9934
11702
|
|
|
9935
11703
|
// src/bin/format.ts
|
|
@@ -9939,19 +11707,7 @@ function quote(text) {
|
|
|
9939
11707
|
function redactedByteCountPlaceholder(text) {
|
|
9940
11708
|
return `[redacted: ${Buffer.byteLength(text, "utf8")} bytes]`;
|
|
9941
11709
|
}
|
|
9942
|
-
var STABLE_GIT_ERROR_CATEGORIES =
|
|
9943
|
-
"git-unavailable",
|
|
9944
|
-
"git-timeout",
|
|
9945
|
-
"git-output-limit",
|
|
9946
|
-
"git-command-failed",
|
|
9947
|
-
"workspace-root-invalid",
|
|
9948
|
-
"workspace-root-conflict",
|
|
9949
|
-
"workspace-not-owned",
|
|
9950
|
-
"repository-root-mismatch",
|
|
9951
|
-
"repository-invalid",
|
|
9952
|
-
"lease-busy",
|
|
9953
|
-
"ledger-invalid"
|
|
9954
|
-
]);
|
|
11710
|
+
var STABLE_GIT_ERROR_CATEGORIES = new Set(GIT_ERROR_CATEGORIES);
|
|
9955
11711
|
function stableGitErrorCategory(value) {
|
|
9956
11712
|
return value !== void 0 && STABLE_GIT_ERROR_CATEGORIES.has(value) ? value : void 0;
|
|
9957
11713
|
}
|
|
@@ -10017,6 +11773,8 @@ function formatDaemonEventLine(event, options = {}) {
|
|
|
10017
11773
|
return `${prefix} shutdown-complete reason=${quote(event.reason)}${event.undeliveredOutboxCount !== void 0 ? ` undeliveredOutboxCount=${event.undeliveredOutboxCount}` : ""}`;
|
|
10018
11774
|
case "stale-approval-decision":
|
|
10019
11775
|
return `${prefix} stale-approval-decision taskId=${event.taskId} decision=${event.decision}${event.reason ? ` reason=${quote(event.reason)}` : ""}`;
|
|
11776
|
+
case "runtime-disposal-failed":
|
|
11777
|
+
return `${prefix} runtime-disposal-failed taskId=${event.taskId} runtime=${event.runtimeId} stage=${event.stage} reason=${quote(event.reason)}`;
|
|
10020
11778
|
case "git-workspace": {
|
|
10021
11779
|
const parts = [
|
|
10022
11780
|
`${prefix} git-workspace taskId=${event.taskId}`,
|
|
@@ -10029,6 +11787,19 @@ function formatDaemonEventLine(event, options = {}) {
|
|
|
10029
11787
|
].filter((part) => part !== void 0);
|
|
10030
11788
|
return parts.join(" ");
|
|
10031
11789
|
}
|
|
11790
|
+
case "device-assertion": {
|
|
11791
|
+
const parts = event.result === "issued" ? [
|
|
11792
|
+
`${prefix} device-assertion result=issued`,
|
|
11793
|
+
`audience=${quote(event.audience)}`,
|
|
11794
|
+
`jti=${event.jti}`,
|
|
11795
|
+
`expiresAt=${event.expiresAt}`
|
|
11796
|
+
] : [
|
|
11797
|
+
`${prefix} device-assertion result=denied`,
|
|
11798
|
+
`reason=${event.reason}`,
|
|
11799
|
+
event.audienceSize !== void 0 ? `audienceSize=${event.audienceSize}` : void 0
|
|
11800
|
+
].filter((part) => part !== void 0);
|
|
11801
|
+
return parts.join(" ");
|
|
11802
|
+
}
|
|
10032
11803
|
}
|
|
10033
11804
|
}
|
|
10034
11805
|
function formatTaskLine(task) {
|
|
@@ -10282,8 +12053,8 @@ async function probeRuntimes(adapters, options = {}) {
|
|
|
10282
12053
|
let resume = false;
|
|
10283
12054
|
let permissionModes = [];
|
|
10284
12055
|
try {
|
|
10285
|
-
id = boundedSingleLine(adapter.id, MAX_RUNTIME_ID_CHARS);
|
|
10286
|
-
const caps = adapter.capabilities
|
|
12056
|
+
id = boundedSingleLine(adapter.descriptor.id, MAX_RUNTIME_ID_CHARS);
|
|
12057
|
+
const caps = adapter.descriptor.capabilities;
|
|
10287
12058
|
steer = caps.steer === true;
|
|
10288
12059
|
resume = caps.resume === true;
|
|
10289
12060
|
permissionModes = caps.permissionModes.slice(0, MAX_PERMISSION_MODES).map((mode) => boundedSingleLine(mode, MAX_PERMISSION_MODE_CHARS));
|
|
@@ -11148,19 +12919,7 @@ function valueByteSize(value) {
|
|
|
11148
12919
|
function placeholderFor(size) {
|
|
11149
12920
|
return size === void 0 ? "[redacted]" : `[redacted: ${size} bytes]`;
|
|
11150
12921
|
}
|
|
11151
|
-
var STABLE_GIT_ERROR_CATEGORIES2 =
|
|
11152
|
-
"git-unavailable",
|
|
11153
|
-
"git-timeout",
|
|
11154
|
-
"git-output-limit",
|
|
11155
|
-
"git-command-failed",
|
|
11156
|
-
"workspace-root-invalid",
|
|
11157
|
-
"workspace-root-conflict",
|
|
11158
|
-
"workspace-not-owned",
|
|
11159
|
-
"repository-root-mismatch",
|
|
11160
|
-
"repository-invalid",
|
|
11161
|
-
"lease-busy",
|
|
11162
|
-
"ledger-invalid"
|
|
11163
|
-
]);
|
|
12922
|
+
var STABLE_GIT_ERROR_CATEGORIES2 = new Set(GIT_ERROR_CATEGORIES);
|
|
11164
12923
|
function stableGitErrorCategory2(value) {
|
|
11165
12924
|
return typeof value === "string" && STABLE_GIT_ERROR_CATEGORIES2.has(value) ? value : void 0;
|
|
11166
12925
|
}
|
|
@@ -11259,6 +13018,16 @@ function redactForAudit(event) {
|
|
|
11259
13018
|
return { ...base, reason: event.reason, undeliveredOutboxCount: event.undeliveredOutboxCount };
|
|
11260
13019
|
case "stale-approval-decision":
|
|
11261
13020
|
return { ...base, taskId: event.taskId, decision: event.decision, reasonSize: byteSize(event.reason) };
|
|
13021
|
+
case "runtime-disposal-failed":
|
|
13022
|
+
return { ...base, taskId: event.taskId, runtimeId: event.runtimeId, stage: event.stage, reason: event.reason };
|
|
13023
|
+
case "device-assertion":
|
|
13024
|
+
return event.result === "issued" ? {
|
|
13025
|
+
...base,
|
|
13026
|
+
result: "issued",
|
|
13027
|
+
audience: event.audience,
|
|
13028
|
+
jti: event.jti,
|
|
13029
|
+
expiresAt: event.expiresAt
|
|
13030
|
+
} : { ...base, result: "denied", reason: event.reason, audienceSize: event.audienceSize };
|
|
11262
13031
|
case "git-workspace":
|
|
11263
13032
|
return {
|
|
11264
13033
|
...base,
|
|
@@ -11399,6 +13168,35 @@ function reconstructDaemonEvent(raw) {
|
|
|
11399
13168
|
reason: reasonSize === void 0 ? void 0 : placeholderFor(reasonSize)
|
|
11400
13169
|
};
|
|
11401
13170
|
}
|
|
13171
|
+
case "runtime-disposal-failed":
|
|
13172
|
+
return {
|
|
13173
|
+
kind: "runtime-disposal-failed",
|
|
13174
|
+
ts,
|
|
13175
|
+
taskId: str(raw.taskId),
|
|
13176
|
+
runtimeId: str(raw.runtimeId),
|
|
13177
|
+
stage: str(raw.stage),
|
|
13178
|
+
reason: str(raw.reason)
|
|
13179
|
+
};
|
|
13180
|
+
case "device-assertion": {
|
|
13181
|
+
if (raw.result === "issued") {
|
|
13182
|
+
return {
|
|
13183
|
+
kind: "device-assertion",
|
|
13184
|
+
ts,
|
|
13185
|
+
result: "issued",
|
|
13186
|
+
audience: typeof raw.audience === "string" ? raw.audience : "",
|
|
13187
|
+
jti: typeof raw.jti === "string" ? raw.jti : "",
|
|
13188
|
+
expiresAt: typeof raw.expiresAt === "string" ? raw.expiresAt : ""
|
|
13189
|
+
};
|
|
13190
|
+
}
|
|
13191
|
+
const audienceSize = num(raw.audienceSize);
|
|
13192
|
+
return {
|
|
13193
|
+
kind: "device-assertion",
|
|
13194
|
+
ts,
|
|
13195
|
+
result: "denied",
|
|
13196
|
+
reason: typeof raw.reason === "string" ? raw.reason : "",
|
|
13197
|
+
...audienceSize === void 0 ? {} : { audienceSize }
|
|
13198
|
+
};
|
|
13199
|
+
}
|
|
11402
13200
|
case "git-workspace": {
|
|
11403
13201
|
const commitsSinceBaseline = gitCount(raw.commitsSinceBaseline);
|
|
11404
13202
|
const dirty = gitDirty(raw.dirty);
|
|
@@ -11651,20 +13449,8 @@ async function runStartCommand(config, deps) {
|
|
|
11651
13449
|
throw err;
|
|
11652
13450
|
}
|
|
11653
13451
|
}
|
|
11654
|
-
var STABLE_GIT_PHASES =
|
|
11655
|
-
var STABLE_GIT_ERROR_CATEGORIES3 =
|
|
11656
|
-
"git-unavailable",
|
|
11657
|
-
"git-timeout",
|
|
11658
|
-
"git-output-limit",
|
|
11659
|
-
"git-command-failed",
|
|
11660
|
-
"workspace-root-invalid",
|
|
11661
|
-
"workspace-root-conflict",
|
|
11662
|
-
"workspace-not-owned",
|
|
11663
|
-
"repository-root-mismatch",
|
|
11664
|
-
"repository-invalid",
|
|
11665
|
-
"lease-busy",
|
|
11666
|
-
"ledger-invalid"
|
|
11667
|
-
]);
|
|
13452
|
+
var STABLE_GIT_PHASES = new Set(GIT_WORKSPACE_PHASES);
|
|
13453
|
+
var STABLE_GIT_ERROR_CATEGORIES3 = new Set(GIT_ERROR_CATEGORIES);
|
|
11668
13454
|
function nonNegativeSafeInteger(value) {
|
|
11669
13455
|
return value !== void 0 && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
11670
13456
|
}
|
|
@@ -12163,19 +13949,7 @@ async function runUnpairCommand(daemon, deps = {}) {
|
|
|
12163
13949
|
}
|
|
12164
13950
|
|
|
12165
13951
|
// src/bin/commands/workspaces.ts
|
|
12166
|
-
var STABLE_ERROR_CATEGORIES =
|
|
12167
|
-
"git-unavailable",
|
|
12168
|
-
"git-timeout",
|
|
12169
|
-
"git-output-limit",
|
|
12170
|
-
"git-command-failed",
|
|
12171
|
-
"workspace-root-invalid",
|
|
12172
|
-
"workspace-root-conflict",
|
|
12173
|
-
"workspace-not-owned",
|
|
12174
|
-
"repository-root-mismatch",
|
|
12175
|
-
"repository-invalid",
|
|
12176
|
-
"lease-busy",
|
|
12177
|
-
"ledger-invalid"
|
|
12178
|
-
]);
|
|
13952
|
+
var STABLE_ERROR_CATEGORIES = new Set(GIT_ERROR_CATEGORIES);
|
|
12179
13953
|
function abbreviateCommit(value) {
|
|
12180
13954
|
if (!value) return "-";
|
|
12181
13955
|
return value.length > 8 ? value.slice(0, 8) : value;
|