@byok-sdk/client 0.2.0 → 0.3.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 +45 -5
- package/dist/adapters/claude/claude-adapter.d.ts +3 -0
- package/dist/adapters/codex/codex-adapter.d.ts +1 -0
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +216 -58
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +22 -0
- package/dist/adapters/provider-credential-environment.d.ts +18 -0
- package/dist/bin/byok-agent.js +1725 -760
- 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/daemon/assertion-client.d.ts +68 -0
- package/dist/daemon/capabilities-client.d.ts +48 -0
- package/dist/daemon/control-protocol.d.ts +81 -4
- package/dist/daemon/create-daemon.d.ts +169 -1
- package/dist/daemon/daemon-owner.d.ts +35 -0
- package/dist/daemon/device-assertion-signer.d.ts +41 -0
- package/dist/daemon/device-keys.d.ts +15 -13
- package/dist/daemon/observer.d.ts +68 -3
- package/dist/daemon/presence-publisher.d.ts +69 -0
- package/dist/daemon/skill-pack-installer.d.ts +116 -0
- package/dist/daemon/task-runner.d.ts +129 -3
- package/dist/index.d.ts +22 -3
- package/dist/index.js +1818 -265
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +29 -0
- 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, 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';
|
|
@@ -1118,13 +1119,8 @@ var PiRpcClient = class {
|
|
|
1118
1119
|
}
|
|
1119
1120
|
};
|
|
1120
1121
|
|
|
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 = [
|
|
1122
|
+
// src/adapters/provider-credential-environment.ts
|
|
1123
|
+
var PROVIDER_CREDENTIAL_ENV_NAMES = [
|
|
1128
1124
|
"ANTHROPIC_API_KEY",
|
|
1129
1125
|
"ANTHROPIC_OAUTH_TOKEN",
|
|
1130
1126
|
"OPENAI_API_KEY",
|
|
@@ -1135,24 +1131,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
|
|
|
1135
1131
|
"MISTRAL_API_KEY",
|
|
1136
1132
|
"OPENROUTER_API_KEY",
|
|
1137
1133
|
"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
1134
|
"ZAI_API_KEY"
|
|
1143
1135
|
];
|
|
1136
|
+
var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
|
|
1137
|
+
...PROVIDER_CREDENTIAL_ENV_NAMES,
|
|
1138
|
+
"ANT_LING_API_KEY",
|
|
1139
|
+
"NVIDIA_API_KEY",
|
|
1140
|
+
"CEREBRAS_API_KEY",
|
|
1141
|
+
"CLOUDFLARE_API_KEY",
|
|
1142
|
+
"AI_GATEWAY_API_KEY",
|
|
1143
|
+
"ZAI_CODING_CN_API_KEY",
|
|
1144
|
+
"OPENCODE_API_KEY",
|
|
1145
|
+
"RADIUS_API_KEY",
|
|
1146
|
+
"FIREWORKS_API_KEY",
|
|
1147
|
+
"TOGETHER_API_KEY",
|
|
1148
|
+
"BASETEN_API_KEY",
|
|
1149
|
+
"KIMI_API_KEY",
|
|
1150
|
+
"MINIMAX_API_KEY",
|
|
1151
|
+
"MINIMAX_CN_API_KEY",
|
|
1152
|
+
"QWEN_TOKEN_PLAN_API_KEY",
|
|
1153
|
+
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
1154
|
+
"XIAOMI_API_KEY",
|
|
1155
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
1156
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
1157
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
1158
|
+
"AWS_ACCESS_KEY_ID",
|
|
1159
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1160
|
+
"AWS_SESSION_TOKEN",
|
|
1161
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
1162
|
+
// Reserved by the keys-owned Pi projection. It must never be inherited
|
|
1163
|
+
// from the daemon; the launcher deletes any ambient copy and injects only
|
|
1164
|
+
// the exact credential it just resolved from OS custody.
|
|
1165
|
+
"PI_PROVIDER_API_KEY"
|
|
1166
|
+
];
|
|
1167
|
+
function withoutProviderCredentials(env) {
|
|
1168
|
+
const sanitized = { ...env };
|
|
1169
|
+
for (const name of PROVIDER_CREDENTIAL_ENV_DENY_NAMES) {
|
|
1170
|
+
delete sanitized[name];
|
|
1171
|
+
}
|
|
1172
|
+
return sanitized;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/adapters/pi/pi-adapter.ts
|
|
1176
|
+
var execFileAsync = promisify(execFile);
|
|
1177
|
+
var DETECT_TIMEOUT_MS = 5e3;
|
|
1178
|
+
function errorMessage(err) {
|
|
1179
|
+
return err instanceof Error ? err.message : String(err);
|
|
1180
|
+
}
|
|
1144
1181
|
var PiAdapter = class {
|
|
1145
1182
|
constructor(options = {}) {
|
|
1146
1183
|
this.options = options;
|
|
1147
1184
|
}
|
|
1148
1185
|
options;
|
|
1149
1186
|
id = "pi";
|
|
1187
|
+
supportsDispatchSelection = true;
|
|
1150
1188
|
async detect() {
|
|
1151
1189
|
try {
|
|
1152
1190
|
const bin = this.resolveBin();
|
|
1153
1191
|
const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
|
|
1154
1192
|
const version = stdout.trim() || stderr.trim();
|
|
1155
|
-
const authPresent =
|
|
1193
|
+
const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
|
|
1156
1194
|
return { present: true, version, authPresent };
|
|
1157
1195
|
} catch {
|
|
1158
1196
|
return { present: false };
|
|
@@ -1171,7 +1209,7 @@ var PiAdapter = class {
|
|
|
1171
1209
|
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
1172
1210
|
*/
|
|
1173
1211
|
environmentRequirements() {
|
|
1174
|
-
return { credentialNames:
|
|
1212
|
+
return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
|
|
1175
1213
|
}
|
|
1176
1214
|
async start(task, ctx) {
|
|
1177
1215
|
if (typeof task.instruction !== "string") {
|
|
@@ -1183,12 +1221,45 @@ var PiAdapter = class {
|
|
|
1183
1221
|
}
|
|
1184
1222
|
const bin = this.resolveBin();
|
|
1185
1223
|
const resumeSessionId = task.sessionRef;
|
|
1186
|
-
const
|
|
1224
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
1225
|
+
const selection = task.dispatchSelection;
|
|
1226
|
+
let command = bin.command;
|
|
1227
|
+
let args = piArgs;
|
|
1228
|
+
if (selection !== void 0) {
|
|
1229
|
+
if (selection.lane !== "byok" || selection.runtimeId !== "pi") {
|
|
1230
|
+
throw new PolicyUnsupportedError(
|
|
1231
|
+
`pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
const launcher = this.options.byokLauncher;
|
|
1235
|
+
if (launcher === void 0) {
|
|
1236
|
+
throw new PolicyUnsupportedError(
|
|
1237
|
+
"pi BYOK selection requires a configured credential-custody launcher"
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
command = launcher.command;
|
|
1241
|
+
args = [
|
|
1242
|
+
...launcher.args ?? [],
|
|
1243
|
+
"--pi-bin",
|
|
1244
|
+
bin.command,
|
|
1245
|
+
"--profile-db",
|
|
1246
|
+
launcher.profileDbPath,
|
|
1247
|
+
"--session-dir",
|
|
1248
|
+
launcher.sessionDir,
|
|
1249
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
1250
|
+
"--provider",
|
|
1251
|
+
selection.providerId,
|
|
1252
|
+
"--model",
|
|
1253
|
+
selection.modelId,
|
|
1254
|
+
"--",
|
|
1255
|
+
...piArgs
|
|
1256
|
+
];
|
|
1257
|
+
}
|
|
1187
1258
|
const rpc = new PiRpcClient({
|
|
1188
|
-
command
|
|
1259
|
+
command,
|
|
1189
1260
|
args,
|
|
1190
1261
|
cwd: ctx.workspaceDir,
|
|
1191
|
-
env: ctx.env,
|
|
1262
|
+
env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
|
|
1192
1263
|
spawnFn: this.options.spawnFn
|
|
1193
1264
|
});
|
|
1194
1265
|
const response = await rpc.send({ type: "prompt", message: task.instruction });
|
|
@@ -1207,7 +1278,7 @@ var PiAdapter = class {
|
|
|
1207
1278
|
throw err;
|
|
1208
1279
|
}
|
|
1209
1280
|
}
|
|
1210
|
-
return new PiSession(sessionRef, rpc);
|
|
1281
|
+
return new PiSession(sessionRef, rpc, selection);
|
|
1211
1282
|
}
|
|
1212
1283
|
resolveBin() {
|
|
1213
1284
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
@@ -1235,12 +1306,14 @@ async function resolveFreshSessionId(rpc) {
|
|
|
1235
1306
|
);
|
|
1236
1307
|
}
|
|
1237
1308
|
var PiSession = class {
|
|
1238
|
-
constructor(sessionRef, rpc) {
|
|
1309
|
+
constructor(sessionRef, rpc, selection) {
|
|
1239
1310
|
this.sessionRef = sessionRef;
|
|
1240
1311
|
this.rpc = rpc;
|
|
1312
|
+
this.selection = selection;
|
|
1241
1313
|
}
|
|
1242
1314
|
sessionRef;
|
|
1243
1315
|
rpc;
|
|
1316
|
+
selection;
|
|
1244
1317
|
get events() {
|
|
1245
1318
|
const rpc = this.rpc;
|
|
1246
1319
|
return {
|
|
@@ -1269,6 +1342,12 @@ var PiSession = class {
|
|
|
1269
1342
|
if (typeof task.instruction !== "string") {
|
|
1270
1343
|
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1271
1344
|
}
|
|
1345
|
+
const requestedSelection = task.dispatchSelection;
|
|
1346
|
+
if (requestedSelection !== void 0 && (this.selection?.lane !== "byok" || requestedSelection.lane !== "byok" || requestedSelection.runtimeId !== "pi" || requestedSelection.providerId !== this.selection.providerId || requestedSelection.modelId !== this.selection.modelId)) {
|
|
1347
|
+
throw new PolicyUnsupportedError(
|
|
1348
|
+
"pi persistent session cannot change its authoritative BYOK provider/model selection"
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1272
1351
|
await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
|
|
1273
1352
|
}
|
|
1274
1353
|
async interrupt() {
|
|
@@ -1693,7 +1772,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1693
1772
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1694
1773
|
var execFileAsync2 = promisify(execFile);
|
|
1695
1774
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
1696
|
-
async function
|
|
1775
|
+
async function cleanupMcpConfigDir(dir) {
|
|
1697
1776
|
if (!dir) return;
|
|
1698
1777
|
await promises.rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1699
1778
|
});
|
|
@@ -1703,6 +1782,7 @@ var ClaudeAdapter = class {
|
|
|
1703
1782
|
this.options = options;
|
|
1704
1783
|
}
|
|
1705
1784
|
options;
|
|
1785
|
+
supportsDispatchSelection = true;
|
|
1706
1786
|
id = "claude";
|
|
1707
1787
|
async detect() {
|
|
1708
1788
|
const bin = this.resolveBin();
|
|
@@ -1716,7 +1796,13 @@ var ClaudeAdapter = class {
|
|
|
1716
1796
|
}
|
|
1717
1797
|
}
|
|
1718
1798
|
capabilities() {
|
|
1719
|
-
return {
|
|
1799
|
+
return {
|
|
1800
|
+
steer: false,
|
|
1801
|
+
resume: true,
|
|
1802
|
+
approvalInteractive: true,
|
|
1803
|
+
mcpToolsets: true,
|
|
1804
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
1805
|
+
};
|
|
1720
1806
|
}
|
|
1721
1807
|
/**
|
|
1722
1808
|
* M5: deliberate product-boundary decision, not an oversight — byok's
|
|
@@ -1742,42 +1828,51 @@ var ClaudeAdapter = class {
|
|
|
1742
1828
|
if (!mapping.ok) {
|
|
1743
1829
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
1744
1830
|
}
|
|
1745
|
-
|
|
1831
|
+
const modelId = subscriptionModel(task, "claude");
|
|
1832
|
+
let mcpConfigDir;
|
|
1833
|
+
const taskMcpServers = ctx.mcpServers ?? {};
|
|
1834
|
+
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
1746
1835
|
if (mapping.needsApprovalMcp) {
|
|
1747
1836
|
if (!ctx.approvalChannel) {
|
|
1748
1837
|
throw new PolicyUnsupportedError(
|
|
1749
1838
|
'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
|
|
1750
1839
|
);
|
|
1751
1840
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1841
|
+
if (Object.prototype.hasOwnProperty.call(taskMcpServers, APPROVAL_MCP_SERVER_NAME)) {
|
|
1842
|
+
throw new PolicyUnsupportedError(
|
|
1843
|
+
`MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (needsMcpConfig) {
|
|
1848
|
+
mcpConfigDir = await promises.mkdtemp(path20.join(os.tmpdir(), "byok-mcp-"));
|
|
1849
|
+
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
1755
1850
|
});
|
|
1756
|
-
const mcpConfigPath = path20.join(
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1851
|
+
const mcpConfigPath = path20.join(mcpConfigDir, "mcp-config.json");
|
|
1852
|
+
const mcpServers = { ...taskMcpServers };
|
|
1853
|
+
if (mapping.needsApprovalMcp) {
|
|
1854
|
+
const approvalChannel = ctx.approvalChannel;
|
|
1855
|
+
if (!approvalChannel) throw new Error("unreachable: approval channel checked above");
|
|
1856
|
+
const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
1857
|
+
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
1858
|
+
command: approvalMcpBin.command,
|
|
1859
|
+
args: approvalMcpBin.args,
|
|
1860
|
+
env: {
|
|
1861
|
+
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
1862
|
+
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
1863
|
+
BYOK_TASK_ID: approvalChannel.taskId,
|
|
1864
|
+
BYOK_APPROVAL_TIMEOUT_MS: String(approvalChannel.timeoutMs)
|
|
1768
1865
|
}
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
await promises.writeFile(mcpConfigPath, JSON.stringify(
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
1772
1869
|
mapping.args = [
|
|
1773
1870
|
...mapping.args,
|
|
1774
|
-
"--permission-prompt-tool",
|
|
1775
|
-
`mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
|
|
1871
|
+
...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
|
|
1776
1872
|
"--mcp-config",
|
|
1777
1873
|
mcpConfigPath,
|
|
1778
|
-
//
|
|
1779
|
-
//
|
|
1780
|
-
// the only MCP server this invocation should ever see.
|
|
1874
|
+
// The generated file is the complete task-scoped MCP authority.
|
|
1875
|
+
// Never merge ambient user/project MCP configuration into it.
|
|
1781
1876
|
"--strict-mcp-config"
|
|
1782
1877
|
];
|
|
1783
1878
|
}
|
|
@@ -1794,6 +1889,7 @@ var ClaudeAdapter = class {
|
|
|
1794
1889
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1795
1890
|
// --verbose", before spawning any model call.
|
|
1796
1891
|
"--verbose",
|
|
1892
|
+
...modelId ? ["--model", modelId] : [],
|
|
1797
1893
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1798
1894
|
...mapping.args
|
|
1799
1895
|
];
|
|
@@ -1801,7 +1897,7 @@ var ClaudeAdapter = class {
|
|
|
1801
1897
|
command: bin.command,
|
|
1802
1898
|
args,
|
|
1803
1899
|
cwd: ctx.workspaceDir,
|
|
1804
|
-
env: ctx.env,
|
|
1900
|
+
env: withoutProviderCredentials(ctx.env),
|
|
1805
1901
|
spawnFn: this.options.spawnFn
|
|
1806
1902
|
});
|
|
1807
1903
|
client.writeUserMessage(task.instruction);
|
|
@@ -1810,17 +1906,24 @@ var ClaudeAdapter = class {
|
|
|
1810
1906
|
sessionRef = await client.waitForInit();
|
|
1811
1907
|
} catch (err) {
|
|
1812
1908
|
client.kill();
|
|
1813
|
-
await
|
|
1909
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1814
1910
|
throw err;
|
|
1815
1911
|
}
|
|
1816
1912
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1817
1913
|
client.kill();
|
|
1818
|
-
await
|
|
1914
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1819
1915
|
throw new Error(
|
|
1820
1916
|
`claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
|
|
1821
1917
|
);
|
|
1822
1918
|
}
|
|
1823
|
-
return new ClaudeSession(
|
|
1919
|
+
return new ClaudeSession(
|
|
1920
|
+
sessionRef,
|
|
1921
|
+
client,
|
|
1922
|
+
ctx.workspaceDir,
|
|
1923
|
+
ctx.approvalChannel,
|
|
1924
|
+
mcpConfigDir,
|
|
1925
|
+
modelId
|
|
1926
|
+
);
|
|
1824
1927
|
}
|
|
1825
1928
|
/**
|
|
1826
1929
|
* `claude auth status --json` is claude's OWN non-secret login-state
|
|
@@ -1854,19 +1957,31 @@ var ClaudeAdapter = class {
|
|
|
1854
1957
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1855
1958
|
}
|
|
1856
1959
|
};
|
|
1960
|
+
function subscriptionModel(task, runtimeId) {
|
|
1961
|
+
const selection = task.dispatchSelection;
|
|
1962
|
+
if (selection === void 0) return void 0;
|
|
1963
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
1964
|
+
throw new PolicyUnsupportedError(
|
|
1965
|
+
`claude adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
return selection.modelId;
|
|
1969
|
+
}
|
|
1857
1970
|
var ClaudeSession = class {
|
|
1858
|
-
constructor(sessionRef, client, workspaceDir, approvalChannel,
|
|
1971
|
+
constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
|
|
1859
1972
|
this.sessionRef = sessionRef;
|
|
1860
1973
|
this.client = client;
|
|
1861
1974
|
this.workspaceDir = workspaceDir;
|
|
1862
1975
|
this.approvalChannel = approvalChannel;
|
|
1863
|
-
this.
|
|
1976
|
+
this.mcpConfigDir = mcpConfigDir;
|
|
1977
|
+
this.modelId = modelId;
|
|
1864
1978
|
}
|
|
1865
1979
|
sessionRef;
|
|
1866
1980
|
client;
|
|
1867
1981
|
workspaceDir;
|
|
1868
1982
|
approvalChannel;
|
|
1869
|
-
|
|
1983
|
+
mcpConfigDir;
|
|
1984
|
+
modelId;
|
|
1870
1985
|
correlation = createToolUseCorrelation();
|
|
1871
1986
|
get events() {
|
|
1872
1987
|
const client = this.client;
|
|
@@ -1917,6 +2032,12 @@ var ClaudeSession = class {
|
|
|
1917
2032
|
if (typeof task.instruction !== "string") {
|
|
1918
2033
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1919
2034
|
}
|
|
2035
|
+
const requestedModel = subscriptionModel(task, "claude");
|
|
2036
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2037
|
+
throw new PolicyUnsupportedError(
|
|
2038
|
+
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
1920
2041
|
this.client.writeUserMessage(task.instruction);
|
|
1921
2042
|
}
|
|
1922
2043
|
/**
|
|
@@ -1936,7 +2057,7 @@ var ClaudeSession = class {
|
|
|
1936
2057
|
}
|
|
1937
2058
|
async close() {
|
|
1938
2059
|
this.client.kill();
|
|
1939
|
-
await
|
|
2060
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
1940
2061
|
}
|
|
1941
2062
|
/**
|
|
1942
2063
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -2260,6 +2381,7 @@ var CodexAdapter = class {
|
|
|
2260
2381
|
this.options = options;
|
|
2261
2382
|
}
|
|
2262
2383
|
options;
|
|
2384
|
+
supportsDispatchSelection = true;
|
|
2263
2385
|
id = "codex";
|
|
2264
2386
|
async detect() {
|
|
2265
2387
|
const bin = this.resolveBin();
|
|
@@ -2332,17 +2454,20 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2332
2454
|
if (!mapping.ok) {
|
|
2333
2455
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2334
2456
|
}
|
|
2457
|
+
const modelId = subscriptionModel2(task);
|
|
2335
2458
|
const bin = this.resolveBin();
|
|
2336
2459
|
const queue = new AsyncQueue();
|
|
2337
2460
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
2338
2461
|
const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
|
|
2462
|
+
const runtimeEnv = withoutProviderCredentials(ctx.env);
|
|
2339
2463
|
const { sessionRef, runner } = await runCodexTurn({
|
|
2340
2464
|
command: bin.command,
|
|
2341
2465
|
resumeRef: task.sessionRef,
|
|
2342
2466
|
instruction: task.instruction,
|
|
2467
|
+
modelId,
|
|
2343
2468
|
policyArgs: mapping.args,
|
|
2344
2469
|
cwd: ctx.workspaceDir,
|
|
2345
|
-
env:
|
|
2470
|
+
env: runtimeEnv,
|
|
2346
2471
|
spawnFn: this.options.spawnFn,
|
|
2347
2472
|
workspaceDir,
|
|
2348
2473
|
queue,
|
|
@@ -2359,7 +2484,8 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2359
2484
|
queue,
|
|
2360
2485
|
recordUnmapped,
|
|
2361
2486
|
initialRunner: runner,
|
|
2362
|
-
preparedGit: ctx.gitWorkspace !== void 0
|
|
2487
|
+
preparedGit: ctx.gitWorkspace !== void 0,
|
|
2488
|
+
modelId
|
|
2363
2489
|
});
|
|
2364
2490
|
}
|
|
2365
2491
|
resolveBin() {
|
|
@@ -2380,12 +2506,25 @@ function makeUnmappedFrameRecorder(counts) {
|
|
|
2380
2506
|
}
|
|
2381
2507
|
};
|
|
2382
2508
|
}
|
|
2383
|
-
function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
|
|
2509
|
+
function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
|
|
2384
2510
|
const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
|
|
2385
|
-
return [
|
|
2511
|
+
return [
|
|
2512
|
+
...base,
|
|
2513
|
+
"--json",
|
|
2514
|
+
...modelId ? ["--model", modelId] : [],
|
|
2515
|
+
...preparedGit ? [] : ["--skip-git-repo-check"],
|
|
2516
|
+
...policyArgs,
|
|
2517
|
+
instruction
|
|
2518
|
+
];
|
|
2386
2519
|
}
|
|
2387
2520
|
async function runCodexTurn(params) {
|
|
2388
|
-
const argv = buildArgv(
|
|
2521
|
+
const argv = buildArgv(
|
|
2522
|
+
params.resumeRef,
|
|
2523
|
+
params.policyArgs,
|
|
2524
|
+
params.instruction,
|
|
2525
|
+
params.modelId,
|
|
2526
|
+
params.preparedGit
|
|
2527
|
+
);
|
|
2389
2528
|
let firstLineSettled = false;
|
|
2390
2529
|
let resolveFirstLine;
|
|
2391
2530
|
let rejectFirstLine;
|
|
@@ -2486,6 +2625,7 @@ var CodexSession = class {
|
|
|
2486
2625
|
queue;
|
|
2487
2626
|
recordUnmapped;
|
|
2488
2627
|
preparedGit;
|
|
2628
|
+
modelId;
|
|
2489
2629
|
currentRunner;
|
|
2490
2630
|
closed = false;
|
|
2491
2631
|
constructor(options) {
|
|
@@ -2497,6 +2637,7 @@ var CodexSession = class {
|
|
|
2497
2637
|
this.queue = options.queue;
|
|
2498
2638
|
this.recordUnmapped = options.recordUnmapped;
|
|
2499
2639
|
this.preparedGit = options.preparedGit;
|
|
2640
|
+
this.modelId = options.modelId;
|
|
2500
2641
|
this.currentRunner = options.initialRunner;
|
|
2501
2642
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
2502
2643
|
}
|
|
@@ -2557,6 +2698,13 @@ var CodexSession = class {
|
|
|
2557
2698
|
if (!mapping.ok) {
|
|
2558
2699
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2559
2700
|
}
|
|
2701
|
+
const requestedModel = subscriptionModel2(task);
|
|
2702
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2703
|
+
throw new PolicyUnsupportedError(
|
|
2704
|
+
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
const modelId = this.modelId;
|
|
2560
2708
|
const resumeRef = this.sessionRef;
|
|
2561
2709
|
let sessionRef;
|
|
2562
2710
|
let runner;
|
|
@@ -2565,9 +2713,10 @@ var CodexSession = class {
|
|
|
2565
2713
|
command: this.command,
|
|
2566
2714
|
resumeRef,
|
|
2567
2715
|
instruction: task.instruction,
|
|
2716
|
+
modelId,
|
|
2568
2717
|
policyArgs: mapping.args,
|
|
2569
2718
|
cwd: this.workspaceDir,
|
|
2570
|
-
env: this.env,
|
|
2719
|
+
env: withoutProviderCredentials(this.env),
|
|
2571
2720
|
spawnFn: this.spawnFn,
|
|
2572
2721
|
workspaceDir: this.workspaceDir,
|
|
2573
2722
|
queue: this.queue,
|
|
@@ -2627,6 +2776,16 @@ var CodexSession = class {
|
|
|
2627
2776
|
);
|
|
2628
2777
|
}
|
|
2629
2778
|
};
|
|
2779
|
+
function subscriptionModel2(task) {
|
|
2780
|
+
const selection = task.dispatchSelection;
|
|
2781
|
+
if (selection === void 0) return void 0;
|
|
2782
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
2783
|
+
throw new PolicyUnsupportedError(
|
|
2784
|
+
`codex adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
return selection.modelId;
|
|
2788
|
+
}
|
|
2630
2789
|
|
|
2631
2790
|
// src/daemon/approvals.ts
|
|
2632
2791
|
var ApprovalNotFoundError = class extends Error {
|
|
@@ -2851,13 +3010,10 @@ function exportPrivateKeyPem(privateKey) {
|
|
|
2851
3010
|
function importPrivateKeyPem(pem) {
|
|
2852
3011
|
return createPrivateKey(pem);
|
|
2853
3012
|
}
|
|
2854
|
-
var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
|
|
2855
3013
|
function signNonce(privateKey, nonce) {
|
|
2856
|
-
const signature = sign(null,
|
|
3014
|
+
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
2857
3015
|
return signature.toString("base64url");
|
|
2858
3016
|
}
|
|
2859
|
-
|
|
2860
|
-
// src/daemon/url.ts
|
|
2861
3017
|
function toHttpBase(serverUrl) {
|
|
2862
3018
|
const url = new URL(serverUrl);
|
|
2863
3019
|
if (url.protocol === "ws:") url.protocol = "http:";
|
|
@@ -2867,7 +3023,7 @@ function toHttpBase(serverUrl) {
|
|
|
2867
3023
|
return url.toString();
|
|
2868
3024
|
}
|
|
2869
3025
|
function toWsUrl(serverUrl) {
|
|
2870
|
-
const url = new URL(
|
|
3026
|
+
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
2871
3027
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2872
3028
|
return url.toString();
|
|
2873
3029
|
}
|
|
@@ -2955,7 +3111,7 @@ var AuthManager = class {
|
|
|
2955
3111
|
return await this.runCredentialMutation(async () => {
|
|
2956
3112
|
const existing = this.record ?? await this.opts.store.load();
|
|
2957
3113
|
const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
|
|
2958
|
-
const url = new URL(
|
|
3114
|
+
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
2959
3115
|
const res = await fetch(url, {
|
|
2960
3116
|
method: "POST",
|
|
2961
3117
|
headers: { "content-type": "application/json" },
|
|
@@ -2989,6 +3145,7 @@ var AuthManager = class {
|
|
|
2989
3145
|
async getValidAccessToken() {
|
|
2990
3146
|
if (!this.record) throw new Error("device is not paired yet; call pair(pairingCode) first");
|
|
2991
3147
|
if (this.revoked) throw new DeviceRevokedError();
|
|
3148
|
+
if (this.renewing) return this.renewing;
|
|
2992
3149
|
if (msUntilExpiry(this.record.expiresAt) > RENEW_MARGIN_MS) return this.record.accessToken;
|
|
2993
3150
|
return this.renew();
|
|
2994
3151
|
}
|
|
@@ -3017,7 +3174,7 @@ var AuthManager = class {
|
|
|
3017
3174
|
const record = this.record;
|
|
3018
3175
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3019
3176
|
const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
|
|
3020
|
-
const challengeRes = await fetch(new URL(
|
|
3177
|
+
const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
|
|
3021
3178
|
method: "POST",
|
|
3022
3179
|
headers: { "content-type": "application/json" },
|
|
3023
3180
|
body: JSON.stringify({ deviceId: record.deviceId })
|
|
@@ -3030,7 +3187,7 @@ var AuthManager = class {
|
|
|
3030
3187
|
}
|
|
3031
3188
|
const { nonce } = await challengeRes.json();
|
|
3032
3189
|
const signature = signNonce(privateKey, nonce);
|
|
3033
|
-
const tokenRes = await fetch(new URL(
|
|
3190
|
+
const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
|
|
3034
3191
|
method: "POST",
|
|
3035
3192
|
headers: { "content-type": "application/json" },
|
|
3036
3193
|
body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
|
|
@@ -3128,7 +3285,7 @@ var BlobClient = class {
|
|
|
3128
3285
|
async resolveInstruction(blobRef) {
|
|
3129
3286
|
const base = toHttpBase(this.serverUrl);
|
|
3130
3287
|
const urlRes = await authedFetch(
|
|
3131
|
-
new URL(
|
|
3288
|
+
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
3132
3289
|
{ method: "GET" },
|
|
3133
3290
|
this.auth
|
|
3134
3291
|
);
|
|
@@ -3156,7 +3313,7 @@ var BlobClient = class {
|
|
|
3156
3313
|
const base = toHttpBase(this.serverUrl);
|
|
3157
3314
|
const reservationId = `blob_${randomUUID()}`;
|
|
3158
3315
|
const createRes = await authedFetch(
|
|
3159
|
-
new URL(
|
|
3316
|
+
new URL(BYOK_BLOBS_PATH, base),
|
|
3160
3317
|
{
|
|
3161
3318
|
method: "POST",
|
|
3162
3319
|
headers: {
|
|
@@ -3188,7 +3345,7 @@ var BlobClient = class {
|
|
|
3188
3345
|
let response;
|
|
3189
3346
|
try {
|
|
3190
3347
|
response = await authedFetch(
|
|
3191
|
-
new URL(
|
|
3348
|
+
new URL(byokBlobFinalizePath(blobId), base),
|
|
3192
3349
|
{
|
|
3193
3350
|
method: "POST",
|
|
3194
3351
|
headers: { "idempotency-key": reservationId }
|
|
@@ -3211,16 +3368,177 @@ var BlobClient = class {
|
|
|
3211
3368
|
throw lastFailure;
|
|
3212
3369
|
}
|
|
3213
3370
|
};
|
|
3371
|
+
var PRESENCE_HINTS_CAPABILITY = "presence.hints";
|
|
3372
|
+
var CapabilityDiscoveryError = class extends Error {
|
|
3373
|
+
constructor(message, options) {
|
|
3374
|
+
super(message, options);
|
|
3375
|
+
this.name = "CapabilityDiscoveryError";
|
|
3376
|
+
}
|
|
3377
|
+
};
|
|
3378
|
+
async function fetchCapabilityDeclaration(serverUrl, options = {}) {
|
|
3379
|
+
const url = new URL(BYOK_CAPABILITIES_PATH, toHttpBase(serverUrl));
|
|
3380
|
+
let response;
|
|
3381
|
+
try {
|
|
3382
|
+
response = await fetch(url, {
|
|
3383
|
+
method: "GET",
|
|
3384
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
3385
|
+
});
|
|
3386
|
+
} catch (err) {
|
|
3387
|
+
throw new CapabilityDiscoveryError(
|
|
3388
|
+
`failed to read the capability declaration from ${url.toString()}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3389
|
+
{ cause: err }
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
if (!response.ok) {
|
|
3393
|
+
throw new CapabilityDiscoveryError(
|
|
3394
|
+
`failed to read the capability declaration from ${url.toString()}: HTTP ${response.status}`
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
let body;
|
|
3398
|
+
try {
|
|
3399
|
+
body = await response.json();
|
|
3400
|
+
} catch (err) {
|
|
3401
|
+
throw new CapabilityDiscoveryError(
|
|
3402
|
+
`the capability declaration at ${url.toString()} is not JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
3403
|
+
{ cause: err }
|
|
3404
|
+
);
|
|
3405
|
+
}
|
|
3406
|
+
const parsed = CapabilityDeclarationSchema.safeParse(body);
|
|
3407
|
+
if (!parsed.success) {
|
|
3408
|
+
throw new CapabilityDiscoveryError(
|
|
3409
|
+
`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("; ")}`,
|
|
3410
|
+
{ cause: parsed.error }
|
|
3411
|
+
);
|
|
3412
|
+
}
|
|
3413
|
+
return parsed.data;
|
|
3414
|
+
}
|
|
3415
|
+
function declares(declaration, capability) {
|
|
3416
|
+
return hasCapability(declaration, capability);
|
|
3417
|
+
}
|
|
3418
|
+
var DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3419
|
+
var DEFAULT_PRESENCE_TTL_MS = 9e4;
|
|
3420
|
+
var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
|
|
3421
|
+
function assertPresenceHeartbeatCadence(cadence) {
|
|
3422
|
+
const { intervalMs, ttlMs, minimumIntervalMs } = cadence;
|
|
3423
|
+
if (!(minimumIntervalMs < intervalMs && intervalMs < ttlMs)) {
|
|
3424
|
+
throw new Error(
|
|
3425
|
+
`presence heartbeat interval must satisfy minimumIntervalMs < intervalMs < ttlMs \u2014 got ${minimumIntervalMs} < ${intervalMs} < ${ttlMs}`
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
var PresencePublisher = class {
|
|
3430
|
+
constructor(opts) {
|
|
3431
|
+
this.opts = opts;
|
|
3432
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS;
|
|
3433
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_PRESENCE_TTL_MS;
|
|
3434
|
+
const minimumIntervalMs = opts.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS;
|
|
3435
|
+
assertPresenceHeartbeatCadence({ intervalMs, ttlMs, minimumIntervalMs });
|
|
3436
|
+
this.intervalMs = intervalMs;
|
|
3437
|
+
this.url = new URL(BYOK_PRESENCE_PATH, toHttpBase(opts.serverUrl));
|
|
3438
|
+
}
|
|
3439
|
+
opts;
|
|
3440
|
+
url;
|
|
3441
|
+
intervalMs;
|
|
3442
|
+
timer;
|
|
3443
|
+
running = false;
|
|
3444
|
+
/** Set once a revoked device is observed. Terminal: `start()` will not restart this instance. */
|
|
3445
|
+
stoppedPermanently = false;
|
|
3446
|
+
/** Publishes immediately, then every `intervalMs`. Idempotent; a no-op after a permanent stop. */
|
|
3447
|
+
start() {
|
|
3448
|
+
if (this.running || this.stoppedPermanently) return;
|
|
3449
|
+
this.running = true;
|
|
3450
|
+
void this.beat();
|
|
3451
|
+
}
|
|
3452
|
+
/** Stops the cadence. Idempotent, and the only "offline" signal this producer emits — the hint's TTL does the rest. */
|
|
3453
|
+
stop() {
|
|
3454
|
+
this.running = false;
|
|
3455
|
+
if (this.timer !== void 0) {
|
|
3456
|
+
clearTimeout(this.timer);
|
|
3457
|
+
this.timer = void 0;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
schedule() {
|
|
3461
|
+
if (!this.running) return;
|
|
3462
|
+
this.timer = setTimeout(() => {
|
|
3463
|
+
this.timer = void 0;
|
|
3464
|
+
void this.beat();
|
|
3465
|
+
}, this.intervalMs);
|
|
3466
|
+
this.timer.unref?.();
|
|
3467
|
+
}
|
|
3468
|
+
async beat() {
|
|
3469
|
+
if (!this.running) return;
|
|
3470
|
+
try {
|
|
3471
|
+
const response = await authedFetch(
|
|
3472
|
+
this.url,
|
|
3473
|
+
{
|
|
3474
|
+
method: "PUT",
|
|
3475
|
+
headers: { "content-type": "application/json" },
|
|
3476
|
+
body: JSON.stringify({ level: "online" })
|
|
3477
|
+
},
|
|
3478
|
+
this.opts.auth
|
|
3479
|
+
);
|
|
3480
|
+
if (!response.ok) {
|
|
3481
|
+
if (response.status === 401) {
|
|
3482
|
+
this.stopPermanently(`presence heartbeat unauthorized after token renewal (HTTP 401)`);
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
this.opts.onDegraded?.(`presence heartbeat failed: HTTP ${response.status}`);
|
|
3486
|
+
}
|
|
3487
|
+
} catch (err) {
|
|
3488
|
+
if (err instanceof DeviceRevokedError) {
|
|
3489
|
+
this.stopPermanently("presence heartbeat stopped: device has been revoked; re-pair required");
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
this.opts.onDegraded?.(`presence heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3493
|
+
}
|
|
3494
|
+
this.schedule();
|
|
3495
|
+
}
|
|
3496
|
+
stopPermanently(reason) {
|
|
3497
|
+
this.stoppedPermanently = true;
|
|
3498
|
+
this.stop();
|
|
3499
|
+
this.opts.onDegraded?.(reason);
|
|
3500
|
+
}
|
|
3501
|
+
};
|
|
3502
|
+
function freshJti() {
|
|
3503
|
+
return randomBytes(16).toString("base64url");
|
|
3504
|
+
}
|
|
3505
|
+
function mintDeviceAssertion(input) {
|
|
3506
|
+
const issuedAtMs = input.now.getTime();
|
|
3507
|
+
const expiresAt = new Date(issuedAtMs + input.ttlMs).toISOString();
|
|
3508
|
+
const claims = DeviceAssertionClaimsSchema.parse({
|
|
3509
|
+
version: 1,
|
|
3510
|
+
issuer: input.issuer,
|
|
3511
|
+
productId: input.productId,
|
|
3512
|
+
deviceId: input.record.deviceId,
|
|
3513
|
+
audience: input.audience,
|
|
3514
|
+
jti: freshJti(),
|
|
3515
|
+
issuedAt: new Date(issuedAtMs).toISOString(),
|
|
3516
|
+
expiresAt
|
|
3517
|
+
});
|
|
3518
|
+
const privateKey = importPrivateKeyPem(input.record.devicePrivateKeyPem);
|
|
3519
|
+
const signature = sign(null, deviceAssertionSigningInput(claims), privateKey).toString("base64url");
|
|
3520
|
+
return {
|
|
3521
|
+
envelope: {
|
|
3522
|
+
schema: DEVICE_ASSERTION_SCHEMA_ID,
|
|
3523
|
+
algorithm: "ed25519",
|
|
3524
|
+
protected: claims,
|
|
3525
|
+
signature
|
|
3526
|
+
},
|
|
3527
|
+
claims,
|
|
3528
|
+
expiresAt
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3214
3531
|
var CONTROL_PROTOCOL_VERSION = 1;
|
|
3215
3532
|
var HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
3216
3533
|
var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
|
|
3534
|
+
var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
|
|
3217
3535
|
function shortHash(input) {
|
|
3218
3536
|
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
|
|
3219
3537
|
}
|
|
3220
3538
|
function controlSocketPath(storeDir) {
|
|
3221
3539
|
const candidate = path20.join(storeDir, "control.sock");
|
|
3222
3540
|
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
|
|
3223
|
-
return path20.join(
|
|
3541
|
+
return path20.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
|
|
3224
3542
|
}
|
|
3225
3543
|
function controlPipeName(productId, storeDir) {
|
|
3226
3544
|
const id = shortHash(`${productId}|${path20.resolve(storeDir)}`);
|
|
@@ -3326,6 +3644,16 @@ function parseApprovalsRequestParams(value) {
|
|
|
3326
3644
|
if (typeof value.summary !== "string") return void 0;
|
|
3327
3645
|
return { taskId: value.taskId, summary: value.summary };
|
|
3328
3646
|
}
|
|
3647
|
+
var ASSERTION_AUDIENCE_MAX_BYTES = 256;
|
|
3648
|
+
function parseAssertionIssueParams(value) {
|
|
3649
|
+
if (!isRecord2(value)) return void 0;
|
|
3650
|
+
const keys = Object.keys(value);
|
|
3651
|
+
if (keys.length !== 1 || keys[0] !== "audience") return void 0;
|
|
3652
|
+
const { audience } = value;
|
|
3653
|
+
if (typeof audience !== "string" || audience.length === 0) return void 0;
|
|
3654
|
+
if (Buffer.byteLength(audience, "utf8") > ASSERTION_AUDIENCE_MAX_BYTES) return void 0;
|
|
3655
|
+
return { audience };
|
|
3656
|
+
}
|
|
3329
3657
|
function parseShutdownParams(value) {
|
|
3330
3658
|
if (!isRecord2(value)) return {};
|
|
3331
3659
|
return value.reason === "unpair" || value.reason === "operator" ? { reason: value.reason } : {};
|
|
@@ -3658,7 +3986,7 @@ var LongPollClient = class {
|
|
|
3658
3986
|
try {
|
|
3659
3987
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3660
3988
|
const res = await authedFetch(
|
|
3661
|
-
new URL(
|
|
3989
|
+
new URL(BYOK_MESSAGES_PATH, base),
|
|
3662
3990
|
{
|
|
3663
3991
|
method: "POST",
|
|
3664
3992
|
headers: { "content-type": "application/json" },
|
|
@@ -3682,7 +4010,7 @@ var LongPollClient = class {
|
|
|
3682
4010
|
while (this.running) {
|
|
3683
4011
|
try {
|
|
3684
4012
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3685
|
-
const url = new URL(
|
|
4013
|
+
const url = new URL(BYOK_EVENTS_PATH, base);
|
|
3686
4014
|
const cursor = this.opts.getCursor();
|
|
3687
4015
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
3688
4016
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
@@ -4945,19 +5273,19 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
|
4945
5273
|
var MAX_OWNER_BYTES = 4096;
|
|
4946
5274
|
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
4947
5275
|
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
4948
|
-
var STORE_MUTEX_PORT_BASE = 1e4;
|
|
4949
|
-
var STORE_MUTEX_PORT_COUNT = 2e4;
|
|
4950
|
-
var STORE_MUTEX_PORT_CANDIDATES = 32;
|
|
4951
5276
|
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
4952
5277
|
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
5278
|
+
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
5279
|
+
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
5280
|
+
var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
|
|
4953
5281
|
function storeMutexIdentity(canonicalStoreDir) {
|
|
4954
5282
|
return createHash("sha256").update(canonicalStoreDir).digest("hex");
|
|
4955
5283
|
}
|
|
4956
|
-
function
|
|
4957
|
-
|
|
4958
|
-
const
|
|
4959
|
-
|
|
4960
|
-
return
|
|
5284
|
+
function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
|
|
5285
|
+
if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
|
|
5286
|
+
const candidate = path20.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
|
|
5287
|
+
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
|
|
5288
|
+
return path20.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
|
|
4961
5289
|
}
|
|
4962
5290
|
var DaemonOwnerActiveError = class extends Error {
|
|
4963
5291
|
constructor(role) {
|
|
@@ -5078,70 +5406,90 @@ async function createLivenessListener() {
|
|
|
5078
5406
|
})
|
|
5079
5407
|
};
|
|
5080
5408
|
}
|
|
5081
|
-
async function probeStoreMutex(
|
|
5409
|
+
async function probeStoreMutex(endpoint, identity) {
|
|
5082
5410
|
return new Promise((resolve) => {
|
|
5083
|
-
const socket = createConnection(
|
|
5411
|
+
const socket = createConnection(endpoint);
|
|
5084
5412
|
let settled = false;
|
|
5085
5413
|
let raw = "";
|
|
5086
5414
|
const finish = (result) => {
|
|
5087
5415
|
if (settled) return;
|
|
5088
5416
|
settled = true;
|
|
5417
|
+
clearTimeout(timer);
|
|
5418
|
+
socket.removeAllListeners();
|
|
5089
5419
|
socket.destroy();
|
|
5090
5420
|
resolve(result);
|
|
5091
5421
|
};
|
|
5422
|
+
const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
|
|
5092
5423
|
socket.setEncoding("utf8");
|
|
5093
|
-
socket.setTimeout(STORE_MUTEX_PROBE_TIMEOUT_MS, () => finish({ kind: "uncertain" }));
|
|
5094
5424
|
socket.on("data", (chunk) => {
|
|
5095
5425
|
raw += chunk;
|
|
5096
|
-
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "
|
|
5426
|
+
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
|
|
5097
5427
|
});
|
|
5098
|
-
socket.once("end", () => {
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
finish(
|
|
5102
|
-
|
|
5103
|
-
);
|
|
5104
|
-
});
|
|
5105
|
-
socket.once("error", () => finish({ kind: "foreign-or-gone" }));
|
|
5428
|
+
socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
|
|
5429
|
+
socket.once(
|
|
5430
|
+
"error",
|
|
5431
|
+
(err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
|
|
5432
|
+
);
|
|
5106
5433
|
});
|
|
5107
5434
|
}
|
|
5435
|
+
async function clearStaleStoreMutexSocket(endpoint, identity) {
|
|
5436
|
+
let stat;
|
|
5437
|
+
try {
|
|
5438
|
+
stat = await promises.lstat(endpoint);
|
|
5439
|
+
} catch (err) {
|
|
5440
|
+
if (err.code === "ENOENT") return;
|
|
5441
|
+
throw err;
|
|
5442
|
+
}
|
|
5443
|
+
if (!stat.isSocket()) throw new Error("store mutation lock path exists but is not a socket");
|
|
5444
|
+
if ((await probeStoreMutex(endpoint, identity)).kind !== "unbound") throw new DaemonOwnerActiveError("unknown");
|
|
5445
|
+
await promises.rm(endpoint, { force: true });
|
|
5446
|
+
}
|
|
5447
|
+
async function assertOwnedPrivateDir2(dir) {
|
|
5448
|
+
const uid = process.getuid?.();
|
|
5449
|
+
if (uid === void 0) return;
|
|
5450
|
+
const stat = await promises.lstat(dir);
|
|
5451
|
+
if (stat.isSymbolicLink() || stat.uid !== uid) {
|
|
5452
|
+
throw new Error(`refusing to bind the store mutation lock under "${dir}": not a real directory owned by this process's own uid`);
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5108
5455
|
async function acquireStoreMutex(canonicalStoreDir) {
|
|
5109
5456
|
const identity = storeMutexIdentity(canonicalStoreDir);
|
|
5110
|
-
|
|
5111
|
-
|
|
5457
|
+
const endpoint = storeMutexEndpoint(canonicalStoreDir, identity);
|
|
5458
|
+
const isPipe = process.platform === "win32";
|
|
5459
|
+
if (!isPipe) {
|
|
5460
|
+
const endpointDir = path20.dirname(endpoint);
|
|
5461
|
+
if (endpointDir !== canonicalStoreDir) {
|
|
5462
|
+
await ensureSecureDir(endpointDir);
|
|
5463
|
+
await assertOwnedPrivateDir2(endpointDir);
|
|
5464
|
+
}
|
|
5465
|
+
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
5466
|
+
}
|
|
5467
|
+
const server = createServer((socket) => socket.end(`${STORE_MUTEX_ID_PREFIX}${identity}
|
|
5112
5468
|
`));
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
server.
|
|
5118
|
-
|
|
5119
|
-
resolve();
|
|
5120
|
-
});
|
|
5469
|
+
try {
|
|
5470
|
+
await new Promise((resolve, reject) => {
|
|
5471
|
+
server.once("error", reject);
|
|
5472
|
+
server.listen(endpoint, () => {
|
|
5473
|
+
server.removeListener("error", reject);
|
|
5474
|
+
resolve();
|
|
5121
5475
|
});
|
|
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
|
-
};
|
|
5476
|
+
});
|
|
5477
|
+
} catch (err) {
|
|
5478
|
+
if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
|
|
5479
|
+
throw err;
|
|
5143
5480
|
}
|
|
5144
|
-
|
|
5481
|
+
if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
5482
|
+
server.unref();
|
|
5483
|
+
let closed = false;
|
|
5484
|
+
return {
|
|
5485
|
+
endpoint,
|
|
5486
|
+
close: async () => {
|
|
5487
|
+
if (closed) return;
|
|
5488
|
+
closed = true;
|
|
5489
|
+
await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
|
|
5490
|
+
if (!isPipe) await promises.rm(endpoint, { force: true }).catch(() => void 0);
|
|
5491
|
+
}
|
|
5492
|
+
};
|
|
5145
5493
|
}
|
|
5146
5494
|
async function reclaimExistsAndIsActive(reclaimPath) {
|
|
5147
5495
|
let stat;
|
|
@@ -5257,6 +5605,7 @@ function toRuntimeInfoCapabilities(caps) {
|
|
|
5257
5605
|
steer: caps.steer,
|
|
5258
5606
|
resume: caps.resume,
|
|
5259
5607
|
approvalInteractive: caps.approvalInteractive,
|
|
5608
|
+
...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
|
|
5260
5609
|
permissionModes: caps.permissionModes
|
|
5261
5610
|
};
|
|
5262
5611
|
}
|
|
@@ -5330,7 +5679,7 @@ var DaemonObserver = class {
|
|
|
5330
5679
|
}
|
|
5331
5680
|
/**
|
|
5332
5681
|
* Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
|
|
5333
|
-
*
|
|
5682
|
+
* either offer variant produces a local event here — every other inbound type
|
|
5334
5683
|
* (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
|
|
5335
5684
|
* best-effort notification whose OWN observable effect already surfaces
|
|
5336
5685
|
* through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
|
|
@@ -5338,7 +5687,7 @@ var DaemonObserver = class {
|
|
|
5338
5687
|
* where those are actually reported from.
|
|
5339
5688
|
*/
|
|
5340
5689
|
handleInboundEnvelope(envelope) {
|
|
5341
|
-
if (envelope.type !== "task.offer") return;
|
|
5690
|
+
if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets") return;
|
|
5342
5691
|
const taskId = envelope.task_id;
|
|
5343
5692
|
if (this.taskInfo.has(taskId)) return;
|
|
5344
5693
|
this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
|
|
@@ -5459,6 +5808,37 @@ var DaemonObserver = class {
|
|
|
5459
5808
|
noteShutdownComplete(reason, undeliveredOutboxCount) {
|
|
5460
5809
|
this.emit({ kind: "shutdown-complete", ts: nowIso(), reason, undeliveredOutboxCount });
|
|
5461
5810
|
}
|
|
5811
|
+
/**
|
|
5812
|
+
* Plan `device-assertion-broker`: see the `device-assertion` `DaemonEvent`
|
|
5813
|
+
* variant's own doc comment. The parameter type is what keeps the signature
|
|
5814
|
+
* out — there is no field to pass one through.
|
|
5815
|
+
*
|
|
5816
|
+
* codex round-2 F4: the DENIED caller can pass its raw `audience` here, but
|
|
5817
|
+
* it is converted to a byte SIZE the instant the event is constructed and the
|
|
5818
|
+
* raw string is dropped — it is never placed on the emitted `DaemonEvent`, so
|
|
5819
|
+
* it cannot reach a subscriber, `format.ts`, stdout, or the audit file. The
|
|
5820
|
+
* ISSUED `audience` came from the allowlist and is kept verbatim.
|
|
5821
|
+
*/
|
|
5822
|
+
noteDeviceAssertion(event) {
|
|
5823
|
+
if (event.result === "issued") {
|
|
5824
|
+
this.emit({
|
|
5825
|
+
kind: "device-assertion",
|
|
5826
|
+
ts: nowIso(),
|
|
5827
|
+
result: "issued",
|
|
5828
|
+
audience: event.audience,
|
|
5829
|
+
jti: event.jti,
|
|
5830
|
+
expiresAt: event.expiresAt
|
|
5831
|
+
});
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5834
|
+
this.emit({
|
|
5835
|
+
kind: "device-assertion",
|
|
5836
|
+
ts: nowIso(),
|
|
5837
|
+
result: "denied",
|
|
5838
|
+
reason: event.reason,
|
|
5839
|
+
audienceSize: event.audience === void 0 ? void 0 : Buffer.byteLength(event.audience, "utf8")
|
|
5840
|
+
});
|
|
5841
|
+
}
|
|
5462
5842
|
/** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
|
|
5463
5843
|
noteStaleApprovalDecision(taskId, decision, reason) {
|
|
5464
5844
|
this.emit({ kind: "stale-approval-decision", ts: nowIso(), taskId, decision, reason });
|
|
@@ -7097,6 +7477,21 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
|
|
|
7097
7477
|
var MAX_TRACKED_TASK_IDS = 2e3;
|
|
7098
7478
|
var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
|
|
7099
7479
|
var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
|
|
7480
|
+
var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
|
|
7481
|
+
function resultDocumentRejectionDetail(check) {
|
|
7482
|
+
switch (check.reason) {
|
|
7483
|
+
case "over-cap":
|
|
7484
|
+
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)`;
|
|
7485
|
+
case "not-serializable":
|
|
7486
|
+
return "not JSON-serializable (JSON.stringify threw, or produced no output at all)";
|
|
7487
|
+
case "not-plain-json":
|
|
7488
|
+
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)";
|
|
7489
|
+
default: {
|
|
7490
|
+
const exhaustive = check;
|
|
7491
|
+
throw new Error(`unhandled result document rejection: ${JSON.stringify(exhaustive)}`);
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7100
7495
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
7101
7496
|
function isKnownRuntimeId(id) {
|
|
7102
7497
|
return RuntimeIdSchema.safeParse(id).success;
|
|
@@ -7109,6 +7504,14 @@ function orderByPreference(candidates, preference) {
|
|
|
7109
7504
|
function adapterSupportsMode(adapter, mode) {
|
|
7110
7505
|
return adapter.capabilities().permissionModes.includes(mode);
|
|
7111
7506
|
}
|
|
7507
|
+
function adapterSupportsMcpToolsets(adapter) {
|
|
7508
|
+
return adapter.capabilities().mcpToolsets === true;
|
|
7509
|
+
}
|
|
7510
|
+
function withoutRequiredToolsets(payload) {
|
|
7511
|
+
if (!("requiredToolsets" in payload)) return payload;
|
|
7512
|
+
const { requiredToolsets, ...offer } = payload;
|
|
7513
|
+
return offer;
|
|
7514
|
+
}
|
|
7112
7515
|
function errorMessage3(err) {
|
|
7113
7516
|
return err instanceof Error ? err.message : String(err);
|
|
7114
7517
|
}
|
|
@@ -7464,6 +7867,9 @@ var TaskRunner = class {
|
|
|
7464
7867
|
case "task.offer":
|
|
7465
7868
|
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7466
7869
|
return;
|
|
7870
|
+
case "task.offer_with_toolsets":
|
|
7871
|
+
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7872
|
+
return;
|
|
7467
7873
|
case "task.cancel":
|
|
7468
7874
|
await this.handleCancel(envelope.task_id, envelope.payload.reason);
|
|
7469
7875
|
return;
|
|
@@ -7517,7 +7923,22 @@ var TaskRunner = class {
|
|
|
7517
7923
|
);
|
|
7518
7924
|
return;
|
|
7519
7925
|
}
|
|
7520
|
-
|
|
7926
|
+
if (payload.dispatchSelection !== void 0 && payload.runtime !== void 0 && payload.runtime !== payload.dispatchSelection.runtimeId) {
|
|
7927
|
+
this.decline(
|
|
7928
|
+
taskId,
|
|
7929
|
+
`offer runtime ${payload.runtime} does not match dispatchSelection.runtimeId ${payload.dispatchSelection.runtimeId}`,
|
|
7930
|
+
false
|
|
7931
|
+
);
|
|
7932
|
+
return;
|
|
7933
|
+
}
|
|
7934
|
+
const requiredToolsets = "requiredToolsets" in payload ? payload.requiredToolsets : void 0;
|
|
7935
|
+
const resolvedMcp = requiredToolsets ? this.resolveMcpServers(requiredToolsets) : void 0;
|
|
7936
|
+
if (resolvedMcp && !resolvedMcp.ok) {
|
|
7937
|
+
this.decline(taskId, resolvedMcp.reason, true);
|
|
7938
|
+
return;
|
|
7939
|
+
}
|
|
7940
|
+
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
7941
|
+
const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
|
|
7521
7942
|
if (!pick.ok) {
|
|
7522
7943
|
this.decline(taskId, pick.reason, pick.retryable);
|
|
7523
7944
|
return;
|
|
@@ -7680,6 +8101,7 @@ var TaskRunner = class {
|
|
|
7680
8101
|
const ctx = {
|
|
7681
8102
|
workspaceDir,
|
|
7682
8103
|
policy: decision.policy,
|
|
8104
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
|
|
7683
8105
|
...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
|
|
7684
8106
|
// M5: no longer `process.env` verbatim (see `environment.ts`'s own
|
|
7685
8107
|
// module doc comment for the credential-leak gap that closed) —
|
|
@@ -7716,7 +8138,7 @@ var TaskRunner = class {
|
|
|
7716
8138
|
}
|
|
7717
8139
|
};
|
|
7718
8140
|
const effectiveOffer = {
|
|
7719
|
-
...payload,
|
|
8141
|
+
...withoutRequiredToolsets(payload),
|
|
7720
8142
|
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
7721
8143
|
// Never forward a sessionRef this device has no recorded workspace
|
|
7722
8144
|
// for (stale, from another device, or simply made up) — an adapter
|
|
@@ -7794,6 +8216,36 @@ var TaskRunner = class {
|
|
|
7794
8216
|
if (typeof instruction === "string") return instruction;
|
|
7795
8217
|
return this.deps.blobClient.resolveInstruction(instruction.blobRef);
|
|
7796
8218
|
}
|
|
8219
|
+
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
8220
|
+
resolveMcpServers(requiredToolsets) {
|
|
8221
|
+
const registry = this.deps.mcpToolsets;
|
|
8222
|
+
if (!registry) {
|
|
8223
|
+
return { ok: false, reason: "offer requires MCP toolsets, but this device has no local mcpToolsets registry" };
|
|
8224
|
+
}
|
|
8225
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
8226
|
+
for (const toolsetId of requiredToolsets) {
|
|
8227
|
+
const toolset = registry.get(toolsetId);
|
|
8228
|
+
if (!toolset) {
|
|
8229
|
+
return { ok: false, reason: `required MCP toolset "${toolsetId}" is not configured on this device` };
|
|
8230
|
+
}
|
|
8231
|
+
for (const [serverName, server] of Object.entries(toolset.mcpServers)) {
|
|
8232
|
+
if (Object.prototype.hasOwnProperty.call(servers, serverName)) {
|
|
8233
|
+
return {
|
|
8234
|
+
ok: false,
|
|
8235
|
+
reason: `required MCP toolsets collide on server name "${serverName}"; refusing ambiguous projection`
|
|
8236
|
+
};
|
|
8237
|
+
}
|
|
8238
|
+
servers[serverName] = Object.freeze({
|
|
8239
|
+
command: server.command,
|
|
8240
|
+
...server.args ? { args: Object.freeze([...server.args]) } : {}
|
|
8241
|
+
});
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
if (Object.keys(servers).length === 0) {
|
|
8245
|
+
return { ok: false, reason: "required MCP toolsets resolved to no servers; refusing to run without tools" };
|
|
8246
|
+
}
|
|
8247
|
+
return { ok: true, servers: Object.freeze(servers) };
|
|
8248
|
+
}
|
|
7797
8249
|
async pump(active) {
|
|
7798
8250
|
try {
|
|
7799
8251
|
for await (const event of active.session.events) {
|
|
@@ -7827,11 +8279,38 @@ var TaskRunner = class {
|
|
|
7827
8279
|
if (event.type === "turn_end") {
|
|
7828
8280
|
active.batcher.push(event);
|
|
7829
8281
|
active.batcher.flush();
|
|
8282
|
+
const finalOutput = active.summaryParts.join("");
|
|
8283
|
+
const outcome = await this.resolveResultDocument(active, finalOutput);
|
|
8284
|
+
if (!outcome.deliver) return;
|
|
7830
8285
|
await this.observeGit(active, "completed");
|
|
8286
|
+
if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
|
|
8287
|
+
await this.fail(
|
|
8288
|
+
active.taskId,
|
|
8289
|
+
`${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`,
|
|
8290
|
+
false
|
|
8291
|
+
);
|
|
8292
|
+
return;
|
|
8293
|
+
}
|
|
7831
8294
|
this.deps.send(
|
|
7832
8295
|
createEnvelope(
|
|
7833
8296
|
"task.complete",
|
|
7834
|
-
{
|
|
8297
|
+
{
|
|
8298
|
+
summary: finalOutput,
|
|
8299
|
+
sessionRef: active.session.sessionRef,
|
|
8300
|
+
// Spread rather than `document: outcome.document`, so a
|
|
8301
|
+
// completion with no document is the exact same payload it
|
|
8302
|
+
// was before this field existed — not one carrying an
|
|
8303
|
+
// explicit `document: undefined` key.
|
|
8304
|
+
//
|
|
8305
|
+
// `outcome.document` is the protocol's CANONICAL SNAPSHOT
|
|
8306
|
+
// (`checkResultDocument`), never the object the extractor
|
|
8307
|
+
// returned: pure data serializes identically at the root
|
|
8308
|
+
// (where it was measured) and nested inside this payload
|
|
8309
|
+
// (where the codec actually serializes it), so a contextual
|
|
8310
|
+
// `toJSON(key)` or an unstable getter cannot make the wire
|
|
8311
|
+
// bytes differ from what the cap gate approved.
|
|
8312
|
+
...outcome.document !== void 0 ? { document: outcome.document } : {}
|
|
8313
|
+
},
|
|
7835
8314
|
{ taskId: active.taskId, sessionRef: active.session.sessionRef }
|
|
7836
8315
|
)
|
|
7837
8316
|
);
|
|
@@ -8369,6 +8848,102 @@ var TaskRunner = class {
|
|
|
8369
8848
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
|
|
8370
8849
|
await this.finish(taskId);
|
|
8371
8850
|
}
|
|
8851
|
+
/**
|
|
8852
|
+
* additive-minor (`task.complete.document`): the whole daemon-side gate
|
|
8853
|
+
* between a configured {@link ResultDocumentExtractor} and the wire —
|
|
8854
|
+
* called once, from the `turn_end` completion path, immediately before
|
|
8855
|
+
* `task.complete` is built.
|
|
8856
|
+
*
|
|
8857
|
+
* `{deliver: true}` means "go on and send `task.complete`", carrying the
|
|
8858
|
+
* document when there is one. `{deliver: false}` means this method has
|
|
8859
|
+
* ALREADY reported `task.fail` and finished the task; the caller must
|
|
8860
|
+
* return without sending anything further.
|
|
8861
|
+
*
|
|
8862
|
+
* Four fail-closed branches, all `retryable: false` (see
|
|
8863
|
+
* {@link RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX} for why none of them
|
|
8864
|
+
* can succeed on a retry):
|
|
8865
|
+
*
|
|
8866
|
+
* 1. The extractor threw — its error is surfaced, never swallowed.
|
|
8867
|
+
* 2. The extractor returned a thenable, violating the synchronous
|
|
8868
|
+
* contract in the one way that would otherwise ship a wrong answer.
|
|
8869
|
+
* 3. The document is over the cap, not JSON-serializable, or not plain
|
|
8870
|
+
* JSON data, per `checkResultDocument` — the protocol's OWN check,
|
|
8871
|
+
* imported rather than reimplemented, so this gate and the server's
|
|
8872
|
+
* schema validation can never disagree about what is legal.
|
|
8873
|
+
* 4. The connected server never advertised `result-document`. Its
|
|
8874
|
+
* tolerant `z.object()` would silently strip the field on arrival
|
|
8875
|
+
* (`version.ts`'s own flag doc comment), so "send anyway" is not a
|
|
8876
|
+
* degraded-but-working path — it is the task's primary structured
|
|
8877
|
+
* result being deleted in transit with nothing reported anywhere.
|
|
8878
|
+
*
|
|
8879
|
+
* The capability is checked LAST, deliberately: a document that is itself
|
|
8880
|
+
* invalid is the host's own bug and is worth reporting as such even when
|
|
8881
|
+
* the connected server could not have accepted any document at all. It is
|
|
8882
|
+
* then re-checked once more by the caller after its own last await, since
|
|
8883
|
+
* a reconnect can invalidate this answer in between (F3).
|
|
8884
|
+
*
|
|
8885
|
+
* **Residual window (bounded, deliberately not hacked around).** Even the
|
|
8886
|
+
* caller's re-check happens before `ConnectionManager.send` hands the
|
|
8887
|
+
* envelope to a transport, and a queued envelope can outlive the
|
|
8888
|
+
* connection it was queued for: a reconnect between `send()` and the
|
|
8889
|
+
* outbox actually draining could still deliver this `task.complete` to a
|
|
8890
|
+
* rolled-back N-1 server that strips the document. Closing that would
|
|
8891
|
+
* mean teaching the transport outbox to inspect payload semantics and
|
|
8892
|
+
* mint a substitute `task.fail` for a task this runner already finished —
|
|
8893
|
+
* a second authority over terminal outcomes living in the queue, which is
|
|
8894
|
+
* worse than the window it closes. Documented instead, here and in
|
|
8895
|
+
* docs/protocol.md §7.2.
|
|
8896
|
+
*/
|
|
8897
|
+
async resolveResultDocument(active, finalOutput) {
|
|
8898
|
+
const extract = this.deps.resultDocument?.extract;
|
|
8899
|
+
if (!extract) return { deliver: true };
|
|
8900
|
+
let document;
|
|
8901
|
+
try {
|
|
8902
|
+
document = extract(finalOutput, { taskId: active.taskId, sessionRef: active.session.sessionRef });
|
|
8903
|
+
} catch (err) {
|
|
8904
|
+
await this.fail(
|
|
8905
|
+
active.taskId,
|
|
8906
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage3(err)}`,
|
|
8907
|
+
false
|
|
8908
|
+
);
|
|
8909
|
+
return { deliver: false };
|
|
8910
|
+
}
|
|
8911
|
+
if (typeof document?.then === "function") {
|
|
8912
|
+
await this.fail(
|
|
8913
|
+
active.taskId,
|
|
8914
|
+
`${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)`,
|
|
8915
|
+
false
|
|
8916
|
+
);
|
|
8917
|
+
return { deliver: false };
|
|
8918
|
+
}
|
|
8919
|
+
if (document === void 0) return { deliver: true };
|
|
8920
|
+
const check = checkResultDocument(document);
|
|
8921
|
+
if (!check.ok) {
|
|
8922
|
+
const detail = resultDocumentRejectionDetail(check);
|
|
8923
|
+
await this.fail(active.taskId, `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: ${detail}`, false);
|
|
8924
|
+
return { deliver: false };
|
|
8925
|
+
}
|
|
8926
|
+
if (!this.hasResultDocumentCapability()) {
|
|
8927
|
+
await this.fail(
|
|
8928
|
+
active.taskId,
|
|
8929
|
+
`${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`,
|
|
8930
|
+
false
|
|
8931
|
+
);
|
|
8932
|
+
return { deliver: false };
|
|
8933
|
+
}
|
|
8934
|
+
return { deliver: true, document: check.canonical };
|
|
8935
|
+
}
|
|
8936
|
+
/**
|
|
8937
|
+
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
8938
|
+
* read fresh on every call, never captured, because the answer changes
|
|
8939
|
+
* across a reconnect (`ConnectionManager.getServerCapabilities` returns
|
|
8940
|
+
* `[]` from the moment an acked connection closes until a fresh
|
|
8941
|
+
* `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
|
|
8942
|
+
* "no capabilities", the fail-closed reading.
|
|
8943
|
+
*/
|
|
8944
|
+
hasResultDocumentCapability() {
|
|
8945
|
+
return (this.deps.getServerCapabilities?.() ?? []).includes("result-document");
|
|
8946
|
+
}
|
|
8372
8947
|
async observeGit(active, phase) {
|
|
8373
8948
|
if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
|
|
8374
8949
|
try {
|
|
@@ -8471,7 +9046,7 @@ var TaskRunner = class {
|
|
|
8471
9046
|
* is device-specific (which runtimes happen to be installed here), so a
|
|
8472
9047
|
* different device's installed runtime set might satisfy it.
|
|
8473
9048
|
*/
|
|
8474
|
-
async pickAdapter(requestedRuntime, policyMode) {
|
|
9049
|
+
async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
|
|
8475
9050
|
const allowlist = this.deps.runtimeAllowlist;
|
|
8476
9051
|
if (requestedRuntime) {
|
|
8477
9052
|
if (allowlist && !allowlist.includes(requestedRuntime)) {
|
|
@@ -8492,6 +9067,13 @@ var TaskRunner = class {
|
|
|
8492
9067
|
retryable: false
|
|
8493
9068
|
};
|
|
8494
9069
|
}
|
|
9070
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) {
|
|
9071
|
+
return {
|
|
9072
|
+
ok: false,
|
|
9073
|
+
reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
|
|
9074
|
+
retryable: false
|
|
9075
|
+
};
|
|
9076
|
+
}
|
|
8495
9077
|
const detected = await adapter.detect();
|
|
8496
9078
|
if (!detected.present) {
|
|
8497
9079
|
return {
|
|
@@ -8506,12 +9088,13 @@ var TaskRunner = class {
|
|
|
8506
9088
|
const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
|
|
8507
9089
|
for (const adapter of candidates) {
|
|
8508
9090
|
if (!adapterSupportsMode(adapter, policyMode)) continue;
|
|
9091
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) continue;
|
|
8509
9092
|
const detected = await adapter.detect();
|
|
8510
9093
|
if (detected.present) return { ok: true, adapter };
|
|
8511
9094
|
}
|
|
8512
9095
|
return {
|
|
8513
9096
|
ok: false,
|
|
8514
|
-
reason: `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
9097
|
+
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
9098
|
retryable: true
|
|
8516
9099
|
};
|
|
8517
9100
|
}
|
|
@@ -8542,7 +9125,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
|
|
|
8542
9125
|
bytes,
|
|
8543
9126
|
bytesHash: journalHash(bytes),
|
|
8544
9127
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8545
|
-
opensTask: envelope.type === "task.offer"
|
|
9128
|
+
opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets"
|
|
8546
9129
|
};
|
|
8547
9130
|
}
|
|
8548
9131
|
function isRuntimeId(id) {
|
|
@@ -8566,53 +9149,240 @@ function computeCapabilities(adapters) {
|
|
|
8566
9149
|
if (adapters.some((adapter) => adapter.capabilities().steer)) flags.push("steer");
|
|
8567
9150
|
flags.push("blob-upload");
|
|
8568
9151
|
flags.push("approval-targeting");
|
|
9152
|
+
const selectionAdapters = adapters.filter(
|
|
9153
|
+
(adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
|
|
9154
|
+
);
|
|
9155
|
+
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
|
|
9156
|
+
flags.push("dispatch-selection");
|
|
9157
|
+
}
|
|
9158
|
+
if (adapters.some((adapter) => adapter.capabilities().mcpToolsets === true)) {
|
|
9159
|
+
flags.push("toolset-selection");
|
|
9160
|
+
}
|
|
8569
9161
|
return flags;
|
|
8570
9162
|
}
|
|
8571
9163
|
var ALL_RUNTIME_IDS = ["pi", "claude", "codex"];
|
|
8572
|
-
function buildAdapter(id) {
|
|
9164
|
+
function buildAdapter(id, config) {
|
|
8573
9165
|
switch (id) {
|
|
8574
9166
|
case "pi":
|
|
8575
|
-
return new PiAdapter();
|
|
9167
|
+
return new PiAdapter({ byokLauncher: config.piByokLauncher });
|
|
8576
9168
|
case "claude":
|
|
8577
9169
|
return new ClaudeAdapter();
|
|
8578
9170
|
case "codex":
|
|
8579
9171
|
return new CodexAdapter();
|
|
8580
9172
|
}
|
|
8581
9173
|
}
|
|
8582
|
-
function buildDefaultAdapters(
|
|
8583
|
-
const ids = runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => runtimeAllowlist
|
|
8584
|
-
return ids.map(buildAdapter);
|
|
9174
|
+
function buildDefaultAdapters(config) {
|
|
9175
|
+
const ids = config.runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => config.runtimeAllowlist?.includes(id)) : ALL_RUNTIME_IDS;
|
|
9176
|
+
return ids.map((id) => buildAdapter(id, config));
|
|
8585
9177
|
}
|
|
8586
|
-
function
|
|
8587
|
-
|
|
9178
|
+
function validatePiByokLauncherConfig(launcher) {
|
|
9179
|
+
for (const [field, value] of [
|
|
9180
|
+
["command", launcher.command],
|
|
9181
|
+
["profileDbPath", launcher.profileDbPath],
|
|
9182
|
+
["sessionDir", launcher.sessionDir]
|
|
9183
|
+
]) {
|
|
9184
|
+
if (value.trim().length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
9185
|
+
throw new Error(`DaemonConfig.piByokLauncher.${field} must be a non-empty single-line string`);
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
9188
|
+
if (!isAbsolute(launcher.profileDbPath) || !isAbsolute(launcher.sessionDir)) {
|
|
8588
9189
|
throw new Error(
|
|
8589
|
-
|
|
9190
|
+
"DaemonConfig.piByokLauncher profileDbPath and sessionDir must be absolute paths"
|
|
8590
9191
|
);
|
|
8591
9192
|
}
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
let maintenanceSequence = 0;
|
|
8597
|
-
const cursorStore = new CursorStore(storeDir);
|
|
8598
|
-
const sessionWorkspaces = new SessionWorkspaceStore(storeDir);
|
|
8599
|
-
const gitWorkspaceManager = config.gitWorkspace ? overrides.gitWorkspace?.manager ?? new GitWorkspaceManager(config.workspaceRoot, { ownerId: stableGitWorkspaceOwnerId(storeDir, config.productId) }) : void 0;
|
|
8600
|
-
const gitWorkspaceStore = config.gitWorkspace ? overrides.gitWorkspace?.store ?? new GitWorkspaceStore(storeDir) : void 0;
|
|
8601
|
-
let resolvedStoragePolicy;
|
|
8602
|
-
if (config.hostedJournal) {
|
|
8603
|
-
if (config.hostedJournal.mode !== "sqlite") {
|
|
8604
|
-
throw new Error(`DaemonConfig.hostedJournal.mode must be "sqlite" \u2014 got ${JSON.stringify(config.hostedJournal.mode)}`);
|
|
8605
|
-
}
|
|
8606
|
-
if (typeof config.hostedJournal.tenantId !== "string" || config.hostedJournal.tenantId.trim() === "") {
|
|
8607
|
-
throw new Error(
|
|
8608
|
-
"DaemonConfig.hostedJournal.tenantId must be a non-empty tenant id \u2014 a hosted journal row with no tenant is durable evidence nobody can act on"
|
|
8609
|
-
);
|
|
8610
|
-
}
|
|
8611
|
-
if (config.hostedJournal.storagePolicy) {
|
|
8612
|
-
resolvedStoragePolicy = resolveLocalStoragePolicy(config.hostedJournal.storagePolicy);
|
|
8613
|
-
}
|
|
9193
|
+
if (launcher.secretServicePrefix !== void 0 && (launcher.secretServicePrefix.trim().length === 0 || /[\u0000\r\n]/u.test(launcher.secretServicePrefix))) {
|
|
9194
|
+
throw new Error(
|
|
9195
|
+
"DaemonConfig.piByokLauncher.secretServicePrefix must be a non-empty single-line string"
|
|
9196
|
+
);
|
|
8614
9197
|
}
|
|
8615
|
-
|
|
9198
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
9199
|
+
"--",
|
|
9200
|
+
"--pi-bin",
|
|
9201
|
+
"--profile-db",
|
|
9202
|
+
"--session-dir",
|
|
9203
|
+
"--secret-service-prefix",
|
|
9204
|
+
"--provider",
|
|
9205
|
+
"--model"
|
|
9206
|
+
]);
|
|
9207
|
+
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
9208
|
+
if (conflicting !== void 0) {
|
|
9209
|
+
throw new Error(
|
|
9210
|
+
`DaemonConfig.piByokLauncher.args must not override reserved launcher argument ${conflicting}`
|
|
9211
|
+
);
|
|
9212
|
+
}
|
|
9213
|
+
const invalidArg = launcher.args?.find((arg) => arg.length === 0 || /[\u0000\r\n]/u.test(arg));
|
|
9214
|
+
if (invalidArg !== void 0) {
|
|
9215
|
+
throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
|
|
9216
|
+
}
|
|
9217
|
+
}
|
|
9218
|
+
var MAX_LOCAL_MCP_TOOLSETS = 64;
|
|
9219
|
+
var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
|
|
9220
|
+
var MAX_LOCAL_MCP_ARGS = 64;
|
|
9221
|
+
var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
|
|
9222
|
+
function isNonEmptySingleLine(value) {
|
|
9223
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_LOCAL_MCP_TOKEN_CHARS && !/[\u0000\r\n]/u.test(value);
|
|
9224
|
+
}
|
|
9225
|
+
function resolveMcpToolsets(configured) {
|
|
9226
|
+
if (configured === void 0) return void 0;
|
|
9227
|
+
if (configured === null || typeof configured !== "object" || Array.isArray(configured)) {
|
|
9228
|
+
throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
|
|
9229
|
+
}
|
|
9230
|
+
const toolsetEntries = Object.entries(configured);
|
|
9231
|
+
if (toolsetEntries.length > MAX_LOCAL_MCP_TOOLSETS) {
|
|
9232
|
+
throw new Error(`DaemonConfig.mcpToolsets may contain at most ${MAX_LOCAL_MCP_TOOLSETS} toolsets`);
|
|
9233
|
+
}
|
|
9234
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
9235
|
+
for (const [toolsetId, rawToolset] of toolsetEntries) {
|
|
9236
|
+
const parsedId = ToolsetIdSchema.safeParse(toolsetId);
|
|
9237
|
+
if (!parsedId.success) {
|
|
9238
|
+
throw new Error(`DaemonConfig.mcpToolsets contains invalid toolset id ${JSON.stringify(toolsetId)}`);
|
|
9239
|
+
}
|
|
9240
|
+
if (rawToolset === null || typeof rawToolset !== "object" || Array.isArray(rawToolset)) {
|
|
9241
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} must be an object`);
|
|
9242
|
+
}
|
|
9243
|
+
const toolsetKeys = Object.keys(rawToolset);
|
|
9244
|
+
if (toolsetKeys.some((key) => key !== "mcpServers")) {
|
|
9245
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} accepts only the mcpServers field`);
|
|
9246
|
+
}
|
|
9247
|
+
const rawServers = rawToolset.mcpServers;
|
|
9248
|
+
if (rawServers === null || typeof rawServers !== "object" || Array.isArray(rawServers)) {
|
|
9249
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must be an object`);
|
|
9250
|
+
}
|
|
9251
|
+
const serverEntries = Object.entries(rawServers);
|
|
9252
|
+
if (serverEntries.length === 0 || serverEntries.length > MAX_LOCAL_MCP_SERVERS_PER_TOOLSET) {
|
|
9253
|
+
throw new Error(
|
|
9254
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must contain 1-${MAX_LOCAL_MCP_SERVERS_PER_TOOLSET} servers`
|
|
9255
|
+
);
|
|
9256
|
+
}
|
|
9257
|
+
const servers = {};
|
|
9258
|
+
for (const [serverName, rawServer] of serverEntries) {
|
|
9259
|
+
if (!ToolsetIdSchema.safeParse(serverName).success) {
|
|
9260
|
+
throw new Error(
|
|
9261
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers contains invalid server name ${JSON.stringify(serverName)}`
|
|
9262
|
+
);
|
|
9263
|
+
}
|
|
9264
|
+
if (serverName === APPROVAL_MCP_SERVER_NAME) {
|
|
9265
|
+
throw new Error(
|
|
9266
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} uses a server name reserved by the daemon`
|
|
9267
|
+
);
|
|
9268
|
+
}
|
|
9269
|
+
if (rawServer === null || typeof rawServer !== "object" || Array.isArray(rawServer)) {
|
|
9270
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} must be an object`);
|
|
9271
|
+
}
|
|
9272
|
+
const serverKeys = Object.keys(rawServer);
|
|
9273
|
+
if (serverKeys.some((key) => key !== "command" && key !== "args")) {
|
|
9274
|
+
throw new Error(
|
|
9275
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} accepts only command and args; env, headers, and remote task data are not supported`
|
|
9276
|
+
);
|
|
9277
|
+
}
|
|
9278
|
+
const server = rawServer;
|
|
9279
|
+
if (!isNonEmptySingleLine(server.command)) {
|
|
9280
|
+
throw new Error(
|
|
9281
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.command must be a non-empty single-line string no longer than ${MAX_LOCAL_MCP_TOKEN_CHARS} characters`
|
|
9282
|
+
);
|
|
9283
|
+
}
|
|
9284
|
+
if (server.args !== void 0 && !Array.isArray(server.args)) {
|
|
9285
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must be an array`);
|
|
9286
|
+
}
|
|
9287
|
+
const args = server.args ?? [];
|
|
9288
|
+
if (args.length > MAX_LOCAL_MCP_ARGS || args.some((arg) => !isNonEmptySingleLine(arg))) {
|
|
9289
|
+
throw new Error(
|
|
9290
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must contain at most ${MAX_LOCAL_MCP_ARGS} non-empty single-line strings`
|
|
9291
|
+
);
|
|
9292
|
+
}
|
|
9293
|
+
servers[serverName] = Object.freeze({
|
|
9294
|
+
command: server.command,
|
|
9295
|
+
...args.length > 0 ? { args: Object.freeze([...args]) } : {}
|
|
9296
|
+
});
|
|
9297
|
+
}
|
|
9298
|
+
resolved.set(toolsetId, Object.freeze({ mcpServers: Object.freeze(servers) }));
|
|
9299
|
+
}
|
|
9300
|
+
return resolved;
|
|
9301
|
+
}
|
|
9302
|
+
function resolveDeviceAssertionAudiences(config) {
|
|
9303
|
+
if (config === void 0) return void 0;
|
|
9304
|
+
if (!Array.isArray(config.audiences)) {
|
|
9305
|
+
throw new Error(
|
|
9306
|
+
`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.`
|
|
9307
|
+
);
|
|
9308
|
+
}
|
|
9309
|
+
if (config.audiences.length === 0) return void 0;
|
|
9310
|
+
const audiences = /* @__PURE__ */ new Set();
|
|
9311
|
+
for (const audience of config.audiences) {
|
|
9312
|
+
if (typeof audience !== "string" || audience.length === 0) {
|
|
9313
|
+
throw new Error(
|
|
9314
|
+
`DaemonConfig.deviceAssertion.audiences entries must be non-empty strings \u2014 got ${JSON.stringify(audience)}`
|
|
9315
|
+
);
|
|
9316
|
+
}
|
|
9317
|
+
if (Buffer.byteLength(audience, "utf8") > DEVICE_ASSERTION_AUDIENCE_MAX_BYTES) {
|
|
9318
|
+
throw new Error(
|
|
9319
|
+
`DaemonConfig.deviceAssertion.audiences entry ${JSON.stringify(audience)} exceeds ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
9320
|
+
);
|
|
9321
|
+
}
|
|
9322
|
+
if (audiences.has(audience)) {
|
|
9323
|
+
throw new Error(
|
|
9324
|
+
`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`
|
|
9325
|
+
);
|
|
9326
|
+
}
|
|
9327
|
+
audiences.add(audience);
|
|
9328
|
+
}
|
|
9329
|
+
return audiences;
|
|
9330
|
+
}
|
|
9331
|
+
function resolveDeviceAssertionTtlMs(config) {
|
|
9332
|
+
const ttlMs = config?.ttlMs ?? DEVICE_ASSERTION_DEFAULT_TTL_MS;
|
|
9333
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_ASSERTION_MAX_TTL_MS) {
|
|
9334
|
+
throw new Error(
|
|
9335
|
+
`DaemonConfig.deviceAssertion.ttlMs must be a positive integer no greater than ${DEVICE_ASSERTION_MAX_TTL_MS} ms \u2014 got ${JSON.stringify(config?.ttlMs)}`
|
|
9336
|
+
);
|
|
9337
|
+
}
|
|
9338
|
+
return ttlMs;
|
|
9339
|
+
}
|
|
9340
|
+
function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
9341
|
+
return buildDaemonWithAdapters(config, adapters, overrides);
|
|
9342
|
+
}
|
|
9343
|
+
function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
|
|
9344
|
+
const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
|
|
9345
|
+
if (config.piByokLauncher !== void 0) {
|
|
9346
|
+
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
9347
|
+
}
|
|
9348
|
+
if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
|
|
9349
|
+
throw new Error(
|
|
9350
|
+
`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".`
|
|
9351
|
+
);
|
|
9352
|
+
}
|
|
9353
|
+
const presenceCadence = {
|
|
9354
|
+
intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
|
|
9355
|
+
ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
|
|
9356
|
+
minimumIntervalMs: config.presence?.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS
|
|
9357
|
+
};
|
|
9358
|
+
assertPresenceHeartbeatCadence(presenceCadence);
|
|
9359
|
+
const deviceAssertionAudiences = resolveDeviceAssertionAudiences(config.deviceAssertion);
|
|
9360
|
+
const deviceAssertionTtlMs = resolveDeviceAssertionTtlMs(config.deviceAssertion);
|
|
9361
|
+
let shuttingDown = false;
|
|
9362
|
+
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
9363
|
+
const store = new DeviceStore(storeDir);
|
|
9364
|
+
const operationalHealth = new OperationalHealthTracker(storeDir);
|
|
9365
|
+
let fleetJitter;
|
|
9366
|
+
let maintenanceSequence = 0;
|
|
9367
|
+
const cursorStore = new CursorStore(storeDir);
|
|
9368
|
+
const sessionWorkspaces = new SessionWorkspaceStore(storeDir);
|
|
9369
|
+
const gitWorkspaceManager = config.gitWorkspace ? overrides.gitWorkspace?.manager ?? new GitWorkspaceManager(config.workspaceRoot, { ownerId: stableGitWorkspaceOwnerId(storeDir, config.productId) }) : void 0;
|
|
9370
|
+
const gitWorkspaceStore = config.gitWorkspace ? overrides.gitWorkspace?.store ?? new GitWorkspaceStore(storeDir) : void 0;
|
|
9371
|
+
let resolvedStoragePolicy;
|
|
9372
|
+
if (config.hostedJournal) {
|
|
9373
|
+
if (config.hostedJournal.mode !== "sqlite") {
|
|
9374
|
+
throw new Error(`DaemonConfig.hostedJournal.mode must be "sqlite" \u2014 got ${JSON.stringify(config.hostedJournal.mode)}`);
|
|
9375
|
+
}
|
|
9376
|
+
if (typeof config.hostedJournal.tenantId !== "string" || config.hostedJournal.tenantId.trim() === "") {
|
|
9377
|
+
throw new Error(
|
|
9378
|
+
"DaemonConfig.hostedJournal.tenantId must be a non-empty tenant id \u2014 a hosted journal row with no tenant is durable evidence nobody can act on"
|
|
9379
|
+
);
|
|
9380
|
+
}
|
|
9381
|
+
if (config.hostedJournal.storagePolicy) {
|
|
9382
|
+
resolvedStoragePolicy = resolveLocalStoragePolicy(config.hostedJournal.storagePolicy);
|
|
9383
|
+
}
|
|
9384
|
+
}
|
|
9385
|
+
if (config.hostedJournal && overrides.hostedJournal?.journal === void 0 && !isSqliteAvailable()) {
|
|
8616
9386
|
throw new JournalUnavailableError(`node:sqlite could not be loaded on Node ${process.versions.node}`);
|
|
8617
9387
|
}
|
|
8618
9388
|
let ownedJournal;
|
|
@@ -8679,6 +9449,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8679
9449
|
let runner;
|
|
8680
9450
|
let controlServerHandle;
|
|
8681
9451
|
let daemonOwnerLease;
|
|
9452
|
+
let presencePublisher;
|
|
9453
|
+
let presenceDiscovery;
|
|
9454
|
+
let presenceDiscoveryInFlight = false;
|
|
8682
9455
|
let shutdownPromise;
|
|
8683
9456
|
const pendingLateMutationBarriers = /* @__PURE__ */ new Set();
|
|
8684
9457
|
let startedAt;
|
|
@@ -8851,6 +9624,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8851
9624
|
deviceId: record.deviceId,
|
|
8852
9625
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
8853
9626
|
runtimeEnvironment: config.runtimeEnvironment,
|
|
9627
|
+
...mcpToolsets ? { mcpToolsets } : {},
|
|
8854
9628
|
// M3-2a: `send` is already this file's OWN closure (not something
|
|
8855
9629
|
// `TaskRunner` builds) — every `task.claim`/`task.started`/
|
|
8856
9630
|
// `task.progress`/`task.artifact`/`task.await_approval`/
|
|
@@ -8890,6 +9664,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8890
9664
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
8891
9665
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
8892
9666
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
9667
|
+
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
9668
|
+
// absent when unconfigured — see `DaemonConfig.resultDocument`'s own
|
|
9669
|
+
// doc comment. Spread rather than assigned so an unconfigured daemon
|
|
9670
|
+
// builds the exact `deps` object it did before this seam existed.
|
|
9671
|
+
...config.resultDocument ? { resultDocument: config.resultDocument } : {},
|
|
8893
9672
|
// M4 Phase 3 hardening: bridges TaskRunner's stale-approval-race
|
|
8894
9673
|
// finding out to the SAME local observability seam every other
|
|
8895
9674
|
// daemon-local event already uses (see observer.ts's own module doc
|
|
@@ -8965,8 +9744,10 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8965
9744
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
8966
9745
|
},
|
|
8967
9746
|
onStateChange: (state) => {
|
|
9747
|
+
const wasSettled = connectionState === "open" || connectionState === "degraded";
|
|
8968
9748
|
connectionState = state;
|
|
8969
9749
|
observer.noteConnectionState(state);
|
|
9750
|
+
if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
|
|
8970
9751
|
},
|
|
8971
9752
|
backoff: overrides.backoff,
|
|
8972
9753
|
liveness: overrides.liveness,
|
|
@@ -8984,6 +9765,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8984
9765
|
});
|
|
8985
9766
|
await connection.start();
|
|
8986
9767
|
await connection.waitForAck();
|
|
9768
|
+
startPresenceProducer();
|
|
8987
9769
|
} catch (err) {
|
|
8988
9770
|
try {
|
|
8989
9771
|
await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
|
|
@@ -8993,7 +9775,41 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8993
9775
|
throw err;
|
|
8994
9776
|
}
|
|
8995
9777
|
}
|
|
9778
|
+
function startPresenceProducer() {
|
|
9779
|
+
presenceDiscovery = new AbortController();
|
|
9780
|
+
runPresenceDiscovery();
|
|
9781
|
+
}
|
|
9782
|
+
function runPresenceDiscovery() {
|
|
9783
|
+
const discovery = presenceDiscovery;
|
|
9784
|
+
if (!discovery || presenceDiscoveryInFlight) return;
|
|
9785
|
+
presenceDiscoveryInFlight = true;
|
|
9786
|
+
void (async () => {
|
|
9787
|
+
try {
|
|
9788
|
+
const declaration = await fetchCapabilityDeclaration(config.serverUrl, { signal: discovery.signal });
|
|
9789
|
+
if (discovery.signal.aborted) return;
|
|
9790
|
+
if (declares(declaration, PRESENCE_HINTS_CAPABILITY)) {
|
|
9791
|
+
presencePublisher ??= new PresencePublisher({
|
|
9792
|
+
serverUrl: config.serverUrl,
|
|
9793
|
+
auth,
|
|
9794
|
+
...presenceCadence,
|
|
9795
|
+
onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
|
|
9796
|
+
});
|
|
9797
|
+
presencePublisher.start();
|
|
9798
|
+
} else {
|
|
9799
|
+
presencePublisher?.stop();
|
|
9800
|
+
}
|
|
9801
|
+
} catch (err) {
|
|
9802
|
+
if (discovery.signal.aborted) return;
|
|
9803
|
+
console.warn(
|
|
9804
|
+
`[byok/client] capability discovery failed; presence publishing stays off until the next reconnect: ${err instanceof Error ? err.message : String(err)}`
|
|
9805
|
+
);
|
|
9806
|
+
} finally {
|
|
9807
|
+
presenceDiscoveryInFlight = false;
|
|
9808
|
+
}
|
|
9809
|
+
})();
|
|
9810
|
+
}
|
|
8996
9811
|
async function runShutdownSequence(reason, opts = {}) {
|
|
9812
|
+
shuttingDown = true;
|
|
8997
9813
|
const errors = [];
|
|
8998
9814
|
let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
|
|
8999
9815
|
if (!hostedStorageInitializationBarrierComplete) {
|
|
@@ -9022,6 +9838,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9022
9838
|
errors.push(new Error("a prior active task teardown remains unsettled; ownership lease retained"));
|
|
9023
9839
|
}
|
|
9024
9840
|
}
|
|
9841
|
+
presenceDiscovery?.abort();
|
|
9842
|
+
presenceDiscovery = void 0;
|
|
9843
|
+
presenceDiscoveryInFlight = false;
|
|
9844
|
+
presencePublisher?.stop();
|
|
9845
|
+
presencePublisher = void 0;
|
|
9025
9846
|
const stoppingOwnedPressureEngine = ownedPressureEngine;
|
|
9026
9847
|
const stoppingOwnedJournal = ownedJournal;
|
|
9027
9848
|
const maintenanceStopped = stoppingOwnedPressureEngine?.stop() ?? Promise.resolve();
|
|
@@ -9082,6 +9903,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9082
9903
|
}
|
|
9083
9904
|
}
|
|
9084
9905
|
async function stop(opts = {}) {
|
|
9906
|
+
shuttingDown = true;
|
|
9085
9907
|
await requestShutdown(opts.reason ?? "operator", { drainTimeoutMs: opts.drainTimeoutMs });
|
|
9086
9908
|
}
|
|
9087
9909
|
function requestShutdown(reason, opts = {}) {
|
|
@@ -9094,6 +9916,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9094
9916
|
return current;
|
|
9095
9917
|
}
|
|
9096
9918
|
async function unpair() {
|
|
9919
|
+
shuttingDown = true;
|
|
9097
9920
|
await runLifecycleMutation(unpairUnderLease);
|
|
9098
9921
|
}
|
|
9099
9922
|
async function unpairUnderLease() {
|
|
@@ -9172,6 +9995,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9172
9995
|
}
|
|
9173
9996
|
async function performControlShutdown(reason) {
|
|
9174
9997
|
const effectiveReason = reason ?? "operator";
|
|
9998
|
+
shuttingDown = true;
|
|
9175
9999
|
observer.noteShutdownRequested(effectiveReason);
|
|
9176
10000
|
try {
|
|
9177
10001
|
await requestShutdown(`control socket shutdown (${effectiveReason})`);
|
|
@@ -9209,7 +10033,107 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9209
10033
|
if (!runner) throw new ControlError("not_found", "daemon is not started");
|
|
9210
10034
|
return runner.requestApproval(parsed.taskId, parsed.summary);
|
|
9211
10035
|
},
|
|
10036
|
+
/**
|
|
10037
|
+
* Plan `device-assertion-broker`: mint one short-lived, audience-scoped
|
|
10038
|
+
* device assertion for a sibling local process.
|
|
10039
|
+
*
|
|
10040
|
+
* SIX fail-closed gates, in this exact order, none of which signs
|
|
10041
|
+
* anything on the way out. The order is part of the contract, not an
|
|
10042
|
+
* implementation detail:
|
|
10043
|
+
*
|
|
10044
|
+
* 1. `assertion_disabled` — before anything else, because a daemon that
|
|
10045
|
+
* was never configured for this must not reveal, by answering
|
|
10046
|
+
* differently for different inputs, that it even validates params.
|
|
10047
|
+
* 2. `bad_request` — shape/length, checked before the allowlist so a
|
|
10048
|
+
* malformed request cannot be used to probe membership.
|
|
10049
|
+
* 3. `audience_denied` — EXACT `Set.has`, never a prefix/suffix rule
|
|
10050
|
+
* (`salesko-api.evil.com` and `salesko-ap` both fail against an entry
|
|
10051
|
+
* of `salesko-api`). The message deliberately does not echo the
|
|
10052
|
+
* allowlist: a refusal must not be an enumeration oracle.
|
|
10053
|
+
* 4. `shutting_down` — see `performControlShutdown`'s own comment for
|
|
10054
|
+
* the minting window this closes.
|
|
10055
|
+
* 5. `revoked` — the server-side revocation this daemon already knows
|
|
10056
|
+
* about.
|
|
10057
|
+
* 6. `not_paired` — the on-disk record, re-read on EVERY call (never
|
|
10058
|
+
* cached), so clearing `device.json` removes local signing authority
|
|
10059
|
+
* immediately.
|
|
10060
|
+
*
|
|
10061
|
+
* Only after all six does the private key get imported, used once, and
|
|
10062
|
+
* dropped (`device-assertion-signer.ts`).
|
|
10063
|
+
*
|
|
10064
|
+
* Honest limit, and it must stay in the docs as well as here: gates 4-6
|
|
10065
|
+
* are only HALF of revocation. They make this daemon stop minting
|
|
10066
|
+
* promptly, but an assertion already in a caller's hands is not recalled
|
|
10067
|
+
* by any of them. The other half is the host's own recheck at exchange
|
|
10068
|
+
* time, which is why core's `verifyDeviceAssertion` makes the device
|
|
10069
|
+
* row's `revoked` state a REQUIRED parameter. Nothing here entitles
|
|
10070
|
+
* anyone to claim this daemon delivers synchronous invalidation on its
|
|
10071
|
+
* own.
|
|
10072
|
+
*/
|
|
10073
|
+
"assertion.issue": async (params) => {
|
|
10074
|
+
if (deviceAssertionAudiences === void 0) {
|
|
10075
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "assertion_disabled" });
|
|
10076
|
+
throw new ControlError(
|
|
10077
|
+
"assertion_disabled",
|
|
10078
|
+
"this daemon is not configured to issue device assertions (DaemonConfig.deviceAssertion.audiences is absent or empty)"
|
|
10079
|
+
);
|
|
10080
|
+
}
|
|
10081
|
+
const parsed = parseAssertionIssueParams(params);
|
|
10082
|
+
if (!parsed) {
|
|
10083
|
+
const rawAudience = typeof params === "object" && params !== null && typeof params.audience === "string" ? params.audience : void 0;
|
|
10084
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "bad_request", audience: rawAudience });
|
|
10085
|
+
throw new ControlError(
|
|
10086
|
+
"bad_request",
|
|
10087
|
+
`assertion.issue requires exactly {audience} where audience is a non-empty string of at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
10088
|
+
);
|
|
10089
|
+
}
|
|
10090
|
+
if (!deviceAssertionAudiences.has(parsed.audience)) {
|
|
10091
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "audience_denied", audience: parsed.audience });
|
|
10092
|
+
throw new ControlError("audience_denied", "the requested audience is not allowed by this daemon");
|
|
10093
|
+
}
|
|
10094
|
+
if (shuttingDown) {
|
|
10095
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10096
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10097
|
+
}
|
|
10098
|
+
if (auth.isRevoked()) {
|
|
10099
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10100
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10101
|
+
}
|
|
10102
|
+
const record = await store.load();
|
|
10103
|
+
if (record === void 0) {
|
|
10104
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "not_paired", audience: parsed.audience });
|
|
10105
|
+
throw new ControlError("not_paired", "this device is not paired; nothing can be asserted about it");
|
|
10106
|
+
}
|
|
10107
|
+
if (shuttingDown) {
|
|
10108
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10109
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10110
|
+
}
|
|
10111
|
+
if (auth.isRevoked()) {
|
|
10112
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10113
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10114
|
+
}
|
|
10115
|
+
const minted = mintDeviceAssertion({
|
|
10116
|
+
record,
|
|
10117
|
+
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
10118
|
+
// (ws:->http:, wss:->https:, path stripped), so an operator who
|
|
10119
|
+
// configured the websocket spelling and one who configured the HTTP
|
|
10120
|
+
// spelling of the same deployment produce the same issuer.
|
|
10121
|
+
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
10122
|
+
productId: config.productId,
|
|
10123
|
+
audience: parsed.audience,
|
|
10124
|
+
ttlMs: deviceAssertionTtlMs,
|
|
10125
|
+
now: /* @__PURE__ */ new Date()
|
|
10126
|
+
});
|
|
10127
|
+
observer.noteDeviceAssertion({
|
|
10128
|
+
result: "issued",
|
|
10129
|
+
audience: minted.claims.audience,
|
|
10130
|
+
jti: minted.claims.jti,
|
|
10131
|
+
expiresAt: minted.expiresAt
|
|
10132
|
+
});
|
|
10133
|
+
return { assertion: minted.envelope, expiresAt: minted.expiresAt };
|
|
10134
|
+
},
|
|
9212
10135
|
shutdown: (params) => {
|
|
10136
|
+
shuttingDown = true;
|
|
9213
10137
|
const { reason } = parseShutdownParams(params);
|
|
9214
10138
|
setImmediate(() => {
|
|
9215
10139
|
void performControlShutdown(reason).catch((err) => {
|
|
@@ -9254,299 +10178,350 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9254
10178
|
return { pair, start, stop, status, subscribe, tasks, unpair, approve, reject };
|
|
9255
10179
|
}
|
|
9256
10180
|
function createDaemon(config) {
|
|
9257
|
-
return createDaemonWithAdapters(config, buildDefaultAdapters(config
|
|
9258
|
-
}
|
|
9259
|
-
|
|
9260
|
-
// src/lifecycle/service-types.ts
|
|
9261
|
-
function nodeAgentProgram(opts) {
|
|
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;
|
|
9268
|
-
}
|
|
9269
|
-
function sanitizeServiceName(name) {
|
|
9270
|
-
const cleaned = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
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;
|
|
9276
|
-
}
|
|
9277
|
-
|
|
9278
|
-
// src/lifecycle/launchd.ts
|
|
9279
|
-
var LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
9280
|
-
/operation not permitted/i,
|
|
9281
|
-
/could not find domain/i,
|
|
9282
|
-
/permission denied/i,
|
|
9283
|
-
/access denied/i
|
|
9284
|
-
];
|
|
9285
|
-
var LAUNCHD_NOT_LOADED = {
|
|
9286
|
-
patterns: [/no such process/i, /could not find (specified )?service/i, /not loaded/i],
|
|
9287
|
-
neverAbsence: LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9288
|
-
};
|
|
9289
|
-
function escapeXml(value) {
|
|
9290
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10181
|
+
return createDaemonWithAdapters(config, buildDefaultAdapters(config));
|
|
9291
10182
|
}
|
|
9292
|
-
|
|
9293
|
-
|
|
10183
|
+
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
10184
|
+
function errorMessage4(err) {
|
|
10185
|
+
return err instanceof Error ? err.message : String(err);
|
|
9294
10186
|
}
|
|
9295
|
-
function
|
|
9296
|
-
|
|
9297
|
-
const args = [program.command, ...program.args];
|
|
9298
|
-
const cwd = program.cwd ?? os.homedir();
|
|
9299
|
-
const outLog = path20.join(logDir, `${label}.out.log`);
|
|
9300
|
-
const errLog = path20.join(logDir, `${label}.err.log`);
|
|
9301
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
9302
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9303
|
-
<plist version="1.0">
|
|
9304
|
-
<dict>
|
|
9305
|
-
<key>Label</key>
|
|
9306
|
-
${plistString(label)}
|
|
9307
|
-
<key>ProgramArguments</key>
|
|
9308
|
-
<array>
|
|
9309
|
-
${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
9310
|
-
</array>
|
|
9311
|
-
<key>WorkingDirectory</key>
|
|
9312
|
-
${plistString(cwd)}
|
|
9313
|
-
<key>RunAtLoad</key>
|
|
9314
|
-
<true/>
|
|
9315
|
-
<key>KeepAlive</key>
|
|
9316
|
-
<dict>
|
|
9317
|
-
<key>SuccessfulExit</key>
|
|
9318
|
-
<false/>
|
|
9319
|
-
</dict>
|
|
9320
|
-
<key>ThrottleInterval</key>
|
|
9321
|
-
<integer>10</integer>
|
|
9322
|
-
<key>StandardOutPath</key>
|
|
9323
|
-
${plistString(outLog)}
|
|
9324
|
-
<key>StandardErrorPath</key>
|
|
9325
|
-
${plistString(errLog)}
|
|
9326
|
-
</dict>
|
|
9327
|
-
</plist>
|
|
9328
|
-
`;
|
|
10187
|
+
function sameFileState3(left, right) {
|
|
10188
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
9329
10189
|
}
|
|
9330
|
-
function
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
if (
|
|
9336
|
-
|
|
9337
|
-
}
|
|
9338
|
-
return process.getuid();
|
|
9339
|
-
});
|
|
9340
|
-
const label = sanitizeServiceName(def.name);
|
|
9341
|
-
const plistPath = () => path20.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
9342
|
-
const domainTarget = () => `gui/${getuid()}`;
|
|
9343
|
-
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
9344
|
-
async function fileExists(p) {
|
|
9345
|
-
try {
|
|
9346
|
-
await fs19.stat(p);
|
|
9347
|
-
return true;
|
|
9348
|
-
} catch {
|
|
9349
|
-
return false;
|
|
9350
|
-
}
|
|
9351
|
-
}
|
|
9352
|
-
async function writePlist(program) {
|
|
9353
|
-
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
9354
|
-
await fs19.mkdir(path20.dirname(plistPath()), { recursive: true });
|
|
9355
|
-
await fs19.mkdir(def.logDir, { recursive: true });
|
|
9356
|
-
await fs19.writeFile(plistPath(), xml, "utf8");
|
|
9357
|
-
}
|
|
9358
|
-
async function install(opts = {}) {
|
|
9359
|
-
await writePlist(opts.program ?? def.program);
|
|
9360
|
-
await run("launchctl", ["bootout", serviceTarget()]);
|
|
9361
|
-
await runOrThrow(run, "launchctl", ["bootstrap", domainTarget(), plistPath()], "launchctl bootstrap");
|
|
9362
|
-
await runOrThrow(run, "launchctl", ["enable", serviceTarget()], "launchctl enable");
|
|
9363
|
-
await run("launchctl", ["kickstart", "-k", serviceTarget()]);
|
|
10190
|
+
async function readControlToken(tokenPath) {
|
|
10191
|
+
let namedBefore;
|
|
10192
|
+
try {
|
|
10193
|
+
namedBefore = await promises.lstat(tokenPath, { bigint: true });
|
|
10194
|
+
} catch (err) {
|
|
10195
|
+
if (err.code === "ENOENT") return void 0;
|
|
10196
|
+
throw err;
|
|
9364
10197
|
}
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
await fs19.rm(plistPath(), { force: true });
|
|
10198
|
+
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
10199
|
+
throw new Error("control token is not a real regular file");
|
|
9368
10200
|
}
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
10201
|
+
const handle = await promises.open(
|
|
10202
|
+
tokenPath,
|
|
10203
|
+
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
10204
|
+
);
|
|
10205
|
+
try {
|
|
10206
|
+
const opened = await handle.stat({ bigint: true });
|
|
10207
|
+
const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
|
|
10208
|
+
if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
|
|
10209
|
+
throw new Error("control token pathname changed before safe open");
|
|
9372
10210
|
}
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
const
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
10211
|
+
if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
|
|
10212
|
+
throw new Error("control token exceeds the bounded read limit");
|
|
10213
|
+
}
|
|
10214
|
+
const size = Number(opened.size);
|
|
10215
|
+
const bytes = Buffer.alloc(size);
|
|
10216
|
+
const { bytesRead } = await handle.read(bytes, 0, size, 0);
|
|
10217
|
+
const afterRead = await handle.stat({ bigint: true });
|
|
10218
|
+
const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
|
|
10219
|
+
if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
|
|
10220
|
+
throw new Error("control token changed during bounded read");
|
|
10221
|
+
}
|
|
10222
|
+
return bytes.toString("utf8").trim();
|
|
10223
|
+
} finally {
|
|
10224
|
+
await handle.close();
|
|
9386
10225
|
}
|
|
9387
|
-
return { install, uninstall, start, stop, status };
|
|
9388
10226
|
}
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
};
|
|
9400
|
-
function hasControlChar(value) {
|
|
9401
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
9402
|
-
const code = value.charCodeAt(i);
|
|
9403
|
-
if (code < 32 || code === 127) return true;
|
|
10227
|
+
async function connectControlClient(opts) {
|
|
10228
|
+
const tokenPath = controlTokenPath(opts.storeDir);
|
|
10229
|
+
let token;
|
|
10230
|
+
try {
|
|
10231
|
+
const read = await readControlToken(tokenPath);
|
|
10232
|
+
if (read === void 0) {
|
|
10233
|
+
return { ok: false, reason: "daemon is not running (no control.token found)" };
|
|
10234
|
+
}
|
|
10235
|
+
token = read;
|
|
10236
|
+
} catch (err) {
|
|
10237
|
+
return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
|
|
9404
10238
|
}
|
|
9405
|
-
|
|
9406
|
-
}
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
10239
|
+
if (!token) {
|
|
10240
|
+
return { ok: false, reason: "control token file is empty" };
|
|
10241
|
+
}
|
|
10242
|
+
const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
|
|
10243
|
+
try {
|
|
10244
|
+
const client = await connectAndHandshake(endpoint, token, opts);
|
|
10245
|
+
return { ok: true, client };
|
|
10246
|
+
} catch (err) {
|
|
10247
|
+
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage4(err)}` };
|
|
9412
10248
|
}
|
|
9413
10249
|
}
|
|
9414
|
-
function
|
|
9415
|
-
return
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
10250
|
+
function connectAndHandshake(endpoint, token, opts) {
|
|
10251
|
+
return new Promise((resolve, reject) => {
|
|
10252
|
+
const socket = net.createConnection(endpoint);
|
|
10253
|
+
const reader = new NdjsonLineReader();
|
|
10254
|
+
let phase = "server-hello";
|
|
10255
|
+
let settled = false;
|
|
10256
|
+
const clientNonce = randomNonceHex();
|
|
10257
|
+
const timer = setTimeout(() => {
|
|
10258
|
+
fail(new Error("handshake timed out"));
|
|
10259
|
+
}, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
|
|
10260
|
+
timer.unref?.();
|
|
10261
|
+
function fail(err) {
|
|
10262
|
+
if (settled) return;
|
|
10263
|
+
settled = true;
|
|
10264
|
+
clearTimeout(timer);
|
|
10265
|
+
socket.removeAllListeners();
|
|
10266
|
+
socket.destroy();
|
|
10267
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
10268
|
+
}
|
|
10269
|
+
function succeed() {
|
|
10270
|
+
settled = true;
|
|
10271
|
+
clearTimeout(timer);
|
|
10272
|
+
socket.removeListener("error", onError);
|
|
10273
|
+
socket.removeListener("data", onData);
|
|
10274
|
+
resolve(createControlClient(socket, reader, opts));
|
|
10275
|
+
}
|
|
10276
|
+
function onData(chunk) {
|
|
10277
|
+
let lines;
|
|
10278
|
+
try {
|
|
10279
|
+
lines = reader.push(chunk);
|
|
10280
|
+
} catch (err) {
|
|
10281
|
+
fail(err);
|
|
10282
|
+
return;
|
|
10283
|
+
}
|
|
10284
|
+
for (const line of lines) {
|
|
10285
|
+
let parsed;
|
|
10286
|
+
try {
|
|
10287
|
+
parsed = JSON.parse(line);
|
|
10288
|
+
} catch {
|
|
10289
|
+
fail(new Error("malformed handshake frame"));
|
|
10290
|
+
return;
|
|
10291
|
+
}
|
|
10292
|
+
if (phase === "server-hello") {
|
|
10293
|
+
const hello = parseServerHello(parsed);
|
|
10294
|
+
if (!hello) {
|
|
10295
|
+
fail(new Error("malformed or unexpected server hello"));
|
|
10296
|
+
return;
|
|
10297
|
+
}
|
|
10298
|
+
if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
|
|
10299
|
+
fail(new Error("server failed to prove it holds the control token"));
|
|
10300
|
+
return;
|
|
10301
|
+
}
|
|
10302
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
|
|
10303
|
+
phase = "ready";
|
|
10304
|
+
continue;
|
|
10305
|
+
}
|
|
10306
|
+
if (!parseServerReady(parsed)) {
|
|
10307
|
+
fail(new Error("server did not confirm readiness"));
|
|
10308
|
+
return;
|
|
10309
|
+
}
|
|
10310
|
+
succeed();
|
|
10311
|
+
return;
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
function onError(err) {
|
|
10315
|
+
fail(err);
|
|
10316
|
+
}
|
|
10317
|
+
socket.once("error", onError);
|
|
10318
|
+
socket.once("connect", () => {
|
|
10319
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
|
|
10320
|
+
socket.on("data", onData);
|
|
10321
|
+
});
|
|
10322
|
+
});
|
|
9421
10323
|
}
|
|
9422
|
-
function
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
Type=simple
|
|
9438
|
-
ExecStart=${execStart}
|
|
9439
|
-
WorkingDirectory=${escapeSystemdPercent(cwd)}
|
|
9440
|
-
Restart=on-failure
|
|
9441
|
-
RestartSec=10
|
|
9442
|
-
StandardOutput=append:${escapeSystemdPercent(outLog)}
|
|
9443
|
-
StandardError=append:${escapeSystemdPercent(errLog)}
|
|
9444
|
-
|
|
9445
|
-
[Install]
|
|
9446
|
-
WantedBy=default.target
|
|
9447
|
-
`;
|
|
10324
|
+
function withTimeout(promise, ms, message) {
|
|
10325
|
+
return new Promise((resolve, reject) => {
|
|
10326
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
10327
|
+
timer.unref?.();
|
|
10328
|
+
promise.then(
|
|
10329
|
+
(value) => {
|
|
10330
|
+
clearTimeout(timer);
|
|
10331
|
+
resolve(value);
|
|
10332
|
+
},
|
|
10333
|
+
(err) => {
|
|
10334
|
+
clearTimeout(timer);
|
|
10335
|
+
reject(err);
|
|
10336
|
+
}
|
|
10337
|
+
);
|
|
10338
|
+
});
|
|
9448
10339
|
}
|
|
9449
|
-
function
|
|
9450
|
-
const
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
10340
|
+
function createControlClient(socket, reader, opts) {
|
|
10341
|
+
const pending = /* @__PURE__ */ new Map();
|
|
10342
|
+
let idSeq = 0;
|
|
10343
|
+
let closed = false;
|
|
10344
|
+
function handleFrame(parsed) {
|
|
10345
|
+
if (!isRecord2(parsed) || typeof parsed.id !== "string") return;
|
|
10346
|
+
const entry = pending.get(parsed.id);
|
|
10347
|
+
if (!entry) return;
|
|
10348
|
+
if ("event" in parsed) {
|
|
10349
|
+
entry.onEvent?.(parsed.event);
|
|
10350
|
+
return;
|
|
10351
|
+
}
|
|
10352
|
+
if (parsed.ok === true) {
|
|
10353
|
+
pending.delete(parsed.id);
|
|
10354
|
+
entry.resolve(parsed.done === true ? void 0 : parsed.result);
|
|
10355
|
+
return;
|
|
10356
|
+
}
|
|
10357
|
+
pending.delete(parsed.id);
|
|
10358
|
+
const shape = parsed.error;
|
|
10359
|
+
entry.reject(
|
|
10360
|
+
new ControlError(
|
|
10361
|
+
typeof shape?.code === "string" ? shape.code : "internal_error",
|
|
10362
|
+
typeof shape?.message === "string" ? shape.message : "unknown control error"
|
|
10363
|
+
)
|
|
10364
|
+
);
|
|
10365
|
+
}
|
|
10366
|
+
socket.on("data", (chunk) => {
|
|
10367
|
+
let lines;
|
|
9457
10368
|
try {
|
|
9458
|
-
|
|
9459
|
-
return true;
|
|
10369
|
+
lines = reader.push(chunk);
|
|
9460
10370
|
} catch {
|
|
9461
|
-
|
|
10371
|
+
socket.destroy();
|
|
10372
|
+
return;
|
|
9462
10373
|
}
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
await writeUnit(opts.program ?? def.program);
|
|
9472
|
-
await runOrThrow(run, "systemctl", ["--user", "daemon-reload"], "systemctl daemon-reload");
|
|
9473
|
-
await runOrThrow(run, "systemctl", ["--user", "enable", "--now", unitName], "systemctl enable --now");
|
|
9474
|
-
}
|
|
9475
|
-
async function uninstall() {
|
|
9476
|
-
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
9477
|
-
await fs19.rm(unitPath(), { force: true });
|
|
9478
|
-
await run("systemctl", ["--user", "daemon-reload"]);
|
|
9479
|
-
}
|
|
9480
|
-
async function start() {
|
|
9481
|
-
if (!await fileExists(unitPath())) {
|
|
9482
|
-
throw new Error(`service "${name}" is not installed (no unit file at ${unitPath()}) \u2014 call install() first`);
|
|
10374
|
+
for (const line of lines) {
|
|
10375
|
+
let parsed;
|
|
10376
|
+
try {
|
|
10377
|
+
parsed = JSON.parse(line);
|
|
10378
|
+
} catch {
|
|
10379
|
+
continue;
|
|
10380
|
+
}
|
|
10381
|
+
handleFrame(parsed);
|
|
9483
10382
|
}
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
10383
|
+
});
|
|
10384
|
+
socket.on("close", () => {
|
|
10385
|
+
closed = true;
|
|
10386
|
+
for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
|
|
10387
|
+
pending.clear();
|
|
10388
|
+
});
|
|
10389
|
+
socket.on("error", () => {
|
|
10390
|
+
});
|
|
10391
|
+
function send(method, params, onEvent) {
|
|
10392
|
+
const id = `c${++idSeq}`;
|
|
10393
|
+
const promise = new Promise((resolve, reject) => {
|
|
10394
|
+
pending.set(id, { resolve, reject, onEvent });
|
|
10395
|
+
});
|
|
10396
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
|
|
10397
|
+
return { id, promise };
|
|
9488
10398
|
}
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
10399
|
+
return {
|
|
10400
|
+
async request(method, params) {
|
|
10401
|
+
if (closed) throw new Error("control connection is closed");
|
|
10402
|
+
const { promise } = send(method, params);
|
|
10403
|
+
const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
|
|
10404
|
+
return result;
|
|
10405
|
+
},
|
|
10406
|
+
subscribe(method, params, onEvent) {
|
|
10407
|
+
const { id, promise } = send(method, params, onEvent);
|
|
10408
|
+
promise.catch(() => {
|
|
10409
|
+
});
|
|
10410
|
+
return {
|
|
10411
|
+
close: () => {
|
|
10412
|
+
pending.delete(id);
|
|
10413
|
+
socket.destroy();
|
|
10414
|
+
}
|
|
10415
|
+
};
|
|
10416
|
+
},
|
|
10417
|
+
close() {
|
|
10418
|
+
socket.destroy();
|
|
10419
|
+
}
|
|
10420
|
+
};
|
|
10421
|
+
}
|
|
10422
|
+
async function isControlDaemonGone(storeDir, productId) {
|
|
10423
|
+
const tokenGone = await promises.stat(controlTokenPath(storeDir)).then(
|
|
10424
|
+
() => false,
|
|
10425
|
+
(err) => err.code === "ENOENT"
|
|
10426
|
+
);
|
|
10427
|
+
if (!tokenGone) return false;
|
|
10428
|
+
const endpoint = controlEndpointPath(productId, storeDir);
|
|
10429
|
+
return new Promise((resolve) => {
|
|
10430
|
+
const socket = net.createConnection(endpoint);
|
|
10431
|
+
const finish = (gone) => {
|
|
10432
|
+
socket.removeAllListeners();
|
|
10433
|
+
socket.destroy();
|
|
10434
|
+
resolve(gone);
|
|
10435
|
+
};
|
|
10436
|
+
socket.once("connect", () => finish(false));
|
|
10437
|
+
socket.once("error", (err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT"));
|
|
10438
|
+
});
|
|
10439
|
+
}
|
|
10440
|
+
|
|
10441
|
+
// src/lifecycle/service-types.ts
|
|
10442
|
+
function nodeAgentProgram(opts) {
|
|
10443
|
+
const program = {
|
|
10444
|
+
command: opts.nodeBin ?? process.execPath,
|
|
10445
|
+
args: [opts.agentBin, "start", "--config", opts.configPath]
|
|
10446
|
+
};
|
|
10447
|
+
if (opts.cwd !== void 0) program.cwd = opts.cwd;
|
|
10448
|
+
return program;
|
|
10449
|
+
}
|
|
10450
|
+
function sanitizeServiceName(name) {
|
|
10451
|
+
const cleaned = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
10452
|
+
const safe = cleaned.replace(/^-+/, "");
|
|
10453
|
+
if (!safe) {
|
|
10454
|
+
throw new Error(`service name "${name}" has no valid characters left after sanitizing (allowed: letters, digits, ".", "-", "_"; cannot consist only of leading "-")`);
|
|
9496
10455
|
}
|
|
9497
|
-
return
|
|
10456
|
+
return safe;
|
|
9498
10457
|
}
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
10458
|
+
|
|
10459
|
+
// src/lifecycle/launchd.ts
|
|
10460
|
+
var LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
10461
|
+
/operation not permitted/i,
|
|
10462
|
+
/could not find domain/i,
|
|
9502
10463
|
/permission denied/i,
|
|
9503
|
-
/
|
|
10464
|
+
/access denied/i
|
|
9504
10465
|
];
|
|
9505
|
-
var
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9509
|
-
};
|
|
9510
|
-
var WINSW_ALREADY_STOPPED = {
|
|
9511
|
-
codes: [1062, ...WINSW_NOT_INSTALLED.codes ?? []],
|
|
9512
|
-
patterns: [/not running/i, /has not been started/i, ...WINSW_NOT_INSTALLED.patterns],
|
|
9513
|
-
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
10466
|
+
var LAUNCHD_NOT_LOADED = {
|
|
10467
|
+
patterns: [/no such process/i, /could not find (specified )?service/i, /not loaded/i],
|
|
10468
|
+
neverAbsence: LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9514
10469
|
};
|
|
9515
|
-
function
|
|
10470
|
+
function escapeXml(value) {
|
|
9516
10471
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9517
10472
|
}
|
|
9518
|
-
function
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
<
|
|
9532
|
-
|
|
9533
|
-
<
|
|
9534
|
-
<
|
|
9535
|
-
|
|
10473
|
+
function plistString(value) {
|
|
10474
|
+
return `<string>${escapeXml(value)}</string>`;
|
|
10475
|
+
}
|
|
10476
|
+
function generateLaunchdPlist(def) {
|
|
10477
|
+
const { label, program, logDir } = def;
|
|
10478
|
+
const args = [program.command, ...program.args];
|
|
10479
|
+
const cwd = program.cwd ?? os.homedir();
|
|
10480
|
+
const outLog = path20.join(logDir, `${label}.out.log`);
|
|
10481
|
+
const errLog = path20.join(logDir, `${label}.err.log`);
|
|
10482
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
10483
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
10484
|
+
<plist version="1.0">
|
|
10485
|
+
<dict>
|
|
10486
|
+
<key>Label</key>
|
|
10487
|
+
${plistString(label)}
|
|
10488
|
+
<key>ProgramArguments</key>
|
|
10489
|
+
<array>
|
|
10490
|
+
${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
10491
|
+
</array>
|
|
10492
|
+
<key>WorkingDirectory</key>
|
|
10493
|
+
${plistString(cwd)}
|
|
10494
|
+
<key>RunAtLoad</key>
|
|
10495
|
+
<true/>
|
|
10496
|
+
<key>KeepAlive</key>
|
|
10497
|
+
<dict>
|
|
10498
|
+
<key>SuccessfulExit</key>
|
|
10499
|
+
<false/>
|
|
10500
|
+
</dict>
|
|
10501
|
+
<key>ThrottleInterval</key>
|
|
10502
|
+
<integer>10</integer>
|
|
10503
|
+
<key>StandardOutPath</key>
|
|
10504
|
+
${plistString(outLog)}
|
|
10505
|
+
<key>StandardErrorPath</key>
|
|
10506
|
+
${plistString(errLog)}
|
|
10507
|
+
</dict>
|
|
10508
|
+
</plist>
|
|
9536
10509
|
`;
|
|
9537
10510
|
}
|
|
9538
|
-
function
|
|
10511
|
+
function createLaunchdLifecycle(def, deps = {}) {
|
|
9539
10512
|
const run = deps.run ?? defaultRunner;
|
|
9540
10513
|
const fs19 = deps.fs ?? promises;
|
|
9541
|
-
const
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
const
|
|
9549
|
-
const
|
|
10514
|
+
const homedir = deps.homedir ?? (() => os.homedir());
|
|
10515
|
+
const getuid = deps.getuid ?? (() => {
|
|
10516
|
+
if (typeof process.getuid !== "function") {
|
|
10517
|
+
throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
|
|
10518
|
+
}
|
|
10519
|
+
return process.getuid();
|
|
10520
|
+
});
|
|
10521
|
+
const label = sanitizeServiceName(def.name);
|
|
10522
|
+
const plistPath = () => path20.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
10523
|
+
const domainTarget = () => `gui/${getuid()}`;
|
|
10524
|
+
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
9550
10525
|
async function fileExists(p) {
|
|
9551
10526
|
try {
|
|
9552
10527
|
await fs19.stat(p);
|
|
@@ -9555,381 +10530,330 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
9555
10530
|
return false;
|
|
9556
10531
|
}
|
|
9557
10532
|
}
|
|
9558
|
-
async function
|
|
9559
|
-
const xml =
|
|
9560
|
-
await fs19.mkdir(
|
|
10533
|
+
async function writePlist(program) {
|
|
10534
|
+
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
10535
|
+
await fs19.mkdir(path20.dirname(plistPath()), { recursive: true });
|
|
9561
10536
|
await fs19.mkdir(def.logDir, { recursive: true });
|
|
9562
|
-
await fs19.
|
|
9563
|
-
await fs19.writeFile(xmlPath, xml, "utf8");
|
|
10537
|
+
await fs19.writeFile(plistPath(), xml, "utf8");
|
|
9564
10538
|
}
|
|
9565
10539
|
async function install(opts = {}) {
|
|
9566
|
-
await
|
|
9567
|
-
await
|
|
9568
|
-
await runOrThrow(run,
|
|
10540
|
+
await writePlist(opts.program ?? def.program);
|
|
10541
|
+
await run("launchctl", ["bootout", serviceTarget()]);
|
|
10542
|
+
await runOrThrow(run, "launchctl", ["bootstrap", domainTarget(), plistPath()], "launchctl bootstrap");
|
|
10543
|
+
await runOrThrow(run, "launchctl", ["enable", serviceTarget()], "launchctl enable");
|
|
10544
|
+
await run("launchctl", ["kickstart", "-k", serviceTarget()]);
|
|
9569
10545
|
}
|
|
9570
10546
|
async function uninstall() {
|
|
9571
|
-
await runIdempotent(run,
|
|
9572
|
-
await
|
|
9573
|
-
await fs19.rm(exePath, { force: true });
|
|
9574
|
-
await fs19.rm(xmlPath, { force: true });
|
|
10547
|
+
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
10548
|
+
await fs19.rm(plistPath(), { force: true });
|
|
9575
10549
|
}
|
|
9576
10550
|
async function start() {
|
|
9577
|
-
if (!await fileExists(
|
|
9578
|
-
throw new Error(`service "${
|
|
10551
|
+
if (!await fileExists(plistPath())) {
|
|
10552
|
+
throw new Error(`service "${label}" is not installed (no plist at ${plistPath()}) \u2014 call install() first`);
|
|
9579
10553
|
}
|
|
9580
|
-
await
|
|
10554
|
+
await run("launchctl", ["bootstrap", domainTarget(), plistPath()]);
|
|
10555
|
+
await runOrThrow(run, "launchctl", ["kickstart", "-k", serviceTarget()], "launchctl kickstart");
|
|
9581
10556
|
}
|
|
9582
10557
|
async function stop() {
|
|
9583
|
-
await runIdempotent(run,
|
|
10558
|
+
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
9584
10559
|
}
|
|
9585
10560
|
async function status() {
|
|
9586
|
-
const installed = await fileExists(
|
|
9587
|
-
const result = await run("
|
|
9588
|
-
const detail =
|
|
9589
|
-
const running = result.code === 0 && /\
|
|
9590
|
-
const determinate = running || !
|
|
10561
|
+
const installed = await fileExists(plistPath());
|
|
10562
|
+
const result = await run("launchctl", ["print", serviceTarget()]);
|
|
10563
|
+
const detail = result.stdout || result.stderr;
|
|
10564
|
+
const running = result.code === 0 && /\bstate\s*=\s*running\b/i.test(detail);
|
|
10565
|
+
const determinate = running || !LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
9591
10566
|
return { installed, running, determinate, detail };
|
|
9592
10567
|
}
|
|
9593
10568
|
return { install, uninstall, start, stop, status };
|
|
9594
10569
|
}
|
|
9595
|
-
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
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
|
-
}
|
|
10570
|
+
var SYSTEMD_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
10571
|
+
/failed to connect to.*bus/i,
|
|
10572
|
+
/connection refused/i,
|
|
10573
|
+
/access denied/i,
|
|
10574
|
+
/permission denied/i,
|
|
10575
|
+
/interactive authentication required/i
|
|
10576
|
+
];
|
|
10577
|
+
var SYSTEMD_NOT_LOADED = {
|
|
10578
|
+
patterns: [/not loaded/i, /does not exist/i],
|
|
10579
|
+
neverAbsence: SYSTEMD_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9622
10580
|
};
|
|
9623
|
-
function
|
|
9624
|
-
let
|
|
9625
|
-
|
|
9626
|
-
|
|
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
|
-
}
|
|
10581
|
+
function hasControlChar(value) {
|
|
10582
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
10583
|
+
const code = value.charCodeAt(i);
|
|
10584
|
+
if (code < 32 || code === 127) return true;
|
|
9645
10585
|
}
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
10586
|
+
return false;
|
|
10587
|
+
}
|
|
10588
|
+
function assertNoControlChars(value, field) {
|
|
10589
|
+
if (hasControlChar(value)) {
|
|
10590
|
+
throw new Error(
|
|
10591
|
+
`systemd unit ${field} must not contain control characters (newline/CR/etc.) \u2014 refusing to generate a unit file that could inject an unintended directive (got ${JSON.stringify(value)})`
|
|
10592
|
+
);
|
|
9650
10593
|
}
|
|
9651
|
-
return merged;
|
|
9652
10594
|
}
|
|
9653
|
-
function
|
|
9654
|
-
return
|
|
10595
|
+
function escapeSystemdPercent(value) {
|
|
10596
|
+
return value.replace(/%/g, "%%");
|
|
9655
10597
|
}
|
|
9656
|
-
function
|
|
9657
|
-
|
|
9658
|
-
const
|
|
9659
|
-
return
|
|
10598
|
+
function quoteSystemdArg(value) {
|
|
10599
|
+
assertNoControlChars(value, "program.command/program.args entry");
|
|
10600
|
+
const escaped = escapeSystemdPercent(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, () => "$$");
|
|
10601
|
+
return `"${escaped}"`;
|
|
9660
10602
|
}
|
|
9661
|
-
function
|
|
9662
|
-
|
|
10603
|
+
function generateSystemdUnit(def) {
|
|
10604
|
+
const { name, displayName, program, logDir } = def;
|
|
10605
|
+
assertNoControlChars(name, "name");
|
|
10606
|
+
assertNoControlChars(displayName, "displayName");
|
|
10607
|
+
const cwd = program.cwd ?? os.homedir();
|
|
10608
|
+
assertNoControlChars(cwd, "program.cwd");
|
|
10609
|
+
const outLog = path20.join(logDir, `${name}.out.log`);
|
|
10610
|
+
const errLog = path20.join(logDir, `${name}.err.log`);
|
|
10611
|
+
assertNoControlChars(outLog, "logDir");
|
|
10612
|
+
assertNoControlChars(errLog, "logDir");
|
|
10613
|
+
const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
|
|
10614
|
+
return `[Unit]
|
|
10615
|
+
Description=${escapeSystemdPercent(displayName)}
|
|
10616
|
+
|
|
10617
|
+
[Service]
|
|
10618
|
+
Type=simple
|
|
10619
|
+
ExecStart=${execStart}
|
|
10620
|
+
WorkingDirectory=${escapeSystemdPercent(cwd)}
|
|
10621
|
+
Restart=on-failure
|
|
10622
|
+
RestartSec=10
|
|
10623
|
+
StandardOutput=append:${escapeSystemdPercent(outLog)}
|
|
10624
|
+
StandardError=append:${escapeSystemdPercent(errLog)}
|
|
10625
|
+
|
|
10626
|
+
[Install]
|
|
10627
|
+
WantedBy=default.target
|
|
10628
|
+
`;
|
|
9663
10629
|
}
|
|
9664
|
-
function
|
|
9665
|
-
const
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
10630
|
+
function createSystemdLifecycle(def, deps = {}) {
|
|
10631
|
+
const run = deps.run ?? defaultRunner;
|
|
10632
|
+
const fs19 = deps.fs ?? promises;
|
|
10633
|
+
const homedir = deps.homedir ?? (() => os.homedir());
|
|
10634
|
+
const name = sanitizeServiceName(def.name);
|
|
10635
|
+
const unitName = `${name}.service`;
|
|
10636
|
+
const unitPath = () => path20.join(homedir(), ".config", "systemd", "user", unitName);
|
|
10637
|
+
async function fileExists(p) {
|
|
10638
|
+
try {
|
|
10639
|
+
await fs19.stat(p);
|
|
10640
|
+
return true;
|
|
10641
|
+
} catch {
|
|
10642
|
+
return false;
|
|
9672
10643
|
}
|
|
9673
|
-
result.push(arg);
|
|
9674
10644
|
}
|
|
9675
|
-
|
|
9676
|
-
}
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
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;
|
|
10645
|
+
async function writeUnit(program) {
|
|
10646
|
+
const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
10647
|
+
await fs19.mkdir(path20.dirname(unitPath()), { recursive: true });
|
|
10648
|
+
await fs19.mkdir(def.logDir, { recursive: true });
|
|
10649
|
+
await fs19.writeFile(unitPath(), unit, "utf8");
|
|
9691
10650
|
}
|
|
9692
|
-
|
|
9693
|
-
|
|
10651
|
+
async function install(opts = {}) {
|
|
10652
|
+
await writeUnit(opts.program ?? def.program);
|
|
10653
|
+
await runOrThrow(run, "systemctl", ["--user", "daemon-reload"], "systemctl daemon-reload");
|
|
10654
|
+
await runOrThrow(run, "systemctl", ["--user", "enable", "--now", unitName], "systemctl enable --now");
|
|
9694
10655
|
}
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
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();
|
|
10656
|
+
async function uninstall() {
|
|
10657
|
+
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
10658
|
+
await fs19.rm(unitPath(), { force: true });
|
|
10659
|
+
await run("systemctl", ["--user", "daemon-reload"]);
|
|
9719
10660
|
}
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
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)" };
|
|
10661
|
+
async function start() {
|
|
10662
|
+
if (!await fileExists(unitPath())) {
|
|
10663
|
+
throw new Error(`service "${name}" is not installed (no unit file at ${unitPath()}) \u2014 call install() first`);
|
|
9728
10664
|
}
|
|
9729
|
-
|
|
9730
|
-
} catch (err) {
|
|
9731
|
-
return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
|
|
10665
|
+
await runOrThrow(run, "systemctl", ["--user", "start", unitName], "systemctl start");
|
|
9732
10666
|
}
|
|
9733
|
-
|
|
9734
|
-
|
|
10667
|
+
async function stop() {
|
|
10668
|
+
await runIdempotent(run, "systemctl", ["--user", "stop", unitName], "systemctl stop", SYSTEMD_NOT_LOADED);
|
|
9735
10669
|
}
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
const
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
10670
|
+
async function status() {
|
|
10671
|
+
const installed = await fileExists(unitPath());
|
|
10672
|
+
const result = await run("systemctl", ["--user", "is-active", unitName]);
|
|
10673
|
+
const detail = (result.stdout || result.stderr).trim();
|
|
10674
|
+
const running = result.code === 0 && detail === "active";
|
|
10675
|
+
const determinate = running || !SYSTEMD_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
10676
|
+
return { installed, running, determinate, detail };
|
|
9742
10677
|
}
|
|
10678
|
+
return { install, uninstall, start, stop, status };
|
|
9743
10679
|
}
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
9771
|
-
|
|
9772
|
-
|
|
9773
|
-
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
9797
|
-
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
fail(new Error("server did not confirm readiness"));
|
|
9802
|
-
return;
|
|
9803
|
-
}
|
|
9804
|
-
succeed();
|
|
9805
|
-
return;
|
|
9806
|
-
}
|
|
10680
|
+
var WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
10681
|
+
/access is denied/i,
|
|
10682
|
+
/access denied/i,
|
|
10683
|
+
/permission denied/i,
|
|
10684
|
+
/being used by another process/i
|
|
10685
|
+
];
|
|
10686
|
+
var WINSW_NOT_INSTALLED = {
|
|
10687
|
+
codes: [1060],
|
|
10688
|
+
patterns: [/does not exist/i, /non-existent service/i],
|
|
10689
|
+
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
10690
|
+
};
|
|
10691
|
+
var WINSW_ALREADY_STOPPED = {
|
|
10692
|
+
codes: [1062, ...WINSW_NOT_INSTALLED.codes ?? []],
|
|
10693
|
+
patterns: [/not running/i, /has not been started/i, ...WINSW_NOT_INSTALLED.patterns],
|
|
10694
|
+
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
10695
|
+
};
|
|
10696
|
+
function xmlEscape(value) {
|
|
10697
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10698
|
+
}
|
|
10699
|
+
function generateWinswXml(def) {
|
|
10700
|
+
const { id, displayName, program, logDir } = def;
|
|
10701
|
+
const argXml = program.args.map((a) => ` <argument>${xmlEscape(a)}</argument>`).join("\n");
|
|
10702
|
+
const cwdXml = program.cwd ? `
|
|
10703
|
+
<workingdirectory>${xmlEscape(program.cwd)}</workingdirectory>` : "";
|
|
10704
|
+
return `<service>
|
|
10705
|
+
<id>${xmlEscape(id)}</id>
|
|
10706
|
+
<name>${xmlEscape(displayName)}</name>
|
|
10707
|
+
<description>${xmlEscape(displayName)} (managed by byok-agent; see templates/service/winsw/README.md)</description>
|
|
10708
|
+
<executable>${xmlEscape(program.command)}</executable>
|
|
10709
|
+
${argXml}${cwdXml}
|
|
10710
|
+
<logpath>${xmlEscape(logDir)}</logpath>
|
|
10711
|
+
<log mode="roll"></log>
|
|
10712
|
+
<startmode>Automatic</startmode>
|
|
10713
|
+
<onfailure action="restart" delay="10 sec"/>
|
|
10714
|
+
<onfailure action="restart" delay="30 sec"/>
|
|
10715
|
+
<resetfailure>1 hour</resetfailure>
|
|
10716
|
+
</service>
|
|
10717
|
+
`;
|
|
10718
|
+
}
|
|
10719
|
+
function createWinswLifecycle(def, deps = {}) {
|
|
10720
|
+
const run = deps.run ?? defaultRunner;
|
|
10721
|
+
const fs19 = deps.fs ?? promises;
|
|
10722
|
+
const windows = def.windows;
|
|
10723
|
+
if (!windows) {
|
|
10724
|
+
throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
|
|
10725
|
+
}
|
|
10726
|
+
const winswBin = windows.winswBin;
|
|
10727
|
+
const id = sanitizeServiceName(def.name);
|
|
10728
|
+
const installDir = windows.installDir ?? def.logDir;
|
|
10729
|
+
const exePath = path20.join(installDir, `${id}.exe`);
|
|
10730
|
+
const xmlPath = path20.join(installDir, `${id}.xml`);
|
|
10731
|
+
async function fileExists(p) {
|
|
10732
|
+
try {
|
|
10733
|
+
await fs19.stat(p);
|
|
10734
|
+
return true;
|
|
10735
|
+
} catch {
|
|
10736
|
+
return false;
|
|
9807
10737
|
}
|
|
9808
|
-
|
|
9809
|
-
|
|
10738
|
+
}
|
|
10739
|
+
async function writeFiles(program) {
|
|
10740
|
+
const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
10741
|
+
await fs19.mkdir(installDir, { recursive: true });
|
|
10742
|
+
await fs19.mkdir(def.logDir, { recursive: true });
|
|
10743
|
+
await fs19.copyFile(winswBin, exePath);
|
|
10744
|
+
await fs19.writeFile(xmlPath, xml, "utf8");
|
|
10745
|
+
}
|
|
10746
|
+
async function install(opts = {}) {
|
|
10747
|
+
await writeFiles(opts.program ?? def.program);
|
|
10748
|
+
await runOrThrow(run, exePath, ["install"], "winsw install");
|
|
10749
|
+
await runOrThrow(run, exePath, ["start"], "winsw start");
|
|
10750
|
+
}
|
|
10751
|
+
async function uninstall() {
|
|
10752
|
+
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
|
|
10753
|
+
await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
|
|
10754
|
+
await fs19.rm(exePath, { force: true });
|
|
10755
|
+
await fs19.rm(xmlPath, { force: true });
|
|
10756
|
+
}
|
|
10757
|
+
async function start() {
|
|
10758
|
+
if (!await fileExists(xmlPath)) {
|
|
10759
|
+
throw new Error(`service "${id}" is not installed (no config at ${xmlPath}) \u2014 call install() first`);
|
|
9810
10760
|
}
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
10761
|
+
await runOrThrow(run, exePath, ["start"], "winsw start");
|
|
10762
|
+
}
|
|
10763
|
+
async function stop() {
|
|
10764
|
+
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_ALREADY_STOPPED);
|
|
10765
|
+
}
|
|
10766
|
+
async function status() {
|
|
10767
|
+
const installed = await fileExists(xmlPath);
|
|
10768
|
+
const result = await run("sc.exe", ["query", id]);
|
|
10769
|
+
const detail = (result.stdout || result.stderr).trim();
|
|
10770
|
+
const running = result.code === 0 && /\bSTATE\b.*\bRUNNING\b/i.test(detail);
|
|
10771
|
+
const determinate = running || !WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
10772
|
+
return { installed, running, determinate, detail };
|
|
10773
|
+
}
|
|
10774
|
+
return { install, uninstall, start, stop, status };
|
|
9817
10775
|
}
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
9822
|
-
|
|
9823
|
-
|
|
9824
|
-
|
|
9825
|
-
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
|
|
9830
|
-
|
|
9831
|
-
|
|
9832
|
-
|
|
10776
|
+
|
|
10777
|
+
// src/lifecycle/create-service-lifecycle.ts
|
|
10778
|
+
var UnsupportedServicePlatformError = class extends Error {
|
|
10779
|
+
constructor(platform) {
|
|
10780
|
+
super(`no OS service lifecycle for platform "${platform}" \u2014 supported: darwin (launchd), linux (systemd --user), win32 (WinSW)`);
|
|
10781
|
+
this.name = "UnsupportedServicePlatformError";
|
|
10782
|
+
}
|
|
10783
|
+
};
|
|
10784
|
+
function createServiceLifecycle(def, opts = {}) {
|
|
10785
|
+
const platform = opts.platform ?? process.platform;
|
|
10786
|
+
switch (platform) {
|
|
10787
|
+
case "darwin":
|
|
10788
|
+
return createLaunchdLifecycle(def, opts.deps);
|
|
10789
|
+
case "linux":
|
|
10790
|
+
return createSystemdLifecycle(def, opts.deps);
|
|
10791
|
+
case "win32":
|
|
10792
|
+
return createWinswLifecycle(def, opts.deps);
|
|
10793
|
+
default:
|
|
10794
|
+
throw new UnsupportedServicePlatformError(platform);
|
|
10795
|
+
}
|
|
9833
10796
|
}
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
|
|
9837
|
-
|
|
9838
|
-
|
|
9839
|
-
|
|
9840
|
-
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
10797
|
+
var REQUIRED_FIELDS = ["productName", "productId", "serverUrl", "workspaceRoot"];
|
|
10798
|
+
var ConfigError = class extends Error {
|
|
10799
|
+
constructor(message) {
|
|
10800
|
+
super(message);
|
|
10801
|
+
this.name = "ConfigError";
|
|
10802
|
+
}
|
|
10803
|
+
};
|
|
10804
|
+
function loadConfig(configPath, overrides = {}) {
|
|
10805
|
+
let base = {};
|
|
10806
|
+
if (configPath) {
|
|
10807
|
+
let raw;
|
|
10808
|
+
try {
|
|
10809
|
+
raw = readFileSync(configPath, "utf8");
|
|
10810
|
+
} catch (err) {
|
|
10811
|
+
throw new ConfigError(`could not read config at "${configPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
9845
10812
|
}
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
10813
|
+
try {
|
|
10814
|
+
base = JSON.parse(raw);
|
|
10815
|
+
} catch (err) {
|
|
10816
|
+
throw new ConfigError(`config at "${configPath}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
9850
10817
|
}
|
|
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
10818
|
}
|
|
9860
|
-
|
|
9861
|
-
|
|
10819
|
+
const merged = { ...base, ...overrides };
|
|
10820
|
+
if (merged.gitWorkspace !== void 0) {
|
|
9862
10821
|
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);
|
|
10822
|
+
GitWorkspaceManager.validateConfig(merged.gitWorkspace);
|
|
10823
|
+
} catch (error) {
|
|
10824
|
+
throw new ConfigError(error instanceof Error ? error.message : "invalid gitWorkspace configuration");
|
|
9876
10825
|
}
|
|
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
10826
|
}
|
|
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();
|
|
10827
|
+
for (const field of REQUIRED_FIELDS) {
|
|
10828
|
+
if (!merged[field]) {
|
|
10829
|
+
throw new ConfigError(`config is missing required field "${field}"`);
|
|
9913
10830
|
}
|
|
9914
|
-
}
|
|
10831
|
+
}
|
|
10832
|
+
return merged;
|
|
9915
10833
|
}
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
);
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
10834
|
+
function resolveStoreDir(config) {
|
|
10835
|
+
return DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
10836
|
+
}
|
|
10837
|
+
function argValue(args, flag) {
|
|
10838
|
+
const idx = args.indexOf(flag);
|
|
10839
|
+
const value = idx >= 0 ? args[idx + 1] : void 0;
|
|
10840
|
+
return value !== void 0 && !value.startsWith("--") ? value : void 0;
|
|
10841
|
+
}
|
|
10842
|
+
function hasFlag(args, flag) {
|
|
10843
|
+
return args.includes(flag);
|
|
10844
|
+
}
|
|
10845
|
+
function positionalArgs(args, valueFlags = []) {
|
|
10846
|
+
const result = [];
|
|
10847
|
+
for (let i = 0; i < args.length; i++) {
|
|
10848
|
+
const arg = args[i];
|
|
10849
|
+
if (arg === void 0) continue;
|
|
10850
|
+
if (valueFlags.includes(arg)) {
|
|
10851
|
+
i++;
|
|
10852
|
+
continue;
|
|
10853
|
+
}
|
|
10854
|
+
result.push(arg);
|
|
10855
|
+
}
|
|
10856
|
+
return result;
|
|
9933
10857
|
}
|
|
9934
10858
|
|
|
9935
10859
|
// src/bin/format.ts
|
|
@@ -10029,6 +10953,19 @@ function formatDaemonEventLine(event, options = {}) {
|
|
|
10029
10953
|
].filter((part) => part !== void 0);
|
|
10030
10954
|
return parts.join(" ");
|
|
10031
10955
|
}
|
|
10956
|
+
case "device-assertion": {
|
|
10957
|
+
const parts = event.result === "issued" ? [
|
|
10958
|
+
`${prefix} device-assertion result=issued`,
|
|
10959
|
+
`audience=${quote(event.audience)}`,
|
|
10960
|
+
`jti=${event.jti}`,
|
|
10961
|
+
`expiresAt=${event.expiresAt}`
|
|
10962
|
+
] : [
|
|
10963
|
+
`${prefix} device-assertion result=denied`,
|
|
10964
|
+
`reason=${event.reason}`,
|
|
10965
|
+
event.audienceSize !== void 0 ? `audienceSize=${event.audienceSize}` : void 0
|
|
10966
|
+
].filter((part) => part !== void 0);
|
|
10967
|
+
return parts.join(" ");
|
|
10968
|
+
}
|
|
10032
10969
|
}
|
|
10033
10970
|
}
|
|
10034
10971
|
function formatTaskLine(task) {
|
|
@@ -11259,6 +12196,14 @@ function redactForAudit(event) {
|
|
|
11259
12196
|
return { ...base, reason: event.reason, undeliveredOutboxCount: event.undeliveredOutboxCount };
|
|
11260
12197
|
case "stale-approval-decision":
|
|
11261
12198
|
return { ...base, taskId: event.taskId, decision: event.decision, reasonSize: byteSize(event.reason) };
|
|
12199
|
+
case "device-assertion":
|
|
12200
|
+
return event.result === "issued" ? {
|
|
12201
|
+
...base,
|
|
12202
|
+
result: "issued",
|
|
12203
|
+
audience: event.audience,
|
|
12204
|
+
jti: event.jti,
|
|
12205
|
+
expiresAt: event.expiresAt
|
|
12206
|
+
} : { ...base, result: "denied", reason: event.reason, audienceSize: event.audienceSize };
|
|
11262
12207
|
case "git-workspace":
|
|
11263
12208
|
return {
|
|
11264
12209
|
...base,
|
|
@@ -11399,6 +12344,26 @@ function reconstructDaemonEvent(raw) {
|
|
|
11399
12344
|
reason: reasonSize === void 0 ? void 0 : placeholderFor(reasonSize)
|
|
11400
12345
|
};
|
|
11401
12346
|
}
|
|
12347
|
+
case "device-assertion": {
|
|
12348
|
+
if (raw.result === "issued") {
|
|
12349
|
+
return {
|
|
12350
|
+
kind: "device-assertion",
|
|
12351
|
+
ts,
|
|
12352
|
+
result: "issued",
|
|
12353
|
+
audience: typeof raw.audience === "string" ? raw.audience : "",
|
|
12354
|
+
jti: typeof raw.jti === "string" ? raw.jti : "",
|
|
12355
|
+
expiresAt: typeof raw.expiresAt === "string" ? raw.expiresAt : ""
|
|
12356
|
+
};
|
|
12357
|
+
}
|
|
12358
|
+
const audienceSize = num(raw.audienceSize);
|
|
12359
|
+
return {
|
|
12360
|
+
kind: "device-assertion",
|
|
12361
|
+
ts,
|
|
12362
|
+
result: "denied",
|
|
12363
|
+
reason: typeof raw.reason === "string" ? raw.reason : "",
|
|
12364
|
+
...audienceSize === void 0 ? {} : { audienceSize }
|
|
12365
|
+
};
|
|
12366
|
+
}
|
|
11402
12367
|
case "git-workspace": {
|
|
11403
12368
|
const commitsSinceBaseline = gitCount(raw.commitsSinceBaseline);
|
|
11404
12369
|
const dirty = gitDirty(raw.dirty);
|