@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/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { execFile, spawn, spawnSync } from 'child_process';
|
|
2
|
-
import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes,
|
|
2
|
+
import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
|
|
3
3
|
import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, readFileSync, realpathSync } from 'fs';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import path17, { join, isAbsolute } from 'path';
|
|
5
|
+
import os5 from 'os';
|
|
6
|
+
import { parseDeviceAssertionEnvelope, tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash, TRUTH_RECORD_KINDS, nonceSigningBytes, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
7
|
+
import { BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, ToolsetIdSchema, encodeEnvelope, createEnvelope, 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';
|
|
7
8
|
import { promisify } from 'util';
|
|
8
9
|
import { fileURLToPath } from 'url';
|
|
9
10
|
import 'readline';
|
|
10
11
|
import net, { createServer, createConnection } from 'net';
|
|
11
12
|
import { WebSocket } from 'ws';
|
|
12
13
|
import { createRequire } from 'module';
|
|
13
|
-
import { tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, contentHash, TRUTH_RECORD_KINDS } from '@byok-sdk/core';
|
|
14
14
|
|
|
15
15
|
// src/types.ts
|
|
16
16
|
var PolicyUnsupportedError = class extends Error {
|
|
@@ -71,7 +71,7 @@ function gitEnvironment(readOnly) {
|
|
|
71
71
|
return env;
|
|
72
72
|
}
|
|
73
73
|
function stableGitWorkspaceOwnerId(storeDir, productId) {
|
|
74
|
-
const identity = `${
|
|
74
|
+
const identity = `${path17.resolve(storeDir)}\\0${productId}`;
|
|
75
75
|
return `store-product:${createHash("sha256").update(identity).digest("hex")}`;
|
|
76
76
|
}
|
|
77
77
|
var GUIDANCE = [
|
|
@@ -83,11 +83,11 @@ var GUIDANCE = [
|
|
|
83
83
|
"Leave incomplete work visible for recovery."
|
|
84
84
|
].join("\n");
|
|
85
85
|
function canonical(value) {
|
|
86
|
-
return
|
|
86
|
+
return path17.resolve(value);
|
|
87
87
|
}
|
|
88
88
|
function isContained(root, candidate) {
|
|
89
|
-
const relative =
|
|
90
|
-
return relative === "" || !relative.startsWith(`..${
|
|
89
|
+
const relative = path17.relative(root, candidate);
|
|
90
|
+
return relative === "" || !relative.startsWith(`..${path17.sep}`) && !path17.isAbsolute(relative);
|
|
91
91
|
}
|
|
92
92
|
function bounded(value, max) {
|
|
93
93
|
return Buffer.byteLength(value, "utf8") <= max ? value : value.slice(0, max);
|
|
@@ -192,7 +192,7 @@ var GitWorkspaceManager = class {
|
|
|
192
192
|
await this.ensureOwnerMarker();
|
|
193
193
|
}
|
|
194
194
|
async ensureOwnerMarker() {
|
|
195
|
-
const markerPath =
|
|
195
|
+
const markerPath = path17.join(this.workspaceRoot, OWNER_MARKER);
|
|
196
196
|
let existing;
|
|
197
197
|
try {
|
|
198
198
|
existing = JSON.parse(await promises.readFile(markerPath, "utf8"));
|
|
@@ -351,7 +351,7 @@ ${instruction}`;
|
|
|
351
351
|
if (error instanceof GitWorkspaceError || code !== "ENOENT" && code !== "ENOTDIR") {
|
|
352
352
|
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
353
353
|
}
|
|
354
|
-
const parent =
|
|
354
|
+
const parent = path17.dirname(current);
|
|
355
355
|
if (parent === current) {
|
|
356
356
|
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
357
357
|
}
|
|
@@ -416,7 +416,7 @@ async function atomicWriteFile(filePath, data, options = {}) {
|
|
|
416
416
|
await target.close();
|
|
417
417
|
}
|
|
418
418
|
if (process.platform !== "win32") {
|
|
419
|
-
const directory = await promises.open(
|
|
419
|
+
const directory = await promises.open(path17.dirname(filePath), "r");
|
|
420
420
|
try {
|
|
421
421
|
await directory.sync();
|
|
422
422
|
} finally {
|
|
@@ -508,7 +508,7 @@ async function ensureSecureDir(dir, opts = {}) {
|
|
|
508
508
|
await promises.chmod(dir, 448).catch(() => {
|
|
509
509
|
});
|
|
510
510
|
if (platform !== "win32") return;
|
|
511
|
-
const { username } =
|
|
511
|
+
const { username } = os5.userInfo();
|
|
512
512
|
let result;
|
|
513
513
|
try {
|
|
514
514
|
result = await run("icacls", buildIcaclsArgs(dir, username));
|
|
@@ -541,7 +541,7 @@ function isProtected(record) {
|
|
|
541
541
|
var GitWorkspaceStore = class {
|
|
542
542
|
constructor(storeDir, options = {}) {
|
|
543
543
|
this.storeDir = storeDir;
|
|
544
|
-
this.filePath =
|
|
544
|
+
this.filePath = path17.join(storeDir, FILE_NAME);
|
|
545
545
|
this.maxRecords = Math.max(1, Math.floor(options.maxRecords ?? MAX_RECORDS));
|
|
546
546
|
}
|
|
547
547
|
storeDir;
|
|
@@ -673,7 +673,7 @@ var GitWorkspaceStore = class {
|
|
|
673
673
|
};
|
|
674
674
|
var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
675
675
|
function readPackageJson(dir) {
|
|
676
|
-
const candidate =
|
|
676
|
+
const candidate = path17.join(dir, "package.json");
|
|
677
677
|
if (!existsSync(candidate)) return void 0;
|
|
678
678
|
try {
|
|
679
679
|
return JSON.parse(readFileSync(candidate, "utf8"));
|
|
@@ -688,17 +688,17 @@ function resolvePiBin() {
|
|
|
688
688
|
}
|
|
689
689
|
try {
|
|
690
690
|
const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
|
|
691
|
-
let dir =
|
|
691
|
+
let dir = path17.dirname(fileURLToPath(mainEntryUrl));
|
|
692
692
|
for (let depth = 0; depth < 6; depth++) {
|
|
693
693
|
const pkg = readPackageJson(dir);
|
|
694
694
|
if (pkg?.name === PI_PACKAGE_NAME) {
|
|
695
695
|
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
|
|
696
696
|
if (binRel) {
|
|
697
|
-
return { command:
|
|
697
|
+
return { command: path17.join(dir, binRel), source: "package" };
|
|
698
698
|
}
|
|
699
699
|
break;
|
|
700
700
|
}
|
|
701
|
-
const parent =
|
|
701
|
+
const parent = path17.dirname(dir);
|
|
702
702
|
if (parent === dir) break;
|
|
703
703
|
dir = parent;
|
|
704
704
|
}
|
|
@@ -1088,13 +1088,8 @@ var PiRpcClient = class {
|
|
|
1088
1088
|
}
|
|
1089
1089
|
};
|
|
1090
1090
|
|
|
1091
|
-
// src/adapters/
|
|
1092
|
-
var
|
|
1093
|
-
var DETECT_TIMEOUT_MS = 5e3;
|
|
1094
|
-
function errorMessage(err) {
|
|
1095
|
-
return err instanceof Error ? err.message : String(err);
|
|
1096
|
-
}
|
|
1097
|
-
var KNOWN_PROVIDER_ENV_VARS = [
|
|
1091
|
+
// src/adapters/provider-credential-environment.ts
|
|
1092
|
+
var PROVIDER_CREDENTIAL_ENV_NAMES = [
|
|
1098
1093
|
"ANTHROPIC_API_KEY",
|
|
1099
1094
|
"ANTHROPIC_OAUTH_TOKEN",
|
|
1100
1095
|
"OPENAI_API_KEY",
|
|
@@ -1105,24 +1100,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
|
|
|
1105
1100
|
"MISTRAL_API_KEY",
|
|
1106
1101
|
"OPENROUTER_API_KEY",
|
|
1107
1102
|
"XAI_API_KEY",
|
|
1108
|
-
// Confirmed against the installed pi's own docs/providers.md ("ZAI |
|
|
1109
|
-
// `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
|
|
1110
|
-
// during this task's acceptance run — omitting it made `authPresent`
|
|
1111
|
-
// silently false for a perfectly valid, working z.ai/GLM setup.
|
|
1112
1103
|
"ZAI_API_KEY"
|
|
1113
1104
|
];
|
|
1105
|
+
var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
|
|
1106
|
+
...PROVIDER_CREDENTIAL_ENV_NAMES,
|
|
1107
|
+
"ANT_LING_API_KEY",
|
|
1108
|
+
"NVIDIA_API_KEY",
|
|
1109
|
+
"CEREBRAS_API_KEY",
|
|
1110
|
+
"CLOUDFLARE_API_KEY",
|
|
1111
|
+
"AI_GATEWAY_API_KEY",
|
|
1112
|
+
"ZAI_CODING_CN_API_KEY",
|
|
1113
|
+
"OPENCODE_API_KEY",
|
|
1114
|
+
"RADIUS_API_KEY",
|
|
1115
|
+
"FIREWORKS_API_KEY",
|
|
1116
|
+
"TOGETHER_API_KEY",
|
|
1117
|
+
"BASETEN_API_KEY",
|
|
1118
|
+
"KIMI_API_KEY",
|
|
1119
|
+
"MINIMAX_API_KEY",
|
|
1120
|
+
"MINIMAX_CN_API_KEY",
|
|
1121
|
+
"QWEN_TOKEN_PLAN_API_KEY",
|
|
1122
|
+
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
1123
|
+
"XIAOMI_API_KEY",
|
|
1124
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
1125
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
1126
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
1127
|
+
"AWS_ACCESS_KEY_ID",
|
|
1128
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1129
|
+
"AWS_SESSION_TOKEN",
|
|
1130
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
1131
|
+
// Reserved by the keys-owned Pi projection. It must never be inherited
|
|
1132
|
+
// from the daemon; the launcher deletes any ambient copy and injects only
|
|
1133
|
+
// the exact credential it just resolved from OS custody.
|
|
1134
|
+
"PI_PROVIDER_API_KEY"
|
|
1135
|
+
];
|
|
1136
|
+
function withoutProviderCredentials(env) {
|
|
1137
|
+
const sanitized = { ...env };
|
|
1138
|
+
for (const name of PROVIDER_CREDENTIAL_ENV_DENY_NAMES) {
|
|
1139
|
+
delete sanitized[name];
|
|
1140
|
+
}
|
|
1141
|
+
return sanitized;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/adapters/pi/pi-adapter.ts
|
|
1145
|
+
var execFileAsync = promisify(execFile);
|
|
1146
|
+
var DETECT_TIMEOUT_MS = 5e3;
|
|
1147
|
+
function errorMessage(err) {
|
|
1148
|
+
return err instanceof Error ? err.message : String(err);
|
|
1149
|
+
}
|
|
1114
1150
|
var PiAdapter = class {
|
|
1115
1151
|
constructor(options = {}) {
|
|
1116
1152
|
this.options = options;
|
|
1117
1153
|
}
|
|
1118
1154
|
options;
|
|
1119
1155
|
id = "pi";
|
|
1156
|
+
supportsDispatchSelection = true;
|
|
1120
1157
|
async detect() {
|
|
1121
1158
|
try {
|
|
1122
1159
|
const bin = this.resolveBin();
|
|
1123
1160
|
const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
|
|
1124
1161
|
const version = stdout.trim() || stderr.trim();
|
|
1125
|
-
const authPresent =
|
|
1162
|
+
const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
|
|
1126
1163
|
return { present: true, version, authPresent };
|
|
1127
1164
|
} catch {
|
|
1128
1165
|
return { present: false };
|
|
@@ -1141,7 +1178,7 @@ var PiAdapter = class {
|
|
|
1141
1178
|
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
1142
1179
|
*/
|
|
1143
1180
|
environmentRequirements() {
|
|
1144
|
-
return { credentialNames:
|
|
1181
|
+
return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
|
|
1145
1182
|
}
|
|
1146
1183
|
async start(task, ctx) {
|
|
1147
1184
|
if (typeof task.instruction !== "string") {
|
|
@@ -1153,12 +1190,45 @@ var PiAdapter = class {
|
|
|
1153
1190
|
}
|
|
1154
1191
|
const bin = this.resolveBin();
|
|
1155
1192
|
const resumeSessionId = task.sessionRef;
|
|
1156
|
-
const
|
|
1193
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
1194
|
+
const selection = task.dispatchSelection;
|
|
1195
|
+
let command = bin.command;
|
|
1196
|
+
let args = piArgs;
|
|
1197
|
+
if (selection !== void 0) {
|
|
1198
|
+
if (selection.lane !== "byok" || selection.runtimeId !== "pi") {
|
|
1199
|
+
throw new PolicyUnsupportedError(
|
|
1200
|
+
`pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
const launcher = this.options.byokLauncher;
|
|
1204
|
+
if (launcher === void 0) {
|
|
1205
|
+
throw new PolicyUnsupportedError(
|
|
1206
|
+
"pi BYOK selection requires a configured credential-custody launcher"
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
command = launcher.command;
|
|
1210
|
+
args = [
|
|
1211
|
+
...launcher.args ?? [],
|
|
1212
|
+
"--pi-bin",
|
|
1213
|
+
bin.command,
|
|
1214
|
+
"--profile-db",
|
|
1215
|
+
launcher.profileDbPath,
|
|
1216
|
+
"--session-dir",
|
|
1217
|
+
launcher.sessionDir,
|
|
1218
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
1219
|
+
"--provider",
|
|
1220
|
+
selection.providerId,
|
|
1221
|
+
"--model",
|
|
1222
|
+
selection.modelId,
|
|
1223
|
+
"--",
|
|
1224
|
+
...piArgs
|
|
1225
|
+
];
|
|
1226
|
+
}
|
|
1157
1227
|
const rpc = new PiRpcClient({
|
|
1158
|
-
command
|
|
1228
|
+
command,
|
|
1159
1229
|
args,
|
|
1160
1230
|
cwd: ctx.workspaceDir,
|
|
1161
|
-
env: ctx.env,
|
|
1231
|
+
env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
|
|
1162
1232
|
spawnFn: this.options.spawnFn
|
|
1163
1233
|
});
|
|
1164
1234
|
const response = await rpc.send({ type: "prompt", message: task.instruction });
|
|
@@ -1177,7 +1247,7 @@ var PiAdapter = class {
|
|
|
1177
1247
|
throw err;
|
|
1178
1248
|
}
|
|
1179
1249
|
}
|
|
1180
|
-
return new PiSession(sessionRef, rpc);
|
|
1250
|
+
return new PiSession(sessionRef, rpc, selection);
|
|
1181
1251
|
}
|
|
1182
1252
|
resolveBin() {
|
|
1183
1253
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
@@ -1205,12 +1275,14 @@ async function resolveFreshSessionId(rpc) {
|
|
|
1205
1275
|
);
|
|
1206
1276
|
}
|
|
1207
1277
|
var PiSession = class {
|
|
1208
|
-
constructor(sessionRef, rpc) {
|
|
1278
|
+
constructor(sessionRef, rpc, selection) {
|
|
1209
1279
|
this.sessionRef = sessionRef;
|
|
1210
1280
|
this.rpc = rpc;
|
|
1281
|
+
this.selection = selection;
|
|
1211
1282
|
}
|
|
1212
1283
|
sessionRef;
|
|
1213
1284
|
rpc;
|
|
1285
|
+
selection;
|
|
1214
1286
|
get events() {
|
|
1215
1287
|
const rpc = this.rpc;
|
|
1216
1288
|
return {
|
|
@@ -1239,6 +1311,12 @@ var PiSession = class {
|
|
|
1239
1311
|
if (typeof task.instruction !== "string") {
|
|
1240
1312
|
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1241
1313
|
}
|
|
1314
|
+
const requestedSelection = task.dispatchSelection;
|
|
1315
|
+
if (requestedSelection !== void 0 && (this.selection?.lane !== "byok" || requestedSelection.lane !== "byok" || requestedSelection.runtimeId !== "pi" || requestedSelection.providerId !== this.selection.providerId || requestedSelection.modelId !== this.selection.modelId)) {
|
|
1316
|
+
throw new PolicyUnsupportedError(
|
|
1317
|
+
"pi persistent session cannot change its authoritative BYOK provider/model selection"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1242
1320
|
await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
|
|
1243
1321
|
}
|
|
1244
1322
|
async interrupt() {
|
|
@@ -1265,7 +1343,7 @@ function resolveApprovalMcpBin() {
|
|
|
1265
1343
|
if (override) {
|
|
1266
1344
|
return { command: override, args: [], source: "env" };
|
|
1267
1345
|
}
|
|
1268
|
-
const distBin =
|
|
1346
|
+
const distBin = path17.join(path17.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
|
|
1269
1347
|
return { command: process.execPath, args: [distBin], source: "dist" };
|
|
1270
1348
|
}
|
|
1271
1349
|
|
|
@@ -1345,7 +1423,7 @@ var EXTENSION_CONTENT_TYPES = {
|
|
|
1345
1423
|
".yml": "application/yaml"
|
|
1346
1424
|
};
|
|
1347
1425
|
function guessContentType(filePath) {
|
|
1348
|
-
const ext =
|
|
1426
|
+
const ext = path17.extname(filePath).toLowerCase();
|
|
1349
1427
|
return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
1350
1428
|
}
|
|
1351
1429
|
function mapAssistant(msg, correlation) {
|
|
@@ -1417,11 +1495,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
|
|
|
1417
1495
|
const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
|
|
1418
1496
|
if (!filePath) return void 0;
|
|
1419
1497
|
const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
|
|
1420
|
-
const fileDir =
|
|
1498
|
+
const fileDir = path17.dirname(filePath);
|
|
1421
1499
|
const realFileDir = tryRealpath(fileDir) ?? fileDir;
|
|
1422
|
-
const realFilePath =
|
|
1423
|
-
const relative =
|
|
1424
|
-
if (relative === "" || relative.startsWith("..") ||
|
|
1500
|
+
const realFilePath = path17.join(realFileDir, path17.basename(filePath));
|
|
1501
|
+
const relative = path17.relative(realWorkspaceDir, realFilePath);
|
|
1502
|
+
if (relative === "" || relative.startsWith("..") || path17.isAbsolute(relative)) {
|
|
1425
1503
|
return void 0;
|
|
1426
1504
|
}
|
|
1427
1505
|
return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
|
|
@@ -1663,7 +1741,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1663
1741
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1664
1742
|
var execFileAsync2 = promisify(execFile);
|
|
1665
1743
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
1666
|
-
async function
|
|
1744
|
+
async function cleanupMcpConfigDir(dir) {
|
|
1667
1745
|
if (!dir) return;
|
|
1668
1746
|
await promises.rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1669
1747
|
});
|
|
@@ -1673,6 +1751,7 @@ var ClaudeAdapter = class {
|
|
|
1673
1751
|
this.options = options;
|
|
1674
1752
|
}
|
|
1675
1753
|
options;
|
|
1754
|
+
supportsDispatchSelection = true;
|
|
1676
1755
|
id = "claude";
|
|
1677
1756
|
async detect() {
|
|
1678
1757
|
const bin = this.resolveBin();
|
|
@@ -1686,7 +1765,13 @@ var ClaudeAdapter = class {
|
|
|
1686
1765
|
}
|
|
1687
1766
|
}
|
|
1688
1767
|
capabilities() {
|
|
1689
|
-
return {
|
|
1768
|
+
return {
|
|
1769
|
+
steer: false,
|
|
1770
|
+
resume: true,
|
|
1771
|
+
approvalInteractive: true,
|
|
1772
|
+
mcpToolsets: true,
|
|
1773
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
1774
|
+
};
|
|
1690
1775
|
}
|
|
1691
1776
|
/**
|
|
1692
1777
|
* M5: deliberate product-boundary decision, not an oversight — byok's
|
|
@@ -1712,42 +1797,51 @@ var ClaudeAdapter = class {
|
|
|
1712
1797
|
if (!mapping.ok) {
|
|
1713
1798
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
1714
1799
|
}
|
|
1715
|
-
|
|
1800
|
+
const modelId = subscriptionModel(task, "claude");
|
|
1801
|
+
let mcpConfigDir;
|
|
1802
|
+
const taskMcpServers = ctx.mcpServers ?? {};
|
|
1803
|
+
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
1716
1804
|
if (mapping.needsApprovalMcp) {
|
|
1717
1805
|
if (!ctx.approvalChannel) {
|
|
1718
1806
|
throw new PolicyUnsupportedError(
|
|
1719
1807
|
'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
|
|
1720
1808
|
);
|
|
1721
1809
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1810
|
+
if (Object.prototype.hasOwnProperty.call(taskMcpServers, APPROVAL_MCP_SERVER_NAME)) {
|
|
1811
|
+
throw new PolicyUnsupportedError(
|
|
1812
|
+
`MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (needsMcpConfig) {
|
|
1817
|
+
mcpConfigDir = await promises.mkdtemp(path17.join(os5.tmpdir(), "byok-mcp-"));
|
|
1818
|
+
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
1725
1819
|
});
|
|
1726
|
-
const mcpConfigPath =
|
|
1727
|
-
const
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1820
|
+
const mcpConfigPath = path17.join(mcpConfigDir, "mcp-config.json");
|
|
1821
|
+
const mcpServers = { ...taskMcpServers };
|
|
1822
|
+
if (mapping.needsApprovalMcp) {
|
|
1823
|
+
const approvalChannel = ctx.approvalChannel;
|
|
1824
|
+
if (!approvalChannel) throw new Error("unreachable: approval channel checked above");
|
|
1825
|
+
const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
1826
|
+
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
1827
|
+
command: approvalMcpBin.command,
|
|
1828
|
+
args: approvalMcpBin.args,
|
|
1829
|
+
env: {
|
|
1830
|
+
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
1831
|
+
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
1832
|
+
BYOK_TASK_ID: approvalChannel.taskId,
|
|
1833
|
+
BYOK_APPROVAL_TIMEOUT_MS: String(approvalChannel.timeoutMs)
|
|
1738
1834
|
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
await promises.writeFile(mcpConfigPath, JSON.stringify(
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
1742
1838
|
mapping.args = [
|
|
1743
1839
|
...mapping.args,
|
|
1744
|
-
"--permission-prompt-tool",
|
|
1745
|
-
`mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
|
|
1840
|
+
...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
|
|
1746
1841
|
"--mcp-config",
|
|
1747
1842
|
mcpConfigPath,
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
// the only MCP server this invocation should ever see.
|
|
1843
|
+
// The generated file is the complete task-scoped MCP authority.
|
|
1844
|
+
// Never merge ambient user/project MCP configuration into it.
|
|
1751
1845
|
"--strict-mcp-config"
|
|
1752
1846
|
];
|
|
1753
1847
|
}
|
|
@@ -1764,6 +1858,7 @@ var ClaudeAdapter = class {
|
|
|
1764
1858
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1765
1859
|
// --verbose", before spawning any model call.
|
|
1766
1860
|
"--verbose",
|
|
1861
|
+
...modelId ? ["--model", modelId] : [],
|
|
1767
1862
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1768
1863
|
...mapping.args
|
|
1769
1864
|
];
|
|
@@ -1771,7 +1866,7 @@ var ClaudeAdapter = class {
|
|
|
1771
1866
|
command: bin.command,
|
|
1772
1867
|
args,
|
|
1773
1868
|
cwd: ctx.workspaceDir,
|
|
1774
|
-
env: ctx.env,
|
|
1869
|
+
env: withoutProviderCredentials(ctx.env),
|
|
1775
1870
|
spawnFn: this.options.spawnFn
|
|
1776
1871
|
});
|
|
1777
1872
|
client.writeUserMessage(task.instruction);
|
|
@@ -1780,17 +1875,24 @@ var ClaudeAdapter = class {
|
|
|
1780
1875
|
sessionRef = await client.waitForInit();
|
|
1781
1876
|
} catch (err) {
|
|
1782
1877
|
client.kill();
|
|
1783
|
-
await
|
|
1878
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1784
1879
|
throw err;
|
|
1785
1880
|
}
|
|
1786
1881
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1787
1882
|
client.kill();
|
|
1788
|
-
await
|
|
1883
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1789
1884
|
throw new Error(
|
|
1790
1885
|
`claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
|
|
1791
1886
|
);
|
|
1792
1887
|
}
|
|
1793
|
-
return new ClaudeSession(
|
|
1888
|
+
return new ClaudeSession(
|
|
1889
|
+
sessionRef,
|
|
1890
|
+
client,
|
|
1891
|
+
ctx.workspaceDir,
|
|
1892
|
+
ctx.approvalChannel,
|
|
1893
|
+
mcpConfigDir,
|
|
1894
|
+
modelId
|
|
1895
|
+
);
|
|
1794
1896
|
}
|
|
1795
1897
|
/**
|
|
1796
1898
|
* `claude auth status --json` is claude's OWN non-secret login-state
|
|
@@ -1824,19 +1926,31 @@ var ClaudeAdapter = class {
|
|
|
1824
1926
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1825
1927
|
}
|
|
1826
1928
|
};
|
|
1929
|
+
function subscriptionModel(task, runtimeId) {
|
|
1930
|
+
const selection = task.dispatchSelection;
|
|
1931
|
+
if (selection === void 0) return void 0;
|
|
1932
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
1933
|
+
throw new PolicyUnsupportedError(
|
|
1934
|
+
`claude adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
return selection.modelId;
|
|
1938
|
+
}
|
|
1827
1939
|
var ClaudeSession = class {
|
|
1828
|
-
constructor(sessionRef, client, workspaceDir, approvalChannel,
|
|
1940
|
+
constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
|
|
1829
1941
|
this.sessionRef = sessionRef;
|
|
1830
1942
|
this.client = client;
|
|
1831
1943
|
this.workspaceDir = workspaceDir;
|
|
1832
1944
|
this.approvalChannel = approvalChannel;
|
|
1833
|
-
this.
|
|
1945
|
+
this.mcpConfigDir = mcpConfigDir;
|
|
1946
|
+
this.modelId = modelId;
|
|
1834
1947
|
}
|
|
1835
1948
|
sessionRef;
|
|
1836
1949
|
client;
|
|
1837
1950
|
workspaceDir;
|
|
1838
1951
|
approvalChannel;
|
|
1839
|
-
|
|
1952
|
+
mcpConfigDir;
|
|
1953
|
+
modelId;
|
|
1840
1954
|
correlation = createToolUseCorrelation();
|
|
1841
1955
|
get events() {
|
|
1842
1956
|
const client = this.client;
|
|
@@ -1887,6 +2001,12 @@ var ClaudeSession = class {
|
|
|
1887
2001
|
if (typeof task.instruction !== "string") {
|
|
1888
2002
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1889
2003
|
}
|
|
2004
|
+
const requestedModel = subscriptionModel(task, "claude");
|
|
2005
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2006
|
+
throw new PolicyUnsupportedError(
|
|
2007
|
+
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
1890
2010
|
this.client.writeUserMessage(task.instruction);
|
|
1891
2011
|
}
|
|
1892
2012
|
/**
|
|
@@ -1906,7 +2026,7 @@ var ClaudeSession = class {
|
|
|
1906
2026
|
}
|
|
1907
2027
|
async close() {
|
|
1908
2028
|
this.client.kill();
|
|
1909
|
-
await
|
|
2029
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
1910
2030
|
}
|
|
1911
2031
|
/**
|
|
1912
2032
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -2070,8 +2190,8 @@ function extractArtifactEvents(changes, workspaceDir) {
|
|
|
2070
2190
|
const absolutePath = typeof change.path === "string" ? change.path : void 0;
|
|
2071
2191
|
const kind = typeof change.kind === "string" ? change.kind : void 0;
|
|
2072
2192
|
if (!absolutePath || kind === "delete") continue;
|
|
2073
|
-
const relative =
|
|
2074
|
-
if (relative.length === 0 || relative.startsWith("..") ||
|
|
2193
|
+
const relative = path17.relative(workspaceDir, absolutePath);
|
|
2194
|
+
if (relative.length === 0 || relative.startsWith("..") || path17.isAbsolute(relative)) continue;
|
|
2075
2195
|
events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
|
|
2076
2196
|
}
|
|
2077
2197
|
return events;
|
|
@@ -2092,7 +2212,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
|
|
|
2092
2212
|
".csv": "text/csv"
|
|
2093
2213
|
};
|
|
2094
2214
|
function guessContentType2(relativePath) {
|
|
2095
|
-
return CONTENT_TYPE_BY_EXTENSION[
|
|
2215
|
+
return CONTENT_TYPE_BY_EXTENSION[path17.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
|
|
2096
2216
|
}
|
|
2097
2217
|
function extractErrorMessage(rawError) {
|
|
2098
2218
|
if (typeof rawError === "string") return rawError;
|
|
@@ -2230,6 +2350,7 @@ var CodexAdapter = class {
|
|
|
2230
2350
|
this.options = options;
|
|
2231
2351
|
}
|
|
2232
2352
|
options;
|
|
2353
|
+
supportsDispatchSelection = true;
|
|
2233
2354
|
id = "codex";
|
|
2234
2355
|
async detect() {
|
|
2235
2356
|
const bin = this.resolveBin();
|
|
@@ -2302,17 +2423,20 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2302
2423
|
if (!mapping.ok) {
|
|
2303
2424
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2304
2425
|
}
|
|
2426
|
+
const modelId = subscriptionModel2(task);
|
|
2305
2427
|
const bin = this.resolveBin();
|
|
2306
2428
|
const queue = new AsyncQueue();
|
|
2307
2429
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
2308
2430
|
const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
|
|
2431
|
+
const runtimeEnv = withoutProviderCredentials(ctx.env);
|
|
2309
2432
|
const { sessionRef, runner } = await runCodexTurn({
|
|
2310
2433
|
command: bin.command,
|
|
2311
2434
|
resumeRef: task.sessionRef,
|
|
2312
2435
|
instruction: task.instruction,
|
|
2436
|
+
modelId,
|
|
2313
2437
|
policyArgs: mapping.args,
|
|
2314
2438
|
cwd: ctx.workspaceDir,
|
|
2315
|
-
env:
|
|
2439
|
+
env: runtimeEnv,
|
|
2316
2440
|
spawnFn: this.options.spawnFn,
|
|
2317
2441
|
workspaceDir,
|
|
2318
2442
|
queue,
|
|
@@ -2329,7 +2453,8 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2329
2453
|
queue,
|
|
2330
2454
|
recordUnmapped,
|
|
2331
2455
|
initialRunner: runner,
|
|
2332
|
-
preparedGit: ctx.gitWorkspace !== void 0
|
|
2456
|
+
preparedGit: ctx.gitWorkspace !== void 0,
|
|
2457
|
+
modelId
|
|
2333
2458
|
});
|
|
2334
2459
|
}
|
|
2335
2460
|
resolveBin() {
|
|
@@ -2350,12 +2475,25 @@ function makeUnmappedFrameRecorder(counts) {
|
|
|
2350
2475
|
}
|
|
2351
2476
|
};
|
|
2352
2477
|
}
|
|
2353
|
-
function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
|
|
2478
|
+
function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
|
|
2354
2479
|
const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
|
|
2355
|
-
return [
|
|
2480
|
+
return [
|
|
2481
|
+
...base,
|
|
2482
|
+
"--json",
|
|
2483
|
+
...modelId ? ["--model", modelId] : [],
|
|
2484
|
+
...preparedGit ? [] : ["--skip-git-repo-check"],
|
|
2485
|
+
...policyArgs,
|
|
2486
|
+
instruction
|
|
2487
|
+
];
|
|
2356
2488
|
}
|
|
2357
2489
|
async function runCodexTurn(params) {
|
|
2358
|
-
const argv = buildArgv(
|
|
2490
|
+
const argv = buildArgv(
|
|
2491
|
+
params.resumeRef,
|
|
2492
|
+
params.policyArgs,
|
|
2493
|
+
params.instruction,
|
|
2494
|
+
params.modelId,
|
|
2495
|
+
params.preparedGit
|
|
2496
|
+
);
|
|
2359
2497
|
let firstLineSettled = false;
|
|
2360
2498
|
let resolveFirstLine;
|
|
2361
2499
|
let rejectFirstLine;
|
|
@@ -2456,6 +2594,7 @@ var CodexSession = class {
|
|
|
2456
2594
|
queue;
|
|
2457
2595
|
recordUnmapped;
|
|
2458
2596
|
preparedGit;
|
|
2597
|
+
modelId;
|
|
2459
2598
|
currentRunner;
|
|
2460
2599
|
closed = false;
|
|
2461
2600
|
constructor(options) {
|
|
@@ -2467,6 +2606,7 @@ var CodexSession = class {
|
|
|
2467
2606
|
this.queue = options.queue;
|
|
2468
2607
|
this.recordUnmapped = options.recordUnmapped;
|
|
2469
2608
|
this.preparedGit = options.preparedGit;
|
|
2609
|
+
this.modelId = options.modelId;
|
|
2470
2610
|
this.currentRunner = options.initialRunner;
|
|
2471
2611
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
2472
2612
|
}
|
|
@@ -2527,6 +2667,13 @@ var CodexSession = class {
|
|
|
2527
2667
|
if (!mapping.ok) {
|
|
2528
2668
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2529
2669
|
}
|
|
2670
|
+
const requestedModel = subscriptionModel2(task);
|
|
2671
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2672
|
+
throw new PolicyUnsupportedError(
|
|
2673
|
+
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
const modelId = this.modelId;
|
|
2530
2677
|
const resumeRef = this.sessionRef;
|
|
2531
2678
|
let sessionRef;
|
|
2532
2679
|
let runner;
|
|
@@ -2535,9 +2682,10 @@ var CodexSession = class {
|
|
|
2535
2682
|
command: this.command,
|
|
2536
2683
|
resumeRef,
|
|
2537
2684
|
instruction: task.instruction,
|
|
2685
|
+
modelId,
|
|
2538
2686
|
policyArgs: mapping.args,
|
|
2539
2687
|
cwd: this.workspaceDir,
|
|
2540
|
-
env: this.env,
|
|
2688
|
+
env: withoutProviderCredentials(this.env),
|
|
2541
2689
|
spawnFn: this.spawnFn,
|
|
2542
2690
|
workspaceDir: this.workspaceDir,
|
|
2543
2691
|
queue: this.queue,
|
|
@@ -2597,6 +2745,16 @@ var CodexSession = class {
|
|
|
2597
2745
|
);
|
|
2598
2746
|
}
|
|
2599
2747
|
};
|
|
2748
|
+
function subscriptionModel2(task) {
|
|
2749
|
+
const selection = task.dispatchSelection;
|
|
2750
|
+
if (selection === void 0) return void 0;
|
|
2751
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
2752
|
+
throw new PolicyUnsupportedError(
|
|
2753
|
+
`codex adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
return selection.modelId;
|
|
2757
|
+
}
|
|
2600
2758
|
|
|
2601
2759
|
// src/daemon/approvals.ts
|
|
2602
2760
|
var ApprovalNotFoundError = class extends Error {
|
|
@@ -2699,12 +2857,12 @@ var DeviceStore = class _DeviceStore {
|
|
|
2699
2857
|
*/
|
|
2700
2858
|
constructor(storeDir, secureDirOptions) {
|
|
2701
2859
|
this.secureDirOptions = secureDirOptions;
|
|
2702
|
-
this.filePath =
|
|
2860
|
+
this.filePath = path17.join(storeDir, "device.json");
|
|
2703
2861
|
}
|
|
2704
2862
|
secureDirOptions;
|
|
2705
2863
|
filePath;
|
|
2706
2864
|
static defaultDir(productId) {
|
|
2707
|
-
return
|
|
2865
|
+
return path17.join(os5.homedir(), ".byok", productId);
|
|
2708
2866
|
}
|
|
2709
2867
|
/**
|
|
2710
2868
|
* Resolve the one store pathname every daemon/CLI component must share.
|
|
@@ -2713,7 +2871,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
2713
2871
|
* cwd to pin a quarantine directory inode.
|
|
2714
2872
|
*/
|
|
2715
2873
|
static resolveDir(productId, configured) {
|
|
2716
|
-
return
|
|
2874
|
+
return path17.resolve(configured ?? _DeviceStore.defaultDir(productId));
|
|
2717
2875
|
}
|
|
2718
2876
|
async load() {
|
|
2719
2877
|
const opened = await this.openBounded();
|
|
@@ -2754,7 +2912,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
2754
2912
|
}
|
|
2755
2913
|
}
|
|
2756
2914
|
async save(record) {
|
|
2757
|
-
const storeDir =
|
|
2915
|
+
const storeDir = path17.dirname(this.filePath);
|
|
2758
2916
|
await ensureSecureDir(storeDir, this.secureDirOptions);
|
|
2759
2917
|
await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
|
|
2760
2918
|
}
|
|
@@ -2821,13 +2979,10 @@ function exportPrivateKeyPem(privateKey) {
|
|
|
2821
2979
|
function importPrivateKeyPem(pem) {
|
|
2822
2980
|
return createPrivateKey(pem);
|
|
2823
2981
|
}
|
|
2824
|
-
var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
|
|
2825
2982
|
function signNonce(privateKey, nonce) {
|
|
2826
|
-
const signature = sign(null,
|
|
2983
|
+
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
2827
2984
|
return signature.toString("base64url");
|
|
2828
2985
|
}
|
|
2829
|
-
|
|
2830
|
-
// src/daemon/url.ts
|
|
2831
2986
|
function toHttpBase(serverUrl) {
|
|
2832
2987
|
const url = new URL(serverUrl);
|
|
2833
2988
|
if (url.protocol === "ws:") url.protocol = "http:";
|
|
@@ -2837,7 +2992,7 @@ function toHttpBase(serverUrl) {
|
|
|
2837
2992
|
return url.toString();
|
|
2838
2993
|
}
|
|
2839
2994
|
function toWsUrl(serverUrl) {
|
|
2840
|
-
const url = new URL(
|
|
2995
|
+
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
2841
2996
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2842
2997
|
return url.toString();
|
|
2843
2998
|
}
|
|
@@ -2925,13 +3080,13 @@ var AuthManager = class {
|
|
|
2925
3080
|
return await this.runCredentialMutation(async () => {
|
|
2926
3081
|
const existing = this.record ?? await this.opts.store.load();
|
|
2927
3082
|
const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
|
|
2928
|
-
const url = new URL(
|
|
3083
|
+
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
2929
3084
|
const res = await fetch(url, {
|
|
2930
3085
|
method: "POST",
|
|
2931
3086
|
headers: { "content-type": "application/json" },
|
|
2932
3087
|
body: JSON.stringify({
|
|
2933
3088
|
pairingCode,
|
|
2934
|
-
deviceName: this.opts.deviceName ??
|
|
3089
|
+
deviceName: this.opts.deviceName ?? os5.hostname(),
|
|
2935
3090
|
devicePublicKey: keyPair.publicKeyBase64Url
|
|
2936
3091
|
})
|
|
2937
3092
|
});
|
|
@@ -2959,6 +3114,7 @@ var AuthManager = class {
|
|
|
2959
3114
|
async getValidAccessToken() {
|
|
2960
3115
|
if (!this.record) throw new Error("device is not paired yet; call pair(pairingCode) first");
|
|
2961
3116
|
if (this.revoked) throw new DeviceRevokedError();
|
|
3117
|
+
if (this.renewing) return this.renewing;
|
|
2962
3118
|
if (msUntilExpiry(this.record.expiresAt) > RENEW_MARGIN_MS) return this.record.accessToken;
|
|
2963
3119
|
return this.renew();
|
|
2964
3120
|
}
|
|
@@ -2987,7 +3143,7 @@ var AuthManager = class {
|
|
|
2987
3143
|
const record = this.record;
|
|
2988
3144
|
const base = toHttpBase(this.opts.serverUrl);
|
|
2989
3145
|
const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
|
|
2990
|
-
const challengeRes = await fetch(new URL(
|
|
3146
|
+
const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
|
|
2991
3147
|
method: "POST",
|
|
2992
3148
|
headers: { "content-type": "application/json" },
|
|
2993
3149
|
body: JSON.stringify({ deviceId: record.deviceId })
|
|
@@ -3000,7 +3156,7 @@ var AuthManager = class {
|
|
|
3000
3156
|
}
|
|
3001
3157
|
const { nonce } = await challengeRes.json();
|
|
3002
3158
|
const signature = signNonce(privateKey, nonce);
|
|
3003
|
-
const tokenRes = await fetch(new URL(
|
|
3159
|
+
const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
|
|
3004
3160
|
method: "POST",
|
|
3005
3161
|
headers: { "content-type": "application/json" },
|
|
3006
3162
|
body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
|
|
@@ -3098,7 +3254,7 @@ var BlobClient = class {
|
|
|
3098
3254
|
async resolveInstruction(blobRef) {
|
|
3099
3255
|
const base = toHttpBase(this.serverUrl);
|
|
3100
3256
|
const urlRes = await authedFetch(
|
|
3101
|
-
new URL(
|
|
3257
|
+
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
3102
3258
|
{ method: "GET" },
|
|
3103
3259
|
this.auth
|
|
3104
3260
|
);
|
|
@@ -3122,18 +3278,18 @@ var BlobClient = class {
|
|
|
3122
3278
|
/** `POST /byok/blobs` (declares size/contentType/contentHash) -> PUT the bytes to the presigned upload URL -> a `BlobRef` for `task.artifact.blobRef`. */
|
|
3123
3279
|
async uploadArtifact(content, contentType) {
|
|
3124
3280
|
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
3125
|
-
const
|
|
3281
|
+
const contentHash3 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
3126
3282
|
const base = toHttpBase(this.serverUrl);
|
|
3127
3283
|
const reservationId = `blob_${randomUUID()}`;
|
|
3128
3284
|
const createRes = await authedFetch(
|
|
3129
|
-
new URL(
|
|
3285
|
+
new URL(BYOK_BLOBS_PATH, base),
|
|
3130
3286
|
{
|
|
3131
3287
|
method: "POST",
|
|
3132
3288
|
headers: {
|
|
3133
3289
|
"content-type": "application/json",
|
|
3134
3290
|
"idempotency-key": reservationId
|
|
3135
3291
|
},
|
|
3136
|
-
body: JSON.stringify({ size: bytes.length, contentType, contentHash:
|
|
3292
|
+
body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash3 })
|
|
3137
3293
|
},
|
|
3138
3294
|
this.auth
|
|
3139
3295
|
);
|
|
@@ -3150,7 +3306,7 @@ var BlobClient = class {
|
|
|
3150
3306
|
throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
|
|
3151
3307
|
}
|
|
3152
3308
|
await this.#finalize(base, blobId, reservationId);
|
|
3153
|
-
return { blobId, contentHash:
|
|
3309
|
+
return { blobId, contentHash: contentHash3, size: bytes.length, contentType };
|
|
3154
3310
|
}
|
|
3155
3311
|
async #finalize(base, blobId, reservationId) {
|
|
3156
3312
|
let lastFailure;
|
|
@@ -3158,7 +3314,7 @@ var BlobClient = class {
|
|
|
3158
3314
|
let response;
|
|
3159
3315
|
try {
|
|
3160
3316
|
response = await authedFetch(
|
|
3161
|
-
new URL(
|
|
3317
|
+
new URL(byokBlobFinalizePath(blobId), base),
|
|
3162
3318
|
{
|
|
3163
3319
|
method: "POST",
|
|
3164
3320
|
headers: { "idempotency-key": reservationId }
|
|
@@ -3181,26 +3337,187 @@ var BlobClient = class {
|
|
|
3181
3337
|
throw lastFailure;
|
|
3182
3338
|
}
|
|
3183
3339
|
};
|
|
3340
|
+
var PRESENCE_HINTS_CAPABILITY = "presence.hints";
|
|
3341
|
+
var CapabilityDiscoveryError = class extends Error {
|
|
3342
|
+
constructor(message, options) {
|
|
3343
|
+
super(message, options);
|
|
3344
|
+
this.name = "CapabilityDiscoveryError";
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
async function fetchCapabilityDeclaration(serverUrl, options = {}) {
|
|
3348
|
+
const url = new URL(BYOK_CAPABILITIES_PATH, toHttpBase(serverUrl));
|
|
3349
|
+
let response;
|
|
3350
|
+
try {
|
|
3351
|
+
response = await fetch(url, {
|
|
3352
|
+
method: "GET",
|
|
3353
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
3354
|
+
});
|
|
3355
|
+
} catch (err) {
|
|
3356
|
+
throw new CapabilityDiscoveryError(
|
|
3357
|
+
`failed to read the capability declaration from ${url.toString()}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3358
|
+
{ cause: err }
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
if (!response.ok) {
|
|
3362
|
+
throw new CapabilityDiscoveryError(
|
|
3363
|
+
`failed to read the capability declaration from ${url.toString()}: HTTP ${response.status}`
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
let body;
|
|
3367
|
+
try {
|
|
3368
|
+
body = await response.json();
|
|
3369
|
+
} catch (err) {
|
|
3370
|
+
throw new CapabilityDiscoveryError(
|
|
3371
|
+
`the capability declaration at ${url.toString()} is not JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
3372
|
+
{ cause: err }
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
const parsed = CapabilityDeclarationSchema.safeParse(body);
|
|
3376
|
+
if (!parsed.success) {
|
|
3377
|
+
throw new CapabilityDiscoveryError(
|
|
3378
|
+
`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("; ")}`,
|
|
3379
|
+
{ cause: parsed.error }
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
return parsed.data;
|
|
3383
|
+
}
|
|
3384
|
+
function declares(declaration, capability) {
|
|
3385
|
+
return hasCapability(declaration, capability);
|
|
3386
|
+
}
|
|
3387
|
+
var DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3388
|
+
var DEFAULT_PRESENCE_TTL_MS = 9e4;
|
|
3389
|
+
var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
|
|
3390
|
+
function assertPresenceHeartbeatCadence(cadence) {
|
|
3391
|
+
const { intervalMs, ttlMs, minimumIntervalMs } = cadence;
|
|
3392
|
+
if (!(minimumIntervalMs < intervalMs && intervalMs < ttlMs)) {
|
|
3393
|
+
throw new Error(
|
|
3394
|
+
`presence heartbeat interval must satisfy minimumIntervalMs < intervalMs < ttlMs \u2014 got ${minimumIntervalMs} < ${intervalMs} < ${ttlMs}`
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
var PresencePublisher = class {
|
|
3399
|
+
constructor(opts) {
|
|
3400
|
+
this.opts = opts;
|
|
3401
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS;
|
|
3402
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_PRESENCE_TTL_MS;
|
|
3403
|
+
const minimumIntervalMs = opts.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS;
|
|
3404
|
+
assertPresenceHeartbeatCadence({ intervalMs, ttlMs, minimumIntervalMs });
|
|
3405
|
+
this.intervalMs = intervalMs;
|
|
3406
|
+
this.url = new URL(BYOK_PRESENCE_PATH, toHttpBase(opts.serverUrl));
|
|
3407
|
+
}
|
|
3408
|
+
opts;
|
|
3409
|
+
url;
|
|
3410
|
+
intervalMs;
|
|
3411
|
+
timer;
|
|
3412
|
+
running = false;
|
|
3413
|
+
/** Set once a revoked device is observed. Terminal: `start()` will not restart this instance. */
|
|
3414
|
+
stoppedPermanently = false;
|
|
3415
|
+
/** Publishes immediately, then every `intervalMs`. Idempotent; a no-op after a permanent stop. */
|
|
3416
|
+
start() {
|
|
3417
|
+
if (this.running || this.stoppedPermanently) return;
|
|
3418
|
+
this.running = true;
|
|
3419
|
+
void this.beat();
|
|
3420
|
+
}
|
|
3421
|
+
/** Stops the cadence. Idempotent, and the only "offline" signal this producer emits — the hint's TTL does the rest. */
|
|
3422
|
+
stop() {
|
|
3423
|
+
this.running = false;
|
|
3424
|
+
if (this.timer !== void 0) {
|
|
3425
|
+
clearTimeout(this.timer);
|
|
3426
|
+
this.timer = void 0;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
schedule() {
|
|
3430
|
+
if (!this.running) return;
|
|
3431
|
+
this.timer = setTimeout(() => {
|
|
3432
|
+
this.timer = void 0;
|
|
3433
|
+
void this.beat();
|
|
3434
|
+
}, this.intervalMs);
|
|
3435
|
+
this.timer.unref?.();
|
|
3436
|
+
}
|
|
3437
|
+
async beat() {
|
|
3438
|
+
if (!this.running) return;
|
|
3439
|
+
try {
|
|
3440
|
+
const response = await authedFetch(
|
|
3441
|
+
this.url,
|
|
3442
|
+
{
|
|
3443
|
+
method: "PUT",
|
|
3444
|
+
headers: { "content-type": "application/json" },
|
|
3445
|
+
body: JSON.stringify({ level: "online" })
|
|
3446
|
+
},
|
|
3447
|
+
this.opts.auth
|
|
3448
|
+
);
|
|
3449
|
+
if (!response.ok) {
|
|
3450
|
+
if (response.status === 401) {
|
|
3451
|
+
this.stopPermanently(`presence heartbeat unauthorized after token renewal (HTTP 401)`);
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
this.opts.onDegraded?.(`presence heartbeat failed: HTTP ${response.status}`);
|
|
3455
|
+
}
|
|
3456
|
+
} catch (err) {
|
|
3457
|
+
if (err instanceof DeviceRevokedError) {
|
|
3458
|
+
this.stopPermanently("presence heartbeat stopped: device has been revoked; re-pair required");
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
this.opts.onDegraded?.(`presence heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3462
|
+
}
|
|
3463
|
+
this.schedule();
|
|
3464
|
+
}
|
|
3465
|
+
stopPermanently(reason) {
|
|
3466
|
+
this.stoppedPermanently = true;
|
|
3467
|
+
this.stop();
|
|
3468
|
+
this.opts.onDegraded?.(reason);
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
function freshJti() {
|
|
3472
|
+
return randomBytes(16).toString("base64url");
|
|
3473
|
+
}
|
|
3474
|
+
function mintDeviceAssertion(input) {
|
|
3475
|
+
const issuedAtMs = input.now.getTime();
|
|
3476
|
+
const expiresAt = new Date(issuedAtMs + input.ttlMs).toISOString();
|
|
3477
|
+
const claims = DeviceAssertionClaimsSchema.parse({
|
|
3478
|
+
version: 1,
|
|
3479
|
+
issuer: input.issuer,
|
|
3480
|
+
productId: input.productId,
|
|
3481
|
+
deviceId: input.record.deviceId,
|
|
3482
|
+
audience: input.audience,
|
|
3483
|
+
jti: freshJti(),
|
|
3484
|
+
issuedAt: new Date(issuedAtMs).toISOString(),
|
|
3485
|
+
expiresAt
|
|
3486
|
+
});
|
|
3487
|
+
const privateKey = importPrivateKeyPem(input.record.devicePrivateKeyPem);
|
|
3488
|
+
const signature = sign(null, deviceAssertionSigningInput(claims), privateKey).toString("base64url");
|
|
3489
|
+
return {
|
|
3490
|
+
envelope: {
|
|
3491
|
+
schema: DEVICE_ASSERTION_SCHEMA_ID,
|
|
3492
|
+
algorithm: "ed25519",
|
|
3493
|
+
protected: claims,
|
|
3494
|
+
signature
|
|
3495
|
+
},
|
|
3496
|
+
claims,
|
|
3497
|
+
expiresAt
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3184
3500
|
var CONTROL_PROTOCOL_VERSION = 1;
|
|
3185
3501
|
var HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
3186
3502
|
var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
|
|
3503
|
+
var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
|
|
3187
3504
|
function shortHash(input) {
|
|
3188
3505
|
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
|
|
3189
3506
|
}
|
|
3190
3507
|
function controlSocketPath(storeDir) {
|
|
3191
|
-
const candidate =
|
|
3508
|
+
const candidate = path17.join(storeDir, "control.sock");
|
|
3192
3509
|
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
|
|
3193
|
-
return
|
|
3510
|
+
return path17.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
|
|
3194
3511
|
}
|
|
3195
3512
|
function controlPipeName(productId, storeDir) {
|
|
3196
|
-
const id = shortHash(`${productId}|${
|
|
3513
|
+
const id = shortHash(`${productId}|${path17.resolve(storeDir)}`);
|
|
3197
3514
|
return `\\\\.\\pipe\\byok-${id}`;
|
|
3198
3515
|
}
|
|
3199
3516
|
function controlEndpointPath(productId, storeDir, platform = process.platform) {
|
|
3200
3517
|
return platform === "win32" ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);
|
|
3201
3518
|
}
|
|
3202
3519
|
function controlTokenPath(storeDir) {
|
|
3203
|
-
return
|
|
3520
|
+
return path17.join(storeDir, "control.token");
|
|
3204
3521
|
}
|
|
3205
3522
|
var SERVER_PROOF_LABEL = "byok-control-server|";
|
|
3206
3523
|
var CLIENT_AUTH_LABEL = "byok-control-client|";
|
|
@@ -3230,11 +3547,23 @@ function parseClientHello(value) {
|
|
|
3230
3547
|
if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "client" || typeof value.nonce !== "string") return void 0;
|
|
3231
3548
|
return { v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: value.nonce };
|
|
3232
3549
|
}
|
|
3550
|
+
function parseServerHello(value) {
|
|
3551
|
+
if (!isRecord2(value)) return void 0;
|
|
3552
|
+
if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "server" || typeof value.proof !== "string" || typeof value.nonce !== "string") {
|
|
3553
|
+
return void 0;
|
|
3554
|
+
}
|
|
3555
|
+
return { v: CONTROL_PROTOCOL_VERSION, hello: "server", proof: value.proof, nonce: value.nonce };
|
|
3556
|
+
}
|
|
3233
3557
|
function parseClientAuth(value) {
|
|
3234
3558
|
if (!isRecord2(value)) return void 0;
|
|
3235
3559
|
if (value.v !== CONTROL_PROTOCOL_VERSION || typeof value.auth !== "string") return void 0;
|
|
3236
3560
|
return { v: CONTROL_PROTOCOL_VERSION, auth: value.auth };
|
|
3237
3561
|
}
|
|
3562
|
+
function parseServerReady(value) {
|
|
3563
|
+
if (!isRecord2(value)) return void 0;
|
|
3564
|
+
if (value.v !== CONTROL_PROTOCOL_VERSION || value.ready !== true) return void 0;
|
|
3565
|
+
return { v: CONTROL_PROTOCOL_VERSION, ready: true };
|
|
3566
|
+
}
|
|
3238
3567
|
function parseRawControlRequest(value) {
|
|
3239
3568
|
if (!isRecord2(value)) return void 0;
|
|
3240
3569
|
if (typeof value.id !== "string" || typeof value.method !== "string") return void 0;
|
|
@@ -3284,6 +3613,16 @@ function parseApprovalsRequestParams(value) {
|
|
|
3284
3613
|
if (typeof value.summary !== "string") return void 0;
|
|
3285
3614
|
return { taskId: value.taskId, summary: value.summary };
|
|
3286
3615
|
}
|
|
3616
|
+
var ASSERTION_AUDIENCE_MAX_BYTES = 256;
|
|
3617
|
+
function parseAssertionIssueParams(value) {
|
|
3618
|
+
if (!isRecord2(value)) return void 0;
|
|
3619
|
+
const keys = Object.keys(value);
|
|
3620
|
+
if (keys.length !== 1 || keys[0] !== "audience") return void 0;
|
|
3621
|
+
const { audience } = value;
|
|
3622
|
+
if (typeof audience !== "string" || audience.length === 0) return void 0;
|
|
3623
|
+
if (Buffer.byteLength(audience, "utf8") > ASSERTION_AUDIENCE_MAX_BYTES) return void 0;
|
|
3624
|
+
return { audience };
|
|
3625
|
+
}
|
|
3287
3626
|
function parseShutdownParams(value) {
|
|
3288
3627
|
if (!isRecord2(value)) return {};
|
|
3289
3628
|
return value.reason === "unpair" || value.reason === "operator" ? { reason: value.reason } : {};
|
|
@@ -3345,7 +3684,7 @@ async function assertOwnedPrivateDir(dir) {
|
|
|
3345
3684
|
}
|
|
3346
3685
|
async function bindControlEndpoint(server, endpoint) {
|
|
3347
3686
|
if (process.platform !== "win32") {
|
|
3348
|
-
const endpointDir =
|
|
3687
|
+
const endpointDir = path17.dirname(endpoint);
|
|
3349
3688
|
await promises.mkdir(endpointDir, { recursive: true, mode: 448 });
|
|
3350
3689
|
await promises.chmod(endpointDir, 448).catch(() => {
|
|
3351
3690
|
});
|
|
@@ -3616,7 +3955,7 @@ var LongPollClient = class {
|
|
|
3616
3955
|
try {
|
|
3617
3956
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3618
3957
|
const res = await authedFetch(
|
|
3619
|
-
new URL(
|
|
3958
|
+
new URL(BYOK_MESSAGES_PATH, base),
|
|
3620
3959
|
{
|
|
3621
3960
|
method: "POST",
|
|
3622
3961
|
headers: { "content-type": "application/json" },
|
|
@@ -3640,7 +3979,7 @@ var LongPollClient = class {
|
|
|
3640
3979
|
while (this.running) {
|
|
3641
3980
|
try {
|
|
3642
3981
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3643
|
-
const url = new URL(
|
|
3982
|
+
const url = new URL(BYOK_EVENTS_PATH, base);
|
|
3644
3983
|
const cursor = this.opts.getCursor();
|
|
3645
3984
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
3646
3985
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
@@ -4626,7 +4965,7 @@ function sameFileState2(left, right) {
|
|
|
4626
4965
|
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
4627
4966
|
}
|
|
4628
4967
|
async function openOperationalHealthFile(storeDir) {
|
|
4629
|
-
const filePath =
|
|
4968
|
+
const filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
4630
4969
|
let namedBefore;
|
|
4631
4970
|
try {
|
|
4632
4971
|
namedBefore = await promises.lstat(filePath, { bigint: true });
|
|
@@ -4668,7 +5007,7 @@ var OperationalHealthTracker = class {
|
|
|
4668
5007
|
#writeTail = Promise.resolve();
|
|
4669
5008
|
#started = false;
|
|
4670
5009
|
constructor(storeDir, options = {}) {
|
|
4671
|
-
this.#filePath =
|
|
5010
|
+
this.#filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
4672
5011
|
this.#windowMs = options.windowMs ?? 6e4;
|
|
4673
5012
|
this.#failureThreshold = options.failureThreshold ?? 3;
|
|
4674
5013
|
this.#maxFailures = options.maxFailures ?? 128;
|
|
@@ -4755,7 +5094,7 @@ var OperationalHealthTracker = class {
|
|
|
4755
5094
|
async #load() {
|
|
4756
5095
|
let opened;
|
|
4757
5096
|
try {
|
|
4758
|
-
opened = await openOperationalHealthFile(
|
|
5097
|
+
opened = await openOperationalHealthFile(path17.dirname(this.#filePath));
|
|
4759
5098
|
} catch (err) {
|
|
4760
5099
|
throw new Error("operational health state could not be read");
|
|
4761
5100
|
}
|
|
@@ -4791,7 +5130,7 @@ var OperationalHealthTracker = class {
|
|
|
4791
5130
|
if (!this.#state) return;
|
|
4792
5131
|
const body = JSON.stringify(this.#state, null, 2);
|
|
4793
5132
|
this.#writeTail = this.#writeTail.then(async () => {
|
|
4794
|
-
await ensureSecureDir(
|
|
5133
|
+
await ensureSecureDir(path17.dirname(this.#filePath));
|
|
4795
5134
|
await atomicWriteFile(this.#filePath, body, { mode: 384, fsync: true });
|
|
4796
5135
|
});
|
|
4797
5136
|
try {
|
|
@@ -4866,19 +5205,19 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
|
4866
5205
|
var MAX_OWNER_BYTES = 4096;
|
|
4867
5206
|
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
4868
5207
|
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
4869
|
-
var STORE_MUTEX_PORT_BASE = 1e4;
|
|
4870
|
-
var STORE_MUTEX_PORT_COUNT = 2e4;
|
|
4871
|
-
var STORE_MUTEX_PORT_CANDIDATES = 32;
|
|
4872
5208
|
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
4873
5209
|
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
5210
|
+
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
5211
|
+
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
5212
|
+
var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
|
|
4874
5213
|
function storeMutexIdentity(canonicalStoreDir) {
|
|
4875
5214
|
return createHash("sha256").update(canonicalStoreDir).digest("hex");
|
|
4876
5215
|
}
|
|
4877
|
-
function
|
|
4878
|
-
|
|
4879
|
-
const
|
|
4880
|
-
|
|
4881
|
-
return
|
|
5216
|
+
function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
|
|
5217
|
+
if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
|
|
5218
|
+
const candidate = path17.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
|
|
5219
|
+
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
|
|
5220
|
+
return path17.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
|
|
4882
5221
|
}
|
|
4883
5222
|
var DaemonOwnerActiveError = class extends Error {
|
|
4884
5223
|
constructor(role) {
|
|
@@ -4999,70 +5338,90 @@ async function createLivenessListener() {
|
|
|
4999
5338
|
})
|
|
5000
5339
|
};
|
|
5001
5340
|
}
|
|
5002
|
-
async function probeStoreMutex(
|
|
5341
|
+
async function probeStoreMutex(endpoint, identity) {
|
|
5003
5342
|
return new Promise((resolve) => {
|
|
5004
|
-
const socket = createConnection(
|
|
5343
|
+
const socket = createConnection(endpoint);
|
|
5005
5344
|
let settled = false;
|
|
5006
5345
|
let raw = "";
|
|
5007
5346
|
const finish = (result) => {
|
|
5008
5347
|
if (settled) return;
|
|
5009
5348
|
settled = true;
|
|
5349
|
+
clearTimeout(timer);
|
|
5350
|
+
socket.removeAllListeners();
|
|
5010
5351
|
socket.destroy();
|
|
5011
5352
|
resolve(result);
|
|
5012
5353
|
};
|
|
5354
|
+
const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
|
|
5013
5355
|
socket.setEncoding("utf8");
|
|
5014
|
-
socket.setTimeout(STORE_MUTEX_PROBE_TIMEOUT_MS, () => finish({ kind: "uncertain" }));
|
|
5015
5356
|
socket.on("data", (chunk) => {
|
|
5016
5357
|
raw += chunk;
|
|
5017
|
-
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "
|
|
5018
|
-
});
|
|
5019
|
-
socket.once("end", () => {
|
|
5020
|
-
const line = raw.trimEnd();
|
|
5021
|
-
const identity = line.startsWith(STORE_MUTEX_ID_PREFIX) ? line.slice(STORE_MUTEX_ID_PREFIX.length) : void 0;
|
|
5022
|
-
finish(
|
|
5023
|
-
identity && /^[a-f0-9]{64}$/.test(identity) ? { kind: "identity", identity } : { kind: "foreign-or-gone" }
|
|
5024
|
-
);
|
|
5358
|
+
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
|
|
5025
5359
|
});
|
|
5026
|
-
socket.once("
|
|
5360
|
+
socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
|
|
5361
|
+
socket.once(
|
|
5362
|
+
"error",
|
|
5363
|
+
(err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
|
|
5364
|
+
);
|
|
5027
5365
|
});
|
|
5028
5366
|
}
|
|
5367
|
+
async function clearStaleStoreMutexSocket(endpoint, identity) {
|
|
5368
|
+
let stat;
|
|
5369
|
+
try {
|
|
5370
|
+
stat = await promises.lstat(endpoint);
|
|
5371
|
+
} catch (err) {
|
|
5372
|
+
if (err.code === "ENOENT") return;
|
|
5373
|
+
throw err;
|
|
5374
|
+
}
|
|
5375
|
+
if (!stat.isSocket()) throw new Error("store mutation lock path exists but is not a socket");
|
|
5376
|
+
if ((await probeStoreMutex(endpoint, identity)).kind !== "unbound") throw new DaemonOwnerActiveError("unknown");
|
|
5377
|
+
await promises.rm(endpoint, { force: true });
|
|
5378
|
+
}
|
|
5379
|
+
async function assertOwnedPrivateDir2(dir) {
|
|
5380
|
+
const uid = process.getuid?.();
|
|
5381
|
+
if (uid === void 0) return;
|
|
5382
|
+
const stat = await promises.lstat(dir);
|
|
5383
|
+
if (stat.isSymbolicLink() || stat.uid !== uid) {
|
|
5384
|
+
throw new Error(`refusing to bind the store mutation lock under "${dir}": not a real directory owned by this process's own uid`);
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5029
5387
|
async function acquireStoreMutex(canonicalStoreDir) {
|
|
5030
5388
|
const identity = storeMutexIdentity(canonicalStoreDir);
|
|
5031
|
-
|
|
5032
|
-
|
|
5389
|
+
const endpoint = storeMutexEndpoint(canonicalStoreDir, identity);
|
|
5390
|
+
const isPipe = process.platform === "win32";
|
|
5391
|
+
if (!isPipe) {
|
|
5392
|
+
const endpointDir = path17.dirname(endpoint);
|
|
5393
|
+
if (endpointDir !== canonicalStoreDir) {
|
|
5394
|
+
await ensureSecureDir(endpointDir);
|
|
5395
|
+
await assertOwnedPrivateDir2(endpointDir);
|
|
5396
|
+
}
|
|
5397
|
+
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
5398
|
+
}
|
|
5399
|
+
const server = createServer((socket) => socket.end(`${STORE_MUTEX_ID_PREFIX}${identity}
|
|
5033
5400
|
`));
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
server.
|
|
5039
|
-
|
|
5040
|
-
resolve();
|
|
5041
|
-
});
|
|
5401
|
+
try {
|
|
5402
|
+
await new Promise((resolve, reject) => {
|
|
5403
|
+
server.once("error", reject);
|
|
5404
|
+
server.listen(endpoint, () => {
|
|
5405
|
+
server.removeListener("error", reject);
|
|
5406
|
+
resolve();
|
|
5042
5407
|
});
|
|
5043
|
-
}
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
throw new DaemonOwnerActiveError("unknown");
|
|
5048
|
-
}
|
|
5049
|
-
continue;
|
|
5050
|
-
}
|
|
5051
|
-
server.unref();
|
|
5052
|
-
let closed = false;
|
|
5053
|
-
return {
|
|
5054
|
-
port,
|
|
5055
|
-
close: () => new Promise((resolve, reject) => {
|
|
5056
|
-
if (closed) {
|
|
5057
|
-
resolve();
|
|
5058
|
-
return;
|
|
5059
|
-
}
|
|
5060
|
-
closed = true;
|
|
5061
|
-
server.close((err) => err ? reject(err) : resolve());
|
|
5062
|
-
})
|
|
5063
|
-
};
|
|
5408
|
+
});
|
|
5409
|
+
} catch (err) {
|
|
5410
|
+
if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
|
|
5411
|
+
throw err;
|
|
5064
5412
|
}
|
|
5065
|
-
|
|
5413
|
+
if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
5414
|
+
server.unref();
|
|
5415
|
+
let closed = false;
|
|
5416
|
+
return {
|
|
5417
|
+
endpoint,
|
|
5418
|
+
close: async () => {
|
|
5419
|
+
if (closed) return;
|
|
5420
|
+
closed = true;
|
|
5421
|
+
await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
|
|
5422
|
+
if (!isPipe) await promises.rm(endpoint, { force: true }).catch(() => void 0);
|
|
5423
|
+
}
|
|
5424
|
+
};
|
|
5066
5425
|
}
|
|
5067
5426
|
async function reclaimExistsAndIsActive(reclaimPath) {
|
|
5068
5427
|
let stat;
|
|
@@ -5110,8 +5469,8 @@ async function acquireDaemonOwner(storeDir, role, clock = () => /* @__PURE__ */
|
|
|
5110
5469
|
await mutex.close().catch(() => void 0);
|
|
5111
5470
|
throw err;
|
|
5112
5471
|
}
|
|
5113
|
-
const ownerPath =
|
|
5114
|
-
const reclaimPath =
|
|
5472
|
+
const ownerPath = path17.join(storeDir, DAEMON_OWNER_FILENAME);
|
|
5473
|
+
const reclaimPath = path17.join(storeDir, RECLAIM_FILENAME);
|
|
5115
5474
|
const record = {
|
|
5116
5475
|
version: 2,
|
|
5117
5476
|
pid: process.pid,
|
|
@@ -5178,6 +5537,7 @@ function toRuntimeInfoCapabilities(caps) {
|
|
|
5178
5537
|
steer: caps.steer,
|
|
5179
5538
|
resume: caps.resume,
|
|
5180
5539
|
approvalInteractive: caps.approvalInteractive,
|
|
5540
|
+
...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
|
|
5181
5541
|
permissionModes: caps.permissionModes
|
|
5182
5542
|
};
|
|
5183
5543
|
}
|
|
@@ -5188,7 +5548,7 @@ var CursorStore = class {
|
|
|
5188
5548
|
storeDir;
|
|
5189
5549
|
fileFor(serverUrl, deviceId) {
|
|
5190
5550
|
const key = createHash("sha256").update(`${serverUrl}::${deviceId}`).digest("hex").slice(0, 32);
|
|
5191
|
-
return
|
|
5551
|
+
return path17.join(this.storeDir, `cursor-${key}.json`);
|
|
5192
5552
|
}
|
|
5193
5553
|
async load(serverUrl, deviceId) {
|
|
5194
5554
|
let raw;
|
|
@@ -5208,7 +5568,7 @@ var CursorStore = class {
|
|
|
5208
5568
|
}
|
|
5209
5569
|
async save(serverUrl, deviceId, cursor) {
|
|
5210
5570
|
const file = this.fileFor(serverUrl, deviceId);
|
|
5211
|
-
await promises.mkdir(
|
|
5571
|
+
await promises.mkdir(path17.dirname(file), { recursive: true, mode: 448 });
|
|
5212
5572
|
await atomicWriteFile(file, JSON.stringify({ cursor }));
|
|
5213
5573
|
}
|
|
5214
5574
|
/** Remove any persisted cursor for (serverUrl, deviceId) — a no-op if none exists. Called from `pair()` (finding F5) so a device that's about to be replaced never leaves a cursor a future, unrelated device could somehow inherit. */
|
|
@@ -5251,7 +5611,7 @@ var DaemonObserver = class {
|
|
|
5251
5611
|
}
|
|
5252
5612
|
/**
|
|
5253
5613
|
* Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
|
|
5254
|
-
*
|
|
5614
|
+
* either offer variant produces a local event here — every other inbound type
|
|
5255
5615
|
* (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
|
|
5256
5616
|
* best-effort notification whose OWN observable effect already surfaces
|
|
5257
5617
|
* through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
|
|
@@ -5259,7 +5619,7 @@ var DaemonObserver = class {
|
|
|
5259
5619
|
* where those are actually reported from.
|
|
5260
5620
|
*/
|
|
5261
5621
|
handleInboundEnvelope(envelope) {
|
|
5262
|
-
if (envelope.type !== "task.offer") return;
|
|
5622
|
+
if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets") return;
|
|
5263
5623
|
const taskId = envelope.task_id;
|
|
5264
5624
|
if (this.taskInfo.has(taskId)) return;
|
|
5265
5625
|
this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
|
|
@@ -5380,6 +5740,37 @@ var DaemonObserver = class {
|
|
|
5380
5740
|
noteShutdownComplete(reason, undeliveredOutboxCount) {
|
|
5381
5741
|
this.emit({ kind: "shutdown-complete", ts: nowIso(), reason, undeliveredOutboxCount });
|
|
5382
5742
|
}
|
|
5743
|
+
/**
|
|
5744
|
+
* Plan `device-assertion-broker`: see the `device-assertion` `DaemonEvent`
|
|
5745
|
+
* variant's own doc comment. The parameter type is what keeps the signature
|
|
5746
|
+
* out — there is no field to pass one through.
|
|
5747
|
+
*
|
|
5748
|
+
* codex round-2 F4: the DENIED caller can pass its raw `audience` here, but
|
|
5749
|
+
* it is converted to a byte SIZE the instant the event is constructed and the
|
|
5750
|
+
* raw string is dropped — it is never placed on the emitted `DaemonEvent`, so
|
|
5751
|
+
* it cannot reach a subscriber, `format.ts`, stdout, or the audit file. The
|
|
5752
|
+
* ISSUED `audience` came from the allowlist and is kept verbatim.
|
|
5753
|
+
*/
|
|
5754
|
+
noteDeviceAssertion(event) {
|
|
5755
|
+
if (event.result === "issued") {
|
|
5756
|
+
this.emit({
|
|
5757
|
+
kind: "device-assertion",
|
|
5758
|
+
ts: nowIso(),
|
|
5759
|
+
result: "issued",
|
|
5760
|
+
audience: event.audience,
|
|
5761
|
+
jti: event.jti,
|
|
5762
|
+
expiresAt: event.expiresAt
|
|
5763
|
+
});
|
|
5764
|
+
return;
|
|
5765
|
+
}
|
|
5766
|
+
this.emit({
|
|
5767
|
+
kind: "device-assertion",
|
|
5768
|
+
ts: nowIso(),
|
|
5769
|
+
result: "denied",
|
|
5770
|
+
reason: event.reason,
|
|
5771
|
+
audienceSize: event.audience === void 0 ? void 0 : Buffer.byteLength(event.audience, "utf8")
|
|
5772
|
+
});
|
|
5773
|
+
}
|
|
5383
5774
|
/** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
|
|
5384
5775
|
noteStaleApprovalDecision(taskId, decision, reason) {
|
|
5385
5776
|
this.emit({ kind: "stale-approval-decision", ts: nowIso(), taskId, decision, reason });
|
|
@@ -5504,7 +5895,7 @@ var SessionWorkspaceStore = class {
|
|
|
5504
5895
|
*/
|
|
5505
5896
|
queue = Promise.resolve();
|
|
5506
5897
|
constructor(storeDir) {
|
|
5507
|
-
this.filePath =
|
|
5898
|
+
this.filePath = path17.join(storeDir, "session-workspaces.json");
|
|
5508
5899
|
}
|
|
5509
5900
|
async get(sessionRef) {
|
|
5510
5901
|
return this.enqueue(async () => {
|
|
@@ -5562,7 +5953,7 @@ var SessionWorkspaceStore = class {
|
|
|
5562
5953
|
}
|
|
5563
5954
|
}
|
|
5564
5955
|
async save(all) {
|
|
5565
|
-
const dir =
|
|
5956
|
+
const dir = path17.dirname(this.filePath);
|
|
5566
5957
|
await promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
5567
5958
|
const tmpPath = `${this.filePath}.${process.pid}-${tmpSeq2++}.tmp`;
|
|
5568
5959
|
try {
|
|
@@ -5667,9 +6058,9 @@ function isSqliteAvailable() {
|
|
|
5667
6058
|
}
|
|
5668
6059
|
}
|
|
5669
6060
|
var SECURE_FILE_MODE = 384;
|
|
5670
|
-
function openJournalDatabase(
|
|
6061
|
+
function openJournalDatabase(path21, busyTimeoutMs, faults) {
|
|
5671
6062
|
const { DatabaseSync } = loadSqliteModule();
|
|
5672
|
-
const db = new DatabaseSync(
|
|
6063
|
+
const db = new DatabaseSync(path21, { timeout: busyTimeoutMs });
|
|
5673
6064
|
try {
|
|
5674
6065
|
faults?.onStep?.("after-open");
|
|
5675
6066
|
db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
|
|
@@ -5812,9 +6203,9 @@ var RECEIVED_STATE = "received";
|
|
|
5812
6203
|
function byteLength(value) {
|
|
5813
6204
|
return Buffer.byteLength(value, "utf8");
|
|
5814
6205
|
}
|
|
5815
|
-
function fileBytes(
|
|
6206
|
+
function fileBytes(path21) {
|
|
5816
6207
|
try {
|
|
5817
|
-
return statSync(
|
|
6208
|
+
return statSync(path21).size;
|
|
5818
6209
|
} catch {
|
|
5819
6210
|
return 0;
|
|
5820
6211
|
}
|
|
@@ -7021,6 +7412,21 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
|
|
|
7021
7412
|
var MAX_TRACKED_TASK_IDS = 2e3;
|
|
7022
7413
|
var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
|
|
7023
7414
|
var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
|
|
7415
|
+
var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
|
|
7416
|
+
function resultDocumentRejectionDetail(check) {
|
|
7417
|
+
switch (check.reason) {
|
|
7418
|
+
case "over-cap":
|
|
7419
|
+
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)`;
|
|
7420
|
+
case "not-serializable":
|
|
7421
|
+
return "not JSON-serializable (JSON.stringify threw, or produced no output at all)";
|
|
7422
|
+
case "not-plain-json":
|
|
7423
|
+
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)";
|
|
7424
|
+
default: {
|
|
7425
|
+
const exhaustive = check;
|
|
7426
|
+
throw new Error(`unhandled result document rejection: ${JSON.stringify(exhaustive)}`);
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7024
7430
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
7025
7431
|
function isKnownRuntimeId(id) {
|
|
7026
7432
|
return RuntimeIdSchema.safeParse(id).success;
|
|
@@ -7033,6 +7439,14 @@ function orderByPreference(candidates, preference) {
|
|
|
7033
7439
|
function adapterSupportsMode(adapter, mode) {
|
|
7034
7440
|
return adapter.capabilities().permissionModes.includes(mode);
|
|
7035
7441
|
}
|
|
7442
|
+
function adapterSupportsMcpToolsets(adapter) {
|
|
7443
|
+
return adapter.capabilities().mcpToolsets === true;
|
|
7444
|
+
}
|
|
7445
|
+
function withoutRequiredToolsets(payload) {
|
|
7446
|
+
if (!("requiredToolsets" in payload)) return payload;
|
|
7447
|
+
const { requiredToolsets, ...offer } = payload;
|
|
7448
|
+
return offer;
|
|
7449
|
+
}
|
|
7036
7450
|
function errorMessage3(err) {
|
|
7037
7451
|
return err instanceof Error ? err.message : String(err);
|
|
7038
7452
|
}
|
|
@@ -7068,8 +7482,8 @@ function estimateEventBytes(event) {
|
|
|
7068
7482
|
}
|
|
7069
7483
|
async function openArtifact(workspaceDir, name) {
|
|
7070
7484
|
const realWorkspaceDir = await promises.realpath(workspaceDir).catch(() => workspaceDir);
|
|
7071
|
-
const candidate =
|
|
7072
|
-
const prefix = realWorkspaceDir.endsWith(
|
|
7485
|
+
const candidate = path17.resolve(realWorkspaceDir, name);
|
|
7486
|
+
const prefix = realWorkspaceDir.endsWith(path17.sep) ? realWorkspaceDir : realWorkspaceDir + path17.sep;
|
|
7073
7487
|
if (candidate !== realWorkspaceDir && !candidate.startsWith(prefix)) {
|
|
7074
7488
|
return { ok: false, reason: `artifact name "${name}" resolves outside the task workspace \u2014 rejected` };
|
|
7075
7489
|
}
|
|
@@ -7388,6 +7802,9 @@ var TaskRunner = class {
|
|
|
7388
7802
|
case "task.offer":
|
|
7389
7803
|
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7390
7804
|
return;
|
|
7805
|
+
case "task.offer_with_toolsets":
|
|
7806
|
+
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7807
|
+
return;
|
|
7391
7808
|
case "task.cancel":
|
|
7392
7809
|
await this.handleCancel(envelope.task_id, envelope.payload.reason);
|
|
7393
7810
|
return;
|
|
@@ -7441,7 +7858,22 @@ var TaskRunner = class {
|
|
|
7441
7858
|
);
|
|
7442
7859
|
return;
|
|
7443
7860
|
}
|
|
7444
|
-
|
|
7861
|
+
if (payload.dispatchSelection !== void 0 && payload.runtime !== void 0 && payload.runtime !== payload.dispatchSelection.runtimeId) {
|
|
7862
|
+
this.decline(
|
|
7863
|
+
taskId,
|
|
7864
|
+
`offer runtime ${payload.runtime} does not match dispatchSelection.runtimeId ${payload.dispatchSelection.runtimeId}`,
|
|
7865
|
+
false
|
|
7866
|
+
);
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7869
|
+
const requiredToolsets = "requiredToolsets" in payload ? payload.requiredToolsets : void 0;
|
|
7870
|
+
const resolvedMcp = requiredToolsets ? this.resolveMcpServers(requiredToolsets) : void 0;
|
|
7871
|
+
if (resolvedMcp && !resolvedMcp.ok) {
|
|
7872
|
+
this.decline(taskId, resolvedMcp.reason, true);
|
|
7873
|
+
return;
|
|
7874
|
+
}
|
|
7875
|
+
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
7876
|
+
const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
|
|
7445
7877
|
if (!pick.ok) {
|
|
7446
7878
|
this.decline(taskId, pick.reason, pick.retryable);
|
|
7447
7879
|
return;
|
|
@@ -7467,7 +7899,7 @@ var TaskRunner = class {
|
|
|
7467
7899
|
const sameProtocolTask = ledger?.taskId === taskId;
|
|
7468
7900
|
const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
|
|
7469
7901
|
const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
|
|
7470
|
-
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef ||
|
|
7902
|
+
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef || path17.resolve(ledger.workspaceDir) !== path17.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
|
|
7471
7903
|
this.decline(taskId, "session is incompatible with Git workspace mode", true);
|
|
7472
7904
|
return;
|
|
7473
7905
|
}
|
|
@@ -7482,7 +7914,7 @@ var TaskRunner = class {
|
|
|
7482
7914
|
return;
|
|
7483
7915
|
}
|
|
7484
7916
|
} else {
|
|
7485
|
-
workspaceDir =
|
|
7917
|
+
workspaceDir = path17.join(this.deps.workspaceRoot, taskId);
|
|
7486
7918
|
}
|
|
7487
7919
|
try {
|
|
7488
7920
|
gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
|
|
@@ -7492,7 +7924,7 @@ var TaskRunner = class {
|
|
|
7492
7924
|
}
|
|
7493
7925
|
} else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
|
|
7494
7926
|
known = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
|
|
7495
|
-
workspaceDir = known?.workspaceDir ??
|
|
7927
|
+
workspaceDir = known?.workspaceDir ?? path17.join(this.deps.workspaceRoot, taskId);
|
|
7496
7928
|
plainWorkspaceNeedsResolve = true;
|
|
7497
7929
|
} else {
|
|
7498
7930
|
this.decline(taskId, "workspace mode is unavailable", true);
|
|
@@ -7604,6 +8036,7 @@ var TaskRunner = class {
|
|
|
7604
8036
|
const ctx = {
|
|
7605
8037
|
workspaceDir,
|
|
7606
8038
|
policy: decision.policy,
|
|
8039
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
|
|
7607
8040
|
...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
|
|
7608
8041
|
// M5: no longer `process.env` verbatim (see `environment.ts`'s own
|
|
7609
8042
|
// module doc comment for the credential-leak gap that closed) —
|
|
@@ -7640,7 +8073,7 @@ var TaskRunner = class {
|
|
|
7640
8073
|
}
|
|
7641
8074
|
};
|
|
7642
8075
|
const effectiveOffer = {
|
|
7643
|
-
...payload,
|
|
8076
|
+
...withoutRequiredToolsets(payload),
|
|
7644
8077
|
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
7645
8078
|
// Never forward a sessionRef this device has no recorded workspace
|
|
7646
8079
|
// for (stale, from another device, or simply made up) — an adapter
|
|
@@ -7718,6 +8151,36 @@ var TaskRunner = class {
|
|
|
7718
8151
|
if (typeof instruction === "string") return instruction;
|
|
7719
8152
|
return this.deps.blobClient.resolveInstruction(instruction.blobRef);
|
|
7720
8153
|
}
|
|
8154
|
+
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
8155
|
+
resolveMcpServers(requiredToolsets) {
|
|
8156
|
+
const registry = this.deps.mcpToolsets;
|
|
8157
|
+
if (!registry) {
|
|
8158
|
+
return { ok: false, reason: "offer requires MCP toolsets, but this device has no local mcpToolsets registry" };
|
|
8159
|
+
}
|
|
8160
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
8161
|
+
for (const toolsetId of requiredToolsets) {
|
|
8162
|
+
const toolset = registry.get(toolsetId);
|
|
8163
|
+
if (!toolset) {
|
|
8164
|
+
return { ok: false, reason: `required MCP toolset "${toolsetId}" is not configured on this device` };
|
|
8165
|
+
}
|
|
8166
|
+
for (const [serverName, server] of Object.entries(toolset.mcpServers)) {
|
|
8167
|
+
if (Object.prototype.hasOwnProperty.call(servers, serverName)) {
|
|
8168
|
+
return {
|
|
8169
|
+
ok: false,
|
|
8170
|
+
reason: `required MCP toolsets collide on server name "${serverName}"; refusing ambiguous projection`
|
|
8171
|
+
};
|
|
8172
|
+
}
|
|
8173
|
+
servers[serverName] = Object.freeze({
|
|
8174
|
+
command: server.command,
|
|
8175
|
+
...server.args ? { args: Object.freeze([...server.args]) } : {}
|
|
8176
|
+
});
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
if (Object.keys(servers).length === 0) {
|
|
8180
|
+
return { ok: false, reason: "required MCP toolsets resolved to no servers; refusing to run without tools" };
|
|
8181
|
+
}
|
|
8182
|
+
return { ok: true, servers: Object.freeze(servers) };
|
|
8183
|
+
}
|
|
7721
8184
|
async pump(active) {
|
|
7722
8185
|
try {
|
|
7723
8186
|
for await (const event of active.session.events) {
|
|
@@ -7751,11 +8214,38 @@ var TaskRunner = class {
|
|
|
7751
8214
|
if (event.type === "turn_end") {
|
|
7752
8215
|
active.batcher.push(event);
|
|
7753
8216
|
active.batcher.flush();
|
|
8217
|
+
const finalOutput = active.summaryParts.join("");
|
|
8218
|
+
const outcome = await this.resolveResultDocument(active, finalOutput);
|
|
8219
|
+
if (!outcome.deliver) return;
|
|
7754
8220
|
await this.observeGit(active, "completed");
|
|
8221
|
+
if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
|
|
8222
|
+
await this.fail(
|
|
8223
|
+
active.taskId,
|
|
8224
|
+
`${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`,
|
|
8225
|
+
false
|
|
8226
|
+
);
|
|
8227
|
+
return;
|
|
8228
|
+
}
|
|
7755
8229
|
this.deps.send(
|
|
7756
8230
|
createEnvelope(
|
|
7757
8231
|
"task.complete",
|
|
7758
|
-
{
|
|
8232
|
+
{
|
|
8233
|
+
summary: finalOutput,
|
|
8234
|
+
sessionRef: active.session.sessionRef,
|
|
8235
|
+
// Spread rather than `document: outcome.document`, so a
|
|
8236
|
+
// completion with no document is the exact same payload it
|
|
8237
|
+
// was before this field existed — not one carrying an
|
|
8238
|
+
// explicit `document: undefined` key.
|
|
8239
|
+
//
|
|
8240
|
+
// `outcome.document` is the protocol's CANONICAL SNAPSHOT
|
|
8241
|
+
// (`checkResultDocument`), never the object the extractor
|
|
8242
|
+
// returned: pure data serializes identically at the root
|
|
8243
|
+
// (where it was measured) and nested inside this payload
|
|
8244
|
+
// (where the codec actually serializes it), so a contextual
|
|
8245
|
+
// `toJSON(key)` or an unstable getter cannot make the wire
|
|
8246
|
+
// bytes differ from what the cap gate approved.
|
|
8247
|
+
...outcome.document !== void 0 ? { document: outcome.document } : {}
|
|
8248
|
+
},
|
|
7759
8249
|
{ taskId: active.taskId, sessionRef: active.session.sessionRef }
|
|
7760
8250
|
)
|
|
7761
8251
|
);
|
|
@@ -8293,6 +8783,102 @@ var TaskRunner = class {
|
|
|
8293
8783
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
|
|
8294
8784
|
await this.finish(taskId);
|
|
8295
8785
|
}
|
|
8786
|
+
/**
|
|
8787
|
+
* additive-minor (`task.complete.document`): the whole daemon-side gate
|
|
8788
|
+
* between a configured {@link ResultDocumentExtractor} and the wire —
|
|
8789
|
+
* called once, from the `turn_end` completion path, immediately before
|
|
8790
|
+
* `task.complete` is built.
|
|
8791
|
+
*
|
|
8792
|
+
* `{deliver: true}` means "go on and send `task.complete`", carrying the
|
|
8793
|
+
* document when there is one. `{deliver: false}` means this method has
|
|
8794
|
+
* ALREADY reported `task.fail` and finished the task; the caller must
|
|
8795
|
+
* return without sending anything further.
|
|
8796
|
+
*
|
|
8797
|
+
* Four fail-closed branches, all `retryable: false` (see
|
|
8798
|
+
* {@link RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX} for why none of them
|
|
8799
|
+
* can succeed on a retry):
|
|
8800
|
+
*
|
|
8801
|
+
* 1. The extractor threw — its error is surfaced, never swallowed.
|
|
8802
|
+
* 2. The extractor returned a thenable, violating the synchronous
|
|
8803
|
+
* contract in the one way that would otherwise ship a wrong answer.
|
|
8804
|
+
* 3. The document is over the cap, not JSON-serializable, or not plain
|
|
8805
|
+
* JSON data, per `checkResultDocument` — the protocol's OWN check,
|
|
8806
|
+
* imported rather than reimplemented, so this gate and the server's
|
|
8807
|
+
* schema validation can never disagree about what is legal.
|
|
8808
|
+
* 4. The connected server never advertised `result-document`. Its
|
|
8809
|
+
* tolerant `z.object()` would silently strip the field on arrival
|
|
8810
|
+
* (`version.ts`'s own flag doc comment), so "send anyway" is not a
|
|
8811
|
+
* degraded-but-working path — it is the task's primary structured
|
|
8812
|
+
* result being deleted in transit with nothing reported anywhere.
|
|
8813
|
+
*
|
|
8814
|
+
* The capability is checked LAST, deliberately: a document that is itself
|
|
8815
|
+
* invalid is the host's own bug and is worth reporting as such even when
|
|
8816
|
+
* the connected server could not have accepted any document at all. It is
|
|
8817
|
+
* then re-checked once more by the caller after its own last await, since
|
|
8818
|
+
* a reconnect can invalidate this answer in between (F3).
|
|
8819
|
+
*
|
|
8820
|
+
* **Residual window (bounded, deliberately not hacked around).** Even the
|
|
8821
|
+
* caller's re-check happens before `ConnectionManager.send` hands the
|
|
8822
|
+
* envelope to a transport, and a queued envelope can outlive the
|
|
8823
|
+
* connection it was queued for: a reconnect between `send()` and the
|
|
8824
|
+
* outbox actually draining could still deliver this `task.complete` to a
|
|
8825
|
+
* rolled-back N-1 server that strips the document. Closing that would
|
|
8826
|
+
* mean teaching the transport outbox to inspect payload semantics and
|
|
8827
|
+
* mint a substitute `task.fail` for a task this runner already finished —
|
|
8828
|
+
* a second authority over terminal outcomes living in the queue, which is
|
|
8829
|
+
* worse than the window it closes. Documented instead, here and in
|
|
8830
|
+
* docs/protocol.md §7.2.
|
|
8831
|
+
*/
|
|
8832
|
+
async resolveResultDocument(active, finalOutput) {
|
|
8833
|
+
const extract = this.deps.resultDocument?.extract;
|
|
8834
|
+
if (!extract) return { deliver: true };
|
|
8835
|
+
let document;
|
|
8836
|
+
try {
|
|
8837
|
+
document = extract(finalOutput, { taskId: active.taskId, sessionRef: active.session.sessionRef });
|
|
8838
|
+
} catch (err) {
|
|
8839
|
+
await this.fail(
|
|
8840
|
+
active.taskId,
|
|
8841
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage3(err)}`,
|
|
8842
|
+
false
|
|
8843
|
+
);
|
|
8844
|
+
return { deliver: false };
|
|
8845
|
+
}
|
|
8846
|
+
if (typeof document?.then === "function") {
|
|
8847
|
+
await this.fail(
|
|
8848
|
+
active.taskId,
|
|
8849
|
+
`${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)`,
|
|
8850
|
+
false
|
|
8851
|
+
);
|
|
8852
|
+
return { deliver: false };
|
|
8853
|
+
}
|
|
8854
|
+
if (document === void 0) return { deliver: true };
|
|
8855
|
+
const check = checkResultDocument(document);
|
|
8856
|
+
if (!check.ok) {
|
|
8857
|
+
const detail = resultDocumentRejectionDetail(check);
|
|
8858
|
+
await this.fail(active.taskId, `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: ${detail}`, false);
|
|
8859
|
+
return { deliver: false };
|
|
8860
|
+
}
|
|
8861
|
+
if (!this.hasResultDocumentCapability()) {
|
|
8862
|
+
await this.fail(
|
|
8863
|
+
active.taskId,
|
|
8864
|
+
`${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`,
|
|
8865
|
+
false
|
|
8866
|
+
);
|
|
8867
|
+
return { deliver: false };
|
|
8868
|
+
}
|
|
8869
|
+
return { deliver: true, document: check.canonical };
|
|
8870
|
+
}
|
|
8871
|
+
/**
|
|
8872
|
+
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
8873
|
+
* read fresh on every call, never captured, because the answer changes
|
|
8874
|
+
* across a reconnect (`ConnectionManager.getServerCapabilities` returns
|
|
8875
|
+
* `[]` from the moment an acked connection closes until a fresh
|
|
8876
|
+
* `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
|
|
8877
|
+
* "no capabilities", the fail-closed reading.
|
|
8878
|
+
*/
|
|
8879
|
+
hasResultDocumentCapability() {
|
|
8880
|
+
return (this.deps.getServerCapabilities?.() ?? []).includes("result-document");
|
|
8881
|
+
}
|
|
8296
8882
|
async observeGit(active, phase) {
|
|
8297
8883
|
if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
|
|
8298
8884
|
try {
|
|
@@ -8364,7 +8950,7 @@ var TaskRunner = class {
|
|
|
8364
8950
|
}
|
|
8365
8951
|
/** `reuseDir`, when set (a known sessionRef's recorded workspace), is used verbatim instead of a fresh `workspaceRoot/<taskId>` directory — `mkdir recursive` is idempotent either way, so ensuring-exists is safe to do unconditionally. */
|
|
8366
8952
|
async resolveWorkspaceDir(taskId, reuseDir) {
|
|
8367
|
-
const dir = reuseDir ??
|
|
8953
|
+
const dir = reuseDir ?? path17.join(this.deps.workspaceRoot, taskId);
|
|
8368
8954
|
await promises.mkdir(dir, { recursive: true });
|
|
8369
8955
|
return dir;
|
|
8370
8956
|
}
|
|
@@ -8395,7 +8981,7 @@ var TaskRunner = class {
|
|
|
8395
8981
|
* is device-specific (which runtimes happen to be installed here), so a
|
|
8396
8982
|
* different device's installed runtime set might satisfy it.
|
|
8397
8983
|
*/
|
|
8398
|
-
async pickAdapter(requestedRuntime, policyMode) {
|
|
8984
|
+
async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
|
|
8399
8985
|
const allowlist = this.deps.runtimeAllowlist;
|
|
8400
8986
|
if (requestedRuntime) {
|
|
8401
8987
|
if (allowlist && !allowlist.includes(requestedRuntime)) {
|
|
@@ -8416,6 +9002,13 @@ var TaskRunner = class {
|
|
|
8416
9002
|
retryable: false
|
|
8417
9003
|
};
|
|
8418
9004
|
}
|
|
9005
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) {
|
|
9006
|
+
return {
|
|
9007
|
+
ok: false,
|
|
9008
|
+
reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
|
|
9009
|
+
retryable: false
|
|
9010
|
+
};
|
|
9011
|
+
}
|
|
8419
9012
|
const detected = await adapter.detect();
|
|
8420
9013
|
if (!detected.present) {
|
|
8421
9014
|
return {
|
|
@@ -8430,12 +9023,13 @@ var TaskRunner = class {
|
|
|
8430
9023
|
const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
|
|
8431
9024
|
for (const adapter of candidates) {
|
|
8432
9025
|
if (!adapterSupportsMode(adapter, policyMode)) continue;
|
|
9026
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) continue;
|
|
8433
9027
|
const detected = await adapter.detect();
|
|
8434
9028
|
if (detected.present) return { ok: true, adapter };
|
|
8435
9029
|
}
|
|
8436
9030
|
return {
|
|
8437
9031
|
ok: false,
|
|
8438
|
-
reason: `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
9032
|
+
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}"`,
|
|
8439
9033
|
retryable: true
|
|
8440
9034
|
};
|
|
8441
9035
|
}
|
|
@@ -8466,7 +9060,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
|
|
|
8466
9060
|
bytes,
|
|
8467
9061
|
bytesHash: journalHash(bytes),
|
|
8468
9062
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8469
|
-
opensTask: envelope.type === "task.offer"
|
|
9063
|
+
opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets"
|
|
8470
9064
|
};
|
|
8471
9065
|
}
|
|
8472
9066
|
function isRuntimeId(id) {
|
|
@@ -8490,48 +9084,235 @@ function computeCapabilities(adapters) {
|
|
|
8490
9084
|
if (adapters.some((adapter) => adapter.capabilities().steer)) flags.push("steer");
|
|
8491
9085
|
flags.push("blob-upload");
|
|
8492
9086
|
flags.push("approval-targeting");
|
|
9087
|
+
const selectionAdapters = adapters.filter(
|
|
9088
|
+
(adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
|
|
9089
|
+
);
|
|
9090
|
+
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
|
|
9091
|
+
flags.push("dispatch-selection");
|
|
9092
|
+
}
|
|
9093
|
+
if (adapters.some((adapter) => adapter.capabilities().mcpToolsets === true)) {
|
|
9094
|
+
flags.push("toolset-selection");
|
|
9095
|
+
}
|
|
8493
9096
|
return flags;
|
|
8494
9097
|
}
|
|
8495
9098
|
var ALL_RUNTIME_IDS = ["pi", "claude", "codex"];
|
|
8496
|
-
function buildAdapter(id) {
|
|
9099
|
+
function buildAdapter(id, config) {
|
|
8497
9100
|
switch (id) {
|
|
8498
9101
|
case "pi":
|
|
8499
|
-
return new PiAdapter();
|
|
9102
|
+
return new PiAdapter({ byokLauncher: config.piByokLauncher });
|
|
8500
9103
|
case "claude":
|
|
8501
9104
|
return new ClaudeAdapter();
|
|
8502
9105
|
case "codex":
|
|
8503
9106
|
return new CodexAdapter();
|
|
8504
9107
|
}
|
|
8505
9108
|
}
|
|
8506
|
-
function buildDefaultAdapters(
|
|
8507
|
-
const ids = runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => runtimeAllowlist
|
|
8508
|
-
return ids.map(buildAdapter);
|
|
9109
|
+
function buildDefaultAdapters(config) {
|
|
9110
|
+
const ids = config.runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => config.runtimeAllowlist?.includes(id)) : ALL_RUNTIME_IDS;
|
|
9111
|
+
return ids.map((id) => buildAdapter(id, config));
|
|
8509
9112
|
}
|
|
8510
|
-
function
|
|
8511
|
-
|
|
9113
|
+
function validatePiByokLauncherConfig(launcher) {
|
|
9114
|
+
for (const [field, value] of [
|
|
9115
|
+
["command", launcher.command],
|
|
9116
|
+
["profileDbPath", launcher.profileDbPath],
|
|
9117
|
+
["sessionDir", launcher.sessionDir]
|
|
9118
|
+
]) {
|
|
9119
|
+
if (value.trim().length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
9120
|
+
throw new Error(`DaemonConfig.piByokLauncher.${field} must be a non-empty single-line string`);
|
|
9121
|
+
}
|
|
9122
|
+
}
|
|
9123
|
+
if (!isAbsolute(launcher.profileDbPath) || !isAbsolute(launcher.sessionDir)) {
|
|
8512
9124
|
throw new Error(
|
|
8513
|
-
|
|
9125
|
+
"DaemonConfig.piByokLauncher profileDbPath and sessionDir must be absolute paths"
|
|
8514
9126
|
);
|
|
8515
9127
|
}
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
const
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8534
|
-
|
|
9128
|
+
if (launcher.secretServicePrefix !== void 0 && (launcher.secretServicePrefix.trim().length === 0 || /[\u0000\r\n]/u.test(launcher.secretServicePrefix))) {
|
|
9129
|
+
throw new Error(
|
|
9130
|
+
"DaemonConfig.piByokLauncher.secretServicePrefix must be a non-empty single-line string"
|
|
9131
|
+
);
|
|
9132
|
+
}
|
|
9133
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
9134
|
+
"--",
|
|
9135
|
+
"--pi-bin",
|
|
9136
|
+
"--profile-db",
|
|
9137
|
+
"--session-dir",
|
|
9138
|
+
"--secret-service-prefix",
|
|
9139
|
+
"--provider",
|
|
9140
|
+
"--model"
|
|
9141
|
+
]);
|
|
9142
|
+
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
9143
|
+
if (conflicting !== void 0) {
|
|
9144
|
+
throw new Error(
|
|
9145
|
+
`DaemonConfig.piByokLauncher.args must not override reserved launcher argument ${conflicting}`
|
|
9146
|
+
);
|
|
9147
|
+
}
|
|
9148
|
+
const invalidArg = launcher.args?.find((arg) => arg.length === 0 || /[\u0000\r\n]/u.test(arg));
|
|
9149
|
+
if (invalidArg !== void 0) {
|
|
9150
|
+
throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
var MAX_LOCAL_MCP_TOOLSETS = 64;
|
|
9154
|
+
var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
|
|
9155
|
+
var MAX_LOCAL_MCP_ARGS = 64;
|
|
9156
|
+
var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
|
|
9157
|
+
function isNonEmptySingleLine(value) {
|
|
9158
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_LOCAL_MCP_TOKEN_CHARS && !/[\u0000\r\n]/u.test(value);
|
|
9159
|
+
}
|
|
9160
|
+
function resolveMcpToolsets(configured) {
|
|
9161
|
+
if (configured === void 0) return void 0;
|
|
9162
|
+
if (configured === null || typeof configured !== "object" || Array.isArray(configured)) {
|
|
9163
|
+
throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
|
|
9164
|
+
}
|
|
9165
|
+
const toolsetEntries = Object.entries(configured);
|
|
9166
|
+
if (toolsetEntries.length > MAX_LOCAL_MCP_TOOLSETS) {
|
|
9167
|
+
throw new Error(`DaemonConfig.mcpToolsets may contain at most ${MAX_LOCAL_MCP_TOOLSETS} toolsets`);
|
|
9168
|
+
}
|
|
9169
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
9170
|
+
for (const [toolsetId, rawToolset] of toolsetEntries) {
|
|
9171
|
+
const parsedId = ToolsetIdSchema.safeParse(toolsetId);
|
|
9172
|
+
if (!parsedId.success) {
|
|
9173
|
+
throw new Error(`DaemonConfig.mcpToolsets contains invalid toolset id ${JSON.stringify(toolsetId)}`);
|
|
9174
|
+
}
|
|
9175
|
+
if (rawToolset === null || typeof rawToolset !== "object" || Array.isArray(rawToolset)) {
|
|
9176
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} must be an object`);
|
|
9177
|
+
}
|
|
9178
|
+
const toolsetKeys = Object.keys(rawToolset);
|
|
9179
|
+
if (toolsetKeys.some((key) => key !== "mcpServers")) {
|
|
9180
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} accepts only the mcpServers field`);
|
|
9181
|
+
}
|
|
9182
|
+
const rawServers = rawToolset.mcpServers;
|
|
9183
|
+
if (rawServers === null || typeof rawServers !== "object" || Array.isArray(rawServers)) {
|
|
9184
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must be an object`);
|
|
9185
|
+
}
|
|
9186
|
+
const serverEntries = Object.entries(rawServers);
|
|
9187
|
+
if (serverEntries.length === 0 || serverEntries.length > MAX_LOCAL_MCP_SERVERS_PER_TOOLSET) {
|
|
9188
|
+
throw new Error(
|
|
9189
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must contain 1-${MAX_LOCAL_MCP_SERVERS_PER_TOOLSET} servers`
|
|
9190
|
+
);
|
|
9191
|
+
}
|
|
9192
|
+
const servers = {};
|
|
9193
|
+
for (const [serverName, rawServer] of serverEntries) {
|
|
9194
|
+
if (!ToolsetIdSchema.safeParse(serverName).success) {
|
|
9195
|
+
throw new Error(
|
|
9196
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers contains invalid server name ${JSON.stringify(serverName)}`
|
|
9197
|
+
);
|
|
9198
|
+
}
|
|
9199
|
+
if (serverName === APPROVAL_MCP_SERVER_NAME) {
|
|
9200
|
+
throw new Error(
|
|
9201
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} uses a server name reserved by the daemon`
|
|
9202
|
+
);
|
|
9203
|
+
}
|
|
9204
|
+
if (rawServer === null || typeof rawServer !== "object" || Array.isArray(rawServer)) {
|
|
9205
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} must be an object`);
|
|
9206
|
+
}
|
|
9207
|
+
const serverKeys = Object.keys(rawServer);
|
|
9208
|
+
if (serverKeys.some((key) => key !== "command" && key !== "args")) {
|
|
9209
|
+
throw new Error(
|
|
9210
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} accepts only command and args; env, headers, and remote task data are not supported`
|
|
9211
|
+
);
|
|
9212
|
+
}
|
|
9213
|
+
const server = rawServer;
|
|
9214
|
+
if (!isNonEmptySingleLine(server.command)) {
|
|
9215
|
+
throw new Error(
|
|
9216
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.command must be a non-empty single-line string no longer than ${MAX_LOCAL_MCP_TOKEN_CHARS} characters`
|
|
9217
|
+
);
|
|
9218
|
+
}
|
|
9219
|
+
if (server.args !== void 0 && !Array.isArray(server.args)) {
|
|
9220
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must be an array`);
|
|
9221
|
+
}
|
|
9222
|
+
const args = server.args ?? [];
|
|
9223
|
+
if (args.length > MAX_LOCAL_MCP_ARGS || args.some((arg) => !isNonEmptySingleLine(arg))) {
|
|
9224
|
+
throw new Error(
|
|
9225
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must contain at most ${MAX_LOCAL_MCP_ARGS} non-empty single-line strings`
|
|
9226
|
+
);
|
|
9227
|
+
}
|
|
9228
|
+
servers[serverName] = Object.freeze({
|
|
9229
|
+
command: server.command,
|
|
9230
|
+
...args.length > 0 ? { args: Object.freeze([...args]) } : {}
|
|
9231
|
+
});
|
|
9232
|
+
}
|
|
9233
|
+
resolved.set(toolsetId, Object.freeze({ mcpServers: Object.freeze(servers) }));
|
|
9234
|
+
}
|
|
9235
|
+
return resolved;
|
|
9236
|
+
}
|
|
9237
|
+
function resolveDeviceAssertionAudiences(config) {
|
|
9238
|
+
if (config === void 0) return void 0;
|
|
9239
|
+
if (!Array.isArray(config.audiences)) {
|
|
9240
|
+
throw new Error(
|
|
9241
|
+
`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.`
|
|
9242
|
+
);
|
|
9243
|
+
}
|
|
9244
|
+
if (config.audiences.length === 0) return void 0;
|
|
9245
|
+
const audiences = /* @__PURE__ */ new Set();
|
|
9246
|
+
for (const audience of config.audiences) {
|
|
9247
|
+
if (typeof audience !== "string" || audience.length === 0) {
|
|
9248
|
+
throw new Error(
|
|
9249
|
+
`DaemonConfig.deviceAssertion.audiences entries must be non-empty strings \u2014 got ${JSON.stringify(audience)}`
|
|
9250
|
+
);
|
|
9251
|
+
}
|
|
9252
|
+
if (Buffer.byteLength(audience, "utf8") > DEVICE_ASSERTION_AUDIENCE_MAX_BYTES) {
|
|
9253
|
+
throw new Error(
|
|
9254
|
+
`DaemonConfig.deviceAssertion.audiences entry ${JSON.stringify(audience)} exceeds ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
9255
|
+
);
|
|
9256
|
+
}
|
|
9257
|
+
if (audiences.has(audience)) {
|
|
9258
|
+
throw new Error(
|
|
9259
|
+
`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`
|
|
9260
|
+
);
|
|
9261
|
+
}
|
|
9262
|
+
audiences.add(audience);
|
|
9263
|
+
}
|
|
9264
|
+
return audiences;
|
|
9265
|
+
}
|
|
9266
|
+
function resolveDeviceAssertionTtlMs(config) {
|
|
9267
|
+
const ttlMs = config?.ttlMs ?? DEVICE_ASSERTION_DEFAULT_TTL_MS;
|
|
9268
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_ASSERTION_MAX_TTL_MS) {
|
|
9269
|
+
throw new Error(
|
|
9270
|
+
`DaemonConfig.deviceAssertion.ttlMs must be a positive integer no greater than ${DEVICE_ASSERTION_MAX_TTL_MS} ms \u2014 got ${JSON.stringify(config?.ttlMs)}`
|
|
9271
|
+
);
|
|
9272
|
+
}
|
|
9273
|
+
return ttlMs;
|
|
9274
|
+
}
|
|
9275
|
+
function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
9276
|
+
return buildDaemonWithAdapters(config, adapters, overrides);
|
|
9277
|
+
}
|
|
9278
|
+
function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
|
|
9279
|
+
const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
|
|
9280
|
+
if (config.piByokLauncher !== void 0) {
|
|
9281
|
+
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
9282
|
+
}
|
|
9283
|
+
if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
|
|
9284
|
+
throw new Error(
|
|
9285
|
+
`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".`
|
|
9286
|
+
);
|
|
9287
|
+
}
|
|
9288
|
+
const presenceCadence = {
|
|
9289
|
+
intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
|
|
9290
|
+
ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
|
|
9291
|
+
minimumIntervalMs: config.presence?.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS
|
|
9292
|
+
};
|
|
9293
|
+
assertPresenceHeartbeatCadence(presenceCadence);
|
|
9294
|
+
const deviceAssertionAudiences = resolveDeviceAssertionAudiences(config.deviceAssertion);
|
|
9295
|
+
const deviceAssertionTtlMs = resolveDeviceAssertionTtlMs(config.deviceAssertion);
|
|
9296
|
+
let shuttingDown = false;
|
|
9297
|
+
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
9298
|
+
const store = new DeviceStore(storeDir);
|
|
9299
|
+
const operationalHealth = new OperationalHealthTracker(storeDir);
|
|
9300
|
+
let fleetJitter;
|
|
9301
|
+
let maintenanceSequence = 0;
|
|
9302
|
+
const cursorStore = new CursorStore(storeDir);
|
|
9303
|
+
const sessionWorkspaces = new SessionWorkspaceStore(storeDir);
|
|
9304
|
+
const gitWorkspaceManager = config.gitWorkspace ? overrides.gitWorkspace?.manager ?? new GitWorkspaceManager(config.workspaceRoot, { ownerId: stableGitWorkspaceOwnerId(storeDir, config.productId) }) : void 0;
|
|
9305
|
+
const gitWorkspaceStore = config.gitWorkspace ? overrides.gitWorkspace?.store ?? new GitWorkspaceStore(storeDir) : void 0;
|
|
9306
|
+
let resolvedStoragePolicy;
|
|
9307
|
+
if (config.hostedJournal) {
|
|
9308
|
+
if (config.hostedJournal.mode !== "sqlite") {
|
|
9309
|
+
throw new Error(`DaemonConfig.hostedJournal.mode must be "sqlite" \u2014 got ${JSON.stringify(config.hostedJournal.mode)}`);
|
|
9310
|
+
}
|
|
9311
|
+
if (typeof config.hostedJournal.tenantId !== "string" || config.hostedJournal.tenantId.trim() === "") {
|
|
9312
|
+
throw new Error(
|
|
9313
|
+
"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"
|
|
9314
|
+
);
|
|
9315
|
+
}
|
|
8535
9316
|
if (config.hostedJournal.storagePolicy) {
|
|
8536
9317
|
resolvedStoragePolicy = resolveLocalStoragePolicy(config.hostedJournal.storagePolicy);
|
|
8537
9318
|
}
|
|
@@ -8603,6 +9384,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8603
9384
|
let runner;
|
|
8604
9385
|
let controlServerHandle;
|
|
8605
9386
|
let daemonOwnerLease;
|
|
9387
|
+
let presencePublisher;
|
|
9388
|
+
let presenceDiscovery;
|
|
9389
|
+
let presenceDiscoveryInFlight = false;
|
|
8606
9390
|
let shutdownPromise;
|
|
8607
9391
|
const pendingLateMutationBarriers = /* @__PURE__ */ new Set();
|
|
8608
9392
|
let startedAt;
|
|
@@ -8775,6 +9559,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8775
9559
|
deviceId: record.deviceId,
|
|
8776
9560
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
8777
9561
|
runtimeEnvironment: config.runtimeEnvironment,
|
|
9562
|
+
...mcpToolsets ? { mcpToolsets } : {},
|
|
8778
9563
|
// M3-2a: `send` is already this file's OWN closure (not something
|
|
8779
9564
|
// `TaskRunner` builds) — every `task.claim`/`task.started`/
|
|
8780
9565
|
// `task.progress`/`task.artifact`/`task.await_approval`/
|
|
@@ -8814,6 +9599,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8814
9599
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
8815
9600
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
8816
9601
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
9602
|
+
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
9603
|
+
// absent when unconfigured — see `DaemonConfig.resultDocument`'s own
|
|
9604
|
+
// doc comment. Spread rather than assigned so an unconfigured daemon
|
|
9605
|
+
// builds the exact `deps` object it did before this seam existed.
|
|
9606
|
+
...config.resultDocument ? { resultDocument: config.resultDocument } : {},
|
|
8817
9607
|
// M4 Phase 3 hardening: bridges TaskRunner's stale-approval-race
|
|
8818
9608
|
// finding out to the SAME local observability seam every other
|
|
8819
9609
|
// daemon-local event already uses (see observer.ts's own module doc
|
|
@@ -8889,8 +9679,10 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8889
9679
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
8890
9680
|
},
|
|
8891
9681
|
onStateChange: (state) => {
|
|
9682
|
+
const wasSettled = connectionState === "open" || connectionState === "degraded";
|
|
8892
9683
|
connectionState = state;
|
|
8893
9684
|
observer.noteConnectionState(state);
|
|
9685
|
+
if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
|
|
8894
9686
|
},
|
|
8895
9687
|
backoff: overrides.backoff,
|
|
8896
9688
|
liveness: overrides.liveness,
|
|
@@ -8908,6 +9700,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8908
9700
|
});
|
|
8909
9701
|
await connection.start();
|
|
8910
9702
|
await connection.waitForAck();
|
|
9703
|
+
startPresenceProducer();
|
|
8911
9704
|
} catch (err) {
|
|
8912
9705
|
try {
|
|
8913
9706
|
await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
|
|
@@ -8917,7 +9710,41 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8917
9710
|
throw err;
|
|
8918
9711
|
}
|
|
8919
9712
|
}
|
|
9713
|
+
function startPresenceProducer() {
|
|
9714
|
+
presenceDiscovery = new AbortController();
|
|
9715
|
+
runPresenceDiscovery();
|
|
9716
|
+
}
|
|
9717
|
+
function runPresenceDiscovery() {
|
|
9718
|
+
const discovery = presenceDiscovery;
|
|
9719
|
+
if (!discovery || presenceDiscoveryInFlight) return;
|
|
9720
|
+
presenceDiscoveryInFlight = true;
|
|
9721
|
+
void (async () => {
|
|
9722
|
+
try {
|
|
9723
|
+
const declaration = await fetchCapabilityDeclaration(config.serverUrl, { signal: discovery.signal });
|
|
9724
|
+
if (discovery.signal.aborted) return;
|
|
9725
|
+
if (declares(declaration, PRESENCE_HINTS_CAPABILITY)) {
|
|
9726
|
+
presencePublisher ??= new PresencePublisher({
|
|
9727
|
+
serverUrl: config.serverUrl,
|
|
9728
|
+
auth,
|
|
9729
|
+
...presenceCadence,
|
|
9730
|
+
onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
|
|
9731
|
+
});
|
|
9732
|
+
presencePublisher.start();
|
|
9733
|
+
} else {
|
|
9734
|
+
presencePublisher?.stop();
|
|
9735
|
+
}
|
|
9736
|
+
} catch (err) {
|
|
9737
|
+
if (discovery.signal.aborted) return;
|
|
9738
|
+
console.warn(
|
|
9739
|
+
`[byok/client] capability discovery failed; presence publishing stays off until the next reconnect: ${err instanceof Error ? err.message : String(err)}`
|
|
9740
|
+
);
|
|
9741
|
+
} finally {
|
|
9742
|
+
presenceDiscoveryInFlight = false;
|
|
9743
|
+
}
|
|
9744
|
+
})();
|
|
9745
|
+
}
|
|
8920
9746
|
async function runShutdownSequence(reason, opts = {}) {
|
|
9747
|
+
shuttingDown = true;
|
|
8921
9748
|
const errors = [];
|
|
8922
9749
|
let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
|
|
8923
9750
|
if (!hostedStorageInitializationBarrierComplete) {
|
|
@@ -8946,6 +9773,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8946
9773
|
errors.push(new Error("a prior active task teardown remains unsettled; ownership lease retained"));
|
|
8947
9774
|
}
|
|
8948
9775
|
}
|
|
9776
|
+
presenceDiscovery?.abort();
|
|
9777
|
+
presenceDiscovery = void 0;
|
|
9778
|
+
presenceDiscoveryInFlight = false;
|
|
9779
|
+
presencePublisher?.stop();
|
|
9780
|
+
presencePublisher = void 0;
|
|
8949
9781
|
const stoppingOwnedPressureEngine = ownedPressureEngine;
|
|
8950
9782
|
const stoppingOwnedJournal = ownedJournal;
|
|
8951
9783
|
const maintenanceStopped = stoppingOwnedPressureEngine?.stop() ?? Promise.resolve();
|
|
@@ -9006,6 +9838,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9006
9838
|
}
|
|
9007
9839
|
}
|
|
9008
9840
|
async function stop(opts = {}) {
|
|
9841
|
+
shuttingDown = true;
|
|
9009
9842
|
await requestShutdown(opts.reason ?? "operator", { drainTimeoutMs: opts.drainTimeoutMs });
|
|
9010
9843
|
}
|
|
9011
9844
|
function requestShutdown(reason, opts = {}) {
|
|
@@ -9018,6 +9851,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9018
9851
|
return current;
|
|
9019
9852
|
}
|
|
9020
9853
|
async function unpair() {
|
|
9854
|
+
shuttingDown = true;
|
|
9021
9855
|
await runLifecycleMutation(unpairUnderLease);
|
|
9022
9856
|
}
|
|
9023
9857
|
async function unpairUnderLease() {
|
|
@@ -9096,6 +9930,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9096
9930
|
}
|
|
9097
9931
|
async function performControlShutdown(reason) {
|
|
9098
9932
|
const effectiveReason = reason ?? "operator";
|
|
9933
|
+
shuttingDown = true;
|
|
9099
9934
|
observer.noteShutdownRequested(effectiveReason);
|
|
9100
9935
|
try {
|
|
9101
9936
|
await requestShutdown(`control socket shutdown (${effectiveReason})`);
|
|
@@ -9133,7 +9968,107 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9133
9968
|
if (!runner) throw new ControlError("not_found", "daemon is not started");
|
|
9134
9969
|
return runner.requestApproval(parsed.taskId, parsed.summary);
|
|
9135
9970
|
},
|
|
9971
|
+
/**
|
|
9972
|
+
* Plan `device-assertion-broker`: mint one short-lived, audience-scoped
|
|
9973
|
+
* device assertion for a sibling local process.
|
|
9974
|
+
*
|
|
9975
|
+
* SIX fail-closed gates, in this exact order, none of which signs
|
|
9976
|
+
* anything on the way out. The order is part of the contract, not an
|
|
9977
|
+
* implementation detail:
|
|
9978
|
+
*
|
|
9979
|
+
* 1. `assertion_disabled` — before anything else, because a daemon that
|
|
9980
|
+
* was never configured for this must not reveal, by answering
|
|
9981
|
+
* differently for different inputs, that it even validates params.
|
|
9982
|
+
* 2. `bad_request` — shape/length, checked before the allowlist so a
|
|
9983
|
+
* malformed request cannot be used to probe membership.
|
|
9984
|
+
* 3. `audience_denied` — EXACT `Set.has`, never a prefix/suffix rule
|
|
9985
|
+
* (`salesko-api.evil.com` and `salesko-ap` both fail against an entry
|
|
9986
|
+
* of `salesko-api`). The message deliberately does not echo the
|
|
9987
|
+
* allowlist: a refusal must not be an enumeration oracle.
|
|
9988
|
+
* 4. `shutting_down` — see `performControlShutdown`'s own comment for
|
|
9989
|
+
* the minting window this closes.
|
|
9990
|
+
* 5. `revoked` — the server-side revocation this daemon already knows
|
|
9991
|
+
* about.
|
|
9992
|
+
* 6. `not_paired` — the on-disk record, re-read on EVERY call (never
|
|
9993
|
+
* cached), so clearing `device.json` removes local signing authority
|
|
9994
|
+
* immediately.
|
|
9995
|
+
*
|
|
9996
|
+
* Only after all six does the private key get imported, used once, and
|
|
9997
|
+
* dropped (`device-assertion-signer.ts`).
|
|
9998
|
+
*
|
|
9999
|
+
* Honest limit, and it must stay in the docs as well as here: gates 4-6
|
|
10000
|
+
* are only HALF of revocation. They make this daemon stop minting
|
|
10001
|
+
* promptly, but an assertion already in a caller's hands is not recalled
|
|
10002
|
+
* by any of them. The other half is the host's own recheck at exchange
|
|
10003
|
+
* time, which is why core's `verifyDeviceAssertion` makes the device
|
|
10004
|
+
* row's `revoked` state a REQUIRED parameter. Nothing here entitles
|
|
10005
|
+
* anyone to claim this daemon delivers synchronous invalidation on its
|
|
10006
|
+
* own.
|
|
10007
|
+
*/
|
|
10008
|
+
"assertion.issue": async (params) => {
|
|
10009
|
+
if (deviceAssertionAudiences === void 0) {
|
|
10010
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "assertion_disabled" });
|
|
10011
|
+
throw new ControlError(
|
|
10012
|
+
"assertion_disabled",
|
|
10013
|
+
"this daemon is not configured to issue device assertions (DaemonConfig.deviceAssertion.audiences is absent or empty)"
|
|
10014
|
+
);
|
|
10015
|
+
}
|
|
10016
|
+
const parsed = parseAssertionIssueParams(params);
|
|
10017
|
+
if (!parsed) {
|
|
10018
|
+
const rawAudience = typeof params === "object" && params !== null && typeof params.audience === "string" ? params.audience : void 0;
|
|
10019
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "bad_request", audience: rawAudience });
|
|
10020
|
+
throw new ControlError(
|
|
10021
|
+
"bad_request",
|
|
10022
|
+
`assertion.issue requires exactly {audience} where audience is a non-empty string of at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
10023
|
+
);
|
|
10024
|
+
}
|
|
10025
|
+
if (!deviceAssertionAudiences.has(parsed.audience)) {
|
|
10026
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "audience_denied", audience: parsed.audience });
|
|
10027
|
+
throw new ControlError("audience_denied", "the requested audience is not allowed by this daemon");
|
|
10028
|
+
}
|
|
10029
|
+
if (shuttingDown) {
|
|
10030
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10031
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10032
|
+
}
|
|
10033
|
+
if (auth.isRevoked()) {
|
|
10034
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10035
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10036
|
+
}
|
|
10037
|
+
const record = await store.load();
|
|
10038
|
+
if (record === void 0) {
|
|
10039
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "not_paired", audience: parsed.audience });
|
|
10040
|
+
throw new ControlError("not_paired", "this device is not paired; nothing can be asserted about it");
|
|
10041
|
+
}
|
|
10042
|
+
if (shuttingDown) {
|
|
10043
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10044
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10045
|
+
}
|
|
10046
|
+
if (auth.isRevoked()) {
|
|
10047
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10048
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10049
|
+
}
|
|
10050
|
+
const minted = mintDeviceAssertion({
|
|
10051
|
+
record,
|
|
10052
|
+
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
10053
|
+
// (ws:->http:, wss:->https:, path stripped), so an operator who
|
|
10054
|
+
// configured the websocket spelling and one who configured the HTTP
|
|
10055
|
+
// spelling of the same deployment produce the same issuer.
|
|
10056
|
+
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
10057
|
+
productId: config.productId,
|
|
10058
|
+
audience: parsed.audience,
|
|
10059
|
+
ttlMs: deviceAssertionTtlMs,
|
|
10060
|
+
now: /* @__PURE__ */ new Date()
|
|
10061
|
+
});
|
|
10062
|
+
observer.noteDeviceAssertion({
|
|
10063
|
+
result: "issued",
|
|
10064
|
+
audience: minted.claims.audience,
|
|
10065
|
+
jti: minted.claims.jti,
|
|
10066
|
+
expiresAt: minted.expiresAt
|
|
10067
|
+
});
|
|
10068
|
+
return { assertion: minted.envelope, expiresAt: minted.expiresAt };
|
|
10069
|
+
},
|
|
9136
10070
|
shutdown: (params) => {
|
|
10071
|
+
shuttingDown = true;
|
|
9137
10072
|
const { reason } = parseShutdownParams(params);
|
|
9138
10073
|
setImmediate(() => {
|
|
9139
10074
|
void performControlShutdown(reason).catch((err) => {
|
|
@@ -9178,7 +10113,275 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9178
10113
|
return { pair, start, stop, status, subscribe, tasks, unpair, approve, reject };
|
|
9179
10114
|
}
|
|
9180
10115
|
function createDaemon(config) {
|
|
9181
|
-
return createDaemonWithAdapters(config, buildDefaultAdapters(config
|
|
10116
|
+
return createDaemonWithAdapters(config, buildDefaultAdapters(config));
|
|
10117
|
+
}
|
|
10118
|
+
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
10119
|
+
function errorMessage4(err) {
|
|
10120
|
+
return err instanceof Error ? err.message : String(err);
|
|
10121
|
+
}
|
|
10122
|
+
function sameFileState3(left, right) {
|
|
10123
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
10124
|
+
}
|
|
10125
|
+
async function readControlToken(tokenPath) {
|
|
10126
|
+
let namedBefore;
|
|
10127
|
+
try {
|
|
10128
|
+
namedBefore = await promises.lstat(tokenPath, { bigint: true });
|
|
10129
|
+
} catch (err) {
|
|
10130
|
+
if (err.code === "ENOENT") return void 0;
|
|
10131
|
+
throw err;
|
|
10132
|
+
}
|
|
10133
|
+
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
10134
|
+
throw new Error("control token is not a real regular file");
|
|
10135
|
+
}
|
|
10136
|
+
const handle = await promises.open(
|
|
10137
|
+
tokenPath,
|
|
10138
|
+
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
10139
|
+
);
|
|
10140
|
+
try {
|
|
10141
|
+
const opened = await handle.stat({ bigint: true });
|
|
10142
|
+
const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
|
|
10143
|
+
if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
|
|
10144
|
+
throw new Error("control token pathname changed before safe open");
|
|
10145
|
+
}
|
|
10146
|
+
if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
|
|
10147
|
+
throw new Error("control token exceeds the bounded read limit");
|
|
10148
|
+
}
|
|
10149
|
+
const size = Number(opened.size);
|
|
10150
|
+
const bytes = Buffer.alloc(size);
|
|
10151
|
+
const { bytesRead } = await handle.read(bytes, 0, size, 0);
|
|
10152
|
+
const afterRead = await handle.stat({ bigint: true });
|
|
10153
|
+
const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
|
|
10154
|
+
if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
|
|
10155
|
+
throw new Error("control token changed during bounded read");
|
|
10156
|
+
}
|
|
10157
|
+
return bytes.toString("utf8").trim();
|
|
10158
|
+
} finally {
|
|
10159
|
+
await handle.close();
|
|
10160
|
+
}
|
|
10161
|
+
}
|
|
10162
|
+
async function connectControlClient(opts) {
|
|
10163
|
+
const tokenPath = controlTokenPath(opts.storeDir);
|
|
10164
|
+
let token;
|
|
10165
|
+
try {
|
|
10166
|
+
const read = await readControlToken(tokenPath);
|
|
10167
|
+
if (read === void 0) {
|
|
10168
|
+
return { ok: false, reason: "daemon is not running (no control.token found)" };
|
|
10169
|
+
}
|
|
10170
|
+
token = read;
|
|
10171
|
+
} catch (err) {
|
|
10172
|
+
return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
|
|
10173
|
+
}
|
|
10174
|
+
if (!token) {
|
|
10175
|
+
return { ok: false, reason: "control token file is empty" };
|
|
10176
|
+
}
|
|
10177
|
+
const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
|
|
10178
|
+
try {
|
|
10179
|
+
const client = await connectAndHandshake(endpoint, token, opts);
|
|
10180
|
+
return { ok: true, client };
|
|
10181
|
+
} catch (err) {
|
|
10182
|
+
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage4(err)}` };
|
|
10183
|
+
}
|
|
10184
|
+
}
|
|
10185
|
+
function connectAndHandshake(endpoint, token, opts) {
|
|
10186
|
+
return new Promise((resolve, reject) => {
|
|
10187
|
+
const socket = net.createConnection(endpoint);
|
|
10188
|
+
const reader = new NdjsonLineReader();
|
|
10189
|
+
let phase = "server-hello";
|
|
10190
|
+
let settled = false;
|
|
10191
|
+
const clientNonce = randomNonceHex();
|
|
10192
|
+
const timer = setTimeout(() => {
|
|
10193
|
+
fail(new Error("handshake timed out"));
|
|
10194
|
+
}, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
|
|
10195
|
+
timer.unref?.();
|
|
10196
|
+
function fail(err) {
|
|
10197
|
+
if (settled) return;
|
|
10198
|
+
settled = true;
|
|
10199
|
+
clearTimeout(timer);
|
|
10200
|
+
socket.removeAllListeners();
|
|
10201
|
+
socket.destroy();
|
|
10202
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
10203
|
+
}
|
|
10204
|
+
function succeed() {
|
|
10205
|
+
settled = true;
|
|
10206
|
+
clearTimeout(timer);
|
|
10207
|
+
socket.removeListener("error", onError);
|
|
10208
|
+
socket.removeListener("data", onData);
|
|
10209
|
+
resolve(createControlClient(socket, reader, opts));
|
|
10210
|
+
}
|
|
10211
|
+
function onData(chunk) {
|
|
10212
|
+
let lines;
|
|
10213
|
+
try {
|
|
10214
|
+
lines = reader.push(chunk);
|
|
10215
|
+
} catch (err) {
|
|
10216
|
+
fail(err);
|
|
10217
|
+
return;
|
|
10218
|
+
}
|
|
10219
|
+
for (const line of lines) {
|
|
10220
|
+
let parsed;
|
|
10221
|
+
try {
|
|
10222
|
+
parsed = JSON.parse(line);
|
|
10223
|
+
} catch {
|
|
10224
|
+
fail(new Error("malformed handshake frame"));
|
|
10225
|
+
return;
|
|
10226
|
+
}
|
|
10227
|
+
if (phase === "server-hello") {
|
|
10228
|
+
const hello = parseServerHello(parsed);
|
|
10229
|
+
if (!hello) {
|
|
10230
|
+
fail(new Error("malformed or unexpected server hello"));
|
|
10231
|
+
return;
|
|
10232
|
+
}
|
|
10233
|
+
if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
|
|
10234
|
+
fail(new Error("server failed to prove it holds the control token"));
|
|
10235
|
+
return;
|
|
10236
|
+
}
|
|
10237
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
|
|
10238
|
+
phase = "ready";
|
|
10239
|
+
continue;
|
|
10240
|
+
}
|
|
10241
|
+
if (!parseServerReady(parsed)) {
|
|
10242
|
+
fail(new Error("server did not confirm readiness"));
|
|
10243
|
+
return;
|
|
10244
|
+
}
|
|
10245
|
+
succeed();
|
|
10246
|
+
return;
|
|
10247
|
+
}
|
|
10248
|
+
}
|
|
10249
|
+
function onError(err) {
|
|
10250
|
+
fail(err);
|
|
10251
|
+
}
|
|
10252
|
+
socket.once("error", onError);
|
|
10253
|
+
socket.once("connect", () => {
|
|
10254
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
|
|
10255
|
+
socket.on("data", onData);
|
|
10256
|
+
});
|
|
10257
|
+
});
|
|
10258
|
+
}
|
|
10259
|
+
function withTimeout(promise, ms, message) {
|
|
10260
|
+
return new Promise((resolve, reject) => {
|
|
10261
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
10262
|
+
timer.unref?.();
|
|
10263
|
+
promise.then(
|
|
10264
|
+
(value) => {
|
|
10265
|
+
clearTimeout(timer);
|
|
10266
|
+
resolve(value);
|
|
10267
|
+
},
|
|
10268
|
+
(err) => {
|
|
10269
|
+
clearTimeout(timer);
|
|
10270
|
+
reject(err);
|
|
10271
|
+
}
|
|
10272
|
+
);
|
|
10273
|
+
});
|
|
10274
|
+
}
|
|
10275
|
+
function createControlClient(socket, reader, opts) {
|
|
10276
|
+
const pending = /* @__PURE__ */ new Map();
|
|
10277
|
+
let idSeq = 0;
|
|
10278
|
+
let closed = false;
|
|
10279
|
+
function handleFrame(parsed) {
|
|
10280
|
+
if (!isRecord2(parsed) || typeof parsed.id !== "string") return;
|
|
10281
|
+
const entry = pending.get(parsed.id);
|
|
10282
|
+
if (!entry) return;
|
|
10283
|
+
if ("event" in parsed) {
|
|
10284
|
+
entry.onEvent?.(parsed.event);
|
|
10285
|
+
return;
|
|
10286
|
+
}
|
|
10287
|
+
if (parsed.ok === true) {
|
|
10288
|
+
pending.delete(parsed.id);
|
|
10289
|
+
entry.resolve(parsed.done === true ? void 0 : parsed.result);
|
|
10290
|
+
return;
|
|
10291
|
+
}
|
|
10292
|
+
pending.delete(parsed.id);
|
|
10293
|
+
const shape = parsed.error;
|
|
10294
|
+
entry.reject(
|
|
10295
|
+
new ControlError(
|
|
10296
|
+
typeof shape?.code === "string" ? shape.code : "internal_error",
|
|
10297
|
+
typeof shape?.message === "string" ? shape.message : "unknown control error"
|
|
10298
|
+
)
|
|
10299
|
+
);
|
|
10300
|
+
}
|
|
10301
|
+
socket.on("data", (chunk) => {
|
|
10302
|
+
let lines;
|
|
10303
|
+
try {
|
|
10304
|
+
lines = reader.push(chunk);
|
|
10305
|
+
} catch {
|
|
10306
|
+
socket.destroy();
|
|
10307
|
+
return;
|
|
10308
|
+
}
|
|
10309
|
+
for (const line of lines) {
|
|
10310
|
+
let parsed;
|
|
10311
|
+
try {
|
|
10312
|
+
parsed = JSON.parse(line);
|
|
10313
|
+
} catch {
|
|
10314
|
+
continue;
|
|
10315
|
+
}
|
|
10316
|
+
handleFrame(parsed);
|
|
10317
|
+
}
|
|
10318
|
+
});
|
|
10319
|
+
socket.on("close", () => {
|
|
10320
|
+
closed = true;
|
|
10321
|
+
for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
|
|
10322
|
+
pending.clear();
|
|
10323
|
+
});
|
|
10324
|
+
socket.on("error", () => {
|
|
10325
|
+
});
|
|
10326
|
+
function send(method, params, onEvent) {
|
|
10327
|
+
const id = `c${++idSeq}`;
|
|
10328
|
+
const promise = new Promise((resolve, reject) => {
|
|
10329
|
+
pending.set(id, { resolve, reject, onEvent });
|
|
10330
|
+
});
|
|
10331
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
|
|
10332
|
+
return { id, promise };
|
|
10333
|
+
}
|
|
10334
|
+
return {
|
|
10335
|
+
async request(method, params) {
|
|
10336
|
+
if (closed) throw new Error("control connection is closed");
|
|
10337
|
+
const { promise } = send(method, params);
|
|
10338
|
+
const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
|
|
10339
|
+
return result;
|
|
10340
|
+
},
|
|
10341
|
+
subscribe(method, params, onEvent) {
|
|
10342
|
+
const { id, promise } = send(method, params, onEvent);
|
|
10343
|
+
promise.catch(() => {
|
|
10344
|
+
});
|
|
10345
|
+
return {
|
|
10346
|
+
close: () => {
|
|
10347
|
+
pending.delete(id);
|
|
10348
|
+
socket.destroy();
|
|
10349
|
+
}
|
|
10350
|
+
};
|
|
10351
|
+
},
|
|
10352
|
+
close() {
|
|
10353
|
+
socket.destroy();
|
|
10354
|
+
}
|
|
10355
|
+
};
|
|
10356
|
+
}
|
|
10357
|
+
|
|
10358
|
+
// src/daemon/assertion-client.ts
|
|
10359
|
+
function errorMessage5(err) {
|
|
10360
|
+
return err instanceof Error ? err.message : String(err);
|
|
10361
|
+
}
|
|
10362
|
+
async function requestDeviceAssertion(options) {
|
|
10363
|
+
const storeDir = DeviceStore.resolveDir(options.productId, options.storeDir);
|
|
10364
|
+
const connected = await connectControlClient({
|
|
10365
|
+
storeDir,
|
|
10366
|
+
productId: options.productId,
|
|
10367
|
+
...options.timeoutMs === void 0 ? {} : { requestTimeoutMs: options.timeoutMs }
|
|
10368
|
+
});
|
|
10369
|
+
if (!connected.ok) return { ok: false, code: "unavailable", reason: connected.reason };
|
|
10370
|
+
try {
|
|
10371
|
+
const result = await connected.client.request("assertion.issue", {
|
|
10372
|
+
audience: options.audience
|
|
10373
|
+
});
|
|
10374
|
+
if (result === null || typeof result !== "object" || typeof result.expiresAt !== "string") {
|
|
10375
|
+
return { ok: false, code: "bad_response", reason: "daemon returned a malformed assertion.issue result" };
|
|
10376
|
+
}
|
|
10377
|
+
const assertion = parseDeviceAssertionEnvelope(result.assertion);
|
|
10378
|
+
return { ok: true, assertion, expiresAt: result.expiresAt };
|
|
10379
|
+
} catch (err) {
|
|
10380
|
+
if (err instanceof ControlError) return { ok: false, code: err.code, reason: err.message };
|
|
10381
|
+
return { ok: false, code: "bad_response", reason: errorMessage5(err) };
|
|
10382
|
+
} finally {
|
|
10383
|
+
connected.client.close();
|
|
10384
|
+
}
|
|
9182
10385
|
}
|
|
9183
10386
|
var StoredDeviceProofSigner = class {
|
|
9184
10387
|
constructor(options) {
|
|
@@ -9237,9 +10440,359 @@ function requireNonEmpty(name, value) {
|
|
|
9237
10440
|
function sha256(bytes) {
|
|
9238
10441
|
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
9239
10442
|
}
|
|
10443
|
+
var SKILL_PACKS_CAPABILITY = "skills.pack";
|
|
10444
|
+
var SKILL_PACKS_DIRNAME = "skill-packs";
|
|
10445
|
+
var SKILL_PACK_LOCK_FILENAME = "lock.json";
|
|
10446
|
+
var SKILL_PACK_AUDIT_FILENAME = "audit.jsonl";
|
|
10447
|
+
var SKILL_PACK_LOCK_SCHEMA = "byok-skill-pack-lock-v1";
|
|
10448
|
+
var DIR_MODE = 448;
|
|
10449
|
+
var FILE_MODE = 384;
|
|
10450
|
+
var SKILL_PACK_RESPONSE_MAX_BYTES = SKILL_PACK_MAX_BYTES * 2;
|
|
10451
|
+
var SKILL_PACK_INSTALL_ERROR_CODES = [
|
|
10452
|
+
"capability_unavailable",
|
|
10453
|
+
"transport_failed",
|
|
10454
|
+
"response_invalid",
|
|
10455
|
+
"response_too_large",
|
|
10456
|
+
"manifest_invalid",
|
|
10457
|
+
"content_rejected",
|
|
10458
|
+
"store_unsafe"
|
|
10459
|
+
];
|
|
10460
|
+
var SkillPackInstallError = class extends Error {
|
|
10461
|
+
code;
|
|
10462
|
+
packName;
|
|
10463
|
+
constructor(code, message, options = {}) {
|
|
10464
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
10465
|
+
this.name = "SkillPackInstallError";
|
|
10466
|
+
this.code = code;
|
|
10467
|
+
this.packName = options.packName;
|
|
10468
|
+
}
|
|
10469
|
+
};
|
|
10470
|
+
function hashBytes(bytes) {
|
|
10471
|
+
return contentHash(`sha256:${createHash("sha256").update(bytes).digest("hex")}`);
|
|
10472
|
+
}
|
|
10473
|
+
function originOf(serverUrl) {
|
|
10474
|
+
return new URL(toHttpBase(serverUrl)).origin;
|
|
10475
|
+
}
|
|
10476
|
+
async function readBoundedJson(response, what) {
|
|
10477
|
+
const declaredLength = response.headers.get("content-length");
|
|
10478
|
+
if (declaredLength !== null) {
|
|
10479
|
+
const parsed = Number(declaredLength);
|
|
10480
|
+
if (Number.isSafeInteger(parsed) && parsed > SKILL_PACK_RESPONSE_MAX_BYTES) {
|
|
10481
|
+
throw new SkillPackInstallError(
|
|
10482
|
+
"response_too_large",
|
|
10483
|
+
`${what} declared ${parsed} bytes, over the ${SKILL_PACK_RESPONSE_MAX_BYTES} byte response limit.`
|
|
10484
|
+
);
|
|
10485
|
+
}
|
|
10486
|
+
}
|
|
10487
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
10488
|
+
if (bytes.byteLength > SKILL_PACK_RESPONSE_MAX_BYTES) {
|
|
10489
|
+
throw new SkillPackInstallError(
|
|
10490
|
+
"response_too_large",
|
|
10491
|
+
`${what} delivered ${bytes.byteLength} bytes, over the ${SKILL_PACK_RESPONSE_MAX_BYTES} byte response limit.`
|
|
10492
|
+
);
|
|
10493
|
+
}
|
|
10494
|
+
try {
|
|
10495
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
10496
|
+
} catch (cause) {
|
|
10497
|
+
throw new SkillPackInstallError("response_invalid", `${what} is not valid JSON.`, { cause });
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
async function getJson(url, auth, signal, what) {
|
|
10501
|
+
let response;
|
|
10502
|
+
try {
|
|
10503
|
+
response = await authedFetch(url, { method: "GET", ...signal === void 0 ? {} : { signal } }, auth);
|
|
10504
|
+
} catch (cause) {
|
|
10505
|
+
throw new SkillPackInstallError("transport_failed", `${what} could not be fetched from ${url.toString()}.`, {
|
|
10506
|
+
cause
|
|
10507
|
+
});
|
|
10508
|
+
}
|
|
10509
|
+
if (!response.ok) {
|
|
10510
|
+
throw new SkillPackInstallError("transport_failed", `${what} answered HTTP ${response.status}.`);
|
|
10511
|
+
}
|
|
10512
|
+
return readBoundedJson(response, what);
|
|
10513
|
+
}
|
|
10514
|
+
function skillPacksRoot(dataDir) {
|
|
10515
|
+
return path17.join(dataDir, SKILL_PACKS_DIRNAME);
|
|
10516
|
+
}
|
|
10517
|
+
function resolveInside(baseDir, relative) {
|
|
10518
|
+
if (!isSkillPackPathSafe(relative)) {
|
|
10519
|
+
throw new SkillPackInstallError("store_unsafe", `${JSON.stringify(relative)} is not a safe pack-relative path.`);
|
|
10520
|
+
}
|
|
10521
|
+
const resolved = path17.resolve(baseDir, relative);
|
|
10522
|
+
const prefix = path17.resolve(baseDir) + path17.sep;
|
|
10523
|
+
if (!resolved.startsWith(prefix)) {
|
|
10524
|
+
throw new SkillPackInstallError(
|
|
10525
|
+
"store_unsafe",
|
|
10526
|
+
`${JSON.stringify(relative)} resolves outside ${baseDir}.`
|
|
10527
|
+
);
|
|
10528
|
+
}
|
|
10529
|
+
return resolved;
|
|
10530
|
+
}
|
|
10531
|
+
async function assertNotSymlink(target) {
|
|
10532
|
+
let stats;
|
|
10533
|
+
try {
|
|
10534
|
+
stats = await promises.lstat(target);
|
|
10535
|
+
} catch (err) {
|
|
10536
|
+
if (err.code === "ENOENT") return;
|
|
10537
|
+
throw err;
|
|
10538
|
+
}
|
|
10539
|
+
if (stats.isSymbolicLink()) {
|
|
10540
|
+
throw new SkillPackInstallError("store_unsafe", `${target} is a symbolic link; refusing to follow it.`);
|
|
10541
|
+
}
|
|
10542
|
+
}
|
|
10543
|
+
async function appendAuditLine(dataDir, record) {
|
|
10544
|
+
const root = skillPacksRoot(dataDir);
|
|
10545
|
+
await promises.mkdir(root, { recursive: true, mode: DIR_MODE });
|
|
10546
|
+
await promises.chmod(root, DIR_MODE).catch(() => {
|
|
10547
|
+
});
|
|
10548
|
+
const filePath = path17.join(root, SKILL_PACK_AUDIT_FILENAME);
|
|
10549
|
+
const handle = await promises.open(filePath, "a", FILE_MODE);
|
|
10550
|
+
try {
|
|
10551
|
+
await handle.chmod(FILE_MODE);
|
|
10552
|
+
await handle.appendFile(`${JSON.stringify(record)}
|
|
10553
|
+
`, "utf8");
|
|
10554
|
+
} finally {
|
|
10555
|
+
await handle.close();
|
|
10556
|
+
}
|
|
10557
|
+
}
|
|
10558
|
+
async function installSkillPacks(options) {
|
|
10559
|
+
if (!hasCapability(options.declaration, SKILL_PACKS_CAPABILITY)) {
|
|
10560
|
+
throw new SkillPackInstallError(
|
|
10561
|
+
"capability_unavailable",
|
|
10562
|
+
`This deployment does not declare ${SKILL_PACKS_CAPABILITY}; refusing to fetch skill packs.`
|
|
10563
|
+
);
|
|
10564
|
+
}
|
|
10565
|
+
const base = toHttpBase(options.serverUrl);
|
|
10566
|
+
const source = originOf(options.serverUrl);
|
|
10567
|
+
const body = await getJson(
|
|
10568
|
+
new URL(BYOK_SKILL_PACKS_PATH, base),
|
|
10569
|
+
options.auth,
|
|
10570
|
+
options.signal,
|
|
10571
|
+
"the skill pack manifest list"
|
|
10572
|
+
);
|
|
10573
|
+
const rawPacks = body?.packs;
|
|
10574
|
+
if (!Array.isArray(rawPacks)) {
|
|
10575
|
+
throw new SkillPackInstallError("response_invalid", "the skill pack manifest list has no `packs` array.");
|
|
10576
|
+
}
|
|
10577
|
+
const installed = [];
|
|
10578
|
+
const unchanged = [];
|
|
10579
|
+
for (const raw of rawPacks) {
|
|
10580
|
+
const manifest = parseManifest(raw);
|
|
10581
|
+
const existing = await readLock(options.dataDir, manifest.name);
|
|
10582
|
+
if (existing?.lock.content_hash === manifest.contentHash) {
|
|
10583
|
+
unchanged.push(manifest.name);
|
|
10584
|
+
continue;
|
|
10585
|
+
}
|
|
10586
|
+
installed.push(await installOne(options, manifest, source, base));
|
|
10587
|
+
}
|
|
10588
|
+
return { installed, unchanged };
|
|
10589
|
+
}
|
|
10590
|
+
function parseManifest(raw) {
|
|
10591
|
+
let manifest;
|
|
10592
|
+
try {
|
|
10593
|
+
manifest = parseSkillPackManifest(raw);
|
|
10594
|
+
} catch (cause) {
|
|
10595
|
+
throw new SkillPackInstallError("manifest_invalid", "a published skill pack manifest is not valid.", { cause });
|
|
10596
|
+
}
|
|
10597
|
+
const structural = checkSkillPackManifest(manifest);
|
|
10598
|
+
if (!structural.ok) {
|
|
10599
|
+
throw new SkillPackInstallError(
|
|
10600
|
+
"manifest_invalid",
|
|
10601
|
+
`skill pack ${JSON.stringify(manifest.name)} was refused: ${structural.reason} \u2014 ${structural.detail}`,
|
|
10602
|
+
{ packName: manifest.name }
|
|
10603
|
+
);
|
|
10604
|
+
}
|
|
10605
|
+
return manifest;
|
|
10606
|
+
}
|
|
10607
|
+
async function installOne(options, manifest, source, base) {
|
|
10608
|
+
const refuse = async (code, message) => {
|
|
10609
|
+
await appendAuditLine(options.dataDir, {
|
|
10610
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10611
|
+
event: "skill-pack-rejected",
|
|
10612
|
+
name: manifest.name,
|
|
10613
|
+
contentHash: manifest.contentHash,
|
|
10614
|
+
source,
|
|
10615
|
+
code,
|
|
10616
|
+
reason: message
|
|
10617
|
+
});
|
|
10618
|
+
throw new SkillPackInstallError(code, message, { packName: manifest.name });
|
|
10619
|
+
};
|
|
10620
|
+
const expectedPackHash = hashBytes(new TextEncoder().encode(skillPackContentHashInput(manifest)));
|
|
10621
|
+
if (expectedPackHash !== manifest.contentHash) {
|
|
10622
|
+
await refuse(
|
|
10623
|
+
"manifest_invalid",
|
|
10624
|
+
`skill pack ${JSON.stringify(manifest.name)} declares ${manifest.contentHash} but its file rows address ${expectedPackHash}.`
|
|
10625
|
+
);
|
|
10626
|
+
}
|
|
10627
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
10628
|
+
let totalBytes = 0;
|
|
10629
|
+
for (const declared of manifest.files) {
|
|
10630
|
+
const url = new URL(byokSkillPackFilePath(manifest.name, declared.path), base);
|
|
10631
|
+
const raw = await getJson(url, options.auth, options.signal, `skill pack file ${JSON.stringify(declared.path)}`);
|
|
10632
|
+
const content = raw?.content;
|
|
10633
|
+
if (typeof content !== "string") {
|
|
10634
|
+
await refuse("response_invalid", `skill pack file ${JSON.stringify(declared.path)} carried no string content.`);
|
|
10635
|
+
}
|
|
10636
|
+
const bytes = new TextEncoder().encode(content);
|
|
10637
|
+
const check = checkSkillPackFileContent(declared, {
|
|
10638
|
+
byteSize: bytes.byteLength,
|
|
10639
|
+
contentHash: hashBytes(bytes)
|
|
10640
|
+
});
|
|
10641
|
+
if (!check.ok) {
|
|
10642
|
+
await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: ${check.reason} \u2014 ${check.detail}`);
|
|
10643
|
+
}
|
|
10644
|
+
totalBytes += bytes.byteLength;
|
|
10645
|
+
bodies.set(declared.path, content);
|
|
10646
|
+
}
|
|
10647
|
+
const entryText = bodies.get(SKILL_PACK_ENTRY_PATH);
|
|
10648
|
+
if (entryText === void 0) {
|
|
10649
|
+
await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: entry-missing \u2014 ${SKILL_PACK_ENTRY_PATH} was not delivered.`);
|
|
10650
|
+
}
|
|
10651
|
+
let entryCheck;
|
|
10652
|
+
try {
|
|
10653
|
+
entryCheck = checkSkillPackEntry(manifest, entryText);
|
|
10654
|
+
} catch (cause) {
|
|
10655
|
+
await refuse(
|
|
10656
|
+
"content_rejected",
|
|
10657
|
+
`skill pack ${JSON.stringify(manifest.name)}: ${SKILL_PACK_ENTRY_PATH} frontmatter was refused (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
10658
|
+
);
|
|
10659
|
+
}
|
|
10660
|
+
if (!entryCheck.ok) {
|
|
10661
|
+
await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: ${entryCheck.reason} \u2014 ${entryCheck.detail}`);
|
|
10662
|
+
}
|
|
10663
|
+
const packRoot = path17.join(skillPacksRoot(options.dataDir), manifest.name);
|
|
10664
|
+
const revisionDir = path17.join(packRoot, manifest.contentHash.slice("sha256:".length));
|
|
10665
|
+
await promises.mkdir(revisionDir, { recursive: true, mode: DIR_MODE });
|
|
10666
|
+
for (const [relative, content] of bodies) {
|
|
10667
|
+
const target = resolveInside(revisionDir, relative);
|
|
10668
|
+
await promises.mkdir(path17.dirname(target), { recursive: true, mode: DIR_MODE });
|
|
10669
|
+
await assertNotSymlink(target);
|
|
10670
|
+
await atomicWriteFile(target, content, { mode: FILE_MODE });
|
|
10671
|
+
}
|
|
10672
|
+
const lock = {
|
|
10673
|
+
schema: SKILL_PACK_LOCK_SCHEMA,
|
|
10674
|
+
name: manifest.name,
|
|
10675
|
+
version: manifest.version,
|
|
10676
|
+
description: manifest.description,
|
|
10677
|
+
content_hash: manifest.contentHash,
|
|
10678
|
+
source,
|
|
10679
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10680
|
+
files: manifest.files.map((file) => ({
|
|
10681
|
+
path: file.path,
|
|
10682
|
+
sha256: file.contentHash,
|
|
10683
|
+
bytes: file.byteSize
|
|
10684
|
+
}))
|
|
10685
|
+
};
|
|
10686
|
+
await atomicWriteFile(path17.join(packRoot, SKILL_PACK_LOCK_FILENAME), `${JSON.stringify(lock, null, 2)}
|
|
10687
|
+
`, {
|
|
10688
|
+
mode: FILE_MODE
|
|
10689
|
+
});
|
|
10690
|
+
await appendAuditLine(options.dataDir, {
|
|
10691
|
+
ts: lock.installed_at,
|
|
10692
|
+
event: "skill-pack-installed",
|
|
10693
|
+
name: lock.name,
|
|
10694
|
+
version: lock.version,
|
|
10695
|
+
contentHash: lock.content_hash,
|
|
10696
|
+
source: lock.source,
|
|
10697
|
+
files: lock.files.length,
|
|
10698
|
+
bytes: totalBytes
|
|
10699
|
+
});
|
|
10700
|
+
return { name: manifest.name, lock, directory: revisionDir };
|
|
10701
|
+
}
|
|
10702
|
+
function isLockShaped(value) {
|
|
10703
|
+
const lock = value;
|
|
10704
|
+
return lock !== null && typeof lock === "object" && lock.schema === SKILL_PACK_LOCK_SCHEMA && typeof lock.name === "string" && typeof lock.content_hash === "string" && CONTENT_HASH_PATTERN.test(lock.content_hash) && Array.isArray(lock.files);
|
|
10705
|
+
}
|
|
10706
|
+
async function readLock(dataDir, name) {
|
|
10707
|
+
if (!isSkillPackPathSafe(name)) return void 0;
|
|
10708
|
+
const packRoot = path17.join(skillPacksRoot(dataDir), name);
|
|
10709
|
+
let raw;
|
|
10710
|
+
try {
|
|
10711
|
+
raw = await promises.readFile(path17.join(packRoot, SKILL_PACK_LOCK_FILENAME), "utf8");
|
|
10712
|
+
} catch (err) {
|
|
10713
|
+
if (err.code === "ENOENT") return void 0;
|
|
10714
|
+
throw err;
|
|
10715
|
+
}
|
|
10716
|
+
let parsed;
|
|
10717
|
+
try {
|
|
10718
|
+
parsed = JSON.parse(raw);
|
|
10719
|
+
} catch {
|
|
10720
|
+
return void 0;
|
|
10721
|
+
}
|
|
10722
|
+
if (!isLockShaped(parsed) || parsed.name !== name) return void 0;
|
|
10723
|
+
return {
|
|
10724
|
+
name,
|
|
10725
|
+
lock: parsed,
|
|
10726
|
+
directory: path17.join(packRoot, parsed.content_hash.slice("sha256:".length))
|
|
10727
|
+
};
|
|
10728
|
+
}
|
|
10729
|
+
async function listInstalledSkillPacks(dataDir) {
|
|
10730
|
+
let entries;
|
|
10731
|
+
try {
|
|
10732
|
+
entries = await promises.readdir(skillPacksRoot(dataDir), { withFileTypes: true });
|
|
10733
|
+
} catch (err) {
|
|
10734
|
+
if (err.code === "ENOENT") return [];
|
|
10735
|
+
throw err;
|
|
10736
|
+
}
|
|
10737
|
+
const packs = [];
|
|
10738
|
+
for (const entry of entries) {
|
|
10739
|
+
if (!entry.isDirectory()) continue;
|
|
10740
|
+
const installed = await readLock(dataDir, entry.name);
|
|
10741
|
+
if (installed !== void 0) packs.push(installed);
|
|
10742
|
+
}
|
|
10743
|
+
packs.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
10744
|
+
return packs;
|
|
10745
|
+
}
|
|
10746
|
+
async function projectSkillPack(dataDir, name, targetDir) {
|
|
10747
|
+
const installed = await readLock(dataDir, name);
|
|
10748
|
+
if (installed === void 0) {
|
|
10749
|
+
throw new SkillPackInstallError("store_unsafe", `no installed skill pack named ${JSON.stringify(name)}.`, {
|
|
10750
|
+
packName: name
|
|
10751
|
+
});
|
|
10752
|
+
}
|
|
10753
|
+
const copied = [];
|
|
10754
|
+
for (const file of installed.lock.files) {
|
|
10755
|
+
const sourcePath = resolveInside(installed.directory, file.path);
|
|
10756
|
+
await assertNotSymlink(sourcePath);
|
|
10757
|
+
let bytes;
|
|
10758
|
+
try {
|
|
10759
|
+
bytes = await promises.readFile(sourcePath);
|
|
10760
|
+
} catch (cause) {
|
|
10761
|
+
throw new SkillPackInstallError(
|
|
10762
|
+
"store_unsafe",
|
|
10763
|
+
`installed skill pack ${JSON.stringify(name)} is missing ${JSON.stringify(file.path)}.`,
|
|
10764
|
+
{ cause, packName: name }
|
|
10765
|
+
);
|
|
10766
|
+
}
|
|
10767
|
+
if (hashBytes(bytes) !== file.sha256 || bytes.byteLength !== file.bytes) {
|
|
10768
|
+
throw new SkillPackInstallError(
|
|
10769
|
+
"store_unsafe",
|
|
10770
|
+
`installed skill pack ${JSON.stringify(name)} no longer matches its lock at ${JSON.stringify(file.path)}.`,
|
|
10771
|
+
{ packName: name }
|
|
10772
|
+
);
|
|
10773
|
+
}
|
|
10774
|
+
const destination = resolveInside(targetDir, file.path);
|
|
10775
|
+
await promises.mkdir(path17.dirname(destination), { recursive: true, mode: DIR_MODE });
|
|
10776
|
+
await assertNotSymlink(destination);
|
|
10777
|
+
await atomicWriteFile(destination, bytes, { mode: FILE_MODE });
|
|
10778
|
+
copied.push(file.path);
|
|
10779
|
+
}
|
|
10780
|
+
await appendAuditLine(dataDir, {
|
|
10781
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10782
|
+
event: "skill-pack-projected",
|
|
10783
|
+
name: installed.name,
|
|
10784
|
+
contentHash: installed.lock.content_hash,
|
|
10785
|
+
files: copied.length
|
|
10786
|
+
});
|
|
10787
|
+
return {
|
|
10788
|
+
name: installed.name,
|
|
10789
|
+
contentHash: installed.lock.content_hash,
|
|
10790
|
+
targetDir: path17.resolve(targetDir),
|
|
10791
|
+
files: copied
|
|
10792
|
+
};
|
|
10793
|
+
}
|
|
9240
10794
|
var EMPTY_BODY = new Uint8Array();
|
|
9241
10795
|
var LARGE_SNAPSHOT_THRESHOLD_BYTES = 1024 * 1024;
|
|
9242
|
-
var DEVICE_PROOF_HEADER = "x-byok-device-proof";
|
|
9243
10796
|
var TruthMemoryClientError = class extends Error {
|
|
9244
10797
|
constructor(code, message, status) {
|
|
9245
10798
|
super(message);
|
|
@@ -9266,8 +10819,8 @@ var TruthMemoryClient = class {
|
|
|
9266
10819
|
#requestId;
|
|
9267
10820
|
#allowedObjectDownloadOrigins;
|
|
9268
10821
|
async listManifest(query = {}) {
|
|
9269
|
-
const
|
|
9270
|
-
const response = await this.#proofFetch(
|
|
10822
|
+
const path21 = manifestPath(query);
|
|
10823
|
+
const response = await this.#proofFetch(path21, {
|
|
9271
10824
|
method: "GET",
|
|
9272
10825
|
operation: "truth.list",
|
|
9273
10826
|
resource: "records",
|
|
@@ -9374,9 +10927,9 @@ var TruthMemoryClient = class {
|
|
|
9374
10927
|
}
|
|
9375
10928
|
async #write(kind, recordKey, requestId, payload, expectedPrimary, expectedSnapshots) {
|
|
9376
10929
|
assertDistinctExpectedWrites([expectedPrimary, ...expectedSnapshots]);
|
|
9377
|
-
const
|
|
10930
|
+
const path21 = recordPath(kind, recordKey);
|
|
9378
10931
|
const body = new TextEncoder().encode(JSON.stringify(payload));
|
|
9379
|
-
const response = await this.#proofFetch(
|
|
10932
|
+
const response = await this.#proofFetch(path21, {
|
|
9380
10933
|
method: "PUT",
|
|
9381
10934
|
operation: "truth.write",
|
|
9382
10935
|
resource: `${kind}/${recordKey}`,
|
|
@@ -9402,8 +10955,8 @@ var TruthMemoryClient = class {
|
|
|
9402
10955
|
};
|
|
9403
10956
|
}
|
|
9404
10957
|
async #readVerified(listed) {
|
|
9405
|
-
const
|
|
9406
|
-
const response = await this.#proofFetch(
|
|
10958
|
+
const path21 = recordPath(listed.kind, listed.recordKey);
|
|
10959
|
+
const response = await this.#proofFetch(path21, {
|
|
9407
10960
|
method: "GET",
|
|
9408
10961
|
operation: "truth.read",
|
|
9409
10962
|
resource: `${listed.kind}/${listed.recordKey}`,
|
|
@@ -9464,16 +11017,16 @@ var TruthMemoryClient = class {
|
|
|
9464
11017
|
}
|
|
9465
11018
|
return { ...listed, bytes };
|
|
9466
11019
|
}
|
|
9467
|
-
async #proofFetch(
|
|
11020
|
+
async #proofFetch(path21, request) {
|
|
9468
11021
|
const proof = await this.options.signer.sign({
|
|
9469
11022
|
method: request.method,
|
|
9470
|
-
path:
|
|
11023
|
+
path: path21,
|
|
9471
11024
|
operation: request.operation,
|
|
9472
11025
|
resource: request.resource,
|
|
9473
11026
|
requestId: request.requestId,
|
|
9474
11027
|
body: request.body
|
|
9475
11028
|
});
|
|
9476
|
-
const response = await this.#fetch(new URL(
|
|
11029
|
+
const response = await this.#fetch(new URL(path21, this.#base), {
|
|
9477
11030
|
method: request.method,
|
|
9478
11031
|
headers: {
|
|
9479
11032
|
...request.headers,
|
|
@@ -9484,7 +11037,7 @@ var TruthMemoryClient = class {
|
|
|
9484
11037
|
if (!response.ok) {
|
|
9485
11038
|
throw new TruthMemoryClientError(
|
|
9486
11039
|
"truth_http_failed",
|
|
9487
|
-
`truth request ${request.method} ${
|
|
11040
|
+
`truth request ${request.method} ${path21} failed with HTTP ${response.status}`,
|
|
9488
11041
|
response.status
|
|
9489
11042
|
);
|
|
9490
11043
|
}
|
|
@@ -9497,10 +11050,10 @@ function manifestPath(query) {
|
|
|
9497
11050
|
if (query.keyPrefix !== void 0) search.set("prefix", query.keyPrefix);
|
|
9498
11051
|
if (query.limit !== void 0) search.set("limit", String(query.limit));
|
|
9499
11052
|
const encoded = search.toString();
|
|
9500
|
-
return encoded.length === 0 ?
|
|
11053
|
+
return encoded.length === 0 ? BYOK_RECORDS_PATH : `${BYOK_RECORDS_PATH}?${encoded}`;
|
|
9501
11054
|
}
|
|
9502
11055
|
function recordPath(kind, recordKey) {
|
|
9503
|
-
return
|
|
11056
|
+
return byokRecordPath(kind, recordKey);
|
|
9504
11057
|
}
|
|
9505
11058
|
function prepareTransportBody(body) {
|
|
9506
11059
|
if (body.kind === "inline") {
|
|
@@ -9742,9 +11295,9 @@ function plistString(value) {
|
|
|
9742
11295
|
function generateLaunchdPlist(def) {
|
|
9743
11296
|
const { label, program, logDir } = def;
|
|
9744
11297
|
const args = [program.command, ...program.args];
|
|
9745
|
-
const cwd = program.cwd ??
|
|
9746
|
-
const outLog =
|
|
9747
|
-
const errLog =
|
|
11298
|
+
const cwd = program.cwd ?? os5.homedir();
|
|
11299
|
+
const outLog = path17.join(logDir, `${label}.out.log`);
|
|
11300
|
+
const errLog = path17.join(logDir, `${label}.err.log`);
|
|
9748
11301
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
9749
11302
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9750
11303
|
<plist version="1.0">
|
|
@@ -9776,8 +11329,8 @@ ${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
|
9776
11329
|
}
|
|
9777
11330
|
function createLaunchdLifecycle(def, deps = {}) {
|
|
9778
11331
|
const run = deps.run ?? defaultRunner;
|
|
9779
|
-
const
|
|
9780
|
-
const homedir = deps.homedir ?? (() =>
|
|
11332
|
+
const fs17 = deps.fs ?? promises;
|
|
11333
|
+
const homedir = deps.homedir ?? (() => os5.homedir());
|
|
9781
11334
|
const getuid = deps.getuid ?? (() => {
|
|
9782
11335
|
if (typeof process.getuid !== "function") {
|
|
9783
11336
|
throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
|
|
@@ -9785,12 +11338,12 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
9785
11338
|
return process.getuid();
|
|
9786
11339
|
});
|
|
9787
11340
|
const label = sanitizeServiceName(def.name);
|
|
9788
|
-
const plistPath = () =>
|
|
11341
|
+
const plistPath = () => path17.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
9789
11342
|
const domainTarget = () => `gui/${getuid()}`;
|
|
9790
11343
|
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
9791
11344
|
async function fileExists(p) {
|
|
9792
11345
|
try {
|
|
9793
|
-
await
|
|
11346
|
+
await fs17.stat(p);
|
|
9794
11347
|
return true;
|
|
9795
11348
|
} catch {
|
|
9796
11349
|
return false;
|
|
@@ -9798,9 +11351,9 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
9798
11351
|
}
|
|
9799
11352
|
async function writePlist(program) {
|
|
9800
11353
|
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
9801
|
-
await
|
|
9802
|
-
await
|
|
9803
|
-
await
|
|
11354
|
+
await fs17.mkdir(path17.dirname(plistPath()), { recursive: true });
|
|
11355
|
+
await fs17.mkdir(def.logDir, { recursive: true });
|
|
11356
|
+
await fs17.writeFile(plistPath(), xml, "utf8");
|
|
9804
11357
|
}
|
|
9805
11358
|
async function install(opts = {}) {
|
|
9806
11359
|
await writePlist(opts.program ?? def.program);
|
|
@@ -9811,7 +11364,7 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
9811
11364
|
}
|
|
9812
11365
|
async function uninstall() {
|
|
9813
11366
|
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
9814
|
-
await
|
|
11367
|
+
await fs17.rm(plistPath(), { force: true });
|
|
9815
11368
|
}
|
|
9816
11369
|
async function start() {
|
|
9817
11370
|
if (!await fileExists(plistPath())) {
|
|
@@ -9870,10 +11423,10 @@ function generateSystemdUnit(def) {
|
|
|
9870
11423
|
const { name, displayName, program, logDir } = def;
|
|
9871
11424
|
assertNoControlChars(name, "name");
|
|
9872
11425
|
assertNoControlChars(displayName, "displayName");
|
|
9873
|
-
const cwd = program.cwd ??
|
|
11426
|
+
const cwd = program.cwd ?? os5.homedir();
|
|
9874
11427
|
assertNoControlChars(cwd, "program.cwd");
|
|
9875
|
-
const outLog =
|
|
9876
|
-
const errLog =
|
|
11428
|
+
const outLog = path17.join(logDir, `${name}.out.log`);
|
|
11429
|
+
const errLog = path17.join(logDir, `${name}.err.log`);
|
|
9877
11430
|
assertNoControlChars(outLog, "logDir");
|
|
9878
11431
|
assertNoControlChars(errLog, "logDir");
|
|
9879
11432
|
const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
|
|
@@ -9895,14 +11448,14 @@ WantedBy=default.target
|
|
|
9895
11448
|
}
|
|
9896
11449
|
function createSystemdLifecycle(def, deps = {}) {
|
|
9897
11450
|
const run = deps.run ?? defaultRunner;
|
|
9898
|
-
const
|
|
9899
|
-
const homedir = deps.homedir ?? (() =>
|
|
11451
|
+
const fs17 = deps.fs ?? promises;
|
|
11452
|
+
const homedir = deps.homedir ?? (() => os5.homedir());
|
|
9900
11453
|
const name = sanitizeServiceName(def.name);
|
|
9901
11454
|
const unitName = `${name}.service`;
|
|
9902
|
-
const unitPath = () =>
|
|
11455
|
+
const unitPath = () => path17.join(homedir(), ".config", "systemd", "user", unitName);
|
|
9903
11456
|
async function fileExists(p) {
|
|
9904
11457
|
try {
|
|
9905
|
-
await
|
|
11458
|
+
await fs17.stat(p);
|
|
9906
11459
|
return true;
|
|
9907
11460
|
} catch {
|
|
9908
11461
|
return false;
|
|
@@ -9910,9 +11463,9 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
9910
11463
|
}
|
|
9911
11464
|
async function writeUnit(program) {
|
|
9912
11465
|
const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
9913
|
-
await
|
|
9914
|
-
await
|
|
9915
|
-
await
|
|
11466
|
+
await fs17.mkdir(path17.dirname(unitPath()), { recursive: true });
|
|
11467
|
+
await fs17.mkdir(def.logDir, { recursive: true });
|
|
11468
|
+
await fs17.writeFile(unitPath(), unit, "utf8");
|
|
9916
11469
|
}
|
|
9917
11470
|
async function install(opts = {}) {
|
|
9918
11471
|
await writeUnit(opts.program ?? def.program);
|
|
@@ -9921,7 +11474,7 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
9921
11474
|
}
|
|
9922
11475
|
async function uninstall() {
|
|
9923
11476
|
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
9924
|
-
await
|
|
11477
|
+
await fs17.rm(unitPath(), { force: true });
|
|
9925
11478
|
await run("systemctl", ["--user", "daemon-reload"]);
|
|
9926
11479
|
}
|
|
9927
11480
|
async function start() {
|
|
@@ -9984,7 +11537,7 @@ ${argXml}${cwdXml}
|
|
|
9984
11537
|
}
|
|
9985
11538
|
function createWinswLifecycle(def, deps = {}) {
|
|
9986
11539
|
const run = deps.run ?? defaultRunner;
|
|
9987
|
-
const
|
|
11540
|
+
const fs17 = deps.fs ?? promises;
|
|
9988
11541
|
const windows = def.windows;
|
|
9989
11542
|
if (!windows) {
|
|
9990
11543
|
throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
|
|
@@ -9992,11 +11545,11 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
9992
11545
|
const winswBin = windows.winswBin;
|
|
9993
11546
|
const id = sanitizeServiceName(def.name);
|
|
9994
11547
|
const installDir = windows.installDir ?? def.logDir;
|
|
9995
|
-
const exePath =
|
|
9996
|
-
const xmlPath =
|
|
11548
|
+
const exePath = path17.join(installDir, `${id}.exe`);
|
|
11549
|
+
const xmlPath = path17.join(installDir, `${id}.xml`);
|
|
9997
11550
|
async function fileExists(p) {
|
|
9998
11551
|
try {
|
|
9999
|
-
await
|
|
11552
|
+
await fs17.stat(p);
|
|
10000
11553
|
return true;
|
|
10001
11554
|
} catch {
|
|
10002
11555
|
return false;
|
|
@@ -10004,10 +11557,10 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
10004
11557
|
}
|
|
10005
11558
|
async function writeFiles(program) {
|
|
10006
11559
|
const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
10007
|
-
await
|
|
10008
|
-
await
|
|
10009
|
-
await
|
|
10010
|
-
await
|
|
11560
|
+
await fs17.mkdir(installDir, { recursive: true });
|
|
11561
|
+
await fs17.mkdir(def.logDir, { recursive: true });
|
|
11562
|
+
await fs17.copyFile(winswBin, exePath);
|
|
11563
|
+
await fs17.writeFile(xmlPath, xml, "utf8");
|
|
10011
11564
|
}
|
|
10012
11565
|
async function install(opts = {}) {
|
|
10013
11566
|
await writeFiles(opts.program ?? def.program);
|
|
@@ -10017,8 +11570,8 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
10017
11570
|
async function uninstall() {
|
|
10018
11571
|
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
|
|
10019
11572
|
await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
|
|
10020
|
-
await
|
|
10021
|
-
await
|
|
11573
|
+
await fs17.rm(exePath, { force: true });
|
|
11574
|
+
await fs17.rm(xmlPath, { force: true });
|
|
10022
11575
|
}
|
|
10023
11576
|
async function start() {
|
|
10024
11577
|
if (!await fileExists(xmlPath)) {
|
|
@@ -10061,6 +11614,6 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
10061
11614
|
}
|
|
10062
11615
|
}
|
|
10063
11616
|
|
|
10064
|
-
export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, SecureDirHardeningError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, isGitWorkspaceConfig, journalHash, nodeAgentProgram, prependGitWorkspaceGuidance, resolveLocalStoragePolicy, sanitizeServiceName };
|
|
11617
|
+
export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isGitWorkspaceConfig, journalHash, listInstalledSkillPacks, nodeAgentProgram, prependGitWorkspaceGuidance, projectSkillPack, requestDeviceAssertion, resolveLocalStoragePolicy, sanitizeServiceName, skillPacksRoot };
|
|
10065
11618
|
//# sourceMappingURL=index.js.map
|
|
10066
11619
|
//# sourceMappingURL=index.js.map
|