@byok-sdk/client 0.1.1 → 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 +46 -6
- package/dist/adapters/claude/claude-adapter.d.ts +3 -0
- package/dist/adapters/claude/resolve-bin.d.ts +2 -2
- package/dist/adapters/codex/codex-adapter.d.ts +5 -3
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +293 -100
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/events.d.ts +10 -37
- package/dist/adapters/pi/permission-mapping.d.ts +4 -20
- package/dist/adapters/pi/pi-adapter.d.ts +22 -0
- package/dist/adapters/pi/resolve-bin.d.ts +18 -14
- package/dist/adapters/pi/rpc-client.d.ts +1 -4
- package/dist/adapters/provider-credential-environment.d.ts +18 -0
- package/dist/bin/byok-agent.js +1839 -838
- 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 +1872 -284
- package/dist/index.js.map +1 -1
- package/dist/lifecycle/create-service-lifecycle.d.ts +2 -2
- package/dist/types.d.ts +29 -0
- package/package.json +6 -5
package/dist/bin/byok-agent.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile, spawn, spawnSync } from 'child_process';
|
|
3
|
-
import { randomUUID, createHash, randomBytes, timingSafeEqual, createPrivateKey, generateKeyPairSync, sign
|
|
4
|
-
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, realpathSync, mkdirSync,
|
|
5
|
-
import
|
|
3
|
+
import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, createPrivateKey, generateKeyPairSync, sign } from 'crypto';
|
|
4
|
+
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
|
|
5
|
+
import path20, { isAbsolute, join } from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
|
-
import {
|
|
7
|
+
import { DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
8
|
+
import { TASK_STATES, ToolsetIdSchema, BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, createEnvelope, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
8
9
|
import { promisify } from 'util';
|
|
9
10
|
import { fileURLToPath } from 'url';
|
|
10
11
|
import 'readline';
|
|
@@ -72,7 +73,7 @@ function gitEnvironment(readOnly) {
|
|
|
72
73
|
return env;
|
|
73
74
|
}
|
|
74
75
|
function stableGitWorkspaceOwnerId(storeDir, productId) {
|
|
75
|
-
const identity = `${
|
|
76
|
+
const identity = `${path20.resolve(storeDir)}\\0${productId}`;
|
|
76
77
|
return `store-product:${createHash("sha256").update(identity).digest("hex")}`;
|
|
77
78
|
}
|
|
78
79
|
var GUIDANCE = [
|
|
@@ -84,11 +85,11 @@ var GUIDANCE = [
|
|
|
84
85
|
"Leave incomplete work visible for recovery."
|
|
85
86
|
].join("\n");
|
|
86
87
|
function canonical(value) {
|
|
87
|
-
return
|
|
88
|
+
return path20.resolve(value);
|
|
88
89
|
}
|
|
89
90
|
function isContained(root, candidate) {
|
|
90
|
-
const relative =
|
|
91
|
-
return relative === "" || !relative.startsWith(`..${
|
|
91
|
+
const relative = path20.relative(root, candidate);
|
|
92
|
+
return relative === "" || !relative.startsWith(`..${path20.sep}`) && !path20.isAbsolute(relative);
|
|
92
93
|
}
|
|
93
94
|
function bounded(value, max) {
|
|
94
95
|
return Buffer.byteLength(value, "utf8") <= max ? value : value.slice(0, max);
|
|
@@ -193,7 +194,7 @@ var GitWorkspaceManager = class {
|
|
|
193
194
|
await this.ensureOwnerMarker();
|
|
194
195
|
}
|
|
195
196
|
async ensureOwnerMarker() {
|
|
196
|
-
const markerPath =
|
|
197
|
+
const markerPath = path20.join(this.workspaceRoot, OWNER_MARKER);
|
|
197
198
|
let existing;
|
|
198
199
|
try {
|
|
199
200
|
existing = JSON.parse(await promises.readFile(markerPath, "utf8"));
|
|
@@ -352,7 +353,7 @@ ${instruction}`;
|
|
|
352
353
|
if (error instanceof GitWorkspaceError || code !== "ENOENT" && code !== "ENOTDIR") {
|
|
353
354
|
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
354
355
|
}
|
|
355
|
-
const parent =
|
|
356
|
+
const parent = path20.dirname(current);
|
|
356
357
|
if (parent === current) {
|
|
357
358
|
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
358
359
|
}
|
|
@@ -410,7 +411,7 @@ async function atomicWriteFile(filePath, data, options = {}) {
|
|
|
410
411
|
await target.close();
|
|
411
412
|
}
|
|
412
413
|
if (process.platform !== "win32") {
|
|
413
|
-
const directory = await promises.open(
|
|
414
|
+
const directory = await promises.open(path20.dirname(filePath), "r");
|
|
414
415
|
try {
|
|
415
416
|
await directory.sync();
|
|
416
417
|
} finally {
|
|
@@ -571,7 +572,7 @@ function isProtected(record) {
|
|
|
571
572
|
var GitWorkspaceStore = class {
|
|
572
573
|
constructor(storeDir, options = {}) {
|
|
573
574
|
this.storeDir = storeDir;
|
|
574
|
-
this.filePath =
|
|
575
|
+
this.filePath = path20.join(storeDir, FILE_NAME);
|
|
575
576
|
this.maxRecords = Math.max(1, Math.floor(options.maxRecords ?? MAX_RECORDS));
|
|
576
577
|
}
|
|
577
578
|
storeDir;
|
|
@@ -701,19 +702,50 @@ var GitWorkspaceStore = class {
|
|
|
701
702
|
await atomicWriteFile(this.filePath, JSON.stringify(ledger, null, 2), { mode: 384 });
|
|
702
703
|
}
|
|
703
704
|
};
|
|
704
|
-
|
|
705
|
-
|
|
705
|
+
var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
706
|
+
function readPackageJson(dir) {
|
|
707
|
+
const candidate = path20.join(dir, "package.json");
|
|
708
|
+
if (!existsSync(candidate)) return void 0;
|
|
709
|
+
try {
|
|
710
|
+
return JSON.parse(readFileSync(candidate, "utf8"));
|
|
711
|
+
} catch {
|
|
712
|
+
return void 0;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
706
715
|
function resolvePiBin() {
|
|
707
716
|
const override = process.env.BYOK_PI_BIN;
|
|
708
717
|
if (override) {
|
|
709
|
-
return { command: override, source: "
|
|
718
|
+
return { command: override, source: "env" };
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
|
|
722
|
+
let dir = path20.dirname(fileURLToPath(mainEntryUrl));
|
|
723
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
724
|
+
const pkg = readPackageJson(dir);
|
|
725
|
+
if (pkg?.name === PI_PACKAGE_NAME) {
|
|
726
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
|
|
727
|
+
if (binRel) {
|
|
728
|
+
return { command: path20.join(dir, binRel), source: "package" };
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
const parent = path20.dirname(dir);
|
|
733
|
+
if (parent === dir) break;
|
|
734
|
+
dir = parent;
|
|
735
|
+
}
|
|
736
|
+
} catch (cause) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Required ${PI_PACKAGE_NAME} could not be resolved; install @byok-sdk/client dependencies or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`,
|
|
739
|
+
{ cause }
|
|
740
|
+
);
|
|
710
741
|
}
|
|
711
|
-
|
|
742
|
+
throw new Error(
|
|
743
|
+
`Required ${PI_PACKAGE_NAME} does not expose the pi CLI; reinstall the pinned dependency or set BYOK_PI_BIN to a Node 22.19+ pi sidecar`
|
|
744
|
+
);
|
|
712
745
|
}
|
|
713
746
|
|
|
714
747
|
// src/adapters/pi/permission-mapping.ts
|
|
715
748
|
var READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
716
|
-
var DEFAULT_ACTIVE_TOOLS = ["read", "bash", "edit", "write"];
|
|
717
749
|
function mapPermissionPolicyToPiArgs(policy) {
|
|
718
750
|
if (policy.network === false) {
|
|
719
751
|
return {
|
|
@@ -732,22 +764,18 @@ function mapPermissionPolicyToPiArgs(policy) {
|
|
|
732
764
|
const denyTools = policy.denyTools ?? [];
|
|
733
765
|
if (policy.mode === "readonly") {
|
|
734
766
|
const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS.includes(tool)) : [...READONLY_TOOLS];
|
|
735
|
-
|
|
736
|
-
return {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const effective = subtractDenied(base, denyTools);
|
|
741
|
-
return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
|
|
767
|
+
if (base.length === 0) return { ok: true, args: ["--no-tools"] };
|
|
768
|
+
return {
|
|
769
|
+
ok: true,
|
|
770
|
+
args: ["--tools", base.join(","), ...denyTools.length > 0 ? ["--exclude-tools", denyTools.join(",")] : []]
|
|
771
|
+
};
|
|
742
772
|
}
|
|
773
|
+
const args = [];
|
|
743
774
|
if (policy.allowTools && policy.allowTools.length > 0) {
|
|
744
|
-
|
|
775
|
+
args.push("--tools", policy.allowTools.join(","));
|
|
745
776
|
}
|
|
746
|
-
|
|
747
|
-
}
|
|
748
|
-
function subtractDenied(tools, denyTools) {
|
|
749
|
-
const denied = new Set(denyTools);
|
|
750
|
-
return tools.filter((tool) => !denied.has(tool));
|
|
777
|
+
if (denyTools.length > 0) args.push("--exclude-tools", denyTools.join(","));
|
|
778
|
+
return { ok: true, args };
|
|
751
779
|
}
|
|
752
780
|
|
|
753
781
|
// src/adapters/pi/events.ts
|
|
@@ -772,7 +800,7 @@ function mapPiMessageToAgentEvent(msg) {
|
|
|
772
800
|
output: { result: msg.result, isError: msg.isError === true }
|
|
773
801
|
};
|
|
774
802
|
}
|
|
775
|
-
case "
|
|
803
|
+
case "agent_settled":
|
|
776
804
|
return { type: "turn_end" };
|
|
777
805
|
/**
|
|
778
806
|
* `artifact` is NOT a real pi RPC message — pi's own `write` tool only
|
|
@@ -808,16 +836,22 @@ function mapPiMessageToAgentEvent(msg) {
|
|
|
808
836
|
// `recordUnmappedFrame`) can tell "known, expected, silently ignored"
|
|
809
837
|
// apart from "genuinely never seen before" (falls to `default` below).
|
|
810
838
|
case "agent_start":
|
|
839
|
+
case "agent_end":
|
|
840
|
+
// one low-level run; `agent_settled` is BYOK completion
|
|
811
841
|
case "turn_start":
|
|
812
842
|
case "turn_end":
|
|
813
|
-
// pi's own per-LLM-turn boundary, not ours
|
|
843
|
+
// pi's own per-LLM-turn boundary, not ours
|
|
814
844
|
case "message_start":
|
|
815
845
|
case "message_end":
|
|
846
|
+
case "bash_execution_update":
|
|
816
847
|
case "tool_execution_update":
|
|
817
848
|
case "queue_update":
|
|
818
849
|
case "compaction_start":
|
|
819
850
|
case "compaction_end":
|
|
820
851
|
case "auto_retry_start":
|
|
852
|
+
case "summarization_retry_scheduled":
|
|
853
|
+
case "summarization_retry_attempt_start":
|
|
854
|
+
case "summarization_retry_finished":
|
|
821
855
|
case "session_info_changed":
|
|
822
856
|
case "thinking_level_changed":
|
|
823
857
|
return void 0;
|
|
@@ -827,15 +861,20 @@ function mapPiMessageToAgentEvent(msg) {
|
|
|
827
861
|
}
|
|
828
862
|
var ROUTINE_PI_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
829
863
|
"agent_start",
|
|
864
|
+
"agent_end",
|
|
830
865
|
"turn_start",
|
|
831
866
|
"turn_end",
|
|
832
867
|
"message_start",
|
|
833
868
|
"message_end",
|
|
869
|
+
"bash_execution_update",
|
|
834
870
|
"tool_execution_update",
|
|
835
871
|
"queue_update",
|
|
836
872
|
"compaction_start",
|
|
837
873
|
"compaction_end",
|
|
838
874
|
"auto_retry_start",
|
|
875
|
+
"summarization_retry_scheduled",
|
|
876
|
+
"summarization_retry_attempt_start",
|
|
877
|
+
"summarization_retry_finished",
|
|
839
878
|
"session_info_changed",
|
|
840
879
|
"thinking_level_changed"
|
|
841
880
|
]);
|
|
@@ -969,10 +1008,7 @@ var PiRpcClient = class {
|
|
|
969
1008
|
* traffic. Logs once per distinct type (not per occurrence, so a
|
|
970
1009
|
* repeating unmapped type can't spam stdout); the running tally is also
|
|
971
1010
|
* folded into this client's exit-time error message (`buildExitError`) so
|
|
972
|
-
* a post-mortem on a failed/hung task has it without
|
|
973
|
-
* scraping. This is the exact mechanism that would have turned this
|
|
974
|
-
* task's root-cause hang (`agent_end` arriving with no mapping) into a
|
|
975
|
-
* one-line, immediate warning instead of a silent stall.
|
|
1011
|
+
* a post-mortem on a failed/hung task has it without separate log scraping.
|
|
976
1012
|
*/
|
|
977
1013
|
recordUnmappedFrame(type) {
|
|
978
1014
|
const next = (this.unmappedFrameCounts.get(type) ?? 0) + 1;
|
|
@@ -1083,13 +1119,8 @@ var PiRpcClient = class {
|
|
|
1083
1119
|
}
|
|
1084
1120
|
};
|
|
1085
1121
|
|
|
1086
|
-
// src/adapters/
|
|
1087
|
-
var
|
|
1088
|
-
var DETECT_TIMEOUT_MS = 5e3;
|
|
1089
|
-
function errorMessage(err) {
|
|
1090
|
-
return err instanceof Error ? err.message : String(err);
|
|
1091
|
-
}
|
|
1092
|
-
var KNOWN_PROVIDER_ENV_VARS = [
|
|
1122
|
+
// src/adapters/provider-credential-environment.ts
|
|
1123
|
+
var PROVIDER_CREDENTIAL_ENV_NAMES = [
|
|
1093
1124
|
"ANTHROPIC_API_KEY",
|
|
1094
1125
|
"ANTHROPIC_OAUTH_TOKEN",
|
|
1095
1126
|
"OPENAI_API_KEY",
|
|
@@ -1100,24 +1131,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
|
|
|
1100
1131
|
"MISTRAL_API_KEY",
|
|
1101
1132
|
"OPENROUTER_API_KEY",
|
|
1102
1133
|
"XAI_API_KEY",
|
|
1103
|
-
// Confirmed against the installed pi's own docs/providers.md ("ZAI |
|
|
1104
|
-
// `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
|
|
1105
|
-
// during this task's acceptance run — omitting it made `authPresent`
|
|
1106
|
-
// silently false for a perfectly valid, working z.ai/GLM setup.
|
|
1107
1134
|
"ZAI_API_KEY"
|
|
1108
1135
|
];
|
|
1136
|
+
var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
|
|
1137
|
+
...PROVIDER_CREDENTIAL_ENV_NAMES,
|
|
1138
|
+
"ANT_LING_API_KEY",
|
|
1139
|
+
"NVIDIA_API_KEY",
|
|
1140
|
+
"CEREBRAS_API_KEY",
|
|
1141
|
+
"CLOUDFLARE_API_KEY",
|
|
1142
|
+
"AI_GATEWAY_API_KEY",
|
|
1143
|
+
"ZAI_CODING_CN_API_KEY",
|
|
1144
|
+
"OPENCODE_API_KEY",
|
|
1145
|
+
"RADIUS_API_KEY",
|
|
1146
|
+
"FIREWORKS_API_KEY",
|
|
1147
|
+
"TOGETHER_API_KEY",
|
|
1148
|
+
"BASETEN_API_KEY",
|
|
1149
|
+
"KIMI_API_KEY",
|
|
1150
|
+
"MINIMAX_API_KEY",
|
|
1151
|
+
"MINIMAX_CN_API_KEY",
|
|
1152
|
+
"QWEN_TOKEN_PLAN_API_KEY",
|
|
1153
|
+
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
1154
|
+
"XIAOMI_API_KEY",
|
|
1155
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
1156
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
1157
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
1158
|
+
"AWS_ACCESS_KEY_ID",
|
|
1159
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1160
|
+
"AWS_SESSION_TOKEN",
|
|
1161
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
1162
|
+
// Reserved by the keys-owned Pi projection. It must never be inherited
|
|
1163
|
+
// from the daemon; the launcher deletes any ambient copy and injects only
|
|
1164
|
+
// the exact credential it just resolved from OS custody.
|
|
1165
|
+
"PI_PROVIDER_API_KEY"
|
|
1166
|
+
];
|
|
1167
|
+
function withoutProviderCredentials(env) {
|
|
1168
|
+
const sanitized = { ...env };
|
|
1169
|
+
for (const name of PROVIDER_CREDENTIAL_ENV_DENY_NAMES) {
|
|
1170
|
+
delete sanitized[name];
|
|
1171
|
+
}
|
|
1172
|
+
return sanitized;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/adapters/pi/pi-adapter.ts
|
|
1176
|
+
var execFileAsync = promisify(execFile);
|
|
1177
|
+
var DETECT_TIMEOUT_MS = 5e3;
|
|
1178
|
+
function errorMessage(err) {
|
|
1179
|
+
return err instanceof Error ? err.message : String(err);
|
|
1180
|
+
}
|
|
1109
1181
|
var PiAdapter = class {
|
|
1110
1182
|
constructor(options = {}) {
|
|
1111
1183
|
this.options = options;
|
|
1112
1184
|
}
|
|
1113
1185
|
options;
|
|
1114
1186
|
id = "pi";
|
|
1187
|
+
supportsDispatchSelection = true;
|
|
1115
1188
|
async detect() {
|
|
1116
|
-
const bin = this.resolveBin();
|
|
1117
1189
|
try {
|
|
1190
|
+
const bin = this.resolveBin();
|
|
1118
1191
|
const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
|
|
1119
1192
|
const version = stdout.trim() || stderr.trim();
|
|
1120
|
-
const authPresent =
|
|
1193
|
+
const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
|
|
1121
1194
|
return { present: true, version, authPresent };
|
|
1122
1195
|
} catch {
|
|
1123
1196
|
return { present: false };
|
|
@@ -1136,7 +1209,7 @@ var PiAdapter = class {
|
|
|
1136
1209
|
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
1137
1210
|
*/
|
|
1138
1211
|
environmentRequirements() {
|
|
1139
|
-
return { credentialNames:
|
|
1212
|
+
return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
|
|
1140
1213
|
}
|
|
1141
1214
|
async start(task, ctx) {
|
|
1142
1215
|
if (typeof task.instruction !== "string") {
|
|
@@ -1148,12 +1221,45 @@ var PiAdapter = class {
|
|
|
1148
1221
|
}
|
|
1149
1222
|
const bin = this.resolveBin();
|
|
1150
1223
|
const resumeSessionId = task.sessionRef;
|
|
1151
|
-
const
|
|
1224
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
1225
|
+
const selection = task.dispatchSelection;
|
|
1226
|
+
let command = bin.command;
|
|
1227
|
+
let args = piArgs;
|
|
1228
|
+
if (selection !== void 0) {
|
|
1229
|
+
if (selection.lane !== "byok" || selection.runtimeId !== "pi") {
|
|
1230
|
+
throw new PolicyUnsupportedError(
|
|
1231
|
+
`pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
const launcher = this.options.byokLauncher;
|
|
1235
|
+
if (launcher === void 0) {
|
|
1236
|
+
throw new PolicyUnsupportedError(
|
|
1237
|
+
"pi BYOK selection requires a configured credential-custody launcher"
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
command = launcher.command;
|
|
1241
|
+
args = [
|
|
1242
|
+
...launcher.args ?? [],
|
|
1243
|
+
"--pi-bin",
|
|
1244
|
+
bin.command,
|
|
1245
|
+
"--profile-db",
|
|
1246
|
+
launcher.profileDbPath,
|
|
1247
|
+
"--session-dir",
|
|
1248
|
+
launcher.sessionDir,
|
|
1249
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
1250
|
+
"--provider",
|
|
1251
|
+
selection.providerId,
|
|
1252
|
+
"--model",
|
|
1253
|
+
selection.modelId,
|
|
1254
|
+
"--",
|
|
1255
|
+
...piArgs
|
|
1256
|
+
];
|
|
1257
|
+
}
|
|
1152
1258
|
const rpc = new PiRpcClient({
|
|
1153
|
-
command
|
|
1259
|
+
command,
|
|
1154
1260
|
args,
|
|
1155
1261
|
cwd: ctx.workspaceDir,
|
|
1156
|
-
env: ctx.env,
|
|
1262
|
+
env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
|
|
1157
1263
|
spawnFn: this.options.spawnFn
|
|
1158
1264
|
});
|
|
1159
1265
|
const response = await rpc.send({ type: "prompt", message: task.instruction });
|
|
@@ -1172,7 +1278,7 @@ var PiAdapter = class {
|
|
|
1172
1278
|
throw err;
|
|
1173
1279
|
}
|
|
1174
1280
|
}
|
|
1175
|
-
return new PiSession(sessionRef, rpc);
|
|
1281
|
+
return new PiSession(sessionRef, rpc, selection);
|
|
1176
1282
|
}
|
|
1177
1283
|
resolveBin() {
|
|
1178
1284
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
@@ -1200,12 +1306,14 @@ async function resolveFreshSessionId(rpc) {
|
|
|
1200
1306
|
);
|
|
1201
1307
|
}
|
|
1202
1308
|
var PiSession = class {
|
|
1203
|
-
constructor(sessionRef, rpc) {
|
|
1309
|
+
constructor(sessionRef, rpc, selection) {
|
|
1204
1310
|
this.sessionRef = sessionRef;
|
|
1205
1311
|
this.rpc = rpc;
|
|
1312
|
+
this.selection = selection;
|
|
1206
1313
|
}
|
|
1207
1314
|
sessionRef;
|
|
1208
1315
|
rpc;
|
|
1316
|
+
selection;
|
|
1209
1317
|
get events() {
|
|
1210
1318
|
const rpc = this.rpc;
|
|
1211
1319
|
return {
|
|
@@ -1234,6 +1342,12 @@ var PiSession = class {
|
|
|
1234
1342
|
if (typeof task.instruction !== "string") {
|
|
1235
1343
|
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1236
1344
|
}
|
|
1345
|
+
const requestedSelection = task.dispatchSelection;
|
|
1346
|
+
if (requestedSelection !== void 0 && (this.selection?.lane !== "byok" || requestedSelection.lane !== "byok" || requestedSelection.runtimeId !== "pi" || requestedSelection.providerId !== this.selection.providerId || requestedSelection.modelId !== this.selection.modelId)) {
|
|
1347
|
+
throw new PolicyUnsupportedError(
|
|
1348
|
+
"pi persistent session cannot change its authoritative BYOK provider/model selection"
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1237
1351
|
await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
|
|
1238
1352
|
}
|
|
1239
1353
|
async interrupt() {
|
|
@@ -1260,7 +1374,7 @@ function resolveApprovalMcpBin() {
|
|
|
1260
1374
|
if (override) {
|
|
1261
1375
|
return { command: override, args: [], source: "env" };
|
|
1262
1376
|
}
|
|
1263
|
-
const distBin =
|
|
1377
|
+
const distBin = path20.join(path20.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
|
|
1264
1378
|
return { command: process.execPath, args: [distBin], source: "dist" };
|
|
1265
1379
|
}
|
|
1266
1380
|
|
|
@@ -1291,7 +1405,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
|
|
|
1291
1405
|
}
|
|
1292
1406
|
if (policy.mode === "readonly") {
|
|
1293
1407
|
const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS2.includes(tool)) : [...READONLY_TOOLS2];
|
|
1294
|
-
const effective =
|
|
1408
|
+
const effective = subtractDenied(base, denyTools);
|
|
1295
1409
|
return { ok: true, args: ["--permission-mode", "default", "--tools", effective.join(",")] };
|
|
1296
1410
|
}
|
|
1297
1411
|
if (denyTools.length > 0) {
|
|
@@ -1308,7 +1422,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
|
|
|
1308
1422
|
}
|
|
1309
1423
|
return { ok: true, args };
|
|
1310
1424
|
}
|
|
1311
|
-
function
|
|
1425
|
+
function subtractDenied(tools, denyTools) {
|
|
1312
1426
|
const denied = new Set(denyTools);
|
|
1313
1427
|
return tools.filter((tool) => !denied.has(tool));
|
|
1314
1428
|
}
|
|
@@ -1340,7 +1454,7 @@ var EXTENSION_CONTENT_TYPES = {
|
|
|
1340
1454
|
".yml": "application/yaml"
|
|
1341
1455
|
};
|
|
1342
1456
|
function guessContentType(filePath) {
|
|
1343
|
-
const ext =
|
|
1457
|
+
const ext = path20.extname(filePath).toLowerCase();
|
|
1344
1458
|
return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
1345
1459
|
}
|
|
1346
1460
|
function mapAssistant(msg, correlation) {
|
|
@@ -1412,11 +1526,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
|
|
|
1412
1526
|
const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
|
|
1413
1527
|
if (!filePath) return void 0;
|
|
1414
1528
|
const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
|
|
1415
|
-
const fileDir =
|
|
1529
|
+
const fileDir = path20.dirname(filePath);
|
|
1416
1530
|
const realFileDir = tryRealpath(fileDir) ?? fileDir;
|
|
1417
|
-
const realFilePath =
|
|
1418
|
-
const relative =
|
|
1419
|
-
if (relative === "" || relative.startsWith("..") ||
|
|
1531
|
+
const realFilePath = path20.join(realFileDir, path20.basename(filePath));
|
|
1532
|
+
const relative = path20.relative(realWorkspaceDir, realFilePath);
|
|
1533
|
+
if (relative === "" || relative.startsWith("..") || path20.isAbsolute(relative)) {
|
|
1420
1534
|
return void 0;
|
|
1421
1535
|
}
|
|
1422
1536
|
return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
|
|
@@ -1658,7 +1772,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1658
1772
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1659
1773
|
var execFileAsync2 = promisify(execFile);
|
|
1660
1774
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
1661
|
-
async function
|
|
1775
|
+
async function cleanupMcpConfigDir(dir) {
|
|
1662
1776
|
if (!dir) return;
|
|
1663
1777
|
await promises.rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1664
1778
|
});
|
|
@@ -1668,6 +1782,7 @@ var ClaudeAdapter = class {
|
|
|
1668
1782
|
this.options = options;
|
|
1669
1783
|
}
|
|
1670
1784
|
options;
|
|
1785
|
+
supportsDispatchSelection = true;
|
|
1671
1786
|
id = "claude";
|
|
1672
1787
|
async detect() {
|
|
1673
1788
|
const bin = this.resolveBin();
|
|
@@ -1681,7 +1796,13 @@ var ClaudeAdapter = class {
|
|
|
1681
1796
|
}
|
|
1682
1797
|
}
|
|
1683
1798
|
capabilities() {
|
|
1684
|
-
return {
|
|
1799
|
+
return {
|
|
1800
|
+
steer: false,
|
|
1801
|
+
resume: true,
|
|
1802
|
+
approvalInteractive: true,
|
|
1803
|
+
mcpToolsets: true,
|
|
1804
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
1805
|
+
};
|
|
1685
1806
|
}
|
|
1686
1807
|
/**
|
|
1687
1808
|
* M5: deliberate product-boundary decision, not an oversight — byok's
|
|
@@ -1707,42 +1828,51 @@ var ClaudeAdapter = class {
|
|
|
1707
1828
|
if (!mapping.ok) {
|
|
1708
1829
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
1709
1830
|
}
|
|
1710
|
-
|
|
1831
|
+
const modelId = subscriptionModel(task, "claude");
|
|
1832
|
+
let mcpConfigDir;
|
|
1833
|
+
const taskMcpServers = ctx.mcpServers ?? {};
|
|
1834
|
+
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
1711
1835
|
if (mapping.needsApprovalMcp) {
|
|
1712
1836
|
if (!ctx.approvalChannel) {
|
|
1713
1837
|
throw new PolicyUnsupportedError(
|
|
1714
1838
|
'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
|
|
1715
1839
|
);
|
|
1716
1840
|
}
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1841
|
+
if (Object.prototype.hasOwnProperty.call(taskMcpServers, APPROVAL_MCP_SERVER_NAME)) {
|
|
1842
|
+
throw new PolicyUnsupportedError(
|
|
1843
|
+
`MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (needsMcpConfig) {
|
|
1848
|
+
mcpConfigDir = await promises.mkdtemp(path20.join(os.tmpdir(), "byok-mcp-"));
|
|
1849
|
+
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
1720
1850
|
});
|
|
1721
|
-
const mcpConfigPath =
|
|
1722
|
-
const
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1851
|
+
const mcpConfigPath = path20.join(mcpConfigDir, "mcp-config.json");
|
|
1852
|
+
const mcpServers = { ...taskMcpServers };
|
|
1853
|
+
if (mapping.needsApprovalMcp) {
|
|
1854
|
+
const approvalChannel = ctx.approvalChannel;
|
|
1855
|
+
if (!approvalChannel) throw new Error("unreachable: approval channel checked above");
|
|
1856
|
+
const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
1857
|
+
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
1858
|
+
command: approvalMcpBin.command,
|
|
1859
|
+
args: approvalMcpBin.args,
|
|
1860
|
+
env: {
|
|
1861
|
+
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
1862
|
+
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
1863
|
+
BYOK_TASK_ID: approvalChannel.taskId,
|
|
1864
|
+
BYOK_APPROVAL_TIMEOUT_MS: String(approvalChannel.timeoutMs)
|
|
1733
1865
|
}
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
await promises.writeFile(mcpConfigPath, JSON.stringify(
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
1737
1869
|
mapping.args = [
|
|
1738
1870
|
...mapping.args,
|
|
1739
|
-
"--permission-prompt-tool",
|
|
1740
|
-
`mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
|
|
1871
|
+
...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
|
|
1741
1872
|
"--mcp-config",
|
|
1742
1873
|
mcpConfigPath,
|
|
1743
|
-
//
|
|
1744
|
-
//
|
|
1745
|
-
// the only MCP server this invocation should ever see.
|
|
1874
|
+
// The generated file is the complete task-scoped MCP authority.
|
|
1875
|
+
// Never merge ambient user/project MCP configuration into it.
|
|
1746
1876
|
"--strict-mcp-config"
|
|
1747
1877
|
];
|
|
1748
1878
|
}
|
|
@@ -1759,6 +1889,7 @@ var ClaudeAdapter = class {
|
|
|
1759
1889
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1760
1890
|
// --verbose", before spawning any model call.
|
|
1761
1891
|
"--verbose",
|
|
1892
|
+
...modelId ? ["--model", modelId] : [],
|
|
1762
1893
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1763
1894
|
...mapping.args
|
|
1764
1895
|
];
|
|
@@ -1766,7 +1897,7 @@ var ClaudeAdapter = class {
|
|
|
1766
1897
|
command: bin.command,
|
|
1767
1898
|
args,
|
|
1768
1899
|
cwd: ctx.workspaceDir,
|
|
1769
|
-
env: ctx.env,
|
|
1900
|
+
env: withoutProviderCredentials(ctx.env),
|
|
1770
1901
|
spawnFn: this.options.spawnFn
|
|
1771
1902
|
});
|
|
1772
1903
|
client.writeUserMessage(task.instruction);
|
|
@@ -1775,17 +1906,24 @@ var ClaudeAdapter = class {
|
|
|
1775
1906
|
sessionRef = await client.waitForInit();
|
|
1776
1907
|
} catch (err) {
|
|
1777
1908
|
client.kill();
|
|
1778
|
-
await
|
|
1909
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1779
1910
|
throw err;
|
|
1780
1911
|
}
|
|
1781
1912
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1782
1913
|
client.kill();
|
|
1783
|
-
await
|
|
1914
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1784
1915
|
throw new Error(
|
|
1785
1916
|
`claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
|
|
1786
1917
|
);
|
|
1787
1918
|
}
|
|
1788
|
-
return new ClaudeSession(
|
|
1919
|
+
return new ClaudeSession(
|
|
1920
|
+
sessionRef,
|
|
1921
|
+
client,
|
|
1922
|
+
ctx.workspaceDir,
|
|
1923
|
+
ctx.approvalChannel,
|
|
1924
|
+
mcpConfigDir,
|
|
1925
|
+
modelId
|
|
1926
|
+
);
|
|
1789
1927
|
}
|
|
1790
1928
|
/**
|
|
1791
1929
|
* `claude auth status --json` is claude's OWN non-secret login-state
|
|
@@ -1819,19 +1957,31 @@ var ClaudeAdapter = class {
|
|
|
1819
1957
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1820
1958
|
}
|
|
1821
1959
|
};
|
|
1960
|
+
function subscriptionModel(task, runtimeId) {
|
|
1961
|
+
const selection = task.dispatchSelection;
|
|
1962
|
+
if (selection === void 0) return void 0;
|
|
1963
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
1964
|
+
throw new PolicyUnsupportedError(
|
|
1965
|
+
`claude adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
return selection.modelId;
|
|
1969
|
+
}
|
|
1822
1970
|
var ClaudeSession = class {
|
|
1823
|
-
constructor(sessionRef, client, workspaceDir, approvalChannel,
|
|
1971
|
+
constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
|
|
1824
1972
|
this.sessionRef = sessionRef;
|
|
1825
1973
|
this.client = client;
|
|
1826
1974
|
this.workspaceDir = workspaceDir;
|
|
1827
1975
|
this.approvalChannel = approvalChannel;
|
|
1828
|
-
this.
|
|
1976
|
+
this.mcpConfigDir = mcpConfigDir;
|
|
1977
|
+
this.modelId = modelId;
|
|
1829
1978
|
}
|
|
1830
1979
|
sessionRef;
|
|
1831
1980
|
client;
|
|
1832
1981
|
workspaceDir;
|
|
1833
1982
|
approvalChannel;
|
|
1834
|
-
|
|
1983
|
+
mcpConfigDir;
|
|
1984
|
+
modelId;
|
|
1835
1985
|
correlation = createToolUseCorrelation();
|
|
1836
1986
|
get events() {
|
|
1837
1987
|
const client = this.client;
|
|
@@ -1882,6 +2032,12 @@ var ClaudeSession = class {
|
|
|
1882
2032
|
if (typeof task.instruction !== "string") {
|
|
1883
2033
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1884
2034
|
}
|
|
2035
|
+
const requestedModel = subscriptionModel(task, "claude");
|
|
2036
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2037
|
+
throw new PolicyUnsupportedError(
|
|
2038
|
+
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
1885
2041
|
this.client.writeUserMessage(task.instruction);
|
|
1886
2042
|
}
|
|
1887
2043
|
/**
|
|
@@ -1901,7 +2057,7 @@ var ClaudeSession = class {
|
|
|
1901
2057
|
}
|
|
1902
2058
|
async close() {
|
|
1903
2059
|
this.client.kill();
|
|
1904
|
-
await
|
|
2060
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
1905
2061
|
}
|
|
1906
2062
|
/**
|
|
1907
2063
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -2065,8 +2221,8 @@ function extractArtifactEvents(changes, workspaceDir) {
|
|
|
2065
2221
|
const absolutePath = typeof change.path === "string" ? change.path : void 0;
|
|
2066
2222
|
const kind = typeof change.kind === "string" ? change.kind : void 0;
|
|
2067
2223
|
if (!absolutePath || kind === "delete") continue;
|
|
2068
|
-
const relative =
|
|
2069
|
-
if (relative.length === 0 || relative.startsWith("..") ||
|
|
2224
|
+
const relative = path20.relative(workspaceDir, absolutePath);
|
|
2225
|
+
if (relative.length === 0 || relative.startsWith("..") || path20.isAbsolute(relative)) continue;
|
|
2070
2226
|
events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
|
|
2071
2227
|
}
|
|
2072
2228
|
return events;
|
|
@@ -2087,7 +2243,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
|
|
|
2087
2243
|
".csv": "text/csv"
|
|
2088
2244
|
};
|
|
2089
2245
|
function guessContentType2(relativePath) {
|
|
2090
|
-
return CONTENT_TYPE_BY_EXTENSION[
|
|
2246
|
+
return CONTENT_TYPE_BY_EXTENSION[path20.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
|
|
2091
2247
|
}
|
|
2092
2248
|
function extractErrorMessage(rawError) {
|
|
2093
2249
|
if (typeof rawError === "string") return rawError;
|
|
@@ -2225,6 +2381,7 @@ var CodexAdapter = class {
|
|
|
2225
2381
|
this.options = options;
|
|
2226
2382
|
}
|
|
2227
2383
|
options;
|
|
2384
|
+
supportsDispatchSelection = true;
|
|
2228
2385
|
id = "codex";
|
|
2229
2386
|
async detect() {
|
|
2230
2387
|
const bin = this.resolveBin();
|
|
@@ -2248,9 +2405,10 @@ var CodexAdapter = class {
|
|
|
2248
2405
|
* Two independently-verified channel gotchas apply here, the "pi lesson"
|
|
2249
2406
|
* yet again:
|
|
2250
2407
|
* - `codex login status`'s human-readable "Logged in using ChatGPT"
|
|
2251
|
-
* message prints on STDERR, not stdout
|
|
2252
|
-
*
|
|
2253
|
-
*
|
|
2408
|
+
* message prints on STDERR, not stdout — both streams are checked
|
|
2409
|
+
* here for exactly that reason. pi's `--version` is the same class of
|
|
2410
|
+
* hazard from the other direction: its channel has moved between pi
|
|
2411
|
+
* releases (see ../pi/pi-adapter.ts), so neither stream is assumed.
|
|
2254
2412
|
* - The NOT-logged-in message/exit-code shape was deliberately never
|
|
2255
2413
|
* empirically tested: this machine has a real, live ChatGPT login, and
|
|
2256
2414
|
* running `codex logout` to observe the negative case would have
|
|
@@ -2296,17 +2454,20 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2296
2454
|
if (!mapping.ok) {
|
|
2297
2455
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2298
2456
|
}
|
|
2457
|
+
const modelId = subscriptionModel2(task);
|
|
2299
2458
|
const bin = this.resolveBin();
|
|
2300
2459
|
const queue = new AsyncQueue();
|
|
2301
2460
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
2302
2461
|
const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
|
|
2462
|
+
const runtimeEnv = withoutProviderCredentials(ctx.env);
|
|
2303
2463
|
const { sessionRef, runner } = await runCodexTurn({
|
|
2304
2464
|
command: bin.command,
|
|
2305
2465
|
resumeRef: task.sessionRef,
|
|
2306
2466
|
instruction: task.instruction,
|
|
2467
|
+
modelId,
|
|
2307
2468
|
policyArgs: mapping.args,
|
|
2308
2469
|
cwd: ctx.workspaceDir,
|
|
2309
|
-
env:
|
|
2470
|
+
env: runtimeEnv,
|
|
2310
2471
|
spawnFn: this.options.spawnFn,
|
|
2311
2472
|
workspaceDir,
|
|
2312
2473
|
queue,
|
|
@@ -2323,7 +2484,8 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2323
2484
|
queue,
|
|
2324
2485
|
recordUnmapped,
|
|
2325
2486
|
initialRunner: runner,
|
|
2326
|
-
preparedGit: ctx.gitWorkspace !== void 0
|
|
2487
|
+
preparedGit: ctx.gitWorkspace !== void 0,
|
|
2488
|
+
modelId
|
|
2327
2489
|
});
|
|
2328
2490
|
}
|
|
2329
2491
|
resolveBin() {
|
|
@@ -2344,12 +2506,25 @@ function makeUnmappedFrameRecorder(counts) {
|
|
|
2344
2506
|
}
|
|
2345
2507
|
};
|
|
2346
2508
|
}
|
|
2347
|
-
function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
|
|
2509
|
+
function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
|
|
2348
2510
|
const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
|
|
2349
|
-
return [
|
|
2511
|
+
return [
|
|
2512
|
+
...base,
|
|
2513
|
+
"--json",
|
|
2514
|
+
...modelId ? ["--model", modelId] : [],
|
|
2515
|
+
...preparedGit ? [] : ["--skip-git-repo-check"],
|
|
2516
|
+
...policyArgs,
|
|
2517
|
+
instruction
|
|
2518
|
+
];
|
|
2350
2519
|
}
|
|
2351
2520
|
async function runCodexTurn(params) {
|
|
2352
|
-
const argv = buildArgv(
|
|
2521
|
+
const argv = buildArgv(
|
|
2522
|
+
params.resumeRef,
|
|
2523
|
+
params.policyArgs,
|
|
2524
|
+
params.instruction,
|
|
2525
|
+
params.modelId,
|
|
2526
|
+
params.preparedGit
|
|
2527
|
+
);
|
|
2353
2528
|
let firstLineSettled = false;
|
|
2354
2529
|
let resolveFirstLine;
|
|
2355
2530
|
let rejectFirstLine;
|
|
@@ -2450,6 +2625,7 @@ var CodexSession = class {
|
|
|
2450
2625
|
queue;
|
|
2451
2626
|
recordUnmapped;
|
|
2452
2627
|
preparedGit;
|
|
2628
|
+
modelId;
|
|
2453
2629
|
currentRunner;
|
|
2454
2630
|
closed = false;
|
|
2455
2631
|
constructor(options) {
|
|
@@ -2461,6 +2637,7 @@ var CodexSession = class {
|
|
|
2461
2637
|
this.queue = options.queue;
|
|
2462
2638
|
this.recordUnmapped = options.recordUnmapped;
|
|
2463
2639
|
this.preparedGit = options.preparedGit;
|
|
2640
|
+
this.modelId = options.modelId;
|
|
2464
2641
|
this.currentRunner = options.initialRunner;
|
|
2465
2642
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
2466
2643
|
}
|
|
@@ -2521,6 +2698,13 @@ var CodexSession = class {
|
|
|
2521
2698
|
if (!mapping.ok) {
|
|
2522
2699
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2523
2700
|
}
|
|
2701
|
+
const requestedModel = subscriptionModel2(task);
|
|
2702
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2703
|
+
throw new PolicyUnsupportedError(
|
|
2704
|
+
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
const modelId = this.modelId;
|
|
2524
2708
|
const resumeRef = this.sessionRef;
|
|
2525
2709
|
let sessionRef;
|
|
2526
2710
|
let runner;
|
|
@@ -2529,9 +2713,10 @@ var CodexSession = class {
|
|
|
2529
2713
|
command: this.command,
|
|
2530
2714
|
resumeRef,
|
|
2531
2715
|
instruction: task.instruction,
|
|
2716
|
+
modelId,
|
|
2532
2717
|
policyArgs: mapping.args,
|
|
2533
2718
|
cwd: this.workspaceDir,
|
|
2534
|
-
env: this.env,
|
|
2719
|
+
env: withoutProviderCredentials(this.env),
|
|
2535
2720
|
spawnFn: this.spawnFn,
|
|
2536
2721
|
workspaceDir: this.workspaceDir,
|
|
2537
2722
|
queue: this.queue,
|
|
@@ -2591,6 +2776,16 @@ var CodexSession = class {
|
|
|
2591
2776
|
);
|
|
2592
2777
|
}
|
|
2593
2778
|
};
|
|
2779
|
+
function subscriptionModel2(task) {
|
|
2780
|
+
const selection = task.dispatchSelection;
|
|
2781
|
+
if (selection === void 0) return void 0;
|
|
2782
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
2783
|
+
throw new PolicyUnsupportedError(
|
|
2784
|
+
`codex adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
return selection.modelId;
|
|
2788
|
+
}
|
|
2594
2789
|
|
|
2595
2790
|
// src/daemon/approvals.ts
|
|
2596
2791
|
var ApprovalNotFoundError = class extends Error {
|
|
@@ -2693,12 +2888,12 @@ var DeviceStore = class _DeviceStore {
|
|
|
2693
2888
|
*/
|
|
2694
2889
|
constructor(storeDir, secureDirOptions) {
|
|
2695
2890
|
this.secureDirOptions = secureDirOptions;
|
|
2696
|
-
this.filePath =
|
|
2891
|
+
this.filePath = path20.join(storeDir, "device.json");
|
|
2697
2892
|
}
|
|
2698
2893
|
secureDirOptions;
|
|
2699
2894
|
filePath;
|
|
2700
2895
|
static defaultDir(productId) {
|
|
2701
|
-
return
|
|
2896
|
+
return path20.join(os.homedir(), ".byok", productId);
|
|
2702
2897
|
}
|
|
2703
2898
|
/**
|
|
2704
2899
|
* Resolve the one store pathname every daemon/CLI component must share.
|
|
@@ -2707,7 +2902,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
2707
2902
|
* cwd to pin a quarantine directory inode.
|
|
2708
2903
|
*/
|
|
2709
2904
|
static resolveDir(productId, configured) {
|
|
2710
|
-
return
|
|
2905
|
+
return path20.resolve(configured ?? _DeviceStore.defaultDir(productId));
|
|
2711
2906
|
}
|
|
2712
2907
|
async load() {
|
|
2713
2908
|
const opened = await this.openBounded();
|
|
@@ -2748,7 +2943,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
2748
2943
|
}
|
|
2749
2944
|
}
|
|
2750
2945
|
async save(record) {
|
|
2751
|
-
const storeDir =
|
|
2946
|
+
const storeDir = path20.dirname(this.filePath);
|
|
2752
2947
|
await ensureSecureDir(storeDir, this.secureDirOptions);
|
|
2753
2948
|
await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
|
|
2754
2949
|
}
|
|
@@ -2815,13 +3010,10 @@ function exportPrivateKeyPem(privateKey) {
|
|
|
2815
3010
|
function importPrivateKeyPem(pem) {
|
|
2816
3011
|
return createPrivateKey(pem);
|
|
2817
3012
|
}
|
|
2818
|
-
var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
|
|
2819
3013
|
function signNonce(privateKey, nonce) {
|
|
2820
|
-
const signature = sign(null,
|
|
3014
|
+
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
2821
3015
|
return signature.toString("base64url");
|
|
2822
3016
|
}
|
|
2823
|
-
|
|
2824
|
-
// src/daemon/url.ts
|
|
2825
3017
|
function toHttpBase(serverUrl) {
|
|
2826
3018
|
const url = new URL(serverUrl);
|
|
2827
3019
|
if (url.protocol === "ws:") url.protocol = "http:";
|
|
@@ -2831,7 +3023,7 @@ function toHttpBase(serverUrl) {
|
|
|
2831
3023
|
return url.toString();
|
|
2832
3024
|
}
|
|
2833
3025
|
function toWsUrl(serverUrl) {
|
|
2834
|
-
const url = new URL(
|
|
3026
|
+
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
2835
3027
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2836
3028
|
return url.toString();
|
|
2837
3029
|
}
|
|
@@ -2919,7 +3111,7 @@ var AuthManager = class {
|
|
|
2919
3111
|
return await this.runCredentialMutation(async () => {
|
|
2920
3112
|
const existing = this.record ?? await this.opts.store.load();
|
|
2921
3113
|
const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
|
|
2922
|
-
const url = new URL(
|
|
3114
|
+
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
2923
3115
|
const res = await fetch(url, {
|
|
2924
3116
|
method: "POST",
|
|
2925
3117
|
headers: { "content-type": "application/json" },
|
|
@@ -2953,6 +3145,7 @@ var AuthManager = class {
|
|
|
2953
3145
|
async getValidAccessToken() {
|
|
2954
3146
|
if (!this.record) throw new Error("device is not paired yet; call pair(pairingCode) first");
|
|
2955
3147
|
if (this.revoked) throw new DeviceRevokedError();
|
|
3148
|
+
if (this.renewing) return this.renewing;
|
|
2956
3149
|
if (msUntilExpiry(this.record.expiresAt) > RENEW_MARGIN_MS) return this.record.accessToken;
|
|
2957
3150
|
return this.renew();
|
|
2958
3151
|
}
|
|
@@ -2981,7 +3174,7 @@ var AuthManager = class {
|
|
|
2981
3174
|
const record = this.record;
|
|
2982
3175
|
const base = toHttpBase(this.opts.serverUrl);
|
|
2983
3176
|
const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
|
|
2984
|
-
const challengeRes = await fetch(new URL(
|
|
3177
|
+
const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
|
|
2985
3178
|
method: "POST",
|
|
2986
3179
|
headers: { "content-type": "application/json" },
|
|
2987
3180
|
body: JSON.stringify({ deviceId: record.deviceId })
|
|
@@ -2994,7 +3187,7 @@ var AuthManager = class {
|
|
|
2994
3187
|
}
|
|
2995
3188
|
const { nonce } = await challengeRes.json();
|
|
2996
3189
|
const signature = signNonce(privateKey, nonce);
|
|
2997
|
-
const tokenRes = await fetch(new URL(
|
|
3190
|
+
const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
|
|
2998
3191
|
method: "POST",
|
|
2999
3192
|
headers: { "content-type": "application/json" },
|
|
3000
3193
|
body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
|
|
@@ -3092,7 +3285,7 @@ var BlobClient = class {
|
|
|
3092
3285
|
async resolveInstruction(blobRef) {
|
|
3093
3286
|
const base = toHttpBase(this.serverUrl);
|
|
3094
3287
|
const urlRes = await authedFetch(
|
|
3095
|
-
new URL(
|
|
3288
|
+
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
3096
3289
|
{ method: "GET" },
|
|
3097
3290
|
this.auth
|
|
3098
3291
|
);
|
|
@@ -3120,7 +3313,7 @@ var BlobClient = class {
|
|
|
3120
3313
|
const base = toHttpBase(this.serverUrl);
|
|
3121
3314
|
const reservationId = `blob_${randomUUID()}`;
|
|
3122
3315
|
const createRes = await authedFetch(
|
|
3123
|
-
new URL(
|
|
3316
|
+
new URL(BYOK_BLOBS_PATH, base),
|
|
3124
3317
|
{
|
|
3125
3318
|
method: "POST",
|
|
3126
3319
|
headers: {
|
|
@@ -3152,7 +3345,7 @@ var BlobClient = class {
|
|
|
3152
3345
|
let response;
|
|
3153
3346
|
try {
|
|
3154
3347
|
response = await authedFetch(
|
|
3155
|
-
new URL(
|
|
3348
|
+
new URL(byokBlobFinalizePath(blobId), base),
|
|
3156
3349
|
{
|
|
3157
3350
|
method: "POST",
|
|
3158
3351
|
headers: { "idempotency-key": reservationId }
|
|
@@ -3175,26 +3368,187 @@ var BlobClient = class {
|
|
|
3175
3368
|
throw lastFailure;
|
|
3176
3369
|
}
|
|
3177
3370
|
};
|
|
3371
|
+
var PRESENCE_HINTS_CAPABILITY = "presence.hints";
|
|
3372
|
+
var CapabilityDiscoveryError = class extends Error {
|
|
3373
|
+
constructor(message, options) {
|
|
3374
|
+
super(message, options);
|
|
3375
|
+
this.name = "CapabilityDiscoveryError";
|
|
3376
|
+
}
|
|
3377
|
+
};
|
|
3378
|
+
async function fetchCapabilityDeclaration(serverUrl, options = {}) {
|
|
3379
|
+
const url = new URL(BYOK_CAPABILITIES_PATH, toHttpBase(serverUrl));
|
|
3380
|
+
let response;
|
|
3381
|
+
try {
|
|
3382
|
+
response = await fetch(url, {
|
|
3383
|
+
method: "GET",
|
|
3384
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
3385
|
+
});
|
|
3386
|
+
} catch (err) {
|
|
3387
|
+
throw new CapabilityDiscoveryError(
|
|
3388
|
+
`failed to read the capability declaration from ${url.toString()}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3389
|
+
{ cause: err }
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
if (!response.ok) {
|
|
3393
|
+
throw new CapabilityDiscoveryError(
|
|
3394
|
+
`failed to read the capability declaration from ${url.toString()}: HTTP ${response.status}`
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
let body;
|
|
3398
|
+
try {
|
|
3399
|
+
body = await response.json();
|
|
3400
|
+
} catch (err) {
|
|
3401
|
+
throw new CapabilityDiscoveryError(
|
|
3402
|
+
`the capability declaration at ${url.toString()} is not JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
3403
|
+
{ cause: err }
|
|
3404
|
+
);
|
|
3405
|
+
}
|
|
3406
|
+
const parsed = CapabilityDeclarationSchema.safeParse(body);
|
|
3407
|
+
if (!parsed.success) {
|
|
3408
|
+
throw new CapabilityDiscoveryError(
|
|
3409
|
+
`the capability declaration at ${url.toString()} is not a valid ADR-010 declaration: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`,
|
|
3410
|
+
{ cause: parsed.error }
|
|
3411
|
+
);
|
|
3412
|
+
}
|
|
3413
|
+
return parsed.data;
|
|
3414
|
+
}
|
|
3415
|
+
function declares(declaration, capability) {
|
|
3416
|
+
return hasCapability(declaration, capability);
|
|
3417
|
+
}
|
|
3418
|
+
var DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3419
|
+
var DEFAULT_PRESENCE_TTL_MS = 9e4;
|
|
3420
|
+
var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
|
|
3421
|
+
function assertPresenceHeartbeatCadence(cadence) {
|
|
3422
|
+
const { intervalMs, ttlMs, minimumIntervalMs } = cadence;
|
|
3423
|
+
if (!(minimumIntervalMs < intervalMs && intervalMs < ttlMs)) {
|
|
3424
|
+
throw new Error(
|
|
3425
|
+
`presence heartbeat interval must satisfy minimumIntervalMs < intervalMs < ttlMs \u2014 got ${minimumIntervalMs} < ${intervalMs} < ${ttlMs}`
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
var PresencePublisher = class {
|
|
3430
|
+
constructor(opts) {
|
|
3431
|
+
this.opts = opts;
|
|
3432
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS;
|
|
3433
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_PRESENCE_TTL_MS;
|
|
3434
|
+
const minimumIntervalMs = opts.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS;
|
|
3435
|
+
assertPresenceHeartbeatCadence({ intervalMs, ttlMs, minimumIntervalMs });
|
|
3436
|
+
this.intervalMs = intervalMs;
|
|
3437
|
+
this.url = new URL(BYOK_PRESENCE_PATH, toHttpBase(opts.serverUrl));
|
|
3438
|
+
}
|
|
3439
|
+
opts;
|
|
3440
|
+
url;
|
|
3441
|
+
intervalMs;
|
|
3442
|
+
timer;
|
|
3443
|
+
running = false;
|
|
3444
|
+
/** Set once a revoked device is observed. Terminal: `start()` will not restart this instance. */
|
|
3445
|
+
stoppedPermanently = false;
|
|
3446
|
+
/** Publishes immediately, then every `intervalMs`. Idempotent; a no-op after a permanent stop. */
|
|
3447
|
+
start() {
|
|
3448
|
+
if (this.running || this.stoppedPermanently) return;
|
|
3449
|
+
this.running = true;
|
|
3450
|
+
void this.beat();
|
|
3451
|
+
}
|
|
3452
|
+
/** Stops the cadence. Idempotent, and the only "offline" signal this producer emits — the hint's TTL does the rest. */
|
|
3453
|
+
stop() {
|
|
3454
|
+
this.running = false;
|
|
3455
|
+
if (this.timer !== void 0) {
|
|
3456
|
+
clearTimeout(this.timer);
|
|
3457
|
+
this.timer = void 0;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
schedule() {
|
|
3461
|
+
if (!this.running) return;
|
|
3462
|
+
this.timer = setTimeout(() => {
|
|
3463
|
+
this.timer = void 0;
|
|
3464
|
+
void this.beat();
|
|
3465
|
+
}, this.intervalMs);
|
|
3466
|
+
this.timer.unref?.();
|
|
3467
|
+
}
|
|
3468
|
+
async beat() {
|
|
3469
|
+
if (!this.running) return;
|
|
3470
|
+
try {
|
|
3471
|
+
const response = await authedFetch(
|
|
3472
|
+
this.url,
|
|
3473
|
+
{
|
|
3474
|
+
method: "PUT",
|
|
3475
|
+
headers: { "content-type": "application/json" },
|
|
3476
|
+
body: JSON.stringify({ level: "online" })
|
|
3477
|
+
},
|
|
3478
|
+
this.opts.auth
|
|
3479
|
+
);
|
|
3480
|
+
if (!response.ok) {
|
|
3481
|
+
if (response.status === 401) {
|
|
3482
|
+
this.stopPermanently(`presence heartbeat unauthorized after token renewal (HTTP 401)`);
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
this.opts.onDegraded?.(`presence heartbeat failed: HTTP ${response.status}`);
|
|
3486
|
+
}
|
|
3487
|
+
} catch (err) {
|
|
3488
|
+
if (err instanceof DeviceRevokedError) {
|
|
3489
|
+
this.stopPermanently("presence heartbeat stopped: device has been revoked; re-pair required");
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
this.opts.onDegraded?.(`presence heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3493
|
+
}
|
|
3494
|
+
this.schedule();
|
|
3495
|
+
}
|
|
3496
|
+
stopPermanently(reason) {
|
|
3497
|
+
this.stoppedPermanently = true;
|
|
3498
|
+
this.stop();
|
|
3499
|
+
this.opts.onDegraded?.(reason);
|
|
3500
|
+
}
|
|
3501
|
+
};
|
|
3502
|
+
function freshJti() {
|
|
3503
|
+
return randomBytes(16).toString("base64url");
|
|
3504
|
+
}
|
|
3505
|
+
function mintDeviceAssertion(input) {
|
|
3506
|
+
const issuedAtMs = input.now.getTime();
|
|
3507
|
+
const expiresAt = new Date(issuedAtMs + input.ttlMs).toISOString();
|
|
3508
|
+
const claims = DeviceAssertionClaimsSchema.parse({
|
|
3509
|
+
version: 1,
|
|
3510
|
+
issuer: input.issuer,
|
|
3511
|
+
productId: input.productId,
|
|
3512
|
+
deviceId: input.record.deviceId,
|
|
3513
|
+
audience: input.audience,
|
|
3514
|
+
jti: freshJti(),
|
|
3515
|
+
issuedAt: new Date(issuedAtMs).toISOString(),
|
|
3516
|
+
expiresAt
|
|
3517
|
+
});
|
|
3518
|
+
const privateKey = importPrivateKeyPem(input.record.devicePrivateKeyPem);
|
|
3519
|
+
const signature = sign(null, deviceAssertionSigningInput(claims), privateKey).toString("base64url");
|
|
3520
|
+
return {
|
|
3521
|
+
envelope: {
|
|
3522
|
+
schema: DEVICE_ASSERTION_SCHEMA_ID,
|
|
3523
|
+
algorithm: "ed25519",
|
|
3524
|
+
protected: claims,
|
|
3525
|
+
signature
|
|
3526
|
+
},
|
|
3527
|
+
claims,
|
|
3528
|
+
expiresAt
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3178
3531
|
var CONTROL_PROTOCOL_VERSION = 1;
|
|
3179
3532
|
var HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
3180
3533
|
var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
|
|
3534
|
+
var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
|
|
3181
3535
|
function shortHash(input) {
|
|
3182
3536
|
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
|
|
3183
3537
|
}
|
|
3184
3538
|
function controlSocketPath(storeDir) {
|
|
3185
|
-
const candidate =
|
|
3539
|
+
const candidate = path20.join(storeDir, "control.sock");
|
|
3186
3540
|
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
|
|
3187
|
-
return
|
|
3541
|
+
return path20.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
|
|
3188
3542
|
}
|
|
3189
3543
|
function controlPipeName(productId, storeDir) {
|
|
3190
|
-
const id = shortHash(`${productId}|${
|
|
3544
|
+
const id = shortHash(`${productId}|${path20.resolve(storeDir)}`);
|
|
3191
3545
|
return `\\\\.\\pipe\\byok-${id}`;
|
|
3192
3546
|
}
|
|
3193
3547
|
function controlEndpointPath(productId, storeDir, platform = process.platform) {
|
|
3194
3548
|
return platform === "win32" ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);
|
|
3195
3549
|
}
|
|
3196
3550
|
function controlTokenPath(storeDir) {
|
|
3197
|
-
return
|
|
3551
|
+
return path20.join(storeDir, "control.token");
|
|
3198
3552
|
}
|
|
3199
3553
|
var SERVER_PROOF_LABEL = "byok-control-server|";
|
|
3200
3554
|
var CLIENT_AUTH_LABEL = "byok-control-client|";
|
|
@@ -3290,6 +3644,16 @@ function parseApprovalsRequestParams(value) {
|
|
|
3290
3644
|
if (typeof value.summary !== "string") return void 0;
|
|
3291
3645
|
return { taskId: value.taskId, summary: value.summary };
|
|
3292
3646
|
}
|
|
3647
|
+
var ASSERTION_AUDIENCE_MAX_BYTES = 256;
|
|
3648
|
+
function parseAssertionIssueParams(value) {
|
|
3649
|
+
if (!isRecord2(value)) return void 0;
|
|
3650
|
+
const keys = Object.keys(value);
|
|
3651
|
+
if (keys.length !== 1 || keys[0] !== "audience") return void 0;
|
|
3652
|
+
const { audience } = value;
|
|
3653
|
+
if (typeof audience !== "string" || audience.length === 0) return void 0;
|
|
3654
|
+
if (Buffer.byteLength(audience, "utf8") > ASSERTION_AUDIENCE_MAX_BYTES) return void 0;
|
|
3655
|
+
return { audience };
|
|
3656
|
+
}
|
|
3293
3657
|
function parseShutdownParams(value) {
|
|
3294
3658
|
if (!isRecord2(value)) return {};
|
|
3295
3659
|
return value.reason === "unpair" || value.reason === "operator" ? { reason: value.reason } : {};
|
|
@@ -3351,7 +3715,7 @@ async function assertOwnedPrivateDir(dir) {
|
|
|
3351
3715
|
}
|
|
3352
3716
|
async function bindControlEndpoint(server, endpoint) {
|
|
3353
3717
|
if (process.platform !== "win32") {
|
|
3354
|
-
const endpointDir =
|
|
3718
|
+
const endpointDir = path20.dirname(endpoint);
|
|
3355
3719
|
await promises.mkdir(endpointDir, { recursive: true, mode: 448 });
|
|
3356
3720
|
await promises.chmod(endpointDir, 448).catch(() => {
|
|
3357
3721
|
});
|
|
@@ -3622,7 +3986,7 @@ var LongPollClient = class {
|
|
|
3622
3986
|
try {
|
|
3623
3987
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3624
3988
|
const res = await authedFetch(
|
|
3625
|
-
new URL(
|
|
3989
|
+
new URL(BYOK_MESSAGES_PATH, base),
|
|
3626
3990
|
{
|
|
3627
3991
|
method: "POST",
|
|
3628
3992
|
headers: { "content-type": "application/json" },
|
|
@@ -3646,7 +4010,7 @@ var LongPollClient = class {
|
|
|
3646
4010
|
while (this.running) {
|
|
3647
4011
|
try {
|
|
3648
4012
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3649
|
-
const url = new URL(
|
|
4013
|
+
const url = new URL(BYOK_EVENTS_PATH, base);
|
|
3650
4014
|
const cursor = this.opts.getCursor();
|
|
3651
4015
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
3652
4016
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
@@ -4632,7 +4996,7 @@ function sameFileState2(left, right) {
|
|
|
4632
4996
|
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
4633
4997
|
}
|
|
4634
4998
|
async function openOperationalHealthFile(storeDir) {
|
|
4635
|
-
const filePath =
|
|
4999
|
+
const filePath = path20.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
4636
5000
|
let namedBefore;
|
|
4637
5001
|
try {
|
|
4638
5002
|
namedBefore = await promises.lstat(filePath, { bigint: true });
|
|
@@ -4674,7 +5038,7 @@ var OperationalHealthTracker = class {
|
|
|
4674
5038
|
#writeTail = Promise.resolve();
|
|
4675
5039
|
#started = false;
|
|
4676
5040
|
constructor(storeDir, options = {}) {
|
|
4677
|
-
this.#filePath =
|
|
5041
|
+
this.#filePath = path20.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
4678
5042
|
this.#windowMs = options.windowMs ?? 6e4;
|
|
4679
5043
|
this.#failureThreshold = options.failureThreshold ?? 3;
|
|
4680
5044
|
this.#maxFailures = options.maxFailures ?? 128;
|
|
@@ -4761,7 +5125,7 @@ var OperationalHealthTracker = class {
|
|
|
4761
5125
|
async #load() {
|
|
4762
5126
|
let opened;
|
|
4763
5127
|
try {
|
|
4764
|
-
opened = await openOperationalHealthFile(
|
|
5128
|
+
opened = await openOperationalHealthFile(path20.dirname(this.#filePath));
|
|
4765
5129
|
} catch (err) {
|
|
4766
5130
|
throw new Error("operational health state could not be read");
|
|
4767
5131
|
}
|
|
@@ -4797,7 +5161,7 @@ var OperationalHealthTracker = class {
|
|
|
4797
5161
|
if (!this.#state) return;
|
|
4798
5162
|
const body = JSON.stringify(this.#state, null, 2);
|
|
4799
5163
|
this.#writeTail = this.#writeTail.then(async () => {
|
|
4800
|
-
await ensureSecureDir(
|
|
5164
|
+
await ensureSecureDir(path20.dirname(this.#filePath));
|
|
4801
5165
|
await atomicWriteFile(this.#filePath, body, { mode: 384, fsync: true });
|
|
4802
5166
|
});
|
|
4803
5167
|
try {
|
|
@@ -4909,19 +5273,19 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
|
4909
5273
|
var MAX_OWNER_BYTES = 4096;
|
|
4910
5274
|
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
4911
5275
|
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
4912
|
-
var STORE_MUTEX_PORT_BASE = 1e4;
|
|
4913
|
-
var STORE_MUTEX_PORT_COUNT = 2e4;
|
|
4914
|
-
var STORE_MUTEX_PORT_CANDIDATES = 32;
|
|
4915
5276
|
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
4916
5277
|
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
5278
|
+
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
5279
|
+
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
5280
|
+
var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
|
|
4917
5281
|
function storeMutexIdentity(canonicalStoreDir) {
|
|
4918
5282
|
return createHash("sha256").update(canonicalStoreDir).digest("hex");
|
|
4919
5283
|
}
|
|
4920
|
-
function
|
|
4921
|
-
|
|
4922
|
-
const
|
|
4923
|
-
|
|
4924
|
-
return
|
|
5284
|
+
function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
|
|
5285
|
+
if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
|
|
5286
|
+
const candidate = path20.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
|
|
5287
|
+
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
|
|
5288
|
+
return path20.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
|
|
4925
5289
|
}
|
|
4926
5290
|
var DaemonOwnerActiveError = class extends Error {
|
|
4927
5291
|
constructor(role) {
|
|
@@ -5042,70 +5406,90 @@ async function createLivenessListener() {
|
|
|
5042
5406
|
})
|
|
5043
5407
|
};
|
|
5044
5408
|
}
|
|
5045
|
-
async function probeStoreMutex(
|
|
5409
|
+
async function probeStoreMutex(endpoint, identity) {
|
|
5046
5410
|
return new Promise((resolve) => {
|
|
5047
|
-
const socket = createConnection(
|
|
5411
|
+
const socket = createConnection(endpoint);
|
|
5048
5412
|
let settled = false;
|
|
5049
5413
|
let raw = "";
|
|
5050
5414
|
const finish = (result) => {
|
|
5051
5415
|
if (settled) return;
|
|
5052
5416
|
settled = true;
|
|
5417
|
+
clearTimeout(timer);
|
|
5418
|
+
socket.removeAllListeners();
|
|
5053
5419
|
socket.destroy();
|
|
5054
5420
|
resolve(result);
|
|
5055
5421
|
};
|
|
5422
|
+
const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
|
|
5056
5423
|
socket.setEncoding("utf8");
|
|
5057
|
-
socket.setTimeout(STORE_MUTEX_PROBE_TIMEOUT_MS, () => finish({ kind: "uncertain" }));
|
|
5058
5424
|
socket.on("data", (chunk) => {
|
|
5059
5425
|
raw += chunk;
|
|
5060
|
-
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "
|
|
5061
|
-
});
|
|
5062
|
-
socket.once("end", () => {
|
|
5063
|
-
const line = raw.trimEnd();
|
|
5064
|
-
const identity = line.startsWith(STORE_MUTEX_ID_PREFIX) ? line.slice(STORE_MUTEX_ID_PREFIX.length) : void 0;
|
|
5065
|
-
finish(
|
|
5066
|
-
identity && /^[a-f0-9]{64}$/.test(identity) ? { kind: "identity", identity } : { kind: "foreign-or-gone" }
|
|
5067
|
-
);
|
|
5426
|
+
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
|
|
5068
5427
|
});
|
|
5069
|
-
socket.once("
|
|
5428
|
+
socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
|
|
5429
|
+
socket.once(
|
|
5430
|
+
"error",
|
|
5431
|
+
(err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
|
|
5432
|
+
);
|
|
5070
5433
|
});
|
|
5071
5434
|
}
|
|
5435
|
+
async function clearStaleStoreMutexSocket(endpoint, identity) {
|
|
5436
|
+
let stat;
|
|
5437
|
+
try {
|
|
5438
|
+
stat = await promises.lstat(endpoint);
|
|
5439
|
+
} catch (err) {
|
|
5440
|
+
if (err.code === "ENOENT") return;
|
|
5441
|
+
throw err;
|
|
5442
|
+
}
|
|
5443
|
+
if (!stat.isSocket()) throw new Error("store mutation lock path exists but is not a socket");
|
|
5444
|
+
if ((await probeStoreMutex(endpoint, identity)).kind !== "unbound") throw new DaemonOwnerActiveError("unknown");
|
|
5445
|
+
await promises.rm(endpoint, { force: true });
|
|
5446
|
+
}
|
|
5447
|
+
async function assertOwnedPrivateDir2(dir) {
|
|
5448
|
+
const uid = process.getuid?.();
|
|
5449
|
+
if (uid === void 0) return;
|
|
5450
|
+
const stat = await promises.lstat(dir);
|
|
5451
|
+
if (stat.isSymbolicLink() || stat.uid !== uid) {
|
|
5452
|
+
throw new Error(`refusing to bind the store mutation lock under "${dir}": not a real directory owned by this process's own uid`);
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5072
5455
|
async function acquireStoreMutex(canonicalStoreDir) {
|
|
5073
5456
|
const identity = storeMutexIdentity(canonicalStoreDir);
|
|
5074
|
-
|
|
5075
|
-
|
|
5457
|
+
const endpoint = storeMutexEndpoint(canonicalStoreDir, identity);
|
|
5458
|
+
const isPipe = process.platform === "win32";
|
|
5459
|
+
if (!isPipe) {
|
|
5460
|
+
const endpointDir = path20.dirname(endpoint);
|
|
5461
|
+
if (endpointDir !== canonicalStoreDir) {
|
|
5462
|
+
await ensureSecureDir(endpointDir);
|
|
5463
|
+
await assertOwnedPrivateDir2(endpointDir);
|
|
5464
|
+
}
|
|
5465
|
+
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
5466
|
+
}
|
|
5467
|
+
const server = createServer((socket) => socket.end(`${STORE_MUTEX_ID_PREFIX}${identity}
|
|
5076
5468
|
`));
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
server.
|
|
5082
|
-
|
|
5083
|
-
resolve();
|
|
5084
|
-
});
|
|
5469
|
+
try {
|
|
5470
|
+
await new Promise((resolve, reject) => {
|
|
5471
|
+
server.once("error", reject);
|
|
5472
|
+
server.listen(endpoint, () => {
|
|
5473
|
+
server.removeListener("error", reject);
|
|
5474
|
+
resolve();
|
|
5085
5475
|
});
|
|
5086
|
-
}
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
throw new DaemonOwnerActiveError("unknown");
|
|
5091
|
-
}
|
|
5092
|
-
continue;
|
|
5093
|
-
}
|
|
5094
|
-
server.unref();
|
|
5095
|
-
let closed = false;
|
|
5096
|
-
return {
|
|
5097
|
-
port,
|
|
5098
|
-
close: () => new Promise((resolve, reject) => {
|
|
5099
|
-
if (closed) {
|
|
5100
|
-
resolve();
|
|
5101
|
-
return;
|
|
5102
|
-
}
|
|
5103
|
-
closed = true;
|
|
5104
|
-
server.close((err) => err ? reject(err) : resolve());
|
|
5105
|
-
})
|
|
5106
|
-
};
|
|
5476
|
+
});
|
|
5477
|
+
} catch (err) {
|
|
5478
|
+
if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
|
|
5479
|
+
throw err;
|
|
5107
5480
|
}
|
|
5108
|
-
|
|
5481
|
+
if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
5482
|
+
server.unref();
|
|
5483
|
+
let closed = false;
|
|
5484
|
+
return {
|
|
5485
|
+
endpoint,
|
|
5486
|
+
close: async () => {
|
|
5487
|
+
if (closed) return;
|
|
5488
|
+
closed = true;
|
|
5489
|
+
await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
|
|
5490
|
+
if (!isPipe) await promises.rm(endpoint, { force: true }).catch(() => void 0);
|
|
5491
|
+
}
|
|
5492
|
+
};
|
|
5109
5493
|
}
|
|
5110
5494
|
async function reclaimExistsAndIsActive(reclaimPath) {
|
|
5111
5495
|
let stat;
|
|
@@ -5153,8 +5537,8 @@ async function acquireDaemonOwner(storeDir, role, clock = () => /* @__PURE__ */
|
|
|
5153
5537
|
await mutex.close().catch(() => void 0);
|
|
5154
5538
|
throw err;
|
|
5155
5539
|
}
|
|
5156
|
-
const ownerPath =
|
|
5157
|
-
const reclaimPath =
|
|
5540
|
+
const ownerPath = path20.join(storeDir, DAEMON_OWNER_FILENAME);
|
|
5541
|
+
const reclaimPath = path20.join(storeDir, RECLAIM_FILENAME);
|
|
5158
5542
|
const record = {
|
|
5159
5543
|
version: 2,
|
|
5160
5544
|
pid: process.pid,
|
|
@@ -5221,6 +5605,7 @@ function toRuntimeInfoCapabilities(caps) {
|
|
|
5221
5605
|
steer: caps.steer,
|
|
5222
5606
|
resume: caps.resume,
|
|
5223
5607
|
approvalInteractive: caps.approvalInteractive,
|
|
5608
|
+
...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
|
|
5224
5609
|
permissionModes: caps.permissionModes
|
|
5225
5610
|
};
|
|
5226
5611
|
}
|
|
@@ -5231,7 +5616,7 @@ var CursorStore = class {
|
|
|
5231
5616
|
storeDir;
|
|
5232
5617
|
fileFor(serverUrl, deviceId) {
|
|
5233
5618
|
const key = createHash("sha256").update(`${serverUrl}::${deviceId}`).digest("hex").slice(0, 32);
|
|
5234
|
-
return
|
|
5619
|
+
return path20.join(this.storeDir, `cursor-${key}.json`);
|
|
5235
5620
|
}
|
|
5236
5621
|
async load(serverUrl, deviceId) {
|
|
5237
5622
|
let raw;
|
|
@@ -5251,7 +5636,7 @@ var CursorStore = class {
|
|
|
5251
5636
|
}
|
|
5252
5637
|
async save(serverUrl, deviceId, cursor) {
|
|
5253
5638
|
const file = this.fileFor(serverUrl, deviceId);
|
|
5254
|
-
await promises.mkdir(
|
|
5639
|
+
await promises.mkdir(path20.dirname(file), { recursive: true, mode: 448 });
|
|
5255
5640
|
await atomicWriteFile(file, JSON.stringify({ cursor }));
|
|
5256
5641
|
}
|
|
5257
5642
|
/** 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. */
|
|
@@ -5294,7 +5679,7 @@ var DaemonObserver = class {
|
|
|
5294
5679
|
}
|
|
5295
5680
|
/**
|
|
5296
5681
|
* Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
|
|
5297
|
-
*
|
|
5682
|
+
* either offer variant produces a local event here — every other inbound type
|
|
5298
5683
|
* (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
|
|
5299
5684
|
* best-effort notification whose OWN observable effect already surfaces
|
|
5300
5685
|
* through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
|
|
@@ -5302,7 +5687,7 @@ var DaemonObserver = class {
|
|
|
5302
5687
|
* where those are actually reported from.
|
|
5303
5688
|
*/
|
|
5304
5689
|
handleInboundEnvelope(envelope) {
|
|
5305
|
-
if (envelope.type !== "task.offer") return;
|
|
5690
|
+
if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets") return;
|
|
5306
5691
|
const taskId = envelope.task_id;
|
|
5307
5692
|
if (this.taskInfo.has(taskId)) return;
|
|
5308
5693
|
this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
|
|
@@ -5423,6 +5808,37 @@ var DaemonObserver = class {
|
|
|
5423
5808
|
noteShutdownComplete(reason, undeliveredOutboxCount) {
|
|
5424
5809
|
this.emit({ kind: "shutdown-complete", ts: nowIso(), reason, undeliveredOutboxCount });
|
|
5425
5810
|
}
|
|
5811
|
+
/**
|
|
5812
|
+
* Plan `device-assertion-broker`: see the `device-assertion` `DaemonEvent`
|
|
5813
|
+
* variant's own doc comment. The parameter type is what keeps the signature
|
|
5814
|
+
* out — there is no field to pass one through.
|
|
5815
|
+
*
|
|
5816
|
+
* codex round-2 F4: the DENIED caller can pass its raw `audience` here, but
|
|
5817
|
+
* it is converted to a byte SIZE the instant the event is constructed and the
|
|
5818
|
+
* raw string is dropped — it is never placed on the emitted `DaemonEvent`, so
|
|
5819
|
+
* it cannot reach a subscriber, `format.ts`, stdout, or the audit file. The
|
|
5820
|
+
* ISSUED `audience` came from the allowlist and is kept verbatim.
|
|
5821
|
+
*/
|
|
5822
|
+
noteDeviceAssertion(event) {
|
|
5823
|
+
if (event.result === "issued") {
|
|
5824
|
+
this.emit({
|
|
5825
|
+
kind: "device-assertion",
|
|
5826
|
+
ts: nowIso(),
|
|
5827
|
+
result: "issued",
|
|
5828
|
+
audience: event.audience,
|
|
5829
|
+
jti: event.jti,
|
|
5830
|
+
expiresAt: event.expiresAt
|
|
5831
|
+
});
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5834
|
+
this.emit({
|
|
5835
|
+
kind: "device-assertion",
|
|
5836
|
+
ts: nowIso(),
|
|
5837
|
+
result: "denied",
|
|
5838
|
+
reason: event.reason,
|
|
5839
|
+
audienceSize: event.audience === void 0 ? void 0 : Buffer.byteLength(event.audience, "utf8")
|
|
5840
|
+
});
|
|
5841
|
+
}
|
|
5426
5842
|
/** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
|
|
5427
5843
|
noteStaleApprovalDecision(taskId, decision, reason) {
|
|
5428
5844
|
this.emit({ kind: "stale-approval-decision", ts: nowIso(), taskId, decision, reason });
|
|
@@ -5547,7 +5963,7 @@ var SessionWorkspaceStore = class {
|
|
|
5547
5963
|
*/
|
|
5548
5964
|
queue = Promise.resolve();
|
|
5549
5965
|
constructor(storeDir) {
|
|
5550
|
-
this.filePath =
|
|
5966
|
+
this.filePath = path20.join(storeDir, "session-workspaces.json");
|
|
5551
5967
|
}
|
|
5552
5968
|
async get(sessionRef) {
|
|
5553
5969
|
return this.enqueue(async () => {
|
|
@@ -5605,7 +6021,7 @@ var SessionWorkspaceStore = class {
|
|
|
5605
6021
|
}
|
|
5606
6022
|
}
|
|
5607
6023
|
async save(all) {
|
|
5608
|
-
const dir =
|
|
6024
|
+
const dir = path20.dirname(this.filePath);
|
|
5609
6025
|
await promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
5610
6026
|
const tmpPath = `${this.filePath}.${process.pid}-${tmpSeq2++}.tmp`;
|
|
5611
6027
|
try {
|
|
@@ -5710,9 +6126,9 @@ function isSqliteAvailable() {
|
|
|
5710
6126
|
}
|
|
5711
6127
|
}
|
|
5712
6128
|
var SECURE_FILE_MODE = 384;
|
|
5713
|
-
function openJournalDatabase(
|
|
6129
|
+
function openJournalDatabase(path25, busyTimeoutMs, faults) {
|
|
5714
6130
|
const { DatabaseSync } = loadSqliteModule();
|
|
5715
|
-
const db = new DatabaseSync(
|
|
6131
|
+
const db = new DatabaseSync(path25, { timeout: busyTimeoutMs });
|
|
5716
6132
|
try {
|
|
5717
6133
|
faults?.onStep?.("after-open");
|
|
5718
6134
|
db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
|
|
@@ -5855,9 +6271,9 @@ var RECEIVED_STATE = "received";
|
|
|
5855
6271
|
function byteLength(value) {
|
|
5856
6272
|
return Buffer.byteLength(value, "utf8");
|
|
5857
6273
|
}
|
|
5858
|
-
function fileBytes(
|
|
6274
|
+
function fileBytes(path25) {
|
|
5859
6275
|
try {
|
|
5860
|
-
return statSync(
|
|
6276
|
+
return statSync(path25).size;
|
|
5861
6277
|
} catch {
|
|
5862
6278
|
return 0;
|
|
5863
6279
|
}
|
|
@@ -7061,6 +7477,21 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
|
|
|
7061
7477
|
var MAX_TRACKED_TASK_IDS = 2e3;
|
|
7062
7478
|
var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
|
|
7063
7479
|
var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
|
|
7480
|
+
var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
|
|
7481
|
+
function resultDocumentRejectionDetail(check) {
|
|
7482
|
+
switch (check.reason) {
|
|
7483
|
+
case "over-cap":
|
|
7484
|
+
return `${check.bytes} bytes as canonical JSON, over the ${RESULT_DOCUMENT_MAX_BYTES}-byte limit (it is never truncated \u2014 a truncated JSON document is not valid JSON; use artifactRefs for a result this size)`;
|
|
7485
|
+
case "not-serializable":
|
|
7486
|
+
return "not JSON-serializable (JSON.stringify threw, or produced no output at all)";
|
|
7487
|
+
case "not-plain-json":
|
|
7488
|
+
return "not plain JSON data: it does not equal its own JSON round trip, so serializing it would silently change it (an undefined-valued key, NaN, a function or symbol value, a Date, a toJSON that rewrites the value, or a getter that answers differently on a second read)";
|
|
7489
|
+
default: {
|
|
7490
|
+
const exhaustive = check;
|
|
7491
|
+
throw new Error(`unhandled result document rejection: ${JSON.stringify(exhaustive)}`);
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7064
7495
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
7065
7496
|
function isKnownRuntimeId(id) {
|
|
7066
7497
|
return RuntimeIdSchema.safeParse(id).success;
|
|
@@ -7073,6 +7504,14 @@ function orderByPreference(candidates, preference) {
|
|
|
7073
7504
|
function adapterSupportsMode(adapter, mode) {
|
|
7074
7505
|
return adapter.capabilities().permissionModes.includes(mode);
|
|
7075
7506
|
}
|
|
7507
|
+
function adapterSupportsMcpToolsets(adapter) {
|
|
7508
|
+
return adapter.capabilities().mcpToolsets === true;
|
|
7509
|
+
}
|
|
7510
|
+
function withoutRequiredToolsets(payload) {
|
|
7511
|
+
if (!("requiredToolsets" in payload)) return payload;
|
|
7512
|
+
const { requiredToolsets, ...offer } = payload;
|
|
7513
|
+
return offer;
|
|
7514
|
+
}
|
|
7076
7515
|
function errorMessage3(err) {
|
|
7077
7516
|
return err instanceof Error ? err.message : String(err);
|
|
7078
7517
|
}
|
|
@@ -7108,8 +7547,8 @@ function estimateEventBytes(event) {
|
|
|
7108
7547
|
}
|
|
7109
7548
|
async function openArtifact(workspaceDir, name) {
|
|
7110
7549
|
const realWorkspaceDir = await promises.realpath(workspaceDir).catch(() => workspaceDir);
|
|
7111
|
-
const candidate =
|
|
7112
|
-
const prefix = realWorkspaceDir.endsWith(
|
|
7550
|
+
const candidate = path20.resolve(realWorkspaceDir, name);
|
|
7551
|
+
const prefix = realWorkspaceDir.endsWith(path20.sep) ? realWorkspaceDir : realWorkspaceDir + path20.sep;
|
|
7113
7552
|
if (candidate !== realWorkspaceDir && !candidate.startsWith(prefix)) {
|
|
7114
7553
|
return { ok: false, reason: `artifact name "${name}" resolves outside the task workspace \u2014 rejected` };
|
|
7115
7554
|
}
|
|
@@ -7428,6 +7867,9 @@ var TaskRunner = class {
|
|
|
7428
7867
|
case "task.offer":
|
|
7429
7868
|
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7430
7869
|
return;
|
|
7870
|
+
case "task.offer_with_toolsets":
|
|
7871
|
+
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7872
|
+
return;
|
|
7431
7873
|
case "task.cancel":
|
|
7432
7874
|
await this.handleCancel(envelope.task_id, envelope.payload.reason);
|
|
7433
7875
|
return;
|
|
@@ -7481,7 +7923,22 @@ var TaskRunner = class {
|
|
|
7481
7923
|
);
|
|
7482
7924
|
return;
|
|
7483
7925
|
}
|
|
7484
|
-
|
|
7926
|
+
if (payload.dispatchSelection !== void 0 && payload.runtime !== void 0 && payload.runtime !== payload.dispatchSelection.runtimeId) {
|
|
7927
|
+
this.decline(
|
|
7928
|
+
taskId,
|
|
7929
|
+
`offer runtime ${payload.runtime} does not match dispatchSelection.runtimeId ${payload.dispatchSelection.runtimeId}`,
|
|
7930
|
+
false
|
|
7931
|
+
);
|
|
7932
|
+
return;
|
|
7933
|
+
}
|
|
7934
|
+
const requiredToolsets = "requiredToolsets" in payload ? payload.requiredToolsets : void 0;
|
|
7935
|
+
const resolvedMcp = requiredToolsets ? this.resolveMcpServers(requiredToolsets) : void 0;
|
|
7936
|
+
if (resolvedMcp && !resolvedMcp.ok) {
|
|
7937
|
+
this.decline(taskId, resolvedMcp.reason, true);
|
|
7938
|
+
return;
|
|
7939
|
+
}
|
|
7940
|
+
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
7941
|
+
const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
|
|
7485
7942
|
if (!pick.ok) {
|
|
7486
7943
|
this.decline(taskId, pick.reason, pick.retryable);
|
|
7487
7944
|
return;
|
|
@@ -7507,7 +7964,7 @@ var TaskRunner = class {
|
|
|
7507
7964
|
const sameProtocolTask = ledger?.taskId === taskId;
|
|
7508
7965
|
const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
|
|
7509
7966
|
const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
|
|
7510
|
-
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef ||
|
|
7967
|
+
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef || path20.resolve(ledger.workspaceDir) !== path20.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
|
|
7511
7968
|
this.decline(taskId, "session is incompatible with Git workspace mode", true);
|
|
7512
7969
|
return;
|
|
7513
7970
|
}
|
|
@@ -7522,7 +7979,7 @@ var TaskRunner = class {
|
|
|
7522
7979
|
return;
|
|
7523
7980
|
}
|
|
7524
7981
|
} else {
|
|
7525
|
-
workspaceDir =
|
|
7982
|
+
workspaceDir = path20.join(this.deps.workspaceRoot, taskId);
|
|
7526
7983
|
}
|
|
7527
7984
|
try {
|
|
7528
7985
|
gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
|
|
@@ -7532,7 +7989,7 @@ var TaskRunner = class {
|
|
|
7532
7989
|
}
|
|
7533
7990
|
} else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
|
|
7534
7991
|
known = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
|
|
7535
|
-
workspaceDir = known?.workspaceDir ??
|
|
7992
|
+
workspaceDir = known?.workspaceDir ?? path20.join(this.deps.workspaceRoot, taskId);
|
|
7536
7993
|
plainWorkspaceNeedsResolve = true;
|
|
7537
7994
|
} else {
|
|
7538
7995
|
this.decline(taskId, "workspace mode is unavailable", true);
|
|
@@ -7644,6 +8101,7 @@ var TaskRunner = class {
|
|
|
7644
8101
|
const ctx = {
|
|
7645
8102
|
workspaceDir,
|
|
7646
8103
|
policy: decision.policy,
|
|
8104
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
|
|
7647
8105
|
...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
|
|
7648
8106
|
// M5: no longer `process.env` verbatim (see `environment.ts`'s own
|
|
7649
8107
|
// module doc comment for the credential-leak gap that closed) —
|
|
@@ -7680,7 +8138,7 @@ var TaskRunner = class {
|
|
|
7680
8138
|
}
|
|
7681
8139
|
};
|
|
7682
8140
|
const effectiveOffer = {
|
|
7683
|
-
...payload,
|
|
8141
|
+
...withoutRequiredToolsets(payload),
|
|
7684
8142
|
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
7685
8143
|
// Never forward a sessionRef this device has no recorded workspace
|
|
7686
8144
|
// for (stale, from another device, or simply made up) — an adapter
|
|
@@ -7758,6 +8216,36 @@ var TaskRunner = class {
|
|
|
7758
8216
|
if (typeof instruction === "string") return instruction;
|
|
7759
8217
|
return this.deps.blobClient.resolveInstruction(instruction.blobRef);
|
|
7760
8218
|
}
|
|
8219
|
+
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
8220
|
+
resolveMcpServers(requiredToolsets) {
|
|
8221
|
+
const registry = this.deps.mcpToolsets;
|
|
8222
|
+
if (!registry) {
|
|
8223
|
+
return { ok: false, reason: "offer requires MCP toolsets, but this device has no local mcpToolsets registry" };
|
|
8224
|
+
}
|
|
8225
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
8226
|
+
for (const toolsetId of requiredToolsets) {
|
|
8227
|
+
const toolset = registry.get(toolsetId);
|
|
8228
|
+
if (!toolset) {
|
|
8229
|
+
return { ok: false, reason: `required MCP toolset "${toolsetId}" is not configured on this device` };
|
|
8230
|
+
}
|
|
8231
|
+
for (const [serverName, server] of Object.entries(toolset.mcpServers)) {
|
|
8232
|
+
if (Object.prototype.hasOwnProperty.call(servers, serverName)) {
|
|
8233
|
+
return {
|
|
8234
|
+
ok: false,
|
|
8235
|
+
reason: `required MCP toolsets collide on server name "${serverName}"; refusing ambiguous projection`
|
|
8236
|
+
};
|
|
8237
|
+
}
|
|
8238
|
+
servers[serverName] = Object.freeze({
|
|
8239
|
+
command: server.command,
|
|
8240
|
+
...server.args ? { args: Object.freeze([...server.args]) } : {}
|
|
8241
|
+
});
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
if (Object.keys(servers).length === 0) {
|
|
8245
|
+
return { ok: false, reason: "required MCP toolsets resolved to no servers; refusing to run without tools" };
|
|
8246
|
+
}
|
|
8247
|
+
return { ok: true, servers: Object.freeze(servers) };
|
|
8248
|
+
}
|
|
7761
8249
|
async pump(active) {
|
|
7762
8250
|
try {
|
|
7763
8251
|
for await (const event of active.session.events) {
|
|
@@ -7791,11 +8279,38 @@ var TaskRunner = class {
|
|
|
7791
8279
|
if (event.type === "turn_end") {
|
|
7792
8280
|
active.batcher.push(event);
|
|
7793
8281
|
active.batcher.flush();
|
|
8282
|
+
const finalOutput = active.summaryParts.join("");
|
|
8283
|
+
const outcome = await this.resolveResultDocument(active, finalOutput);
|
|
8284
|
+
if (!outcome.deliver) return;
|
|
7794
8285
|
await this.observeGit(active, "completed");
|
|
8286
|
+
if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
|
|
8287
|
+
await this.fail(
|
|
8288
|
+
active.taskId,
|
|
8289
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server stopped advertising the result-document capability before this completion could be sent (a reconnect to an older server), so it would silently discard this document`,
|
|
8290
|
+
false
|
|
8291
|
+
);
|
|
8292
|
+
return;
|
|
8293
|
+
}
|
|
7795
8294
|
this.deps.send(
|
|
7796
8295
|
createEnvelope(
|
|
7797
8296
|
"task.complete",
|
|
7798
|
-
{
|
|
8297
|
+
{
|
|
8298
|
+
summary: finalOutput,
|
|
8299
|
+
sessionRef: active.session.sessionRef,
|
|
8300
|
+
// Spread rather than `document: outcome.document`, so a
|
|
8301
|
+
// completion with no document is the exact same payload it
|
|
8302
|
+
// was before this field existed — not one carrying an
|
|
8303
|
+
// explicit `document: undefined` key.
|
|
8304
|
+
//
|
|
8305
|
+
// `outcome.document` is the protocol's CANONICAL SNAPSHOT
|
|
8306
|
+
// (`checkResultDocument`), never the object the extractor
|
|
8307
|
+
// returned: pure data serializes identically at the root
|
|
8308
|
+
// (where it was measured) and nested inside this payload
|
|
8309
|
+
// (where the codec actually serializes it), so a contextual
|
|
8310
|
+
// `toJSON(key)` or an unstable getter cannot make the wire
|
|
8311
|
+
// bytes differ from what the cap gate approved.
|
|
8312
|
+
...outcome.document !== void 0 ? { document: outcome.document } : {}
|
|
8313
|
+
},
|
|
7799
8314
|
{ taskId: active.taskId, sessionRef: active.session.sessionRef }
|
|
7800
8315
|
)
|
|
7801
8316
|
);
|
|
@@ -8333,6 +8848,102 @@ var TaskRunner = class {
|
|
|
8333
8848
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
|
|
8334
8849
|
await this.finish(taskId);
|
|
8335
8850
|
}
|
|
8851
|
+
/**
|
|
8852
|
+
* additive-minor (`task.complete.document`): the whole daemon-side gate
|
|
8853
|
+
* between a configured {@link ResultDocumentExtractor} and the wire —
|
|
8854
|
+
* called once, from the `turn_end` completion path, immediately before
|
|
8855
|
+
* `task.complete` is built.
|
|
8856
|
+
*
|
|
8857
|
+
* `{deliver: true}` means "go on and send `task.complete`", carrying the
|
|
8858
|
+
* document when there is one. `{deliver: false}` means this method has
|
|
8859
|
+
* ALREADY reported `task.fail` and finished the task; the caller must
|
|
8860
|
+
* return without sending anything further.
|
|
8861
|
+
*
|
|
8862
|
+
* Four fail-closed branches, all `retryable: false` (see
|
|
8863
|
+
* {@link RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX} for why none of them
|
|
8864
|
+
* can succeed on a retry):
|
|
8865
|
+
*
|
|
8866
|
+
* 1. The extractor threw — its error is surfaced, never swallowed.
|
|
8867
|
+
* 2. The extractor returned a thenable, violating the synchronous
|
|
8868
|
+
* contract in the one way that would otherwise ship a wrong answer.
|
|
8869
|
+
* 3. The document is over the cap, not JSON-serializable, or not plain
|
|
8870
|
+
* JSON data, per `checkResultDocument` — the protocol's OWN check,
|
|
8871
|
+
* imported rather than reimplemented, so this gate and the server's
|
|
8872
|
+
* schema validation can never disagree about what is legal.
|
|
8873
|
+
* 4. The connected server never advertised `result-document`. Its
|
|
8874
|
+
* tolerant `z.object()` would silently strip the field on arrival
|
|
8875
|
+
* (`version.ts`'s own flag doc comment), so "send anyway" is not a
|
|
8876
|
+
* degraded-but-working path — it is the task's primary structured
|
|
8877
|
+
* result being deleted in transit with nothing reported anywhere.
|
|
8878
|
+
*
|
|
8879
|
+
* The capability is checked LAST, deliberately: a document that is itself
|
|
8880
|
+
* invalid is the host's own bug and is worth reporting as such even when
|
|
8881
|
+
* the connected server could not have accepted any document at all. It is
|
|
8882
|
+
* then re-checked once more by the caller after its own last await, since
|
|
8883
|
+
* a reconnect can invalidate this answer in between (F3).
|
|
8884
|
+
*
|
|
8885
|
+
* **Residual window (bounded, deliberately not hacked around).** Even the
|
|
8886
|
+
* caller's re-check happens before `ConnectionManager.send` hands the
|
|
8887
|
+
* envelope to a transport, and a queued envelope can outlive the
|
|
8888
|
+
* connection it was queued for: a reconnect between `send()` and the
|
|
8889
|
+
* outbox actually draining could still deliver this `task.complete` to a
|
|
8890
|
+
* rolled-back N-1 server that strips the document. Closing that would
|
|
8891
|
+
* mean teaching the transport outbox to inspect payload semantics and
|
|
8892
|
+
* mint a substitute `task.fail` for a task this runner already finished —
|
|
8893
|
+
* a second authority over terminal outcomes living in the queue, which is
|
|
8894
|
+
* worse than the window it closes. Documented instead, here and in
|
|
8895
|
+
* docs/protocol.md §7.2.
|
|
8896
|
+
*/
|
|
8897
|
+
async resolveResultDocument(active, finalOutput) {
|
|
8898
|
+
const extract = this.deps.resultDocument?.extract;
|
|
8899
|
+
if (!extract) return { deliver: true };
|
|
8900
|
+
let document;
|
|
8901
|
+
try {
|
|
8902
|
+
document = extract(finalOutput, { taskId: active.taskId, sessionRef: active.session.sessionRef });
|
|
8903
|
+
} catch (err) {
|
|
8904
|
+
await this.fail(
|
|
8905
|
+
active.taskId,
|
|
8906
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage3(err)}`,
|
|
8907
|
+
false
|
|
8908
|
+
);
|
|
8909
|
+
return { deliver: false };
|
|
8910
|
+
}
|
|
8911
|
+
if (typeof document?.then === "function") {
|
|
8912
|
+
await this.fail(
|
|
8913
|
+
active.taskId,
|
|
8914
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract returned a promise; the contract is synchronous (an awaited value is never read, and a promise encodes to an empty document)`,
|
|
8915
|
+
false
|
|
8916
|
+
);
|
|
8917
|
+
return { deliver: false };
|
|
8918
|
+
}
|
|
8919
|
+
if (document === void 0) return { deliver: true };
|
|
8920
|
+
const check = checkResultDocument(document);
|
|
8921
|
+
if (!check.ok) {
|
|
8922
|
+
const detail = resultDocumentRejectionDetail(check);
|
|
8923
|
+
await this.fail(active.taskId, `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: ${detail}`, false);
|
|
8924
|
+
return { deliver: false };
|
|
8925
|
+
}
|
|
8926
|
+
if (!this.hasResultDocumentCapability()) {
|
|
8927
|
+
await this.fail(
|
|
8928
|
+
active.taskId,
|
|
8929
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server did not advertise the result-document capability, so it would silently discard this ${check.bytes}-byte document`,
|
|
8930
|
+
false
|
|
8931
|
+
);
|
|
8932
|
+
return { deliver: false };
|
|
8933
|
+
}
|
|
8934
|
+
return { deliver: true, document: check.canonical };
|
|
8935
|
+
}
|
|
8936
|
+
/**
|
|
8937
|
+
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
8938
|
+
* read fresh on every call, never captured, because the answer changes
|
|
8939
|
+
* across a reconnect (`ConnectionManager.getServerCapabilities` returns
|
|
8940
|
+
* `[]` from the moment an acked connection closes until a fresh
|
|
8941
|
+
* `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
|
|
8942
|
+
* "no capabilities", the fail-closed reading.
|
|
8943
|
+
*/
|
|
8944
|
+
hasResultDocumentCapability() {
|
|
8945
|
+
return (this.deps.getServerCapabilities?.() ?? []).includes("result-document");
|
|
8946
|
+
}
|
|
8336
8947
|
async observeGit(active, phase) {
|
|
8337
8948
|
if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
|
|
8338
8949
|
try {
|
|
@@ -8404,7 +9015,7 @@ var TaskRunner = class {
|
|
|
8404
9015
|
}
|
|
8405
9016
|
/** `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. */
|
|
8406
9017
|
async resolveWorkspaceDir(taskId, reuseDir) {
|
|
8407
|
-
const dir = reuseDir ??
|
|
9018
|
+
const dir = reuseDir ?? path20.join(this.deps.workspaceRoot, taskId);
|
|
8408
9019
|
await promises.mkdir(dir, { recursive: true });
|
|
8409
9020
|
return dir;
|
|
8410
9021
|
}
|
|
@@ -8435,7 +9046,7 @@ var TaskRunner = class {
|
|
|
8435
9046
|
* is device-specific (which runtimes happen to be installed here), so a
|
|
8436
9047
|
* different device's installed runtime set might satisfy it.
|
|
8437
9048
|
*/
|
|
8438
|
-
async pickAdapter(requestedRuntime, policyMode) {
|
|
9049
|
+
async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
|
|
8439
9050
|
const allowlist = this.deps.runtimeAllowlist;
|
|
8440
9051
|
if (requestedRuntime) {
|
|
8441
9052
|
if (allowlist && !allowlist.includes(requestedRuntime)) {
|
|
@@ -8456,6 +9067,13 @@ var TaskRunner = class {
|
|
|
8456
9067
|
retryable: false
|
|
8457
9068
|
};
|
|
8458
9069
|
}
|
|
9070
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) {
|
|
9071
|
+
return {
|
|
9072
|
+
ok: false,
|
|
9073
|
+
reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
|
|
9074
|
+
retryable: false
|
|
9075
|
+
};
|
|
9076
|
+
}
|
|
8459
9077
|
const detected = await adapter.detect();
|
|
8460
9078
|
if (!detected.present) {
|
|
8461
9079
|
return {
|
|
@@ -8470,12 +9088,13 @@ var TaskRunner = class {
|
|
|
8470
9088
|
const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
|
|
8471
9089
|
for (const adapter of candidates) {
|
|
8472
9090
|
if (!adapterSupportsMode(adapter, policyMode)) continue;
|
|
9091
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) continue;
|
|
8473
9092
|
const detected = await adapter.detect();
|
|
8474
9093
|
if (detected.present) return { ok: true, adapter };
|
|
8475
9094
|
}
|
|
8476
9095
|
return {
|
|
8477
9096
|
ok: false,
|
|
8478
|
-
reason: `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
9097
|
+
reason: requiresMcpToolsets ? `no available runtime on this device can express permission mode "${policyMode}" with required MCP toolsets` : `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
8479
9098
|
retryable: true
|
|
8480
9099
|
};
|
|
8481
9100
|
}
|
|
@@ -8506,7 +9125,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
|
|
|
8506
9125
|
bytes,
|
|
8507
9126
|
bytesHash: journalHash(bytes),
|
|
8508
9127
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8509
|
-
opensTask: envelope.type === "task.offer"
|
|
9128
|
+
opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets"
|
|
8510
9129
|
};
|
|
8511
9130
|
}
|
|
8512
9131
|
function isRuntimeId(id) {
|
|
@@ -8530,29 +9149,216 @@ function computeCapabilities(adapters) {
|
|
|
8530
9149
|
if (adapters.some((adapter) => adapter.capabilities().steer)) flags.push("steer");
|
|
8531
9150
|
flags.push("blob-upload");
|
|
8532
9151
|
flags.push("approval-targeting");
|
|
9152
|
+
const selectionAdapters = adapters.filter(
|
|
9153
|
+
(adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
|
|
9154
|
+
);
|
|
9155
|
+
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
|
|
9156
|
+
flags.push("dispatch-selection");
|
|
9157
|
+
}
|
|
9158
|
+
if (adapters.some((adapter) => adapter.capabilities().mcpToolsets === true)) {
|
|
9159
|
+
flags.push("toolset-selection");
|
|
9160
|
+
}
|
|
8533
9161
|
return flags;
|
|
8534
9162
|
}
|
|
8535
9163
|
var ALL_RUNTIME_IDS = ["pi", "claude", "codex"];
|
|
8536
|
-
function buildAdapter(id) {
|
|
9164
|
+
function buildAdapter(id, config) {
|
|
8537
9165
|
switch (id) {
|
|
8538
9166
|
case "pi":
|
|
8539
|
-
return new PiAdapter();
|
|
9167
|
+
return new PiAdapter({ byokLauncher: config.piByokLauncher });
|
|
8540
9168
|
case "claude":
|
|
8541
9169
|
return new ClaudeAdapter();
|
|
8542
9170
|
case "codex":
|
|
8543
9171
|
return new CodexAdapter();
|
|
8544
9172
|
}
|
|
8545
9173
|
}
|
|
8546
|
-
function buildDefaultAdapters(
|
|
8547
|
-
const ids = runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => runtimeAllowlist
|
|
8548
|
-
return ids.map(buildAdapter);
|
|
9174
|
+
function buildDefaultAdapters(config) {
|
|
9175
|
+
const ids = config.runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => config.runtimeAllowlist?.includes(id)) : ALL_RUNTIME_IDS;
|
|
9176
|
+
return ids.map((id) => buildAdapter(id, config));
|
|
9177
|
+
}
|
|
9178
|
+
function validatePiByokLauncherConfig(launcher) {
|
|
9179
|
+
for (const [field, value] of [
|
|
9180
|
+
["command", launcher.command],
|
|
9181
|
+
["profileDbPath", launcher.profileDbPath],
|
|
9182
|
+
["sessionDir", launcher.sessionDir]
|
|
9183
|
+
]) {
|
|
9184
|
+
if (value.trim().length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
9185
|
+
throw new Error(`DaemonConfig.piByokLauncher.${field} must be a non-empty single-line string`);
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
9188
|
+
if (!isAbsolute(launcher.profileDbPath) || !isAbsolute(launcher.sessionDir)) {
|
|
9189
|
+
throw new Error(
|
|
9190
|
+
"DaemonConfig.piByokLauncher profileDbPath and sessionDir must be absolute paths"
|
|
9191
|
+
);
|
|
9192
|
+
}
|
|
9193
|
+
if (launcher.secretServicePrefix !== void 0 && (launcher.secretServicePrefix.trim().length === 0 || /[\u0000\r\n]/u.test(launcher.secretServicePrefix))) {
|
|
9194
|
+
throw new Error(
|
|
9195
|
+
"DaemonConfig.piByokLauncher.secretServicePrefix must be a non-empty single-line string"
|
|
9196
|
+
);
|
|
9197
|
+
}
|
|
9198
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
9199
|
+
"--",
|
|
9200
|
+
"--pi-bin",
|
|
9201
|
+
"--profile-db",
|
|
9202
|
+
"--session-dir",
|
|
9203
|
+
"--secret-service-prefix",
|
|
9204
|
+
"--provider",
|
|
9205
|
+
"--model"
|
|
9206
|
+
]);
|
|
9207
|
+
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
9208
|
+
if (conflicting !== void 0) {
|
|
9209
|
+
throw new Error(
|
|
9210
|
+
`DaemonConfig.piByokLauncher.args must not override reserved launcher argument ${conflicting}`
|
|
9211
|
+
);
|
|
9212
|
+
}
|
|
9213
|
+
const invalidArg = launcher.args?.find((arg) => arg.length === 0 || /[\u0000\r\n]/u.test(arg));
|
|
9214
|
+
if (invalidArg !== void 0) {
|
|
9215
|
+
throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
|
|
9216
|
+
}
|
|
9217
|
+
}
|
|
9218
|
+
var MAX_LOCAL_MCP_TOOLSETS = 64;
|
|
9219
|
+
var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
|
|
9220
|
+
var MAX_LOCAL_MCP_ARGS = 64;
|
|
9221
|
+
var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
|
|
9222
|
+
function isNonEmptySingleLine(value) {
|
|
9223
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_LOCAL_MCP_TOKEN_CHARS && !/[\u0000\r\n]/u.test(value);
|
|
9224
|
+
}
|
|
9225
|
+
function resolveMcpToolsets(configured) {
|
|
9226
|
+
if (configured === void 0) return void 0;
|
|
9227
|
+
if (configured === null || typeof configured !== "object" || Array.isArray(configured)) {
|
|
9228
|
+
throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
|
|
9229
|
+
}
|
|
9230
|
+
const toolsetEntries = Object.entries(configured);
|
|
9231
|
+
if (toolsetEntries.length > MAX_LOCAL_MCP_TOOLSETS) {
|
|
9232
|
+
throw new Error(`DaemonConfig.mcpToolsets may contain at most ${MAX_LOCAL_MCP_TOOLSETS} toolsets`);
|
|
9233
|
+
}
|
|
9234
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
9235
|
+
for (const [toolsetId, rawToolset] of toolsetEntries) {
|
|
9236
|
+
const parsedId = ToolsetIdSchema.safeParse(toolsetId);
|
|
9237
|
+
if (!parsedId.success) {
|
|
9238
|
+
throw new Error(`DaemonConfig.mcpToolsets contains invalid toolset id ${JSON.stringify(toolsetId)}`);
|
|
9239
|
+
}
|
|
9240
|
+
if (rawToolset === null || typeof rawToolset !== "object" || Array.isArray(rawToolset)) {
|
|
9241
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} must be an object`);
|
|
9242
|
+
}
|
|
9243
|
+
const toolsetKeys = Object.keys(rawToolset);
|
|
9244
|
+
if (toolsetKeys.some((key) => key !== "mcpServers")) {
|
|
9245
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} accepts only the mcpServers field`);
|
|
9246
|
+
}
|
|
9247
|
+
const rawServers = rawToolset.mcpServers;
|
|
9248
|
+
if (rawServers === null || typeof rawServers !== "object" || Array.isArray(rawServers)) {
|
|
9249
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must be an object`);
|
|
9250
|
+
}
|
|
9251
|
+
const serverEntries = Object.entries(rawServers);
|
|
9252
|
+
if (serverEntries.length === 0 || serverEntries.length > MAX_LOCAL_MCP_SERVERS_PER_TOOLSET) {
|
|
9253
|
+
throw new Error(
|
|
9254
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must contain 1-${MAX_LOCAL_MCP_SERVERS_PER_TOOLSET} servers`
|
|
9255
|
+
);
|
|
9256
|
+
}
|
|
9257
|
+
const servers = {};
|
|
9258
|
+
for (const [serverName, rawServer] of serverEntries) {
|
|
9259
|
+
if (!ToolsetIdSchema.safeParse(serverName).success) {
|
|
9260
|
+
throw new Error(
|
|
9261
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers contains invalid server name ${JSON.stringify(serverName)}`
|
|
9262
|
+
);
|
|
9263
|
+
}
|
|
9264
|
+
if (serverName === APPROVAL_MCP_SERVER_NAME) {
|
|
9265
|
+
throw new Error(
|
|
9266
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} uses a server name reserved by the daemon`
|
|
9267
|
+
);
|
|
9268
|
+
}
|
|
9269
|
+
if (rawServer === null || typeof rawServer !== "object" || Array.isArray(rawServer)) {
|
|
9270
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} must be an object`);
|
|
9271
|
+
}
|
|
9272
|
+
const serverKeys = Object.keys(rawServer);
|
|
9273
|
+
if (serverKeys.some((key) => key !== "command" && key !== "args")) {
|
|
9274
|
+
throw new Error(
|
|
9275
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} accepts only command and args; env, headers, and remote task data are not supported`
|
|
9276
|
+
);
|
|
9277
|
+
}
|
|
9278
|
+
const server = rawServer;
|
|
9279
|
+
if (!isNonEmptySingleLine(server.command)) {
|
|
9280
|
+
throw new Error(
|
|
9281
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.command must be a non-empty single-line string no longer than ${MAX_LOCAL_MCP_TOKEN_CHARS} characters`
|
|
9282
|
+
);
|
|
9283
|
+
}
|
|
9284
|
+
if (server.args !== void 0 && !Array.isArray(server.args)) {
|
|
9285
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must be an array`);
|
|
9286
|
+
}
|
|
9287
|
+
const args = server.args ?? [];
|
|
9288
|
+
if (args.length > MAX_LOCAL_MCP_ARGS || args.some((arg) => !isNonEmptySingleLine(arg))) {
|
|
9289
|
+
throw new Error(
|
|
9290
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must contain at most ${MAX_LOCAL_MCP_ARGS} non-empty single-line strings`
|
|
9291
|
+
);
|
|
9292
|
+
}
|
|
9293
|
+
servers[serverName] = Object.freeze({
|
|
9294
|
+
command: server.command,
|
|
9295
|
+
...args.length > 0 ? { args: Object.freeze([...args]) } : {}
|
|
9296
|
+
});
|
|
9297
|
+
}
|
|
9298
|
+
resolved.set(toolsetId, Object.freeze({ mcpServers: Object.freeze(servers) }));
|
|
9299
|
+
}
|
|
9300
|
+
return resolved;
|
|
9301
|
+
}
|
|
9302
|
+
function resolveDeviceAssertionAudiences(config) {
|
|
9303
|
+
if (config === void 0) return void 0;
|
|
9304
|
+
if (!Array.isArray(config.audiences)) {
|
|
9305
|
+
throw new Error(
|
|
9306
|
+
`DaemonConfig.deviceAssertion.audiences must be an array of exact audience strings \u2014 got ${JSON.stringify(config.audiences)}. Omit the deviceAssertion section (or pass an empty array) to leave the assertion broker disabled.`
|
|
9307
|
+
);
|
|
9308
|
+
}
|
|
9309
|
+
if (config.audiences.length === 0) return void 0;
|
|
9310
|
+
const audiences = /* @__PURE__ */ new Set();
|
|
9311
|
+
for (const audience of config.audiences) {
|
|
9312
|
+
if (typeof audience !== "string" || audience.length === 0) {
|
|
9313
|
+
throw new Error(
|
|
9314
|
+
`DaemonConfig.deviceAssertion.audiences entries must be non-empty strings \u2014 got ${JSON.stringify(audience)}`
|
|
9315
|
+
);
|
|
9316
|
+
}
|
|
9317
|
+
if (Buffer.byteLength(audience, "utf8") > DEVICE_ASSERTION_AUDIENCE_MAX_BYTES) {
|
|
9318
|
+
throw new Error(
|
|
9319
|
+
`DaemonConfig.deviceAssertion.audiences entry ${JSON.stringify(audience)} exceeds ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
9320
|
+
);
|
|
9321
|
+
}
|
|
9322
|
+
if (audiences.has(audience)) {
|
|
9323
|
+
throw new Error(
|
|
9324
|
+
`DaemonConfig.deviceAssertion.audiences contains ${JSON.stringify(audience)} twice \u2014 rejected rather than de-duplicated, because a duplicate is usually a copy-paste that hid a typo in the entry that was meant to be different`
|
|
9325
|
+
);
|
|
9326
|
+
}
|
|
9327
|
+
audiences.add(audience);
|
|
9328
|
+
}
|
|
9329
|
+
return audiences;
|
|
9330
|
+
}
|
|
9331
|
+
function resolveDeviceAssertionTtlMs(config) {
|
|
9332
|
+
const ttlMs = config?.ttlMs ?? DEVICE_ASSERTION_DEFAULT_TTL_MS;
|
|
9333
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_ASSERTION_MAX_TTL_MS) {
|
|
9334
|
+
throw new Error(
|
|
9335
|
+
`DaemonConfig.deviceAssertion.ttlMs must be a positive integer no greater than ${DEVICE_ASSERTION_MAX_TTL_MS} ms \u2014 got ${JSON.stringify(config?.ttlMs)}`
|
|
9336
|
+
);
|
|
9337
|
+
}
|
|
9338
|
+
return ttlMs;
|
|
8549
9339
|
}
|
|
8550
9340
|
function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
9341
|
+
return buildDaemonWithAdapters(config, adapters, overrides);
|
|
9342
|
+
}
|
|
9343
|
+
function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
|
|
9344
|
+
const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
|
|
9345
|
+
if (config.piByokLauncher !== void 0) {
|
|
9346
|
+
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
9347
|
+
}
|
|
8551
9348
|
if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
|
|
8552
9349
|
throw new Error(
|
|
8553
9350
|
`DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
|
|
8554
9351
|
);
|
|
8555
9352
|
}
|
|
9353
|
+
const presenceCadence = {
|
|
9354
|
+
intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
|
|
9355
|
+
ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
|
|
9356
|
+
minimumIntervalMs: config.presence?.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS
|
|
9357
|
+
};
|
|
9358
|
+
assertPresenceHeartbeatCadence(presenceCadence);
|
|
9359
|
+
const deviceAssertionAudiences = resolveDeviceAssertionAudiences(config.deviceAssertion);
|
|
9360
|
+
const deviceAssertionTtlMs = resolveDeviceAssertionTtlMs(config.deviceAssertion);
|
|
9361
|
+
let shuttingDown = false;
|
|
8556
9362
|
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
8557
9363
|
const store = new DeviceStore(storeDir);
|
|
8558
9364
|
const operationalHealth = new OperationalHealthTracker(storeDir);
|
|
@@ -8643,6 +9449,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8643
9449
|
let runner;
|
|
8644
9450
|
let controlServerHandle;
|
|
8645
9451
|
let daemonOwnerLease;
|
|
9452
|
+
let presencePublisher;
|
|
9453
|
+
let presenceDiscovery;
|
|
9454
|
+
let presenceDiscoveryInFlight = false;
|
|
8646
9455
|
let shutdownPromise;
|
|
8647
9456
|
const pendingLateMutationBarriers = /* @__PURE__ */ new Set();
|
|
8648
9457
|
let startedAt;
|
|
@@ -8815,6 +9624,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8815
9624
|
deviceId: record.deviceId,
|
|
8816
9625
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
8817
9626
|
runtimeEnvironment: config.runtimeEnvironment,
|
|
9627
|
+
...mcpToolsets ? { mcpToolsets } : {},
|
|
8818
9628
|
// M3-2a: `send` is already this file's OWN closure (not something
|
|
8819
9629
|
// `TaskRunner` builds) — every `task.claim`/`task.started`/
|
|
8820
9630
|
// `task.progress`/`task.artifact`/`task.await_approval`/
|
|
@@ -8854,6 +9664,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8854
9664
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
8855
9665
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
8856
9666
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
9667
|
+
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
9668
|
+
// absent when unconfigured — see `DaemonConfig.resultDocument`'s own
|
|
9669
|
+
// doc comment. Spread rather than assigned so an unconfigured daemon
|
|
9670
|
+
// builds the exact `deps` object it did before this seam existed.
|
|
9671
|
+
...config.resultDocument ? { resultDocument: config.resultDocument } : {},
|
|
8857
9672
|
// M4 Phase 3 hardening: bridges TaskRunner's stale-approval-race
|
|
8858
9673
|
// finding out to the SAME local observability seam every other
|
|
8859
9674
|
// daemon-local event already uses (see observer.ts's own module doc
|
|
@@ -8929,8 +9744,10 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8929
9744
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
8930
9745
|
},
|
|
8931
9746
|
onStateChange: (state) => {
|
|
9747
|
+
const wasSettled = connectionState === "open" || connectionState === "degraded";
|
|
8932
9748
|
connectionState = state;
|
|
8933
9749
|
observer.noteConnectionState(state);
|
|
9750
|
+
if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
|
|
8934
9751
|
},
|
|
8935
9752
|
backoff: overrides.backoff,
|
|
8936
9753
|
liveness: overrides.liveness,
|
|
@@ -8948,6 +9765,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8948
9765
|
});
|
|
8949
9766
|
await connection.start();
|
|
8950
9767
|
await connection.waitForAck();
|
|
9768
|
+
startPresenceProducer();
|
|
8951
9769
|
} catch (err) {
|
|
8952
9770
|
try {
|
|
8953
9771
|
await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
|
|
@@ -8957,7 +9775,41 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8957
9775
|
throw err;
|
|
8958
9776
|
}
|
|
8959
9777
|
}
|
|
9778
|
+
function startPresenceProducer() {
|
|
9779
|
+
presenceDiscovery = new AbortController();
|
|
9780
|
+
runPresenceDiscovery();
|
|
9781
|
+
}
|
|
9782
|
+
function runPresenceDiscovery() {
|
|
9783
|
+
const discovery = presenceDiscovery;
|
|
9784
|
+
if (!discovery || presenceDiscoveryInFlight) return;
|
|
9785
|
+
presenceDiscoveryInFlight = true;
|
|
9786
|
+
void (async () => {
|
|
9787
|
+
try {
|
|
9788
|
+
const declaration = await fetchCapabilityDeclaration(config.serverUrl, { signal: discovery.signal });
|
|
9789
|
+
if (discovery.signal.aborted) return;
|
|
9790
|
+
if (declares(declaration, PRESENCE_HINTS_CAPABILITY)) {
|
|
9791
|
+
presencePublisher ??= new PresencePublisher({
|
|
9792
|
+
serverUrl: config.serverUrl,
|
|
9793
|
+
auth,
|
|
9794
|
+
...presenceCadence,
|
|
9795
|
+
onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
|
|
9796
|
+
});
|
|
9797
|
+
presencePublisher.start();
|
|
9798
|
+
} else {
|
|
9799
|
+
presencePublisher?.stop();
|
|
9800
|
+
}
|
|
9801
|
+
} catch (err) {
|
|
9802
|
+
if (discovery.signal.aborted) return;
|
|
9803
|
+
console.warn(
|
|
9804
|
+
`[byok/client] capability discovery failed; presence publishing stays off until the next reconnect: ${err instanceof Error ? err.message : String(err)}`
|
|
9805
|
+
);
|
|
9806
|
+
} finally {
|
|
9807
|
+
presenceDiscoveryInFlight = false;
|
|
9808
|
+
}
|
|
9809
|
+
})();
|
|
9810
|
+
}
|
|
8960
9811
|
async function runShutdownSequence(reason, opts = {}) {
|
|
9812
|
+
shuttingDown = true;
|
|
8961
9813
|
const errors = [];
|
|
8962
9814
|
let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
|
|
8963
9815
|
if (!hostedStorageInitializationBarrierComplete) {
|
|
@@ -8986,6 +9838,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8986
9838
|
errors.push(new Error("a prior active task teardown remains unsettled; ownership lease retained"));
|
|
8987
9839
|
}
|
|
8988
9840
|
}
|
|
9841
|
+
presenceDiscovery?.abort();
|
|
9842
|
+
presenceDiscovery = void 0;
|
|
9843
|
+
presenceDiscoveryInFlight = false;
|
|
9844
|
+
presencePublisher?.stop();
|
|
9845
|
+
presencePublisher = void 0;
|
|
8989
9846
|
const stoppingOwnedPressureEngine = ownedPressureEngine;
|
|
8990
9847
|
const stoppingOwnedJournal = ownedJournal;
|
|
8991
9848
|
const maintenanceStopped = stoppingOwnedPressureEngine?.stop() ?? Promise.resolve();
|
|
@@ -9046,6 +9903,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9046
9903
|
}
|
|
9047
9904
|
}
|
|
9048
9905
|
async function stop(opts = {}) {
|
|
9906
|
+
shuttingDown = true;
|
|
9049
9907
|
await requestShutdown(opts.reason ?? "operator", { drainTimeoutMs: opts.drainTimeoutMs });
|
|
9050
9908
|
}
|
|
9051
9909
|
function requestShutdown(reason, opts = {}) {
|
|
@@ -9058,6 +9916,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9058
9916
|
return current;
|
|
9059
9917
|
}
|
|
9060
9918
|
async function unpair() {
|
|
9919
|
+
shuttingDown = true;
|
|
9061
9920
|
await runLifecycleMutation(unpairUnderLease);
|
|
9062
9921
|
}
|
|
9063
9922
|
async function unpairUnderLease() {
|
|
@@ -9136,6 +9995,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9136
9995
|
}
|
|
9137
9996
|
async function performControlShutdown(reason) {
|
|
9138
9997
|
const effectiveReason = reason ?? "operator";
|
|
9998
|
+
shuttingDown = true;
|
|
9139
9999
|
observer.noteShutdownRequested(effectiveReason);
|
|
9140
10000
|
try {
|
|
9141
10001
|
await requestShutdown(`control socket shutdown (${effectiveReason})`);
|
|
@@ -9173,7 +10033,107 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9173
10033
|
if (!runner) throw new ControlError("not_found", "daemon is not started");
|
|
9174
10034
|
return runner.requestApproval(parsed.taskId, parsed.summary);
|
|
9175
10035
|
},
|
|
10036
|
+
/**
|
|
10037
|
+
* Plan `device-assertion-broker`: mint one short-lived, audience-scoped
|
|
10038
|
+
* device assertion for a sibling local process.
|
|
10039
|
+
*
|
|
10040
|
+
* SIX fail-closed gates, in this exact order, none of which signs
|
|
10041
|
+
* anything on the way out. The order is part of the contract, not an
|
|
10042
|
+
* implementation detail:
|
|
10043
|
+
*
|
|
10044
|
+
* 1. `assertion_disabled` — before anything else, because a daemon that
|
|
10045
|
+
* was never configured for this must not reveal, by answering
|
|
10046
|
+
* differently for different inputs, that it even validates params.
|
|
10047
|
+
* 2. `bad_request` — shape/length, checked before the allowlist so a
|
|
10048
|
+
* malformed request cannot be used to probe membership.
|
|
10049
|
+
* 3. `audience_denied` — EXACT `Set.has`, never a prefix/suffix rule
|
|
10050
|
+
* (`salesko-api.evil.com` and `salesko-ap` both fail against an entry
|
|
10051
|
+
* of `salesko-api`). The message deliberately does not echo the
|
|
10052
|
+
* allowlist: a refusal must not be an enumeration oracle.
|
|
10053
|
+
* 4. `shutting_down` — see `performControlShutdown`'s own comment for
|
|
10054
|
+
* the minting window this closes.
|
|
10055
|
+
* 5. `revoked` — the server-side revocation this daemon already knows
|
|
10056
|
+
* about.
|
|
10057
|
+
* 6. `not_paired` — the on-disk record, re-read on EVERY call (never
|
|
10058
|
+
* cached), so clearing `device.json` removes local signing authority
|
|
10059
|
+
* immediately.
|
|
10060
|
+
*
|
|
10061
|
+
* Only after all six does the private key get imported, used once, and
|
|
10062
|
+
* dropped (`device-assertion-signer.ts`).
|
|
10063
|
+
*
|
|
10064
|
+
* Honest limit, and it must stay in the docs as well as here: gates 4-6
|
|
10065
|
+
* are only HALF of revocation. They make this daemon stop minting
|
|
10066
|
+
* promptly, but an assertion already in a caller's hands is not recalled
|
|
10067
|
+
* by any of them. The other half is the host's own recheck at exchange
|
|
10068
|
+
* time, which is why core's `verifyDeviceAssertion` makes the device
|
|
10069
|
+
* row's `revoked` state a REQUIRED parameter. Nothing here entitles
|
|
10070
|
+
* anyone to claim this daemon delivers synchronous invalidation on its
|
|
10071
|
+
* own.
|
|
10072
|
+
*/
|
|
10073
|
+
"assertion.issue": async (params) => {
|
|
10074
|
+
if (deviceAssertionAudiences === void 0) {
|
|
10075
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "assertion_disabled" });
|
|
10076
|
+
throw new ControlError(
|
|
10077
|
+
"assertion_disabled",
|
|
10078
|
+
"this daemon is not configured to issue device assertions (DaemonConfig.deviceAssertion.audiences is absent or empty)"
|
|
10079
|
+
);
|
|
10080
|
+
}
|
|
10081
|
+
const parsed = parseAssertionIssueParams(params);
|
|
10082
|
+
if (!parsed) {
|
|
10083
|
+
const rawAudience = typeof params === "object" && params !== null && typeof params.audience === "string" ? params.audience : void 0;
|
|
10084
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "bad_request", audience: rawAudience });
|
|
10085
|
+
throw new ControlError(
|
|
10086
|
+
"bad_request",
|
|
10087
|
+
`assertion.issue requires exactly {audience} where audience is a non-empty string of at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
10088
|
+
);
|
|
10089
|
+
}
|
|
10090
|
+
if (!deviceAssertionAudiences.has(parsed.audience)) {
|
|
10091
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "audience_denied", audience: parsed.audience });
|
|
10092
|
+
throw new ControlError("audience_denied", "the requested audience is not allowed by this daemon");
|
|
10093
|
+
}
|
|
10094
|
+
if (shuttingDown) {
|
|
10095
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10096
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10097
|
+
}
|
|
10098
|
+
if (auth.isRevoked()) {
|
|
10099
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10100
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10101
|
+
}
|
|
10102
|
+
const record = await store.load();
|
|
10103
|
+
if (record === void 0) {
|
|
10104
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "not_paired", audience: parsed.audience });
|
|
10105
|
+
throw new ControlError("not_paired", "this device is not paired; nothing can be asserted about it");
|
|
10106
|
+
}
|
|
10107
|
+
if (shuttingDown) {
|
|
10108
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10109
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10110
|
+
}
|
|
10111
|
+
if (auth.isRevoked()) {
|
|
10112
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10113
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10114
|
+
}
|
|
10115
|
+
const minted = mintDeviceAssertion({
|
|
10116
|
+
record,
|
|
10117
|
+
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
10118
|
+
// (ws:->http:, wss:->https:, path stripped), so an operator who
|
|
10119
|
+
// configured the websocket spelling and one who configured the HTTP
|
|
10120
|
+
// spelling of the same deployment produce the same issuer.
|
|
10121
|
+
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
10122
|
+
productId: config.productId,
|
|
10123
|
+
audience: parsed.audience,
|
|
10124
|
+
ttlMs: deviceAssertionTtlMs,
|
|
10125
|
+
now: /* @__PURE__ */ new Date()
|
|
10126
|
+
});
|
|
10127
|
+
observer.noteDeviceAssertion({
|
|
10128
|
+
result: "issued",
|
|
10129
|
+
audience: minted.claims.audience,
|
|
10130
|
+
jti: minted.claims.jti,
|
|
10131
|
+
expiresAt: minted.expiresAt
|
|
10132
|
+
});
|
|
10133
|
+
return { assertion: minted.envelope, expiresAt: minted.expiresAt };
|
|
10134
|
+
},
|
|
9176
10135
|
shutdown: (params) => {
|
|
10136
|
+
shuttingDown = true;
|
|
9177
10137
|
const { reason } = parseShutdownParams(params);
|
|
9178
10138
|
setImmediate(() => {
|
|
9179
10139
|
void performControlShutdown(reason).catch((err) => {
|
|
@@ -9218,299 +10178,350 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9218
10178
|
return { pair, start, stop, status, subscribe, tasks, unpair, approve, reject };
|
|
9219
10179
|
}
|
|
9220
10180
|
function createDaemon(config) {
|
|
9221
|
-
return createDaemonWithAdapters(config, buildDefaultAdapters(config
|
|
9222
|
-
}
|
|
9223
|
-
|
|
9224
|
-
// src/lifecycle/service-types.ts
|
|
9225
|
-
function nodeAgentProgram(opts) {
|
|
9226
|
-
const program = {
|
|
9227
|
-
command: opts.nodeBin ?? process.execPath,
|
|
9228
|
-
args: [opts.agentBin, "start", "--config", opts.configPath]
|
|
9229
|
-
};
|
|
9230
|
-
if (opts.cwd !== void 0) program.cwd = opts.cwd;
|
|
9231
|
-
return program;
|
|
9232
|
-
}
|
|
9233
|
-
function sanitizeServiceName(name) {
|
|
9234
|
-
const cleaned = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
9235
|
-
const safe = cleaned.replace(/^-+/, "");
|
|
9236
|
-
if (!safe) {
|
|
9237
|
-
throw new Error(`service name "${name}" has no valid characters left after sanitizing (allowed: letters, digits, ".", "-", "_"; cannot consist only of leading "-")`);
|
|
9238
|
-
}
|
|
9239
|
-
return safe;
|
|
9240
|
-
}
|
|
9241
|
-
|
|
9242
|
-
// src/lifecycle/launchd.ts
|
|
9243
|
-
var LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
9244
|
-
/operation not permitted/i,
|
|
9245
|
-
/could not find domain/i,
|
|
9246
|
-
/permission denied/i,
|
|
9247
|
-
/access denied/i
|
|
9248
|
-
];
|
|
9249
|
-
var LAUNCHD_NOT_LOADED = {
|
|
9250
|
-
patterns: [/no such process/i, /could not find (specified )?service/i, /not loaded/i],
|
|
9251
|
-
neverAbsence: LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9252
|
-
};
|
|
9253
|
-
function escapeXml(value) {
|
|
9254
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10181
|
+
return createDaemonWithAdapters(config, buildDefaultAdapters(config));
|
|
9255
10182
|
}
|
|
9256
|
-
|
|
9257
|
-
|
|
10183
|
+
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
10184
|
+
function errorMessage4(err) {
|
|
10185
|
+
return err instanceof Error ? err.message : String(err);
|
|
9258
10186
|
}
|
|
9259
|
-
function
|
|
9260
|
-
|
|
9261
|
-
const args = [program.command, ...program.args];
|
|
9262
|
-
const cwd = program.cwd ?? os.homedir();
|
|
9263
|
-
const outLog = path19.join(logDir, `${label}.out.log`);
|
|
9264
|
-
const errLog = path19.join(logDir, `${label}.err.log`);
|
|
9265
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
9266
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9267
|
-
<plist version="1.0">
|
|
9268
|
-
<dict>
|
|
9269
|
-
<key>Label</key>
|
|
9270
|
-
${plistString(label)}
|
|
9271
|
-
<key>ProgramArguments</key>
|
|
9272
|
-
<array>
|
|
9273
|
-
${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
9274
|
-
</array>
|
|
9275
|
-
<key>WorkingDirectory</key>
|
|
9276
|
-
${plistString(cwd)}
|
|
9277
|
-
<key>RunAtLoad</key>
|
|
9278
|
-
<true/>
|
|
9279
|
-
<key>KeepAlive</key>
|
|
9280
|
-
<dict>
|
|
9281
|
-
<key>SuccessfulExit</key>
|
|
9282
|
-
<false/>
|
|
9283
|
-
</dict>
|
|
9284
|
-
<key>ThrottleInterval</key>
|
|
9285
|
-
<integer>10</integer>
|
|
9286
|
-
<key>StandardOutPath</key>
|
|
9287
|
-
${plistString(outLog)}
|
|
9288
|
-
<key>StandardErrorPath</key>
|
|
9289
|
-
${plistString(errLog)}
|
|
9290
|
-
</dict>
|
|
9291
|
-
</plist>
|
|
9292
|
-
`;
|
|
10187
|
+
function sameFileState3(left, right) {
|
|
10188
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
9293
10189
|
}
|
|
9294
|
-
function
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
if (
|
|
9300
|
-
|
|
9301
|
-
}
|
|
9302
|
-
return process.getuid();
|
|
9303
|
-
});
|
|
9304
|
-
const label = sanitizeServiceName(def.name);
|
|
9305
|
-
const plistPath = () => path19.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
9306
|
-
const domainTarget = () => `gui/${getuid()}`;
|
|
9307
|
-
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
9308
|
-
async function fileExists(p) {
|
|
9309
|
-
try {
|
|
9310
|
-
await fs19.stat(p);
|
|
9311
|
-
return true;
|
|
9312
|
-
} catch {
|
|
9313
|
-
return false;
|
|
9314
|
-
}
|
|
9315
|
-
}
|
|
9316
|
-
async function writePlist(program) {
|
|
9317
|
-
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
9318
|
-
await fs19.mkdir(path19.dirname(plistPath()), { recursive: true });
|
|
9319
|
-
await fs19.mkdir(def.logDir, { recursive: true });
|
|
9320
|
-
await fs19.writeFile(plistPath(), xml, "utf8");
|
|
10190
|
+
async function readControlToken(tokenPath) {
|
|
10191
|
+
let namedBefore;
|
|
10192
|
+
try {
|
|
10193
|
+
namedBefore = await promises.lstat(tokenPath, { bigint: true });
|
|
10194
|
+
} catch (err) {
|
|
10195
|
+
if (err.code === "ENOENT") return void 0;
|
|
10196
|
+
throw err;
|
|
9321
10197
|
}
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
await run("launchctl", ["bootout", serviceTarget()]);
|
|
9325
|
-
await runOrThrow(run, "launchctl", ["bootstrap", domainTarget(), plistPath()], "launchctl bootstrap");
|
|
9326
|
-
await runOrThrow(run, "launchctl", ["enable", serviceTarget()], "launchctl enable");
|
|
9327
|
-
await run("launchctl", ["kickstart", "-k", serviceTarget()]);
|
|
10198
|
+
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
10199
|
+
throw new Error("control token is not a real regular file");
|
|
9328
10200
|
}
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
10201
|
+
const handle = await promises.open(
|
|
10202
|
+
tokenPath,
|
|
10203
|
+
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
10204
|
+
);
|
|
10205
|
+
try {
|
|
10206
|
+
const opened = await handle.stat({ bigint: true });
|
|
10207
|
+
const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
|
|
10208
|
+
if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
|
|
10209
|
+
throw new Error("control token pathname changed before safe open");
|
|
10210
|
+
}
|
|
10211
|
+
if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
|
|
10212
|
+
throw new Error("control token exceeds the bounded read limit");
|
|
10213
|
+
}
|
|
10214
|
+
const size = Number(opened.size);
|
|
10215
|
+
const bytes = Buffer.alloc(size);
|
|
10216
|
+
const { bytesRead } = await handle.read(bytes, 0, size, 0);
|
|
10217
|
+
const afterRead = await handle.stat({ bigint: true });
|
|
10218
|
+
const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
|
|
10219
|
+
if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
|
|
10220
|
+
throw new Error("control token changed during bounded read");
|
|
10221
|
+
}
|
|
10222
|
+
return bytes.toString("utf8").trim();
|
|
10223
|
+
} finally {
|
|
10224
|
+
await handle.close();
|
|
9332
10225
|
}
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
10226
|
+
}
|
|
10227
|
+
async function connectControlClient(opts) {
|
|
10228
|
+
const tokenPath = controlTokenPath(opts.storeDir);
|
|
10229
|
+
let token;
|
|
10230
|
+
try {
|
|
10231
|
+
const read = await readControlToken(tokenPath);
|
|
10232
|
+
if (read === void 0) {
|
|
10233
|
+
return { ok: false, reason: "daemon is not running (no control.token found)" };
|
|
9336
10234
|
}
|
|
9337
|
-
|
|
9338
|
-
|
|
10235
|
+
token = read;
|
|
10236
|
+
} catch (err) {
|
|
10237
|
+
return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
|
|
9339
10238
|
}
|
|
9340
|
-
|
|
9341
|
-
|
|
10239
|
+
if (!token) {
|
|
10240
|
+
return { ok: false, reason: "control token file is empty" };
|
|
9342
10241
|
}
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
const
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
return { installed, running, determinate, detail };
|
|
10242
|
+
const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
|
|
10243
|
+
try {
|
|
10244
|
+
const client = await connectAndHandshake(endpoint, token, opts);
|
|
10245
|
+
return { ok: true, client };
|
|
10246
|
+
} catch (err) {
|
|
10247
|
+
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage4(err)}` };
|
|
9350
10248
|
}
|
|
9351
|
-
return { install, uninstall, start, stop, status };
|
|
9352
10249
|
}
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
function
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
10250
|
+
function connectAndHandshake(endpoint, token, opts) {
|
|
10251
|
+
return new Promise((resolve, reject) => {
|
|
10252
|
+
const socket = net.createConnection(endpoint);
|
|
10253
|
+
const reader = new NdjsonLineReader();
|
|
10254
|
+
let phase = "server-hello";
|
|
10255
|
+
let settled = false;
|
|
10256
|
+
const clientNonce = randomNonceHex();
|
|
10257
|
+
const timer = setTimeout(() => {
|
|
10258
|
+
fail(new Error("handshake timed out"));
|
|
10259
|
+
}, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
|
|
10260
|
+
timer.unref?.();
|
|
10261
|
+
function fail(err) {
|
|
10262
|
+
if (settled) return;
|
|
10263
|
+
settled = true;
|
|
10264
|
+
clearTimeout(timer);
|
|
10265
|
+
socket.removeAllListeners();
|
|
10266
|
+
socket.destroy();
|
|
10267
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
10268
|
+
}
|
|
10269
|
+
function succeed() {
|
|
10270
|
+
settled = true;
|
|
10271
|
+
clearTimeout(timer);
|
|
10272
|
+
socket.removeListener("error", onError);
|
|
10273
|
+
socket.removeListener("data", onData);
|
|
10274
|
+
resolve(createControlClient(socket, reader, opts));
|
|
10275
|
+
}
|
|
10276
|
+
function onData(chunk) {
|
|
10277
|
+
let lines;
|
|
10278
|
+
try {
|
|
10279
|
+
lines = reader.push(chunk);
|
|
10280
|
+
} catch (err) {
|
|
10281
|
+
fail(err);
|
|
10282
|
+
return;
|
|
10283
|
+
}
|
|
10284
|
+
for (const line of lines) {
|
|
10285
|
+
let parsed;
|
|
10286
|
+
try {
|
|
10287
|
+
parsed = JSON.parse(line);
|
|
10288
|
+
} catch {
|
|
10289
|
+
fail(new Error("malformed handshake frame"));
|
|
10290
|
+
return;
|
|
10291
|
+
}
|
|
10292
|
+
if (phase === "server-hello") {
|
|
10293
|
+
const hello = parseServerHello(parsed);
|
|
10294
|
+
if (!hello) {
|
|
10295
|
+
fail(new Error("malformed or unexpected server hello"));
|
|
10296
|
+
return;
|
|
10297
|
+
}
|
|
10298
|
+
if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
|
|
10299
|
+
fail(new Error("server failed to prove it holds the control token"));
|
|
10300
|
+
return;
|
|
10301
|
+
}
|
|
10302
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
|
|
10303
|
+
phase = "ready";
|
|
10304
|
+
continue;
|
|
10305
|
+
}
|
|
10306
|
+
if (!parseServerReady(parsed)) {
|
|
10307
|
+
fail(new Error("server did not confirm readiness"));
|
|
10308
|
+
return;
|
|
10309
|
+
}
|
|
10310
|
+
succeed();
|
|
10311
|
+
return;
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
function onError(err) {
|
|
10315
|
+
fail(err);
|
|
10316
|
+
}
|
|
10317
|
+
socket.once("error", onError);
|
|
10318
|
+
socket.once("connect", () => {
|
|
10319
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
|
|
10320
|
+
socket.on("data", onData);
|
|
10321
|
+
});
|
|
10322
|
+
});
|
|
9370
10323
|
}
|
|
9371
|
-
function
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
10324
|
+
function withTimeout(promise, ms, message) {
|
|
10325
|
+
return new Promise((resolve, reject) => {
|
|
10326
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
10327
|
+
timer.unref?.();
|
|
10328
|
+
promise.then(
|
|
10329
|
+
(value) => {
|
|
10330
|
+
clearTimeout(timer);
|
|
10331
|
+
resolve(value);
|
|
10332
|
+
},
|
|
10333
|
+
(err) => {
|
|
10334
|
+
clearTimeout(timer);
|
|
10335
|
+
reject(err);
|
|
10336
|
+
}
|
|
9375
10337
|
);
|
|
9376
|
-
}
|
|
9377
|
-
}
|
|
9378
|
-
function escapeSystemdPercent(value) {
|
|
9379
|
-
return value.replace(/%/g, "%%");
|
|
9380
|
-
}
|
|
9381
|
-
function quoteSystemdArg(value) {
|
|
9382
|
-
assertNoControlChars(value, "program.command/program.args entry");
|
|
9383
|
-
const escaped = escapeSystemdPercent(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, () => "$$");
|
|
9384
|
-
return `"${escaped}"`;
|
|
9385
|
-
}
|
|
9386
|
-
function generateSystemdUnit(def) {
|
|
9387
|
-
const { name, displayName, program, logDir } = def;
|
|
9388
|
-
assertNoControlChars(name, "name");
|
|
9389
|
-
assertNoControlChars(displayName, "displayName");
|
|
9390
|
-
const cwd = program.cwd ?? os.homedir();
|
|
9391
|
-
assertNoControlChars(cwd, "program.cwd");
|
|
9392
|
-
const outLog = path19.join(logDir, `${name}.out.log`);
|
|
9393
|
-
const errLog = path19.join(logDir, `${name}.err.log`);
|
|
9394
|
-
assertNoControlChars(outLog, "logDir");
|
|
9395
|
-
assertNoControlChars(errLog, "logDir");
|
|
9396
|
-
const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
|
|
9397
|
-
return `[Unit]
|
|
9398
|
-
Description=${escapeSystemdPercent(displayName)}
|
|
9399
|
-
|
|
9400
|
-
[Service]
|
|
9401
|
-
Type=simple
|
|
9402
|
-
ExecStart=${execStart}
|
|
9403
|
-
WorkingDirectory=${escapeSystemdPercent(cwd)}
|
|
9404
|
-
Restart=on-failure
|
|
9405
|
-
RestartSec=10
|
|
9406
|
-
StandardOutput=append:${escapeSystemdPercent(outLog)}
|
|
9407
|
-
StandardError=append:${escapeSystemdPercent(errLog)}
|
|
9408
|
-
|
|
9409
|
-
[Install]
|
|
9410
|
-
WantedBy=default.target
|
|
9411
|
-
`;
|
|
10338
|
+
});
|
|
9412
10339
|
}
|
|
9413
|
-
function
|
|
9414
|
-
const
|
|
9415
|
-
|
|
9416
|
-
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
10340
|
+
function createControlClient(socket, reader, opts) {
|
|
10341
|
+
const pending = /* @__PURE__ */ new Map();
|
|
10342
|
+
let idSeq = 0;
|
|
10343
|
+
let closed = false;
|
|
10344
|
+
function handleFrame(parsed) {
|
|
10345
|
+
if (!isRecord2(parsed) || typeof parsed.id !== "string") return;
|
|
10346
|
+
const entry = pending.get(parsed.id);
|
|
10347
|
+
if (!entry) return;
|
|
10348
|
+
if ("event" in parsed) {
|
|
10349
|
+
entry.onEvent?.(parsed.event);
|
|
10350
|
+
return;
|
|
10351
|
+
}
|
|
10352
|
+
if (parsed.ok === true) {
|
|
10353
|
+
pending.delete(parsed.id);
|
|
10354
|
+
entry.resolve(parsed.done === true ? void 0 : parsed.result);
|
|
10355
|
+
return;
|
|
10356
|
+
}
|
|
10357
|
+
pending.delete(parsed.id);
|
|
10358
|
+
const shape = parsed.error;
|
|
10359
|
+
entry.reject(
|
|
10360
|
+
new ControlError(
|
|
10361
|
+
typeof shape?.code === "string" ? shape.code : "internal_error",
|
|
10362
|
+
typeof shape?.message === "string" ? shape.message : "unknown control error"
|
|
10363
|
+
)
|
|
10364
|
+
);
|
|
10365
|
+
}
|
|
10366
|
+
socket.on("data", (chunk) => {
|
|
10367
|
+
let lines;
|
|
9421
10368
|
try {
|
|
9422
|
-
|
|
9423
|
-
return true;
|
|
10369
|
+
lines = reader.push(chunk);
|
|
9424
10370
|
} catch {
|
|
9425
|
-
|
|
10371
|
+
socket.destroy();
|
|
10372
|
+
return;
|
|
9426
10373
|
}
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
await writeUnit(opts.program ?? def.program);
|
|
9436
|
-
await runOrThrow(run, "systemctl", ["--user", "daemon-reload"], "systemctl daemon-reload");
|
|
9437
|
-
await runOrThrow(run, "systemctl", ["--user", "enable", "--now", unitName], "systemctl enable --now");
|
|
9438
|
-
}
|
|
9439
|
-
async function uninstall() {
|
|
9440
|
-
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
9441
|
-
await fs19.rm(unitPath(), { force: true });
|
|
9442
|
-
await run("systemctl", ["--user", "daemon-reload"]);
|
|
9443
|
-
}
|
|
9444
|
-
async function start() {
|
|
9445
|
-
if (!await fileExists(unitPath())) {
|
|
9446
|
-
throw new Error(`service "${name}" is not installed (no unit file at ${unitPath()}) \u2014 call install() first`);
|
|
10374
|
+
for (const line of lines) {
|
|
10375
|
+
let parsed;
|
|
10376
|
+
try {
|
|
10377
|
+
parsed = JSON.parse(line);
|
|
10378
|
+
} catch {
|
|
10379
|
+
continue;
|
|
10380
|
+
}
|
|
10381
|
+
handleFrame(parsed);
|
|
9447
10382
|
}
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
10383
|
+
});
|
|
10384
|
+
socket.on("close", () => {
|
|
10385
|
+
closed = true;
|
|
10386
|
+
for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
|
|
10387
|
+
pending.clear();
|
|
10388
|
+
});
|
|
10389
|
+
socket.on("error", () => {
|
|
10390
|
+
});
|
|
10391
|
+
function send(method, params, onEvent) {
|
|
10392
|
+
const id = `c${++idSeq}`;
|
|
10393
|
+
const promise = new Promise((resolve, reject) => {
|
|
10394
|
+
pending.set(id, { resolve, reject, onEvent });
|
|
10395
|
+
});
|
|
10396
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
|
|
10397
|
+
return { id, promise };
|
|
9452
10398
|
}
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
|
|
10399
|
+
return {
|
|
10400
|
+
async request(method, params) {
|
|
10401
|
+
if (closed) throw new Error("control connection is closed");
|
|
10402
|
+
const { promise } = send(method, params);
|
|
10403
|
+
const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
|
|
10404
|
+
return result;
|
|
10405
|
+
},
|
|
10406
|
+
subscribe(method, params, onEvent) {
|
|
10407
|
+
const { id, promise } = send(method, params, onEvent);
|
|
10408
|
+
promise.catch(() => {
|
|
10409
|
+
});
|
|
10410
|
+
return {
|
|
10411
|
+
close: () => {
|
|
10412
|
+
pending.delete(id);
|
|
10413
|
+
socket.destroy();
|
|
10414
|
+
}
|
|
10415
|
+
};
|
|
10416
|
+
},
|
|
10417
|
+
close() {
|
|
10418
|
+
socket.destroy();
|
|
10419
|
+
}
|
|
10420
|
+
};
|
|
10421
|
+
}
|
|
10422
|
+
async function isControlDaemonGone(storeDir, productId) {
|
|
10423
|
+
const tokenGone = await promises.stat(controlTokenPath(storeDir)).then(
|
|
10424
|
+
() => false,
|
|
10425
|
+
(err) => err.code === "ENOENT"
|
|
10426
|
+
);
|
|
10427
|
+
if (!tokenGone) return false;
|
|
10428
|
+
const endpoint = controlEndpointPath(productId, storeDir);
|
|
10429
|
+
return new Promise((resolve) => {
|
|
10430
|
+
const socket = net.createConnection(endpoint);
|
|
10431
|
+
const finish = (gone) => {
|
|
10432
|
+
socket.removeAllListeners();
|
|
10433
|
+
socket.destroy();
|
|
10434
|
+
resolve(gone);
|
|
10435
|
+
};
|
|
10436
|
+
socket.once("connect", () => finish(false));
|
|
10437
|
+
socket.once("error", (err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT"));
|
|
10438
|
+
});
|
|
10439
|
+
}
|
|
10440
|
+
|
|
10441
|
+
// src/lifecycle/service-types.ts
|
|
10442
|
+
function nodeAgentProgram(opts) {
|
|
10443
|
+
const program = {
|
|
10444
|
+
command: opts.nodeBin ?? process.execPath,
|
|
10445
|
+
args: [opts.agentBin, "start", "--config", opts.configPath]
|
|
10446
|
+
};
|
|
10447
|
+
if (opts.cwd !== void 0) program.cwd = opts.cwd;
|
|
10448
|
+
return program;
|
|
10449
|
+
}
|
|
10450
|
+
function sanitizeServiceName(name) {
|
|
10451
|
+
const cleaned = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
10452
|
+
const safe = cleaned.replace(/^-+/, "");
|
|
10453
|
+
if (!safe) {
|
|
10454
|
+
throw new Error(`service name "${name}" has no valid characters left after sanitizing (allowed: letters, digits, ".", "-", "_"; cannot consist only of leading "-")`);
|
|
9460
10455
|
}
|
|
9461
|
-
return
|
|
10456
|
+
return safe;
|
|
9462
10457
|
}
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
10458
|
+
|
|
10459
|
+
// src/lifecycle/launchd.ts
|
|
10460
|
+
var LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
10461
|
+
/operation not permitted/i,
|
|
10462
|
+
/could not find domain/i,
|
|
9466
10463
|
/permission denied/i,
|
|
9467
|
-
/
|
|
10464
|
+
/access denied/i
|
|
9468
10465
|
];
|
|
9469
|
-
var
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9473
|
-
};
|
|
9474
|
-
var WINSW_ALREADY_STOPPED = {
|
|
9475
|
-
codes: [1062, ...WINSW_NOT_INSTALLED.codes ?? []],
|
|
9476
|
-
patterns: [/not running/i, /has not been started/i, ...WINSW_NOT_INSTALLED.patterns],
|
|
9477
|
-
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
10466
|
+
var LAUNCHD_NOT_LOADED = {
|
|
10467
|
+
patterns: [/no such process/i, /could not find (specified )?service/i, /not loaded/i],
|
|
10468
|
+
neverAbsence: LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9478
10469
|
};
|
|
9479
|
-
function
|
|
10470
|
+
function escapeXml(value) {
|
|
9480
10471
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9481
10472
|
}
|
|
9482
|
-
function
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
<
|
|
9496
|
-
|
|
9497
|
-
<
|
|
9498
|
-
<
|
|
9499
|
-
|
|
10473
|
+
function plistString(value) {
|
|
10474
|
+
return `<string>${escapeXml(value)}</string>`;
|
|
10475
|
+
}
|
|
10476
|
+
function generateLaunchdPlist(def) {
|
|
10477
|
+
const { label, program, logDir } = def;
|
|
10478
|
+
const args = [program.command, ...program.args];
|
|
10479
|
+
const cwd = program.cwd ?? os.homedir();
|
|
10480
|
+
const outLog = path20.join(logDir, `${label}.out.log`);
|
|
10481
|
+
const errLog = path20.join(logDir, `${label}.err.log`);
|
|
10482
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
10483
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
10484
|
+
<plist version="1.0">
|
|
10485
|
+
<dict>
|
|
10486
|
+
<key>Label</key>
|
|
10487
|
+
${plistString(label)}
|
|
10488
|
+
<key>ProgramArguments</key>
|
|
10489
|
+
<array>
|
|
10490
|
+
${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
10491
|
+
</array>
|
|
10492
|
+
<key>WorkingDirectory</key>
|
|
10493
|
+
${plistString(cwd)}
|
|
10494
|
+
<key>RunAtLoad</key>
|
|
10495
|
+
<true/>
|
|
10496
|
+
<key>KeepAlive</key>
|
|
10497
|
+
<dict>
|
|
10498
|
+
<key>SuccessfulExit</key>
|
|
10499
|
+
<false/>
|
|
10500
|
+
</dict>
|
|
10501
|
+
<key>ThrottleInterval</key>
|
|
10502
|
+
<integer>10</integer>
|
|
10503
|
+
<key>StandardOutPath</key>
|
|
10504
|
+
${plistString(outLog)}
|
|
10505
|
+
<key>StandardErrorPath</key>
|
|
10506
|
+
${plistString(errLog)}
|
|
10507
|
+
</dict>
|
|
10508
|
+
</plist>
|
|
9500
10509
|
`;
|
|
9501
10510
|
}
|
|
9502
|
-
function
|
|
10511
|
+
function createLaunchdLifecycle(def, deps = {}) {
|
|
9503
10512
|
const run = deps.run ?? defaultRunner;
|
|
9504
10513
|
const fs19 = deps.fs ?? promises;
|
|
9505
|
-
const
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
const
|
|
9513
|
-
const
|
|
10514
|
+
const homedir = deps.homedir ?? (() => os.homedir());
|
|
10515
|
+
const getuid = deps.getuid ?? (() => {
|
|
10516
|
+
if (typeof process.getuid !== "function") {
|
|
10517
|
+
throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
|
|
10518
|
+
}
|
|
10519
|
+
return process.getuid();
|
|
10520
|
+
});
|
|
10521
|
+
const label = sanitizeServiceName(def.name);
|
|
10522
|
+
const plistPath = () => path20.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
10523
|
+
const domainTarget = () => `gui/${getuid()}`;
|
|
10524
|
+
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
9514
10525
|
async function fileExists(p) {
|
|
9515
10526
|
try {
|
|
9516
10527
|
await fs19.stat(p);
|
|
@@ -9519,381 +10530,330 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
9519
10530
|
return false;
|
|
9520
10531
|
}
|
|
9521
10532
|
}
|
|
9522
|
-
async function
|
|
9523
|
-
const xml =
|
|
9524
|
-
await fs19.mkdir(
|
|
10533
|
+
async function writePlist(program) {
|
|
10534
|
+
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
10535
|
+
await fs19.mkdir(path20.dirname(plistPath()), { recursive: true });
|
|
9525
10536
|
await fs19.mkdir(def.logDir, { recursive: true });
|
|
9526
|
-
await fs19.
|
|
9527
|
-
await fs19.writeFile(xmlPath, xml, "utf8");
|
|
10537
|
+
await fs19.writeFile(plistPath(), xml, "utf8");
|
|
9528
10538
|
}
|
|
9529
10539
|
async function install(opts = {}) {
|
|
9530
|
-
await
|
|
9531
|
-
await
|
|
9532
|
-
await runOrThrow(run,
|
|
10540
|
+
await writePlist(opts.program ?? def.program);
|
|
10541
|
+
await run("launchctl", ["bootout", serviceTarget()]);
|
|
10542
|
+
await runOrThrow(run, "launchctl", ["bootstrap", domainTarget(), plistPath()], "launchctl bootstrap");
|
|
10543
|
+
await runOrThrow(run, "launchctl", ["enable", serviceTarget()], "launchctl enable");
|
|
10544
|
+
await run("launchctl", ["kickstart", "-k", serviceTarget()]);
|
|
9533
10545
|
}
|
|
9534
10546
|
async function uninstall() {
|
|
9535
|
-
await runIdempotent(run,
|
|
9536
|
-
await
|
|
9537
|
-
await fs19.rm(exePath, { force: true });
|
|
9538
|
-
await fs19.rm(xmlPath, { force: true });
|
|
10547
|
+
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
10548
|
+
await fs19.rm(plistPath(), { force: true });
|
|
9539
10549
|
}
|
|
9540
10550
|
async function start() {
|
|
9541
|
-
if (!await fileExists(
|
|
9542
|
-
throw new Error(`service "${
|
|
10551
|
+
if (!await fileExists(plistPath())) {
|
|
10552
|
+
throw new Error(`service "${label}" is not installed (no plist at ${plistPath()}) \u2014 call install() first`);
|
|
9543
10553
|
}
|
|
9544
|
-
await
|
|
10554
|
+
await run("launchctl", ["bootstrap", domainTarget(), plistPath()]);
|
|
10555
|
+
await runOrThrow(run, "launchctl", ["kickstart", "-k", serviceTarget()], "launchctl kickstart");
|
|
9545
10556
|
}
|
|
9546
10557
|
async function stop() {
|
|
9547
|
-
await runIdempotent(run,
|
|
10558
|
+
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
9548
10559
|
}
|
|
9549
10560
|
async function status() {
|
|
9550
|
-
const installed = await fileExists(
|
|
9551
|
-
const result = await run("
|
|
9552
|
-
const detail =
|
|
9553
|
-
const running = result.code === 0 && /\
|
|
9554
|
-
const determinate = running || !
|
|
10561
|
+
const installed = await fileExists(plistPath());
|
|
10562
|
+
const result = await run("launchctl", ["print", serviceTarget()]);
|
|
10563
|
+
const detail = result.stdout || result.stderr;
|
|
10564
|
+
const running = result.code === 0 && /\bstate\s*=\s*running\b/i.test(detail);
|
|
10565
|
+
const determinate = running || !LAUNCHD_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
9555
10566
|
return { installed, running, determinate, detail };
|
|
9556
10567
|
}
|
|
9557
10568
|
return { install, uninstall, start, stop, status };
|
|
9558
10569
|
}
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
switch (platform) {
|
|
9570
|
-
case "darwin":
|
|
9571
|
-
return createLaunchdLifecycle(def, opts.deps);
|
|
9572
|
-
case "linux":
|
|
9573
|
-
return createSystemdLifecycle(def, opts.deps);
|
|
9574
|
-
case "win32":
|
|
9575
|
-
return createWinswLifecycle(def, opts.deps);
|
|
9576
|
-
default:
|
|
9577
|
-
throw new UnsupportedServicePlatformError(platform);
|
|
9578
|
-
}
|
|
9579
|
-
}
|
|
9580
|
-
var REQUIRED_FIELDS = ["productName", "productId", "serverUrl", "workspaceRoot"];
|
|
9581
|
-
var ConfigError = class extends Error {
|
|
9582
|
-
constructor(message) {
|
|
9583
|
-
super(message);
|
|
9584
|
-
this.name = "ConfigError";
|
|
9585
|
-
}
|
|
10570
|
+
var SYSTEMD_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
10571
|
+
/failed to connect to.*bus/i,
|
|
10572
|
+
/connection refused/i,
|
|
10573
|
+
/access denied/i,
|
|
10574
|
+
/permission denied/i,
|
|
10575
|
+
/interactive authentication required/i
|
|
10576
|
+
];
|
|
10577
|
+
var SYSTEMD_NOT_LOADED = {
|
|
10578
|
+
patterns: [/not loaded/i, /does not exist/i],
|
|
10579
|
+
neverAbsence: SYSTEMD_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
9586
10580
|
};
|
|
9587
|
-
function
|
|
9588
|
-
let
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
try {
|
|
9592
|
-
raw = readFileSync(configPath, "utf8");
|
|
9593
|
-
} catch (err) {
|
|
9594
|
-
throw new ConfigError(`could not read config at "${configPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
9595
|
-
}
|
|
9596
|
-
try {
|
|
9597
|
-
base = JSON.parse(raw);
|
|
9598
|
-
} catch (err) {
|
|
9599
|
-
throw new ConfigError(`config at "${configPath}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
9600
|
-
}
|
|
9601
|
-
}
|
|
9602
|
-
const merged = { ...base, ...overrides };
|
|
9603
|
-
if (merged.gitWorkspace !== void 0) {
|
|
9604
|
-
try {
|
|
9605
|
-
GitWorkspaceManager.validateConfig(merged.gitWorkspace);
|
|
9606
|
-
} catch (error) {
|
|
9607
|
-
throw new ConfigError(error instanceof Error ? error.message : "invalid gitWorkspace configuration");
|
|
9608
|
-
}
|
|
10581
|
+
function hasControlChar(value) {
|
|
10582
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
10583
|
+
const code = value.charCodeAt(i);
|
|
10584
|
+
if (code < 32 || code === 127) return true;
|
|
9609
10585
|
}
|
|
9610
|
-
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
10586
|
+
return false;
|
|
10587
|
+
}
|
|
10588
|
+
function assertNoControlChars(value, field) {
|
|
10589
|
+
if (hasControlChar(value)) {
|
|
10590
|
+
throw new Error(
|
|
10591
|
+
`systemd unit ${field} must not contain control characters (newline/CR/etc.) \u2014 refusing to generate a unit file that could inject an unintended directive (got ${JSON.stringify(value)})`
|
|
10592
|
+
);
|
|
9614
10593
|
}
|
|
9615
|
-
return merged;
|
|
9616
10594
|
}
|
|
9617
|
-
function
|
|
9618
|
-
return
|
|
10595
|
+
function escapeSystemdPercent(value) {
|
|
10596
|
+
return value.replace(/%/g, "%%");
|
|
9619
10597
|
}
|
|
9620
|
-
function
|
|
9621
|
-
|
|
9622
|
-
const
|
|
9623
|
-
return
|
|
10598
|
+
function quoteSystemdArg(value) {
|
|
10599
|
+
assertNoControlChars(value, "program.command/program.args entry");
|
|
10600
|
+
const escaped = escapeSystemdPercent(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, () => "$$");
|
|
10601
|
+
return `"${escaped}"`;
|
|
9624
10602
|
}
|
|
9625
|
-
function
|
|
9626
|
-
|
|
10603
|
+
function generateSystemdUnit(def) {
|
|
10604
|
+
const { name, displayName, program, logDir } = def;
|
|
10605
|
+
assertNoControlChars(name, "name");
|
|
10606
|
+
assertNoControlChars(displayName, "displayName");
|
|
10607
|
+
const cwd = program.cwd ?? os.homedir();
|
|
10608
|
+
assertNoControlChars(cwd, "program.cwd");
|
|
10609
|
+
const outLog = path20.join(logDir, `${name}.out.log`);
|
|
10610
|
+
const errLog = path20.join(logDir, `${name}.err.log`);
|
|
10611
|
+
assertNoControlChars(outLog, "logDir");
|
|
10612
|
+
assertNoControlChars(errLog, "logDir");
|
|
10613
|
+
const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
|
|
10614
|
+
return `[Unit]
|
|
10615
|
+
Description=${escapeSystemdPercent(displayName)}
|
|
10616
|
+
|
|
10617
|
+
[Service]
|
|
10618
|
+
Type=simple
|
|
10619
|
+
ExecStart=${execStart}
|
|
10620
|
+
WorkingDirectory=${escapeSystemdPercent(cwd)}
|
|
10621
|
+
Restart=on-failure
|
|
10622
|
+
RestartSec=10
|
|
10623
|
+
StandardOutput=append:${escapeSystemdPercent(outLog)}
|
|
10624
|
+
StandardError=append:${escapeSystemdPercent(errLog)}
|
|
10625
|
+
|
|
10626
|
+
[Install]
|
|
10627
|
+
WantedBy=default.target
|
|
10628
|
+
`;
|
|
9627
10629
|
}
|
|
9628
|
-
function
|
|
9629
|
-
const
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
10630
|
+
function createSystemdLifecycle(def, deps = {}) {
|
|
10631
|
+
const run = deps.run ?? defaultRunner;
|
|
10632
|
+
const fs19 = deps.fs ?? promises;
|
|
10633
|
+
const homedir = deps.homedir ?? (() => os.homedir());
|
|
10634
|
+
const name = sanitizeServiceName(def.name);
|
|
10635
|
+
const unitName = `${name}.service`;
|
|
10636
|
+
const unitPath = () => path20.join(homedir(), ".config", "systemd", "user", unitName);
|
|
10637
|
+
async function fileExists(p) {
|
|
10638
|
+
try {
|
|
10639
|
+
await fs19.stat(p);
|
|
10640
|
+
return true;
|
|
10641
|
+
} catch {
|
|
10642
|
+
return false;
|
|
9636
10643
|
}
|
|
9637
|
-
result.push(arg);
|
|
9638
10644
|
}
|
|
9639
|
-
|
|
9640
|
-
}
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
}
|
|
9645
|
-
function sameFileState3(left, right) {
|
|
9646
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
9647
|
-
}
|
|
9648
|
-
async function readControlToken(tokenPath) {
|
|
9649
|
-
let namedBefore;
|
|
9650
|
-
try {
|
|
9651
|
-
namedBefore = await promises.lstat(tokenPath, { bigint: true });
|
|
9652
|
-
} catch (err) {
|
|
9653
|
-
if (err.code === "ENOENT") return void 0;
|
|
9654
|
-
throw err;
|
|
10645
|
+
async function writeUnit(program) {
|
|
10646
|
+
const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
10647
|
+
await fs19.mkdir(path20.dirname(unitPath()), { recursive: true });
|
|
10648
|
+
await fs19.mkdir(def.logDir, { recursive: true });
|
|
10649
|
+
await fs19.writeFile(unitPath(), unit, "utf8");
|
|
9655
10650
|
}
|
|
9656
|
-
|
|
9657
|
-
|
|
10651
|
+
async function install(opts = {}) {
|
|
10652
|
+
await writeUnit(opts.program ?? def.program);
|
|
10653
|
+
await runOrThrow(run, "systemctl", ["--user", "daemon-reload"], "systemctl daemon-reload");
|
|
10654
|
+
await runOrThrow(run, "systemctl", ["--user", "enable", "--now", unitName], "systemctl enable --now");
|
|
9658
10655
|
}
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
try {
|
|
9664
|
-
const opened = await handle.stat({ bigint: true });
|
|
9665
|
-
const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
|
|
9666
|
-
if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
|
|
9667
|
-
throw new Error("control token pathname changed before safe open");
|
|
9668
|
-
}
|
|
9669
|
-
if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
|
|
9670
|
-
throw new Error("control token exceeds the bounded read limit");
|
|
9671
|
-
}
|
|
9672
|
-
const size = Number(opened.size);
|
|
9673
|
-
const bytes = Buffer.alloc(size);
|
|
9674
|
-
const { bytesRead } = await handle.read(bytes, 0, size, 0);
|
|
9675
|
-
const afterRead = await handle.stat({ bigint: true });
|
|
9676
|
-
const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
|
|
9677
|
-
if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
|
|
9678
|
-
throw new Error("control token changed during bounded read");
|
|
9679
|
-
}
|
|
9680
|
-
return bytes.toString("utf8").trim();
|
|
9681
|
-
} finally {
|
|
9682
|
-
await handle.close();
|
|
10656
|
+
async function uninstall() {
|
|
10657
|
+
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
10658
|
+
await fs19.rm(unitPath(), { force: true });
|
|
10659
|
+
await run("systemctl", ["--user", "daemon-reload"]);
|
|
9683
10660
|
}
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
let token;
|
|
9688
|
-
try {
|
|
9689
|
-
const read = await readControlToken(tokenPath);
|
|
9690
|
-
if (read === void 0) {
|
|
9691
|
-
return { ok: false, reason: "daemon is not running (no control.token found)" };
|
|
10661
|
+
async function start() {
|
|
10662
|
+
if (!await fileExists(unitPath())) {
|
|
10663
|
+
throw new Error(`service "${name}" is not installed (no unit file at ${unitPath()}) \u2014 call install() first`);
|
|
9692
10664
|
}
|
|
9693
|
-
|
|
9694
|
-
} catch (err) {
|
|
9695
|
-
return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
|
|
10665
|
+
await runOrThrow(run, "systemctl", ["--user", "start", unitName], "systemctl start");
|
|
9696
10666
|
}
|
|
9697
|
-
|
|
9698
|
-
|
|
10667
|
+
async function stop() {
|
|
10668
|
+
await runIdempotent(run, "systemctl", ["--user", "stop", unitName], "systemctl stop", SYSTEMD_NOT_LOADED);
|
|
9699
10669
|
}
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
const
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
10670
|
+
async function status() {
|
|
10671
|
+
const installed = await fileExists(unitPath());
|
|
10672
|
+
const result = await run("systemctl", ["--user", "is-active", unitName]);
|
|
10673
|
+
const detail = (result.stdout || result.stderr).trim();
|
|
10674
|
+
const running = result.code === 0 && detail === "active";
|
|
10675
|
+
const determinate = running || !SYSTEMD_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
10676
|
+
return { installed, running, determinate, detail };
|
|
9706
10677
|
}
|
|
10678
|
+
return { install, uninstall, start, stop, status };
|
|
9707
10679
|
}
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
fail(new Error("server did not confirm readiness"));
|
|
9766
|
-
return;
|
|
9767
|
-
}
|
|
9768
|
-
succeed();
|
|
9769
|
-
return;
|
|
9770
|
-
}
|
|
10680
|
+
var WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE = [
|
|
10681
|
+
/access is denied/i,
|
|
10682
|
+
/access denied/i,
|
|
10683
|
+
/permission denied/i,
|
|
10684
|
+
/being used by another process/i
|
|
10685
|
+
];
|
|
10686
|
+
var WINSW_NOT_INSTALLED = {
|
|
10687
|
+
codes: [1060],
|
|
10688
|
+
patterns: [/does not exist/i, /non-existent service/i],
|
|
10689
|
+
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
10690
|
+
};
|
|
10691
|
+
var WINSW_ALREADY_STOPPED = {
|
|
10692
|
+
codes: [1062, ...WINSW_NOT_INSTALLED.codes ?? []],
|
|
10693
|
+
patterns: [/not running/i, /has not been started/i, ...WINSW_NOT_INSTALLED.patterns],
|
|
10694
|
+
neverAbsence: WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE
|
|
10695
|
+
};
|
|
10696
|
+
function xmlEscape(value) {
|
|
10697
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
10698
|
+
}
|
|
10699
|
+
function generateWinswXml(def) {
|
|
10700
|
+
const { id, displayName, program, logDir } = def;
|
|
10701
|
+
const argXml = program.args.map((a) => ` <argument>${xmlEscape(a)}</argument>`).join("\n");
|
|
10702
|
+
const cwdXml = program.cwd ? `
|
|
10703
|
+
<workingdirectory>${xmlEscape(program.cwd)}</workingdirectory>` : "";
|
|
10704
|
+
return `<service>
|
|
10705
|
+
<id>${xmlEscape(id)}</id>
|
|
10706
|
+
<name>${xmlEscape(displayName)}</name>
|
|
10707
|
+
<description>${xmlEscape(displayName)} (managed by byok-agent; see templates/service/winsw/README.md)</description>
|
|
10708
|
+
<executable>${xmlEscape(program.command)}</executable>
|
|
10709
|
+
${argXml}${cwdXml}
|
|
10710
|
+
<logpath>${xmlEscape(logDir)}</logpath>
|
|
10711
|
+
<log mode="roll"></log>
|
|
10712
|
+
<startmode>Automatic</startmode>
|
|
10713
|
+
<onfailure action="restart" delay="10 sec"/>
|
|
10714
|
+
<onfailure action="restart" delay="30 sec"/>
|
|
10715
|
+
<resetfailure>1 hour</resetfailure>
|
|
10716
|
+
</service>
|
|
10717
|
+
`;
|
|
10718
|
+
}
|
|
10719
|
+
function createWinswLifecycle(def, deps = {}) {
|
|
10720
|
+
const run = deps.run ?? defaultRunner;
|
|
10721
|
+
const fs19 = deps.fs ?? promises;
|
|
10722
|
+
const windows = def.windows;
|
|
10723
|
+
if (!windows) {
|
|
10724
|
+
throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
|
|
10725
|
+
}
|
|
10726
|
+
const winswBin = windows.winswBin;
|
|
10727
|
+
const id = sanitizeServiceName(def.name);
|
|
10728
|
+
const installDir = windows.installDir ?? def.logDir;
|
|
10729
|
+
const exePath = path20.join(installDir, `${id}.exe`);
|
|
10730
|
+
const xmlPath = path20.join(installDir, `${id}.xml`);
|
|
10731
|
+
async function fileExists(p) {
|
|
10732
|
+
try {
|
|
10733
|
+
await fs19.stat(p);
|
|
10734
|
+
return true;
|
|
10735
|
+
} catch {
|
|
10736
|
+
return false;
|
|
9771
10737
|
}
|
|
9772
|
-
|
|
9773
|
-
|
|
10738
|
+
}
|
|
10739
|
+
async function writeFiles(program) {
|
|
10740
|
+
const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
10741
|
+
await fs19.mkdir(installDir, { recursive: true });
|
|
10742
|
+
await fs19.mkdir(def.logDir, { recursive: true });
|
|
10743
|
+
await fs19.copyFile(winswBin, exePath);
|
|
10744
|
+
await fs19.writeFile(xmlPath, xml, "utf8");
|
|
10745
|
+
}
|
|
10746
|
+
async function install(opts = {}) {
|
|
10747
|
+
await writeFiles(opts.program ?? def.program);
|
|
10748
|
+
await runOrThrow(run, exePath, ["install"], "winsw install");
|
|
10749
|
+
await runOrThrow(run, exePath, ["start"], "winsw start");
|
|
10750
|
+
}
|
|
10751
|
+
async function uninstall() {
|
|
10752
|
+
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
|
|
10753
|
+
await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
|
|
10754
|
+
await fs19.rm(exePath, { force: true });
|
|
10755
|
+
await fs19.rm(xmlPath, { force: true });
|
|
10756
|
+
}
|
|
10757
|
+
async function start() {
|
|
10758
|
+
if (!await fileExists(xmlPath)) {
|
|
10759
|
+
throw new Error(`service "${id}" is not installed (no config at ${xmlPath}) \u2014 call install() first`);
|
|
9774
10760
|
}
|
|
9775
|
-
|
|
9776
|
-
|
|
9777
|
-
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
|
-
|
|
10761
|
+
await runOrThrow(run, exePath, ["start"], "winsw start");
|
|
10762
|
+
}
|
|
10763
|
+
async function stop() {
|
|
10764
|
+
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_ALREADY_STOPPED);
|
|
10765
|
+
}
|
|
10766
|
+
async function status() {
|
|
10767
|
+
const installed = await fileExists(xmlPath);
|
|
10768
|
+
const result = await run("sc.exe", ["query", id]);
|
|
10769
|
+
const detail = (result.stdout || result.stderr).trim();
|
|
10770
|
+
const running = result.code === 0 && /\bSTATE\b.*\bRUNNING\b/i.test(detail);
|
|
10771
|
+
const determinate = running || !WINSW_CONNECTIVITY_OR_PERMISSION_FAILURE.some((pattern) => pattern.test(detail));
|
|
10772
|
+
return { installed, running, determinate, detail };
|
|
10773
|
+
}
|
|
10774
|
+
return { install, uninstall, start, stop, status };
|
|
9781
10775
|
}
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
|
|
9789
|
-
|
|
9790
|
-
|
|
9791
|
-
|
|
9792
|
-
|
|
9793
|
-
|
|
9794
|
-
|
|
9795
|
-
|
|
9796
|
-
|
|
10776
|
+
|
|
10777
|
+
// src/lifecycle/create-service-lifecycle.ts
|
|
10778
|
+
var UnsupportedServicePlatformError = class extends Error {
|
|
10779
|
+
constructor(platform) {
|
|
10780
|
+
super(`no OS service lifecycle for platform "${platform}" \u2014 supported: darwin (launchd), linux (systemd --user), win32 (WinSW)`);
|
|
10781
|
+
this.name = "UnsupportedServicePlatformError";
|
|
10782
|
+
}
|
|
10783
|
+
};
|
|
10784
|
+
function createServiceLifecycle(def, opts = {}) {
|
|
10785
|
+
const platform = opts.platform ?? process.platform;
|
|
10786
|
+
switch (platform) {
|
|
10787
|
+
case "darwin":
|
|
10788
|
+
return createLaunchdLifecycle(def, opts.deps);
|
|
10789
|
+
case "linux":
|
|
10790
|
+
return createSystemdLifecycle(def, opts.deps);
|
|
10791
|
+
case "win32":
|
|
10792
|
+
return createWinswLifecycle(def, opts.deps);
|
|
10793
|
+
default:
|
|
10794
|
+
throw new UnsupportedServicePlatformError(platform);
|
|
10795
|
+
}
|
|
9797
10796
|
}
|
|
9798
|
-
|
|
9799
|
-
|
|
9800
|
-
|
|
9801
|
-
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
10797
|
+
var REQUIRED_FIELDS = ["productName", "productId", "serverUrl", "workspaceRoot"];
|
|
10798
|
+
var ConfigError = class extends Error {
|
|
10799
|
+
constructor(message) {
|
|
10800
|
+
super(message);
|
|
10801
|
+
this.name = "ConfigError";
|
|
10802
|
+
}
|
|
10803
|
+
};
|
|
10804
|
+
function loadConfig(configPath, overrides = {}) {
|
|
10805
|
+
let base = {};
|
|
10806
|
+
if (configPath) {
|
|
10807
|
+
let raw;
|
|
10808
|
+
try {
|
|
10809
|
+
raw = readFileSync(configPath, "utf8");
|
|
10810
|
+
} catch (err) {
|
|
10811
|
+
throw new ConfigError(`could not read config at "${configPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
9809
10812
|
}
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
10813
|
+
try {
|
|
10814
|
+
base = JSON.parse(raw);
|
|
10815
|
+
} catch (err) {
|
|
10816
|
+
throw new ConfigError(`config at "${configPath}" is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
9814
10817
|
}
|
|
9815
|
-
pending.delete(parsed.id);
|
|
9816
|
-
const shape = parsed.error;
|
|
9817
|
-
entry.reject(
|
|
9818
|
-
new ControlError(
|
|
9819
|
-
typeof shape?.code === "string" ? shape.code : "internal_error",
|
|
9820
|
-
typeof shape?.message === "string" ? shape.message : "unknown control error"
|
|
9821
|
-
)
|
|
9822
|
-
);
|
|
9823
10818
|
}
|
|
9824
|
-
|
|
9825
|
-
|
|
10819
|
+
const merged = { ...base, ...overrides };
|
|
10820
|
+
if (merged.gitWorkspace !== void 0) {
|
|
9826
10821
|
try {
|
|
9827
|
-
|
|
9828
|
-
} catch {
|
|
9829
|
-
|
|
9830
|
-
return;
|
|
9831
|
-
}
|
|
9832
|
-
for (const line of lines) {
|
|
9833
|
-
let parsed;
|
|
9834
|
-
try {
|
|
9835
|
-
parsed = JSON.parse(line);
|
|
9836
|
-
} catch {
|
|
9837
|
-
continue;
|
|
9838
|
-
}
|
|
9839
|
-
handleFrame(parsed);
|
|
10822
|
+
GitWorkspaceManager.validateConfig(merged.gitWorkspace);
|
|
10823
|
+
} catch (error) {
|
|
10824
|
+
throw new ConfigError(error instanceof Error ? error.message : "invalid gitWorkspace configuration");
|
|
9840
10825
|
}
|
|
9841
|
-
});
|
|
9842
|
-
socket.on("close", () => {
|
|
9843
|
-
closed = true;
|
|
9844
|
-
for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
|
|
9845
|
-
pending.clear();
|
|
9846
|
-
});
|
|
9847
|
-
socket.on("error", () => {
|
|
9848
|
-
});
|
|
9849
|
-
function send(method, params, onEvent) {
|
|
9850
|
-
const id = `c${++idSeq}`;
|
|
9851
|
-
const promise = new Promise((resolve, reject) => {
|
|
9852
|
-
pending.set(id, { resolve, reject, onEvent });
|
|
9853
|
-
});
|
|
9854
|
-
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
|
|
9855
|
-
return { id, promise };
|
|
9856
10826
|
}
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
const { promise } = send(method, params);
|
|
9861
|
-
const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
|
|
9862
|
-
return result;
|
|
9863
|
-
},
|
|
9864
|
-
subscribe(method, params, onEvent) {
|
|
9865
|
-
const { id, promise } = send(method, params, onEvent);
|
|
9866
|
-
promise.catch(() => {
|
|
9867
|
-
});
|
|
9868
|
-
return {
|
|
9869
|
-
close: () => {
|
|
9870
|
-
pending.delete(id);
|
|
9871
|
-
socket.destroy();
|
|
9872
|
-
}
|
|
9873
|
-
};
|
|
9874
|
-
},
|
|
9875
|
-
close() {
|
|
9876
|
-
socket.destroy();
|
|
10827
|
+
for (const field of REQUIRED_FIELDS) {
|
|
10828
|
+
if (!merged[field]) {
|
|
10829
|
+
throw new ConfigError(`config is missing required field "${field}"`);
|
|
9877
10830
|
}
|
|
9878
|
-
}
|
|
10831
|
+
}
|
|
10832
|
+
return merged;
|
|
9879
10833
|
}
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
);
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
10834
|
+
function resolveStoreDir(config) {
|
|
10835
|
+
return DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
10836
|
+
}
|
|
10837
|
+
function argValue(args, flag) {
|
|
10838
|
+
const idx = args.indexOf(flag);
|
|
10839
|
+
const value = idx >= 0 ? args[idx + 1] : void 0;
|
|
10840
|
+
return value !== void 0 && !value.startsWith("--") ? value : void 0;
|
|
10841
|
+
}
|
|
10842
|
+
function hasFlag(args, flag) {
|
|
10843
|
+
return args.includes(flag);
|
|
10844
|
+
}
|
|
10845
|
+
function positionalArgs(args, valueFlags = []) {
|
|
10846
|
+
const result = [];
|
|
10847
|
+
for (let i = 0; i < args.length; i++) {
|
|
10848
|
+
const arg = args[i];
|
|
10849
|
+
if (arg === void 0) continue;
|
|
10850
|
+
if (valueFlags.includes(arg)) {
|
|
10851
|
+
i++;
|
|
10852
|
+
continue;
|
|
10853
|
+
}
|
|
10854
|
+
result.push(arg);
|
|
10855
|
+
}
|
|
10856
|
+
return result;
|
|
9897
10857
|
}
|
|
9898
10858
|
|
|
9899
10859
|
// src/bin/format.ts
|
|
@@ -9993,6 +10953,19 @@ function formatDaemonEventLine(event, options = {}) {
|
|
|
9993
10953
|
].filter((part) => part !== void 0);
|
|
9994
10954
|
return parts.join(" ");
|
|
9995
10955
|
}
|
|
10956
|
+
case "device-assertion": {
|
|
10957
|
+
const parts = event.result === "issued" ? [
|
|
10958
|
+
`${prefix} device-assertion result=issued`,
|
|
10959
|
+
`audience=${quote(event.audience)}`,
|
|
10960
|
+
`jti=${event.jti}`,
|
|
10961
|
+
`expiresAt=${event.expiresAt}`
|
|
10962
|
+
] : [
|
|
10963
|
+
`${prefix} device-assertion result=denied`,
|
|
10964
|
+
`reason=${event.reason}`,
|
|
10965
|
+
event.audienceSize !== void 0 ? `audienceSize=${event.audienceSize}` : void 0
|
|
10966
|
+
].filter((part) => part !== void 0);
|
|
10967
|
+
return parts.join(" ");
|
|
10968
|
+
}
|
|
9996
10969
|
}
|
|
9997
10970
|
}
|
|
9998
10971
|
function formatTaskLine(task) {
|
|
@@ -10284,7 +11257,7 @@ function safeProtocol(serverUrl) {
|
|
|
10284
11257
|
}
|
|
10285
11258
|
}
|
|
10286
11259
|
async function inspectDevice(storeDir) {
|
|
10287
|
-
const filePath =
|
|
11260
|
+
const filePath = path20.join(storeDir, "device.json");
|
|
10288
11261
|
let pathStat;
|
|
10289
11262
|
try {
|
|
10290
11263
|
pathStat = await promises.lstat(filePath);
|
|
@@ -10364,11 +11337,11 @@ async function copyOpenFileBounded(source, expected, destinationPath) {
|
|
|
10364
11337
|
}
|
|
10365
11338
|
}
|
|
10366
11339
|
async function inspectJournal(storeDir) {
|
|
10367
|
-
const journalPath =
|
|
11340
|
+
const journalPath = path20.join(storeDir, JOURNAL_DB_FILENAME);
|
|
10368
11341
|
try {
|
|
10369
11342
|
const mainIdentity = await regularFileIdentity(journalPath);
|
|
10370
11343
|
if (mainIdentity === void 0) return { status: "missing" };
|
|
10371
|
-
const walIdentity = await regularFileIdentity(
|
|
11344
|
+
const walIdentity = await regularFileIdentity(path20.join(storeDir, `${JOURNAL_DB_FILENAME}-wal`));
|
|
10372
11345
|
let sizeBytes = Number(mainIdentity.size);
|
|
10373
11346
|
let walBytes = walIdentity === void 0 ? void 0 : Number(walIdentity.size);
|
|
10374
11347
|
if (!isSqliteAvailable()) {
|
|
@@ -10376,7 +11349,7 @@ async function inspectJournal(storeDir) {
|
|
|
10376
11349
|
}
|
|
10377
11350
|
const componentNames = [JOURNAL_DB_FILENAME, `${JOURNAL_DB_FILENAME}-wal`, `${JOURNAL_DB_FILENAME}-shm`];
|
|
10378
11351
|
const initial = /* @__PURE__ */ new Map();
|
|
10379
|
-
for (const name of componentNames) initial.set(name, await regularFileIdentity(
|
|
11352
|
+
for (const name of componentNames) initial.set(name, await regularFileIdentity(path20.join(storeDir, name)));
|
|
10380
11353
|
const snapshotMain = initial.get(JOURNAL_DB_FILENAME);
|
|
10381
11354
|
if (!snapshotMain) return { status: "unavailable", reason: "journal changed during diagnostics snapshot" };
|
|
10382
11355
|
sizeBytes = Number(snapshotMain.size);
|
|
@@ -10388,7 +11361,7 @@ async function inspectJournal(storeDir) {
|
|
|
10388
11361
|
let handle;
|
|
10389
11362
|
try {
|
|
10390
11363
|
handle = await promises.open(
|
|
10391
|
-
|
|
11364
|
+
path20.join(storeDir, name),
|
|
10392
11365
|
constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0)
|
|
10393
11366
|
);
|
|
10394
11367
|
} catch (err) {
|
|
@@ -10417,28 +11390,28 @@ async function inspectJournal(storeDir) {
|
|
|
10417
11390
|
reason: "journal exceeds the bounded diagnostics copy limit"
|
|
10418
11391
|
};
|
|
10419
11392
|
}
|
|
10420
|
-
const tempDir = await promises.mkdtemp(
|
|
11393
|
+
const tempDir = await promises.mkdtemp(path20.join(os.tmpdir(), "byok-journal-inspect-"));
|
|
10421
11394
|
const { DatabaseSync } = loadSqliteModule();
|
|
10422
11395
|
let db;
|
|
10423
11396
|
try {
|
|
10424
11397
|
for (const name of componentNames) {
|
|
10425
11398
|
const component = opened.get(name);
|
|
10426
|
-
if (component && !await copyOpenFileBounded(component.handle, component.identity,
|
|
11399
|
+
if (component && !await copyOpenFileBounded(component.handle, component.identity, path20.join(tempDir, name))) {
|
|
10427
11400
|
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
10428
11401
|
}
|
|
10429
11402
|
}
|
|
10430
11403
|
for (const [name, component] of opened) {
|
|
10431
|
-
if (!sameIdentity(component.identity, identityFromBigIntStat(await component.handle.stat({ bigint: true }))) || !sameIdentity(component.identity, await regularFileIdentity(
|
|
11404
|
+
if (!sameIdentity(component.identity, identityFromBigIntStat(await component.handle.stat({ bigint: true }))) || !sameIdentity(component.identity, await regularFileIdentity(path20.join(storeDir, name)))) {
|
|
10432
11405
|
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
10433
11406
|
}
|
|
10434
11407
|
}
|
|
10435
11408
|
for (const name of componentNames) {
|
|
10436
|
-
if (!opened.has(name) && await regularFileIdentity(
|
|
11409
|
+
if (!opened.has(name) && await regularFileIdentity(path20.join(storeDir, name)) !== void 0) {
|
|
10437
11410
|
return { status: "unavailable", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal changed during diagnostics snapshot" };
|
|
10438
11411
|
}
|
|
10439
11412
|
}
|
|
10440
11413
|
const header = Buffer.alloc(16);
|
|
10441
|
-
const copiedHandle = await promises.open(
|
|
11414
|
+
const copiedHandle = await promises.open(path20.join(tempDir, JOURNAL_DB_FILENAME), "r");
|
|
10442
11415
|
try {
|
|
10443
11416
|
const { bytesRead } = await copiedHandle.read(header, 0, header.length, 0);
|
|
10444
11417
|
if (bytesRead !== 16 || header.toString("binary") !== "SQLite format 3\0") {
|
|
@@ -10447,7 +11420,7 @@ async function inspectJournal(storeDir) {
|
|
|
10447
11420
|
} finally {
|
|
10448
11421
|
await copiedHandle.close();
|
|
10449
11422
|
}
|
|
10450
|
-
db = new DatabaseSync(
|
|
11423
|
+
db = new DatabaseSync(path20.join(tempDir, JOURNAL_DB_FILENAME), { readOnly: true });
|
|
10451
11424
|
const result = db.prepare("PRAGMA quick_check(1)").get();
|
|
10452
11425
|
if (result?.quick_check !== "ok") {
|
|
10453
11426
|
return { status: "corrupt", sizeBytes, ...walBytes === void 0 ? {} : { walBytes }, reason: "journal quick_check failed" };
|
|
@@ -10482,7 +11455,7 @@ async function inspectWorkspace(workspaceRoot) {
|
|
|
10482
11455
|
}
|
|
10483
11456
|
}
|
|
10484
11457
|
function readPinnedQuarantineFile(name, maxBytes, includeBytes, budget) {
|
|
10485
|
-
if (
|
|
11458
|
+
if (path20.basename(name) !== name || name === "." || name === "..") {
|
|
10486
11459
|
throw new Error("quarantine manifest contains an invalid evidence name");
|
|
10487
11460
|
}
|
|
10488
11461
|
const namedBefore = lstatSync(name, { bigint: true });
|
|
@@ -10586,10 +11559,10 @@ function inspectQuarantinePinned(dir, expectedDirectory) {
|
|
|
10586
11559
|
if (isJournalQuarantineManifest(parsed)) {
|
|
10587
11560
|
const manifestBase = manifestName.slice(0, -".manifest.json".length);
|
|
10588
11561
|
const boundNames = parsed.files.map((file) => {
|
|
10589
|
-
if (
|
|
11562
|
+
if (path20.dirname(path20.resolve(file)) !== path20.resolve(".")) {
|
|
10590
11563
|
throw new Error("journal quarantine manifest points outside quarantine");
|
|
10591
11564
|
}
|
|
10592
|
-
return
|
|
11565
|
+
return path20.basename(file);
|
|
10593
11566
|
});
|
|
10594
11567
|
if (!boundNames.includes(manifestBase)) {
|
|
10595
11568
|
throw new Error("journal quarantine manifest is not bound to its primary database evidence");
|
|
@@ -10629,7 +11602,7 @@ function inspectQuarantinePinned(dir, expectedDirectory) {
|
|
|
10629
11602
|
}
|
|
10630
11603
|
}
|
|
10631
11604
|
async function inspectQuarantine(storeDir) {
|
|
10632
|
-
const dir =
|
|
11605
|
+
const dir = path20.join(storeDir, JOURNAL_QUARANTINE_DIRNAME);
|
|
10633
11606
|
let directory;
|
|
10634
11607
|
try {
|
|
10635
11608
|
directory = await promises.lstat(dir, { bigint: true });
|
|
@@ -10700,7 +11673,7 @@ function checksFor(snapshot) {
|
|
|
10700
11673
|
];
|
|
10701
11674
|
}
|
|
10702
11675
|
async function collectDiagnostics(config, storeDir, options = {}) {
|
|
10703
|
-
const resolvedStoreDir =
|
|
11676
|
+
const resolvedStoreDir = path20.resolve(storeDir);
|
|
10704
11677
|
const adapters = options.adapters ?? defaultRuntimeAdapters(config.runtimeAllowlist);
|
|
10705
11678
|
const connectControl = options.connectControl ?? connectControlClient;
|
|
10706
11679
|
const [device, probedRuntimes, health, journal, workspace, quarantine, controlConnection] = await Promise.all([
|
|
@@ -10875,7 +11848,7 @@ function publishQuarantineEvidencePinned(quarantineDir, expectedDirectory, sourc
|
|
|
10875
11848
|
unlinkSync(sourcePath);
|
|
10876
11849
|
sourceRemoved = true;
|
|
10877
11850
|
if (process.platform !== "win32") {
|
|
10878
|
-
const directoryFd = openSync(
|
|
11851
|
+
const directoryFd = openSync(path20.dirname(sourcePath), constants.O_RDONLY);
|
|
10879
11852
|
try {
|
|
10880
11853
|
fsyncSync(directoryFd);
|
|
10881
11854
|
} finally {
|
|
@@ -10914,10 +11887,10 @@ function publishQuarantineEvidencePinned(quarantineDir, expectedDirectory, sourc
|
|
|
10914
11887
|
}
|
|
10915
11888
|
}
|
|
10916
11889
|
async function quarantineCorruptOperationalHealth(storeDir, options = {}) {
|
|
10917
|
-
const resolvedStoreDir =
|
|
11890
|
+
const resolvedStoreDir = path20.resolve(storeDir);
|
|
10918
11891
|
const owner = await acquireDaemonOwner(resolvedStoreDir, "doctor", options.clock);
|
|
10919
11892
|
try {
|
|
10920
|
-
const sourcePath =
|
|
11893
|
+
const sourcePath = path20.join(resolvedStoreDir, OPERATIONAL_HEALTH_FILENAME);
|
|
10921
11894
|
let opened;
|
|
10922
11895
|
try {
|
|
10923
11896
|
opened = await openOperationalHealthFile(resolvedStoreDir);
|
|
@@ -10934,7 +11907,7 @@ async function quarantineCorruptOperationalHealth(storeDir, options = {}) {
|
|
|
10934
11907
|
}
|
|
10935
11908
|
const sourceStat = await source.stat({ bigint: true });
|
|
10936
11909
|
if (!sourceStat.isFile()) throw new Error("operational health state is not a regular file; refusing quarantine");
|
|
10937
|
-
const quarantineDir =
|
|
11910
|
+
const quarantineDir = path20.join(resolvedStoreDir, JOURNAL_QUARANTINE_DIRNAME);
|
|
10938
11911
|
try {
|
|
10939
11912
|
const existing = await promises.lstat(quarantineDir);
|
|
10940
11913
|
if (!existing.isDirectory() || existing.isSymbolicLink()) {
|
|
@@ -11035,8 +12008,8 @@ function buildServiceDefinition(config, configPath, rest) {
|
|
|
11035
12008
|
const name = argValue(rest, "--name") ?? config.productId;
|
|
11036
12009
|
const agentBin = argValue(rest, "--agent-bin") ?? process.argv[1] ?? "byok-agent";
|
|
11037
12010
|
const nodeBin = argValue(rest, "--node-bin") ?? process.execPath;
|
|
11038
|
-
const absoluteConfigPath =
|
|
11039
|
-
const logDir =
|
|
12011
|
+
const absoluteConfigPath = path20.resolve(configPath);
|
|
12012
|
+
const logDir = path20.join(resolveStoreDir(config), "service-logs");
|
|
11040
12013
|
const definition = {
|
|
11041
12014
|
name,
|
|
11042
12015
|
displayName: config.branding?.displayName ?? config.productName,
|
|
@@ -11090,7 +12063,7 @@ async function runServiceStatusCommand(config, configPath, rest, deps = {}) {
|
|
|
11090
12063
|
log(`detail: ${status.detail.trim() || "(none)"}`);
|
|
11091
12064
|
}
|
|
11092
12065
|
function auditLogPath(storeDir) {
|
|
11093
|
-
return
|
|
12066
|
+
return path20.join(storeDir, "audit.jsonl");
|
|
11094
12067
|
}
|
|
11095
12068
|
var AUDIT_LOG_MODE = 384;
|
|
11096
12069
|
var AUDIT_STORE_DIR_MODE = 448;
|
|
@@ -11223,6 +12196,14 @@ function redactForAudit(event) {
|
|
|
11223
12196
|
return { ...base, reason: event.reason, undeliveredOutboxCount: event.undeliveredOutboxCount };
|
|
11224
12197
|
case "stale-approval-decision":
|
|
11225
12198
|
return { ...base, taskId: event.taskId, decision: event.decision, reasonSize: byteSize(event.reason) };
|
|
12199
|
+
case "device-assertion":
|
|
12200
|
+
return event.result === "issued" ? {
|
|
12201
|
+
...base,
|
|
12202
|
+
result: "issued",
|
|
12203
|
+
audience: event.audience,
|
|
12204
|
+
jti: event.jti,
|
|
12205
|
+
expiresAt: event.expiresAt
|
|
12206
|
+
} : { ...base, result: "denied", reason: event.reason, audienceSize: event.audienceSize };
|
|
11226
12207
|
case "git-workspace":
|
|
11227
12208
|
return {
|
|
11228
12209
|
...base,
|
|
@@ -11363,6 +12344,26 @@ function reconstructDaemonEvent(raw) {
|
|
|
11363
12344
|
reason: reasonSize === void 0 ? void 0 : placeholderFor(reasonSize)
|
|
11364
12345
|
};
|
|
11365
12346
|
}
|
|
12347
|
+
case "device-assertion": {
|
|
12348
|
+
if (raw.result === "issued") {
|
|
12349
|
+
return {
|
|
12350
|
+
kind: "device-assertion",
|
|
12351
|
+
ts,
|
|
12352
|
+
result: "issued",
|
|
12353
|
+
audience: typeof raw.audience === "string" ? raw.audience : "",
|
|
12354
|
+
jti: typeof raw.jti === "string" ? raw.jti : "",
|
|
12355
|
+
expiresAt: typeof raw.expiresAt === "string" ? raw.expiresAt : ""
|
|
12356
|
+
};
|
|
12357
|
+
}
|
|
12358
|
+
const audienceSize = num(raw.audienceSize);
|
|
12359
|
+
return {
|
|
12360
|
+
kind: "device-assertion",
|
|
12361
|
+
ts,
|
|
12362
|
+
result: "denied",
|
|
12363
|
+
reason: typeof raw.reason === "string" ? raw.reason : "",
|
|
12364
|
+
...audienceSize === void 0 ? {} : { audienceSize }
|
|
12365
|
+
};
|
|
12366
|
+
}
|
|
11366
12367
|
case "git-workspace": {
|
|
11367
12368
|
const commitsSinceBaseline = gitCount(raw.commitsSinceBaseline);
|
|
11368
12369
|
const dirty = gitDirty(raw.dirty);
|
|
@@ -11912,11 +12913,11 @@ async function createSupportBundle(config, storeDir, options = {}) {
|
|
|
11912
12913
|
};
|
|
11913
12914
|
}
|
|
11914
12915
|
async function writeSupportBundle(outputPath, bundle, secureFileOptions = {}) {
|
|
11915
|
-
const dir =
|
|
12916
|
+
const dir = path20.dirname(outputPath);
|
|
11916
12917
|
const parentStat = await promises.stat(dir);
|
|
11917
12918
|
if (!parentStat.isDirectory()) throw new Error("support bundle output parent is not a directory");
|
|
11918
|
-
const privateDir =
|
|
11919
|
-
const tempPath =
|
|
12919
|
+
const privateDir = path20.join(dir, `.${path20.basename(outputPath)}.${process.pid}.${randomUUID()}.private`);
|
|
12920
|
+
const tempPath = path20.join(privateDir, "bundle.tmp");
|
|
11920
12921
|
try {
|
|
11921
12922
|
await promises.mkdir(privateDir, { mode: 448 });
|
|
11922
12923
|
await ensureSecureDir(privateDir, secureFileOptions);
|
|
@@ -11946,7 +12947,7 @@ async function writeSupportBundle(outputPath, bundle, secureFileOptions = {}) {
|
|
|
11946
12947
|
// src/bin/commands/support-bundle.ts
|
|
11947
12948
|
async function runSupportBundleCommand(config, options) {
|
|
11948
12949
|
if (!options.outputPath) throw new Error("support-bundle requires --output <path>");
|
|
11949
|
-
const outputPath =
|
|
12950
|
+
const outputPath = path20.resolve(options.outputPath);
|
|
11950
12951
|
const bundle = await createSupportBundle(config, resolveStoreDir(config), options);
|
|
11951
12952
|
await writeSupportBundle(outputPath, bundle);
|
|
11952
12953
|
const log = options.log ?? ((line) => console.log(line));
|
|
@@ -11982,8 +12983,8 @@ async function runTasksFollowCommand(config, deps) {
|
|
|
11982
12983
|
});
|
|
11983
12984
|
return;
|
|
11984
12985
|
}
|
|
11985
|
-
const
|
|
11986
|
-
await followAuditLog(
|
|
12986
|
+
const path25 = auditLogPath(storeDir);
|
|
12987
|
+
await followAuditLog(path25, (event) => log(formatDaemonEventLine(event)), {
|
|
11987
12988
|
signal: deps.signal,
|
|
11988
12989
|
pollIntervalMs: deps.pollIntervalMs,
|
|
11989
12990
|
fromEnd: true
|