@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/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { execFile, spawn, spawnSync } from 'child_process';
|
|
2
|
-
import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes,
|
|
3
|
-
import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, realpathSync } from 'fs';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
2
|
+
import { createHash, randomUUID, sign, createPrivateKey, generateKeyPairSync, randomBytes, timingSafeEqual, createHmac } from 'crypto';
|
|
3
|
+
import { promises, mkdirSync, existsSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, linkSync, fstatSync, lstatSync, unlinkSync, constants, readFileSync, realpathSync } from 'fs';
|
|
4
|
+
import path17, { join, isAbsolute } from 'path';
|
|
5
|
+
import os5 from 'os';
|
|
6
|
+
import { parseDeviceAssertionEnvelope, tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash, TRUTH_RECORD_KINDS, nonceSigningBytes, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
7
|
+
import { BYOK_PAIR_PATH, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, ToolsetIdSchema, encodeEnvelope, createEnvelope, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, RuntimeIdSchema, RESULT_DOCUMENT_MAX_BYTES, PROTOCOL_VERSION, decodeEnvelope, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, BYOK_EVENTS_PATH, parseMessage, UnknownMessageTypeError, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
7
8
|
import { promisify } from 'util';
|
|
8
9
|
import { fileURLToPath } from 'url';
|
|
9
10
|
import 'readline';
|
|
10
11
|
import net, { createServer, createConnection } from 'net';
|
|
11
12
|
import { WebSocket } from 'ws';
|
|
12
13
|
import { createRequire } from 'module';
|
|
13
|
-
import { tenantId, DeviceProofProtectedClaimsSchema, deviceProofSigningInput, DEVICE_PROOF_SCHEMA_ID, contentHash, TRUTH_RECORD_KINDS } from '@byok-sdk/core';
|
|
14
14
|
|
|
15
15
|
// src/types.ts
|
|
16
16
|
var PolicyUnsupportedError = class extends Error {
|
|
@@ -71,7 +71,7 @@ function gitEnvironment(readOnly) {
|
|
|
71
71
|
return env;
|
|
72
72
|
}
|
|
73
73
|
function stableGitWorkspaceOwnerId(storeDir, productId) {
|
|
74
|
-
const identity = `${
|
|
74
|
+
const identity = `${path17.resolve(storeDir)}\\0${productId}`;
|
|
75
75
|
return `store-product:${createHash("sha256").update(identity).digest("hex")}`;
|
|
76
76
|
}
|
|
77
77
|
var GUIDANCE = [
|
|
@@ -83,11 +83,11 @@ var GUIDANCE = [
|
|
|
83
83
|
"Leave incomplete work visible for recovery."
|
|
84
84
|
].join("\n");
|
|
85
85
|
function canonical(value) {
|
|
86
|
-
return
|
|
86
|
+
return path17.resolve(value);
|
|
87
87
|
}
|
|
88
88
|
function isContained(root, candidate) {
|
|
89
|
-
const relative =
|
|
90
|
-
return relative === "" || !relative.startsWith(`..${
|
|
89
|
+
const relative = path17.relative(root, candidate);
|
|
90
|
+
return relative === "" || !relative.startsWith(`..${path17.sep}`) && !path17.isAbsolute(relative);
|
|
91
91
|
}
|
|
92
92
|
function bounded(value, max) {
|
|
93
93
|
return Buffer.byteLength(value, "utf8") <= max ? value : value.slice(0, max);
|
|
@@ -192,7 +192,7 @@ var GitWorkspaceManager = class {
|
|
|
192
192
|
await this.ensureOwnerMarker();
|
|
193
193
|
}
|
|
194
194
|
async ensureOwnerMarker() {
|
|
195
|
-
const markerPath =
|
|
195
|
+
const markerPath = path17.join(this.workspaceRoot, OWNER_MARKER);
|
|
196
196
|
let existing;
|
|
197
197
|
try {
|
|
198
198
|
existing = JSON.parse(await promises.readFile(markerPath, "utf8"));
|
|
@@ -351,7 +351,7 @@ ${instruction}`;
|
|
|
351
351
|
if (error instanceof GitWorkspaceError || code !== "ENOENT" && code !== "ENOTDIR") {
|
|
352
352
|
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
353
353
|
}
|
|
354
|
-
const parent =
|
|
354
|
+
const parent = path17.dirname(current);
|
|
355
355
|
if (parent === current) {
|
|
356
356
|
throw new GitWorkspaceError("workspace-root-invalid", "workspace directory is invalid");
|
|
357
357
|
}
|
|
@@ -416,7 +416,7 @@ async function atomicWriteFile(filePath, data, options = {}) {
|
|
|
416
416
|
await target.close();
|
|
417
417
|
}
|
|
418
418
|
if (process.platform !== "win32") {
|
|
419
|
-
const directory = await promises.open(
|
|
419
|
+
const directory = await promises.open(path17.dirname(filePath), "r");
|
|
420
420
|
try {
|
|
421
421
|
await directory.sync();
|
|
422
422
|
} finally {
|
|
@@ -508,7 +508,7 @@ async function ensureSecureDir(dir, opts = {}) {
|
|
|
508
508
|
await promises.chmod(dir, 448).catch(() => {
|
|
509
509
|
});
|
|
510
510
|
if (platform !== "win32") return;
|
|
511
|
-
const { username } =
|
|
511
|
+
const { username } = os5.userInfo();
|
|
512
512
|
let result;
|
|
513
513
|
try {
|
|
514
514
|
result = await run("icacls", buildIcaclsArgs(dir, username));
|
|
@@ -541,7 +541,7 @@ function isProtected(record) {
|
|
|
541
541
|
var GitWorkspaceStore = class {
|
|
542
542
|
constructor(storeDir, options = {}) {
|
|
543
543
|
this.storeDir = storeDir;
|
|
544
|
-
this.filePath =
|
|
544
|
+
this.filePath = path17.join(storeDir, FILE_NAME);
|
|
545
545
|
this.maxRecords = Math.max(1, Math.floor(options.maxRecords ?? MAX_RECORDS));
|
|
546
546
|
}
|
|
547
547
|
storeDir;
|
|
@@ -671,20 +671,50 @@ var GitWorkspaceStore = class {
|
|
|
671
671
|
await atomicWriteFile(this.filePath, JSON.stringify(ledger, null, 2), { mode: 384 });
|
|
672
672
|
}
|
|
673
673
|
};
|
|
674
|
-
|
|
675
|
-
// src/adapters/pi/resolve-bin.ts
|
|
676
674
|
var PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
675
|
+
function readPackageJson(dir) {
|
|
676
|
+
const candidate = path17.join(dir, "package.json");
|
|
677
|
+
if (!existsSync(candidate)) return void 0;
|
|
678
|
+
try {
|
|
679
|
+
return JSON.parse(readFileSync(candidate, "utf8"));
|
|
680
|
+
} catch {
|
|
681
|
+
return void 0;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
677
684
|
function resolvePiBin() {
|
|
678
685
|
const override = process.env.BYOK_PI_BIN;
|
|
679
686
|
if (override) {
|
|
680
|
-
return { command: override, source: "
|
|
687
|
+
return { command: override, source: "env" };
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
const mainEntryUrl = import.meta.resolve(PI_PACKAGE_NAME);
|
|
691
|
+
let dir = path17.dirname(fileURLToPath(mainEntryUrl));
|
|
692
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
693
|
+
const pkg = readPackageJson(dir);
|
|
694
|
+
if (pkg?.name === PI_PACKAGE_NAME) {
|
|
695
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
|
|
696
|
+
if (binRel) {
|
|
697
|
+
return { command: path17.join(dir, binRel), source: "package" };
|
|
698
|
+
}
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
const parent = path17.dirname(dir);
|
|
702
|
+
if (parent === dir) break;
|
|
703
|
+
dir = parent;
|
|
704
|
+
}
|
|
705
|
+
} catch (cause) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`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`,
|
|
708
|
+
{ cause }
|
|
709
|
+
);
|
|
681
710
|
}
|
|
682
|
-
|
|
711
|
+
throw new Error(
|
|
712
|
+
`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`
|
|
713
|
+
);
|
|
683
714
|
}
|
|
684
715
|
|
|
685
716
|
// src/adapters/pi/permission-mapping.ts
|
|
686
717
|
var READONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
687
|
-
var DEFAULT_ACTIVE_TOOLS = ["read", "bash", "edit", "write"];
|
|
688
718
|
function mapPermissionPolicyToPiArgs(policy) {
|
|
689
719
|
if (policy.network === false) {
|
|
690
720
|
return {
|
|
@@ -703,22 +733,18 @@ function mapPermissionPolicyToPiArgs(policy) {
|
|
|
703
733
|
const denyTools = policy.denyTools ?? [];
|
|
704
734
|
if (policy.mode === "readonly") {
|
|
705
735
|
const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS.includes(tool)) : [...READONLY_TOOLS];
|
|
706
|
-
|
|
707
|
-
return {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const effective = subtractDenied(base, denyTools);
|
|
712
|
-
return { ok: true, args: effective.length === 0 ? ["--no-tools"] : ["--tools", effective.join(",")] };
|
|
736
|
+
if (base.length === 0) return { ok: true, args: ["--no-tools"] };
|
|
737
|
+
return {
|
|
738
|
+
ok: true,
|
|
739
|
+
args: ["--tools", base.join(","), ...denyTools.length > 0 ? ["--exclude-tools", denyTools.join(",")] : []]
|
|
740
|
+
};
|
|
713
741
|
}
|
|
742
|
+
const args = [];
|
|
714
743
|
if (policy.allowTools && policy.allowTools.length > 0) {
|
|
715
|
-
|
|
744
|
+
args.push("--tools", policy.allowTools.join(","));
|
|
716
745
|
}
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
function subtractDenied(tools, denyTools) {
|
|
720
|
-
const denied = new Set(denyTools);
|
|
721
|
-
return tools.filter((tool) => !denied.has(tool));
|
|
746
|
+
if (denyTools.length > 0) args.push("--exclude-tools", denyTools.join(","));
|
|
747
|
+
return { ok: true, args };
|
|
722
748
|
}
|
|
723
749
|
|
|
724
750
|
// src/adapters/pi/events.ts
|
|
@@ -743,7 +769,7 @@ function mapPiMessageToAgentEvent(msg) {
|
|
|
743
769
|
output: { result: msg.result, isError: msg.isError === true }
|
|
744
770
|
};
|
|
745
771
|
}
|
|
746
|
-
case "
|
|
772
|
+
case "agent_settled":
|
|
747
773
|
return { type: "turn_end" };
|
|
748
774
|
/**
|
|
749
775
|
* `artifact` is NOT a real pi RPC message — pi's own `write` tool only
|
|
@@ -779,16 +805,22 @@ function mapPiMessageToAgentEvent(msg) {
|
|
|
779
805
|
// `recordUnmappedFrame`) can tell "known, expected, silently ignored"
|
|
780
806
|
// apart from "genuinely never seen before" (falls to `default` below).
|
|
781
807
|
case "agent_start":
|
|
808
|
+
case "agent_end":
|
|
809
|
+
// one low-level run; `agent_settled` is BYOK completion
|
|
782
810
|
case "turn_start":
|
|
783
811
|
case "turn_end":
|
|
784
|
-
// pi's own per-LLM-turn boundary, not ours
|
|
812
|
+
// pi's own per-LLM-turn boundary, not ours
|
|
785
813
|
case "message_start":
|
|
786
814
|
case "message_end":
|
|
815
|
+
case "bash_execution_update":
|
|
787
816
|
case "tool_execution_update":
|
|
788
817
|
case "queue_update":
|
|
789
818
|
case "compaction_start":
|
|
790
819
|
case "compaction_end":
|
|
791
820
|
case "auto_retry_start":
|
|
821
|
+
case "summarization_retry_scheduled":
|
|
822
|
+
case "summarization_retry_attempt_start":
|
|
823
|
+
case "summarization_retry_finished":
|
|
792
824
|
case "session_info_changed":
|
|
793
825
|
case "thinking_level_changed":
|
|
794
826
|
return void 0;
|
|
@@ -798,15 +830,20 @@ function mapPiMessageToAgentEvent(msg) {
|
|
|
798
830
|
}
|
|
799
831
|
var ROUTINE_PI_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
800
832
|
"agent_start",
|
|
833
|
+
"agent_end",
|
|
801
834
|
"turn_start",
|
|
802
835
|
"turn_end",
|
|
803
836
|
"message_start",
|
|
804
837
|
"message_end",
|
|
838
|
+
"bash_execution_update",
|
|
805
839
|
"tool_execution_update",
|
|
806
840
|
"queue_update",
|
|
807
841
|
"compaction_start",
|
|
808
842
|
"compaction_end",
|
|
809
843
|
"auto_retry_start",
|
|
844
|
+
"summarization_retry_scheduled",
|
|
845
|
+
"summarization_retry_attempt_start",
|
|
846
|
+
"summarization_retry_finished",
|
|
810
847
|
"session_info_changed",
|
|
811
848
|
"thinking_level_changed"
|
|
812
849
|
]);
|
|
@@ -940,10 +977,7 @@ var PiRpcClient = class {
|
|
|
940
977
|
* traffic. Logs once per distinct type (not per occurrence, so a
|
|
941
978
|
* repeating unmapped type can't spam stdout); the running tally is also
|
|
942
979
|
* folded into this client's exit-time error message (`buildExitError`) so
|
|
943
|
-
* a post-mortem on a failed/hung task has it without
|
|
944
|
-
* scraping. This is the exact mechanism that would have turned this
|
|
945
|
-
* task's root-cause hang (`agent_end` arriving with no mapping) into a
|
|
946
|
-
* one-line, immediate warning instead of a silent stall.
|
|
980
|
+
* a post-mortem on a failed/hung task has it without separate log scraping.
|
|
947
981
|
*/
|
|
948
982
|
recordUnmappedFrame(type) {
|
|
949
983
|
const next = (this.unmappedFrameCounts.get(type) ?? 0) + 1;
|
|
@@ -1054,13 +1088,8 @@ var PiRpcClient = class {
|
|
|
1054
1088
|
}
|
|
1055
1089
|
};
|
|
1056
1090
|
|
|
1057
|
-
// src/adapters/
|
|
1058
|
-
var
|
|
1059
|
-
var DETECT_TIMEOUT_MS = 5e3;
|
|
1060
|
-
function errorMessage(err) {
|
|
1061
|
-
return err instanceof Error ? err.message : String(err);
|
|
1062
|
-
}
|
|
1063
|
-
var KNOWN_PROVIDER_ENV_VARS = [
|
|
1091
|
+
// src/adapters/provider-credential-environment.ts
|
|
1092
|
+
var PROVIDER_CREDENTIAL_ENV_NAMES = [
|
|
1064
1093
|
"ANTHROPIC_API_KEY",
|
|
1065
1094
|
"ANTHROPIC_OAUTH_TOKEN",
|
|
1066
1095
|
"OPENAI_API_KEY",
|
|
@@ -1071,24 +1100,66 @@ var KNOWN_PROVIDER_ENV_VARS = [
|
|
|
1071
1100
|
"MISTRAL_API_KEY",
|
|
1072
1101
|
"OPENROUTER_API_KEY",
|
|
1073
1102
|
"XAI_API_KEY",
|
|
1074
|
-
// Confirmed against the installed pi's own docs/providers.md ("ZAI |
|
|
1075
|
-
// `ZAI_API_KEY` | `zai`") and exercised live against real GLM traffic
|
|
1076
|
-
// during this task's acceptance run — omitting it made `authPresent`
|
|
1077
|
-
// silently false for a perfectly valid, working z.ai/GLM setup.
|
|
1078
1103
|
"ZAI_API_KEY"
|
|
1079
1104
|
];
|
|
1105
|
+
var PROVIDER_CREDENTIAL_ENV_DENY_NAMES = [
|
|
1106
|
+
...PROVIDER_CREDENTIAL_ENV_NAMES,
|
|
1107
|
+
"ANT_LING_API_KEY",
|
|
1108
|
+
"NVIDIA_API_KEY",
|
|
1109
|
+
"CEREBRAS_API_KEY",
|
|
1110
|
+
"CLOUDFLARE_API_KEY",
|
|
1111
|
+
"AI_GATEWAY_API_KEY",
|
|
1112
|
+
"ZAI_CODING_CN_API_KEY",
|
|
1113
|
+
"OPENCODE_API_KEY",
|
|
1114
|
+
"RADIUS_API_KEY",
|
|
1115
|
+
"FIREWORKS_API_KEY",
|
|
1116
|
+
"TOGETHER_API_KEY",
|
|
1117
|
+
"BASETEN_API_KEY",
|
|
1118
|
+
"KIMI_API_KEY",
|
|
1119
|
+
"MINIMAX_API_KEY",
|
|
1120
|
+
"MINIMAX_CN_API_KEY",
|
|
1121
|
+
"QWEN_TOKEN_PLAN_API_KEY",
|
|
1122
|
+
"QWEN_TOKEN_PLAN_CN_API_KEY",
|
|
1123
|
+
"XIAOMI_API_KEY",
|
|
1124
|
+
"XIAOMI_TOKEN_PLAN_CN_API_KEY",
|
|
1125
|
+
"XIAOMI_TOKEN_PLAN_AMS_API_KEY",
|
|
1126
|
+
"XIAOMI_TOKEN_PLAN_SGP_API_KEY",
|
|
1127
|
+
"AWS_ACCESS_KEY_ID",
|
|
1128
|
+
"AWS_SECRET_ACCESS_KEY",
|
|
1129
|
+
"AWS_SESSION_TOKEN",
|
|
1130
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
1131
|
+
// Reserved by the keys-owned Pi projection. It must never be inherited
|
|
1132
|
+
// from the daemon; the launcher deletes any ambient copy and injects only
|
|
1133
|
+
// the exact credential it just resolved from OS custody.
|
|
1134
|
+
"PI_PROVIDER_API_KEY"
|
|
1135
|
+
];
|
|
1136
|
+
function withoutProviderCredentials(env) {
|
|
1137
|
+
const sanitized = { ...env };
|
|
1138
|
+
for (const name of PROVIDER_CREDENTIAL_ENV_DENY_NAMES) {
|
|
1139
|
+
delete sanitized[name];
|
|
1140
|
+
}
|
|
1141
|
+
return sanitized;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/adapters/pi/pi-adapter.ts
|
|
1145
|
+
var execFileAsync = promisify(execFile);
|
|
1146
|
+
var DETECT_TIMEOUT_MS = 5e3;
|
|
1147
|
+
function errorMessage(err) {
|
|
1148
|
+
return err instanceof Error ? err.message : String(err);
|
|
1149
|
+
}
|
|
1080
1150
|
var PiAdapter = class {
|
|
1081
1151
|
constructor(options = {}) {
|
|
1082
1152
|
this.options = options;
|
|
1083
1153
|
}
|
|
1084
1154
|
options;
|
|
1085
1155
|
id = "pi";
|
|
1156
|
+
supportsDispatchSelection = true;
|
|
1086
1157
|
async detect() {
|
|
1087
|
-
const bin = this.resolveBin();
|
|
1088
1158
|
try {
|
|
1159
|
+
const bin = this.resolveBin();
|
|
1089
1160
|
const { stdout, stderr } = await execFileAsync(bin.command, ["--version"], { timeout: DETECT_TIMEOUT_MS });
|
|
1090
1161
|
const version = stdout.trim() || stderr.trim();
|
|
1091
|
-
const authPresent =
|
|
1162
|
+
const authPresent = PROVIDER_CREDENTIAL_ENV_NAMES.some((name) => process.env[name] !== void 0);
|
|
1092
1163
|
return { present: true, version, authPresent };
|
|
1093
1164
|
} catch {
|
|
1094
1165
|
return { present: false };
|
|
@@ -1107,7 +1178,7 @@ var PiAdapter = class {
|
|
|
1107
1178
|
* variable beyond the platform baseline (`daemon/environment.ts`).
|
|
1108
1179
|
*/
|
|
1109
1180
|
environmentRequirements() {
|
|
1110
|
-
return { credentialNames:
|
|
1181
|
+
return { credentialNames: PROVIDER_CREDENTIAL_ENV_NAMES };
|
|
1111
1182
|
}
|
|
1112
1183
|
async start(task, ctx) {
|
|
1113
1184
|
if (typeof task.instruction !== "string") {
|
|
@@ -1119,12 +1190,45 @@ var PiAdapter = class {
|
|
|
1119
1190
|
}
|
|
1120
1191
|
const bin = this.resolveBin();
|
|
1121
1192
|
const resumeSessionId = task.sessionRef;
|
|
1122
|
-
const
|
|
1193
|
+
const piArgs = ["--mode", "rpc", ...resumeSessionId ? ["--session", resumeSessionId] : [], ...mapping.args];
|
|
1194
|
+
const selection = task.dispatchSelection;
|
|
1195
|
+
let command = bin.command;
|
|
1196
|
+
let args = piArgs;
|
|
1197
|
+
if (selection !== void 0) {
|
|
1198
|
+
if (selection.lane !== "byok" || selection.runtimeId !== "pi") {
|
|
1199
|
+
throw new PolicyUnsupportedError(
|
|
1200
|
+
`pi adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
const launcher = this.options.byokLauncher;
|
|
1204
|
+
if (launcher === void 0) {
|
|
1205
|
+
throw new PolicyUnsupportedError(
|
|
1206
|
+
"pi BYOK selection requires a configured credential-custody launcher"
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
command = launcher.command;
|
|
1210
|
+
args = [
|
|
1211
|
+
...launcher.args ?? [],
|
|
1212
|
+
"--pi-bin",
|
|
1213
|
+
bin.command,
|
|
1214
|
+
"--profile-db",
|
|
1215
|
+
launcher.profileDbPath,
|
|
1216
|
+
"--session-dir",
|
|
1217
|
+
launcher.sessionDir,
|
|
1218
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
1219
|
+
"--provider",
|
|
1220
|
+
selection.providerId,
|
|
1221
|
+
"--model",
|
|
1222
|
+
selection.modelId,
|
|
1223
|
+
"--",
|
|
1224
|
+
...piArgs
|
|
1225
|
+
];
|
|
1226
|
+
}
|
|
1123
1227
|
const rpc = new PiRpcClient({
|
|
1124
|
-
command
|
|
1228
|
+
command,
|
|
1125
1229
|
args,
|
|
1126
1230
|
cwd: ctx.workspaceDir,
|
|
1127
|
-
env: ctx.env,
|
|
1231
|
+
env: selection === void 0 ? ctx.env : withoutProviderCredentials(ctx.env),
|
|
1128
1232
|
spawnFn: this.options.spawnFn
|
|
1129
1233
|
});
|
|
1130
1234
|
const response = await rpc.send({ type: "prompt", message: task.instruction });
|
|
@@ -1143,7 +1247,7 @@ var PiAdapter = class {
|
|
|
1143
1247
|
throw err;
|
|
1144
1248
|
}
|
|
1145
1249
|
}
|
|
1146
|
-
return new PiSession(sessionRef, rpc);
|
|
1250
|
+
return new PiSession(sessionRef, rpc, selection);
|
|
1147
1251
|
}
|
|
1148
1252
|
resolveBin() {
|
|
1149
1253
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
@@ -1171,12 +1275,14 @@ async function resolveFreshSessionId(rpc) {
|
|
|
1171
1275
|
);
|
|
1172
1276
|
}
|
|
1173
1277
|
var PiSession = class {
|
|
1174
|
-
constructor(sessionRef, rpc) {
|
|
1278
|
+
constructor(sessionRef, rpc, selection) {
|
|
1175
1279
|
this.sessionRef = sessionRef;
|
|
1176
1280
|
this.rpc = rpc;
|
|
1281
|
+
this.selection = selection;
|
|
1177
1282
|
}
|
|
1178
1283
|
sessionRef;
|
|
1179
1284
|
rpc;
|
|
1285
|
+
selection;
|
|
1180
1286
|
get events() {
|
|
1181
1287
|
const rpc = this.rpc;
|
|
1182
1288
|
return {
|
|
@@ -1205,6 +1311,12 @@ var PiSession = class {
|
|
|
1205
1311
|
if (typeof task.instruction !== "string") {
|
|
1206
1312
|
throw new PolicyUnsupportedError("pi adapter only supports string instructions in M0 (no blob-ref fetch yet)");
|
|
1207
1313
|
}
|
|
1314
|
+
const requestedSelection = task.dispatchSelection;
|
|
1315
|
+
if (requestedSelection !== void 0 && (this.selection?.lane !== "byok" || requestedSelection.lane !== "byok" || requestedSelection.runtimeId !== "pi" || requestedSelection.providerId !== this.selection.providerId || requestedSelection.modelId !== this.selection.modelId)) {
|
|
1316
|
+
throw new PolicyUnsupportedError(
|
|
1317
|
+
"pi persistent session cannot change its authoritative BYOK provider/model selection"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1208
1320
|
await this.rpc.send({ type: "prompt", message: task.instruction, streamingBehavior: "followUp" });
|
|
1209
1321
|
}
|
|
1210
1322
|
async interrupt() {
|
|
@@ -1231,7 +1343,7 @@ function resolveApprovalMcpBin() {
|
|
|
1231
1343
|
if (override) {
|
|
1232
1344
|
return { command: override, args: [], source: "env" };
|
|
1233
1345
|
}
|
|
1234
|
-
const distBin =
|
|
1346
|
+
const distBin = path17.join(path17.dirname(fileURLToPath(import.meta.url)), "bin", "byok-approval-mcp.js");
|
|
1235
1347
|
return { command: process.execPath, args: [distBin], source: "dist" };
|
|
1236
1348
|
}
|
|
1237
1349
|
|
|
@@ -1262,7 +1374,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
|
|
|
1262
1374
|
}
|
|
1263
1375
|
if (policy.mode === "readonly") {
|
|
1264
1376
|
const base = policy.allowTools ? policy.allowTools.filter((tool) => READONLY_TOOLS2.includes(tool)) : [...READONLY_TOOLS2];
|
|
1265
|
-
const effective =
|
|
1377
|
+
const effective = subtractDenied(base, denyTools);
|
|
1266
1378
|
return { ok: true, args: ["--permission-mode", "default", "--tools", effective.join(",")] };
|
|
1267
1379
|
}
|
|
1268
1380
|
if (denyTools.length > 0) {
|
|
@@ -1279,7 +1391,7 @@ function mapPermissionPolicyToClaudeArgs(policy) {
|
|
|
1279
1391
|
}
|
|
1280
1392
|
return { ok: true, args };
|
|
1281
1393
|
}
|
|
1282
|
-
function
|
|
1394
|
+
function subtractDenied(tools, denyTools) {
|
|
1283
1395
|
const denied = new Set(denyTools);
|
|
1284
1396
|
return tools.filter((tool) => !denied.has(tool));
|
|
1285
1397
|
}
|
|
@@ -1311,7 +1423,7 @@ var EXTENSION_CONTENT_TYPES = {
|
|
|
1311
1423
|
".yml": "application/yaml"
|
|
1312
1424
|
};
|
|
1313
1425
|
function guessContentType(filePath) {
|
|
1314
|
-
const ext =
|
|
1426
|
+
const ext = path17.extname(filePath).toLowerCase();
|
|
1315
1427
|
return EXTENSION_CONTENT_TYPES[ext] ?? "application/octet-stream";
|
|
1316
1428
|
}
|
|
1317
1429
|
function mapAssistant(msg, correlation) {
|
|
@@ -1383,11 +1495,11 @@ function tryBuildArtifactEvent(msg, workspaceDir) {
|
|
|
1383
1495
|
const filePath = toolUseResult && typeof toolUseResult.filePath === "string" ? toolUseResult.filePath : void 0;
|
|
1384
1496
|
if (!filePath) return void 0;
|
|
1385
1497
|
const realWorkspaceDir = tryRealpath(workspaceDir) ?? workspaceDir;
|
|
1386
|
-
const fileDir =
|
|
1498
|
+
const fileDir = path17.dirname(filePath);
|
|
1387
1499
|
const realFileDir = tryRealpath(fileDir) ?? fileDir;
|
|
1388
|
-
const realFilePath =
|
|
1389
|
-
const relative =
|
|
1390
|
-
if (relative === "" || relative.startsWith("..") ||
|
|
1500
|
+
const realFilePath = path17.join(realFileDir, path17.basename(filePath));
|
|
1501
|
+
const relative = path17.relative(realWorkspaceDir, realFilePath);
|
|
1502
|
+
if (relative === "" || relative.startsWith("..") || path17.isAbsolute(relative)) {
|
|
1391
1503
|
return void 0;
|
|
1392
1504
|
}
|
|
1393
1505
|
return { type: "artifact", name: relative, contentType: guessContentType(filePath) };
|
|
@@ -1629,7 +1741,7 @@ var APPROVAL_TOOL_NAME = "approval_prompt";
|
|
|
1629
1741
|
var APPROVAL_MCP_SERVER_NAME = "byokapproval";
|
|
1630
1742
|
var execFileAsync2 = promisify(execFile);
|
|
1631
1743
|
var DETECT_TIMEOUT_MS2 = 5e3;
|
|
1632
|
-
async function
|
|
1744
|
+
async function cleanupMcpConfigDir(dir) {
|
|
1633
1745
|
if (!dir) return;
|
|
1634
1746
|
await promises.rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1635
1747
|
});
|
|
@@ -1639,6 +1751,7 @@ var ClaudeAdapter = class {
|
|
|
1639
1751
|
this.options = options;
|
|
1640
1752
|
}
|
|
1641
1753
|
options;
|
|
1754
|
+
supportsDispatchSelection = true;
|
|
1642
1755
|
id = "claude";
|
|
1643
1756
|
async detect() {
|
|
1644
1757
|
const bin = this.resolveBin();
|
|
@@ -1652,7 +1765,13 @@ var ClaudeAdapter = class {
|
|
|
1652
1765
|
}
|
|
1653
1766
|
}
|
|
1654
1767
|
capabilities() {
|
|
1655
|
-
return {
|
|
1768
|
+
return {
|
|
1769
|
+
steer: false,
|
|
1770
|
+
resume: true,
|
|
1771
|
+
approvalInteractive: true,
|
|
1772
|
+
mcpToolsets: true,
|
|
1773
|
+
permissionModes: ["auto", "readonly", "plan", "confirm"]
|
|
1774
|
+
};
|
|
1656
1775
|
}
|
|
1657
1776
|
/**
|
|
1658
1777
|
* M5: deliberate product-boundary decision, not an oversight — byok's
|
|
@@ -1678,42 +1797,51 @@ var ClaudeAdapter = class {
|
|
|
1678
1797
|
if (!mapping.ok) {
|
|
1679
1798
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by claude adapter");
|
|
1680
1799
|
}
|
|
1681
|
-
|
|
1800
|
+
const modelId = subscriptionModel(task, "claude");
|
|
1801
|
+
let mcpConfigDir;
|
|
1802
|
+
const taskMcpServers = ctx.mcpServers ?? {};
|
|
1803
|
+
const needsMcpConfig = mapping.needsApprovalMcp || Object.keys(taskMcpServers).length > 0;
|
|
1682
1804
|
if (mapping.needsApprovalMcp) {
|
|
1683
1805
|
if (!ctx.approvalChannel) {
|
|
1684
1806
|
throw new PolicyUnsupportedError(
|
|
1685
1807
|
'claude adapter requires policy.mode "confirm" to be started with an approval channel (TaskContext.approvalChannel) \u2014 none was provided'
|
|
1686
1808
|
);
|
|
1687
1809
|
}
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1810
|
+
if (Object.prototype.hasOwnProperty.call(taskMcpServers, APPROVAL_MCP_SERVER_NAME)) {
|
|
1811
|
+
throw new PolicyUnsupportedError(
|
|
1812
|
+
`MCP server name "${APPROVAL_MCP_SERVER_NAME}" is reserved by the claude approval channel`
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
if (needsMcpConfig) {
|
|
1817
|
+
mcpConfigDir = await promises.mkdtemp(path17.join(os5.tmpdir(), "byok-mcp-"));
|
|
1818
|
+
await promises.chmod(mcpConfigDir, 448).catch(() => {
|
|
1691
1819
|
});
|
|
1692
|
-
const mcpConfigPath =
|
|
1693
|
-
const
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1820
|
+
const mcpConfigPath = path17.join(mcpConfigDir, "mcp-config.json");
|
|
1821
|
+
const mcpServers = { ...taskMcpServers };
|
|
1822
|
+
if (mapping.needsApprovalMcp) {
|
|
1823
|
+
const approvalChannel = ctx.approvalChannel;
|
|
1824
|
+
if (!approvalChannel) throw new Error("unreachable: approval channel checked above");
|
|
1825
|
+
const approvalMcpBin = (this.options.resolveApprovalMcpBin ?? resolveApprovalMcpBin)();
|
|
1826
|
+
mcpServers[APPROVAL_MCP_SERVER_NAME] = {
|
|
1827
|
+
command: approvalMcpBin.command,
|
|
1828
|
+
args: approvalMcpBin.args,
|
|
1829
|
+
env: {
|
|
1830
|
+
BYOK_STORE_DIR: approvalChannel.storeDir,
|
|
1831
|
+
BYOK_PRODUCT_ID: approvalChannel.productId,
|
|
1832
|
+
BYOK_TASK_ID: approvalChannel.taskId,
|
|
1833
|
+
BYOK_APPROVAL_TIMEOUT_MS: String(approvalChannel.timeoutMs)
|
|
1704
1834
|
}
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
await promises.writeFile(mcpConfigPath, JSON.stringify(
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
await promises.writeFile(mcpConfigPath, JSON.stringify({ mcpServers }), { mode: 384 });
|
|
1708
1838
|
mapping.args = [
|
|
1709
1839
|
...mapping.args,
|
|
1710
|
-
"--permission-prompt-tool",
|
|
1711
|
-
`mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`,
|
|
1840
|
+
...mapping.needsApprovalMcp ? ["--permission-prompt-tool", `mcp__${APPROVAL_MCP_SERVER_NAME}__${APPROVAL_TOOL_NAME}`] : [],
|
|
1712
1841
|
"--mcp-config",
|
|
1713
1842
|
mcpConfigPath,
|
|
1714
|
-
//
|
|
1715
|
-
//
|
|
1716
|
-
// the only MCP server this invocation should ever see.
|
|
1843
|
+
// The generated file is the complete task-scoped MCP authority.
|
|
1844
|
+
// Never merge ambient user/project MCP configuration into it.
|
|
1717
1845
|
"--strict-mcp-config"
|
|
1718
1846
|
];
|
|
1719
1847
|
}
|
|
@@ -1730,6 +1858,7 @@ var ClaudeAdapter = class {
|
|
|
1730
1858
|
// "Error: When using --print, --output-format=stream-json requires
|
|
1731
1859
|
// --verbose", before spawning any model call.
|
|
1732
1860
|
"--verbose",
|
|
1861
|
+
...modelId ? ["--model", modelId] : [],
|
|
1733
1862
|
...resumeSessionId ? ["--resume", resumeSessionId] : [],
|
|
1734
1863
|
...mapping.args
|
|
1735
1864
|
];
|
|
@@ -1737,7 +1866,7 @@ var ClaudeAdapter = class {
|
|
|
1737
1866
|
command: bin.command,
|
|
1738
1867
|
args,
|
|
1739
1868
|
cwd: ctx.workspaceDir,
|
|
1740
|
-
env: ctx.env,
|
|
1869
|
+
env: withoutProviderCredentials(ctx.env),
|
|
1741
1870
|
spawnFn: this.options.spawnFn
|
|
1742
1871
|
});
|
|
1743
1872
|
client.writeUserMessage(task.instruction);
|
|
@@ -1746,17 +1875,24 @@ var ClaudeAdapter = class {
|
|
|
1746
1875
|
sessionRef = await client.waitForInit();
|
|
1747
1876
|
} catch (err) {
|
|
1748
1877
|
client.kill();
|
|
1749
|
-
await
|
|
1878
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1750
1879
|
throw err;
|
|
1751
1880
|
}
|
|
1752
1881
|
if (resumeSessionId !== void 0 && sessionRef !== resumeSessionId) {
|
|
1753
1882
|
client.kill();
|
|
1754
|
-
await
|
|
1883
|
+
await cleanupMcpConfigDir(mcpConfigDir);
|
|
1755
1884
|
throw new Error(
|
|
1756
1885
|
`claude --resume echoed a different session id than requested (requested ${resumeSessionId}, got ${sessionRef}) \u2014 refusing to continue in a possibly-wrong session (fail-closed)`
|
|
1757
1886
|
);
|
|
1758
1887
|
}
|
|
1759
|
-
return new ClaudeSession(
|
|
1888
|
+
return new ClaudeSession(
|
|
1889
|
+
sessionRef,
|
|
1890
|
+
client,
|
|
1891
|
+
ctx.workspaceDir,
|
|
1892
|
+
ctx.approvalChannel,
|
|
1893
|
+
mcpConfigDir,
|
|
1894
|
+
modelId
|
|
1895
|
+
);
|
|
1760
1896
|
}
|
|
1761
1897
|
/**
|
|
1762
1898
|
* `claude auth status --json` is claude's OWN non-secret login-state
|
|
@@ -1790,19 +1926,31 @@ var ClaudeAdapter = class {
|
|
|
1790
1926
|
return (this.options.resolveBin ?? resolveClaudeBin)();
|
|
1791
1927
|
}
|
|
1792
1928
|
};
|
|
1929
|
+
function subscriptionModel(task, runtimeId) {
|
|
1930
|
+
const selection = task.dispatchSelection;
|
|
1931
|
+
if (selection === void 0) return void 0;
|
|
1932
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== runtimeId) {
|
|
1933
|
+
throw new PolicyUnsupportedError(
|
|
1934
|
+
`claude adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
return selection.modelId;
|
|
1938
|
+
}
|
|
1793
1939
|
var ClaudeSession = class {
|
|
1794
|
-
constructor(sessionRef, client, workspaceDir, approvalChannel,
|
|
1940
|
+
constructor(sessionRef, client, workspaceDir, approvalChannel, mcpConfigDir, modelId) {
|
|
1795
1941
|
this.sessionRef = sessionRef;
|
|
1796
1942
|
this.client = client;
|
|
1797
1943
|
this.workspaceDir = workspaceDir;
|
|
1798
1944
|
this.approvalChannel = approvalChannel;
|
|
1799
|
-
this.
|
|
1945
|
+
this.mcpConfigDir = mcpConfigDir;
|
|
1946
|
+
this.modelId = modelId;
|
|
1800
1947
|
}
|
|
1801
1948
|
sessionRef;
|
|
1802
1949
|
client;
|
|
1803
1950
|
workspaceDir;
|
|
1804
1951
|
approvalChannel;
|
|
1805
|
-
|
|
1952
|
+
mcpConfigDir;
|
|
1953
|
+
modelId;
|
|
1806
1954
|
correlation = createToolUseCorrelation();
|
|
1807
1955
|
get events() {
|
|
1808
1956
|
const client = this.client;
|
|
@@ -1853,6 +2001,12 @@ var ClaudeSession = class {
|
|
|
1853
2001
|
if (typeof task.instruction !== "string") {
|
|
1854
2002
|
throw new PolicyUnsupportedError("claude adapter only supports string instructions in M2 (no blob-ref fetch yet)");
|
|
1855
2003
|
}
|
|
2004
|
+
const requestedModel = subscriptionModel(task, "claude");
|
|
2005
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2006
|
+
throw new PolicyUnsupportedError(
|
|
2007
|
+
`claude persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
1856
2010
|
this.client.writeUserMessage(task.instruction);
|
|
1857
2011
|
}
|
|
1858
2012
|
/**
|
|
@@ -1872,7 +2026,7 @@ var ClaudeSession = class {
|
|
|
1872
2026
|
}
|
|
1873
2027
|
async close() {
|
|
1874
2028
|
this.client.kill();
|
|
1875
|
-
await
|
|
2029
|
+
await cleanupMcpConfigDir(this.mcpConfigDir);
|
|
1876
2030
|
}
|
|
1877
2031
|
/**
|
|
1878
2032
|
* M4 Phase 3: routes into the out-of-band approval channel `start()`
|
|
@@ -2036,8 +2190,8 @@ function extractArtifactEvents(changes, workspaceDir) {
|
|
|
2036
2190
|
const absolutePath = typeof change.path === "string" ? change.path : void 0;
|
|
2037
2191
|
const kind = typeof change.kind === "string" ? change.kind : void 0;
|
|
2038
2192
|
if (!absolutePath || kind === "delete") continue;
|
|
2039
|
-
const relative =
|
|
2040
|
-
if (relative.length === 0 || relative.startsWith("..") ||
|
|
2193
|
+
const relative = path17.relative(workspaceDir, absolutePath);
|
|
2194
|
+
if (relative.length === 0 || relative.startsWith("..") || path17.isAbsolute(relative)) continue;
|
|
2041
2195
|
events.push({ type: "artifact", name: relative, contentType: guessContentType2(relative) });
|
|
2042
2196
|
}
|
|
2043
2197
|
return events;
|
|
@@ -2058,7 +2212,7 @@ var CONTENT_TYPE_BY_EXTENSION = {
|
|
|
2058
2212
|
".csv": "text/csv"
|
|
2059
2213
|
};
|
|
2060
2214
|
function guessContentType2(relativePath) {
|
|
2061
|
-
return CONTENT_TYPE_BY_EXTENSION[
|
|
2215
|
+
return CONTENT_TYPE_BY_EXTENSION[path17.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
|
|
2062
2216
|
}
|
|
2063
2217
|
function extractErrorMessage(rawError) {
|
|
2064
2218
|
if (typeof rawError === "string") return rawError;
|
|
@@ -2196,6 +2350,7 @@ var CodexAdapter = class {
|
|
|
2196
2350
|
this.options = options;
|
|
2197
2351
|
}
|
|
2198
2352
|
options;
|
|
2353
|
+
supportsDispatchSelection = true;
|
|
2199
2354
|
id = "codex";
|
|
2200
2355
|
async detect() {
|
|
2201
2356
|
const bin = this.resolveBin();
|
|
@@ -2219,9 +2374,10 @@ var CodexAdapter = class {
|
|
|
2219
2374
|
* Two independently-verified channel gotchas apply here, the "pi lesson"
|
|
2220
2375
|
* yet again:
|
|
2221
2376
|
* - `codex login status`'s human-readable "Logged in using ChatGPT"
|
|
2222
|
-
* message prints on STDERR, not stdout
|
|
2223
|
-
*
|
|
2224
|
-
*
|
|
2377
|
+
* message prints on STDERR, not stdout — both streams are checked
|
|
2378
|
+
* here for exactly that reason. pi's `--version` is the same class of
|
|
2379
|
+
* hazard from the other direction: its channel has moved between pi
|
|
2380
|
+
* releases (see ../pi/pi-adapter.ts), so neither stream is assumed.
|
|
2225
2381
|
* - The NOT-logged-in message/exit-code shape was deliberately never
|
|
2226
2382
|
* empirically tested: this machine has a real, live ChatGPT login, and
|
|
2227
2383
|
* running `codex logout` to observe the negative case would have
|
|
@@ -2267,17 +2423,20 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2267
2423
|
if (!mapping.ok) {
|
|
2268
2424
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2269
2425
|
}
|
|
2426
|
+
const modelId = subscriptionModel2(task);
|
|
2270
2427
|
const bin = this.resolveBin();
|
|
2271
2428
|
const queue = new AsyncQueue();
|
|
2272
2429
|
const recordUnmapped = makeUnmappedFrameRecorder(/* @__PURE__ */ new Map());
|
|
2273
2430
|
const workspaceDir = await resolveRealWorkspaceDir(ctx.workspaceDir);
|
|
2431
|
+
const runtimeEnv = withoutProviderCredentials(ctx.env);
|
|
2274
2432
|
const { sessionRef, runner } = await runCodexTurn({
|
|
2275
2433
|
command: bin.command,
|
|
2276
2434
|
resumeRef: task.sessionRef,
|
|
2277
2435
|
instruction: task.instruction,
|
|
2436
|
+
modelId,
|
|
2278
2437
|
policyArgs: mapping.args,
|
|
2279
2438
|
cwd: ctx.workspaceDir,
|
|
2280
|
-
env:
|
|
2439
|
+
env: runtimeEnv,
|
|
2281
2440
|
spawnFn: this.options.spawnFn,
|
|
2282
2441
|
workspaceDir,
|
|
2283
2442
|
queue,
|
|
@@ -2294,7 +2453,8 @@ ${withStreams.stderr ?? ""}`);
|
|
|
2294
2453
|
queue,
|
|
2295
2454
|
recordUnmapped,
|
|
2296
2455
|
initialRunner: runner,
|
|
2297
|
-
preparedGit: ctx.gitWorkspace !== void 0
|
|
2456
|
+
preparedGit: ctx.gitWorkspace !== void 0,
|
|
2457
|
+
modelId
|
|
2298
2458
|
});
|
|
2299
2459
|
}
|
|
2300
2460
|
resolveBin() {
|
|
@@ -2315,12 +2475,25 @@ function makeUnmappedFrameRecorder(counts) {
|
|
|
2315
2475
|
}
|
|
2316
2476
|
};
|
|
2317
2477
|
}
|
|
2318
|
-
function buildArgv(resumeRef, policyArgs, instruction, preparedGit = false) {
|
|
2478
|
+
function buildArgv(resumeRef, policyArgs, instruction, modelId, preparedGit = false) {
|
|
2319
2479
|
const base = resumeRef !== void 0 ? ["exec", "resume", resumeRef] : ["exec"];
|
|
2320
|
-
return [
|
|
2480
|
+
return [
|
|
2481
|
+
...base,
|
|
2482
|
+
"--json",
|
|
2483
|
+
...modelId ? ["--model", modelId] : [],
|
|
2484
|
+
...preparedGit ? [] : ["--skip-git-repo-check"],
|
|
2485
|
+
...policyArgs,
|
|
2486
|
+
instruction
|
|
2487
|
+
];
|
|
2321
2488
|
}
|
|
2322
2489
|
async function runCodexTurn(params) {
|
|
2323
|
-
const argv = buildArgv(
|
|
2490
|
+
const argv = buildArgv(
|
|
2491
|
+
params.resumeRef,
|
|
2492
|
+
params.policyArgs,
|
|
2493
|
+
params.instruction,
|
|
2494
|
+
params.modelId,
|
|
2495
|
+
params.preparedGit
|
|
2496
|
+
);
|
|
2324
2497
|
let firstLineSettled = false;
|
|
2325
2498
|
let resolveFirstLine;
|
|
2326
2499
|
let rejectFirstLine;
|
|
@@ -2421,6 +2594,7 @@ var CodexSession = class {
|
|
|
2421
2594
|
queue;
|
|
2422
2595
|
recordUnmapped;
|
|
2423
2596
|
preparedGit;
|
|
2597
|
+
modelId;
|
|
2424
2598
|
currentRunner;
|
|
2425
2599
|
closed = false;
|
|
2426
2600
|
constructor(options) {
|
|
@@ -2432,6 +2606,7 @@ var CodexSession = class {
|
|
|
2432
2606
|
this.queue = options.queue;
|
|
2433
2607
|
this.recordUnmapped = options.recordUnmapped;
|
|
2434
2608
|
this.preparedGit = options.preparedGit;
|
|
2609
|
+
this.modelId = options.modelId;
|
|
2435
2610
|
this.currentRunner = options.initialRunner;
|
|
2436
2611
|
void this.forgetRunnerOnceClosed(options.initialRunner);
|
|
2437
2612
|
}
|
|
@@ -2492,6 +2667,13 @@ var CodexSession = class {
|
|
|
2492
2667
|
if (!mapping.ok) {
|
|
2493
2668
|
throw new PolicyUnsupportedError(mapping.reason ?? "policy rejected by codex adapter");
|
|
2494
2669
|
}
|
|
2670
|
+
const requestedModel = subscriptionModel2(task);
|
|
2671
|
+
if (requestedModel !== void 0 && requestedModel !== this.modelId) {
|
|
2672
|
+
throw new PolicyUnsupportedError(
|
|
2673
|
+
`codex persistent session cannot change model from ${this.modelId ?? "(legacy default)"} to ${requestedModel}`
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
const modelId = this.modelId;
|
|
2495
2677
|
const resumeRef = this.sessionRef;
|
|
2496
2678
|
let sessionRef;
|
|
2497
2679
|
let runner;
|
|
@@ -2500,9 +2682,10 @@ var CodexSession = class {
|
|
|
2500
2682
|
command: this.command,
|
|
2501
2683
|
resumeRef,
|
|
2502
2684
|
instruction: task.instruction,
|
|
2685
|
+
modelId,
|
|
2503
2686
|
policyArgs: mapping.args,
|
|
2504
2687
|
cwd: this.workspaceDir,
|
|
2505
|
-
env: this.env,
|
|
2688
|
+
env: withoutProviderCredentials(this.env),
|
|
2506
2689
|
spawnFn: this.spawnFn,
|
|
2507
2690
|
workspaceDir: this.workspaceDir,
|
|
2508
2691
|
queue: this.queue,
|
|
@@ -2562,6 +2745,16 @@ var CodexSession = class {
|
|
|
2562
2745
|
);
|
|
2563
2746
|
}
|
|
2564
2747
|
};
|
|
2748
|
+
function subscriptionModel2(task) {
|
|
2749
|
+
const selection = task.dispatchSelection;
|
|
2750
|
+
if (selection === void 0) return void 0;
|
|
2751
|
+
if (selection.lane !== "subscription" || selection.runtimeId !== "codex") {
|
|
2752
|
+
throw new PolicyUnsupportedError(
|
|
2753
|
+
`codex adapter cannot execute ${selection.lane} selection for runtime ${selection.runtimeId}`
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
return selection.modelId;
|
|
2757
|
+
}
|
|
2565
2758
|
|
|
2566
2759
|
// src/daemon/approvals.ts
|
|
2567
2760
|
var ApprovalNotFoundError = class extends Error {
|
|
@@ -2664,12 +2857,12 @@ var DeviceStore = class _DeviceStore {
|
|
|
2664
2857
|
*/
|
|
2665
2858
|
constructor(storeDir, secureDirOptions) {
|
|
2666
2859
|
this.secureDirOptions = secureDirOptions;
|
|
2667
|
-
this.filePath =
|
|
2860
|
+
this.filePath = path17.join(storeDir, "device.json");
|
|
2668
2861
|
}
|
|
2669
2862
|
secureDirOptions;
|
|
2670
2863
|
filePath;
|
|
2671
2864
|
static defaultDir(productId) {
|
|
2672
|
-
return
|
|
2865
|
+
return path17.join(os5.homedir(), ".byok", productId);
|
|
2673
2866
|
}
|
|
2674
2867
|
/**
|
|
2675
2868
|
* Resolve the one store pathname every daemon/CLI component must share.
|
|
@@ -2678,7 +2871,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
2678
2871
|
* cwd to pin a quarantine directory inode.
|
|
2679
2872
|
*/
|
|
2680
2873
|
static resolveDir(productId, configured) {
|
|
2681
|
-
return
|
|
2874
|
+
return path17.resolve(configured ?? _DeviceStore.defaultDir(productId));
|
|
2682
2875
|
}
|
|
2683
2876
|
async load() {
|
|
2684
2877
|
const opened = await this.openBounded();
|
|
@@ -2719,7 +2912,7 @@ var DeviceStore = class _DeviceStore {
|
|
|
2719
2912
|
}
|
|
2720
2913
|
}
|
|
2721
2914
|
async save(record) {
|
|
2722
|
-
const storeDir =
|
|
2915
|
+
const storeDir = path17.dirname(this.filePath);
|
|
2723
2916
|
await ensureSecureDir(storeDir, this.secureDirOptions);
|
|
2724
2917
|
await atomicWriteFile(this.filePath, JSON.stringify(record, null, 2), { mode: 384 });
|
|
2725
2918
|
}
|
|
@@ -2786,13 +2979,10 @@ function exportPrivateKeyPem(privateKey) {
|
|
|
2786
2979
|
function importPrivateKeyPem(pem) {
|
|
2787
2980
|
return createPrivateKey(pem);
|
|
2788
2981
|
}
|
|
2789
|
-
var NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
|
|
2790
2982
|
function signNonce(privateKey, nonce) {
|
|
2791
|
-
const signature = sign(null,
|
|
2983
|
+
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
2792
2984
|
return signature.toString("base64url");
|
|
2793
2985
|
}
|
|
2794
|
-
|
|
2795
|
-
// src/daemon/url.ts
|
|
2796
2986
|
function toHttpBase(serverUrl) {
|
|
2797
2987
|
const url = new URL(serverUrl);
|
|
2798
2988
|
if (url.protocol === "ws:") url.protocol = "http:";
|
|
@@ -2802,7 +2992,7 @@ function toHttpBase(serverUrl) {
|
|
|
2802
2992
|
return url.toString();
|
|
2803
2993
|
}
|
|
2804
2994
|
function toWsUrl(serverUrl) {
|
|
2805
|
-
const url = new URL(
|
|
2995
|
+
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
2806
2996
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
2807
2997
|
return url.toString();
|
|
2808
2998
|
}
|
|
@@ -2890,13 +3080,13 @@ var AuthManager = class {
|
|
|
2890
3080
|
return await this.runCredentialMutation(async () => {
|
|
2891
3081
|
const existing = this.record ?? await this.opts.store.load();
|
|
2892
3082
|
const keyPair = existing ? { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey } : generateDeviceKeyPair();
|
|
2893
|
-
const url = new URL(
|
|
3083
|
+
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
2894
3084
|
const res = await fetch(url, {
|
|
2895
3085
|
method: "POST",
|
|
2896
3086
|
headers: { "content-type": "application/json" },
|
|
2897
3087
|
body: JSON.stringify({
|
|
2898
3088
|
pairingCode,
|
|
2899
|
-
deviceName: this.opts.deviceName ??
|
|
3089
|
+
deviceName: this.opts.deviceName ?? os5.hostname(),
|
|
2900
3090
|
devicePublicKey: keyPair.publicKeyBase64Url
|
|
2901
3091
|
})
|
|
2902
3092
|
});
|
|
@@ -2924,6 +3114,7 @@ var AuthManager = class {
|
|
|
2924
3114
|
async getValidAccessToken() {
|
|
2925
3115
|
if (!this.record) throw new Error("device is not paired yet; call pair(pairingCode) first");
|
|
2926
3116
|
if (this.revoked) throw new DeviceRevokedError();
|
|
3117
|
+
if (this.renewing) return this.renewing;
|
|
2927
3118
|
if (msUntilExpiry(this.record.expiresAt) > RENEW_MARGIN_MS) return this.record.accessToken;
|
|
2928
3119
|
return this.renew();
|
|
2929
3120
|
}
|
|
@@ -2952,7 +3143,7 @@ var AuthManager = class {
|
|
|
2952
3143
|
const record = this.record;
|
|
2953
3144
|
const base = toHttpBase(this.opts.serverUrl);
|
|
2954
3145
|
const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
|
|
2955
|
-
const challengeRes = await fetch(new URL(
|
|
3146
|
+
const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
|
|
2956
3147
|
method: "POST",
|
|
2957
3148
|
headers: { "content-type": "application/json" },
|
|
2958
3149
|
body: JSON.stringify({ deviceId: record.deviceId })
|
|
@@ -2965,7 +3156,7 @@ var AuthManager = class {
|
|
|
2965
3156
|
}
|
|
2966
3157
|
const { nonce } = await challengeRes.json();
|
|
2967
3158
|
const signature = signNonce(privateKey, nonce);
|
|
2968
|
-
const tokenRes = await fetch(new URL(
|
|
3159
|
+
const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
|
|
2969
3160
|
method: "POST",
|
|
2970
3161
|
headers: { "content-type": "application/json" },
|
|
2971
3162
|
body: JSON.stringify({ deviceId: record.deviceId, nonce, signature })
|
|
@@ -3063,7 +3254,7 @@ var BlobClient = class {
|
|
|
3063
3254
|
async resolveInstruction(blobRef) {
|
|
3064
3255
|
const base = toHttpBase(this.serverUrl);
|
|
3065
3256
|
const urlRes = await authedFetch(
|
|
3066
|
-
new URL(
|
|
3257
|
+
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
3067
3258
|
{ method: "GET" },
|
|
3068
3259
|
this.auth
|
|
3069
3260
|
);
|
|
@@ -3087,18 +3278,18 @@ var BlobClient = class {
|
|
|
3087
3278
|
/** `POST /byok/blobs` (declares size/contentType/contentHash) -> PUT the bytes to the presigned upload URL -> a `BlobRef` for `task.artifact.blobRef`. */
|
|
3088
3279
|
async uploadArtifact(content, contentType) {
|
|
3089
3280
|
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
3090
|
-
const
|
|
3281
|
+
const contentHash3 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
3091
3282
|
const base = toHttpBase(this.serverUrl);
|
|
3092
3283
|
const reservationId = `blob_${randomUUID()}`;
|
|
3093
3284
|
const createRes = await authedFetch(
|
|
3094
|
-
new URL(
|
|
3285
|
+
new URL(BYOK_BLOBS_PATH, base),
|
|
3095
3286
|
{
|
|
3096
3287
|
method: "POST",
|
|
3097
3288
|
headers: {
|
|
3098
3289
|
"content-type": "application/json",
|
|
3099
3290
|
"idempotency-key": reservationId
|
|
3100
3291
|
},
|
|
3101
|
-
body: JSON.stringify({ size: bytes.length, contentType, contentHash:
|
|
3292
|
+
body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash3 })
|
|
3102
3293
|
},
|
|
3103
3294
|
this.auth
|
|
3104
3295
|
);
|
|
@@ -3115,7 +3306,7 @@ var BlobClient = class {
|
|
|
3115
3306
|
throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
|
|
3116
3307
|
}
|
|
3117
3308
|
await this.#finalize(base, blobId, reservationId);
|
|
3118
|
-
return { blobId, contentHash:
|
|
3309
|
+
return { blobId, contentHash: contentHash3, size: bytes.length, contentType };
|
|
3119
3310
|
}
|
|
3120
3311
|
async #finalize(base, blobId, reservationId) {
|
|
3121
3312
|
let lastFailure;
|
|
@@ -3123,7 +3314,7 @@ var BlobClient = class {
|
|
|
3123
3314
|
let response;
|
|
3124
3315
|
try {
|
|
3125
3316
|
response = await authedFetch(
|
|
3126
|
-
new URL(
|
|
3317
|
+
new URL(byokBlobFinalizePath(blobId), base),
|
|
3127
3318
|
{
|
|
3128
3319
|
method: "POST",
|
|
3129
3320
|
headers: { "idempotency-key": reservationId }
|
|
@@ -3146,26 +3337,187 @@ var BlobClient = class {
|
|
|
3146
3337
|
throw lastFailure;
|
|
3147
3338
|
}
|
|
3148
3339
|
};
|
|
3340
|
+
var PRESENCE_HINTS_CAPABILITY = "presence.hints";
|
|
3341
|
+
var CapabilityDiscoveryError = class extends Error {
|
|
3342
|
+
constructor(message, options) {
|
|
3343
|
+
super(message, options);
|
|
3344
|
+
this.name = "CapabilityDiscoveryError";
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
async function fetchCapabilityDeclaration(serverUrl, options = {}) {
|
|
3348
|
+
const url = new URL(BYOK_CAPABILITIES_PATH, toHttpBase(serverUrl));
|
|
3349
|
+
let response;
|
|
3350
|
+
try {
|
|
3351
|
+
response = await fetch(url, {
|
|
3352
|
+
method: "GET",
|
|
3353
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
3354
|
+
});
|
|
3355
|
+
} catch (err) {
|
|
3356
|
+
throw new CapabilityDiscoveryError(
|
|
3357
|
+
`failed to read the capability declaration from ${url.toString()}: ${err instanceof Error ? err.message : String(err)}`,
|
|
3358
|
+
{ cause: err }
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
if (!response.ok) {
|
|
3362
|
+
throw new CapabilityDiscoveryError(
|
|
3363
|
+
`failed to read the capability declaration from ${url.toString()}: HTTP ${response.status}`
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
let body;
|
|
3367
|
+
try {
|
|
3368
|
+
body = await response.json();
|
|
3369
|
+
} catch (err) {
|
|
3370
|
+
throw new CapabilityDiscoveryError(
|
|
3371
|
+
`the capability declaration at ${url.toString()} is not JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
3372
|
+
{ cause: err }
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
const parsed = CapabilityDeclarationSchema.safeParse(body);
|
|
3376
|
+
if (!parsed.success) {
|
|
3377
|
+
throw new CapabilityDiscoveryError(
|
|
3378
|
+
`the capability declaration at ${url.toString()} is not a valid ADR-010 declaration: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ")}`,
|
|
3379
|
+
{ cause: parsed.error }
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
return parsed.data;
|
|
3383
|
+
}
|
|
3384
|
+
function declares(declaration, capability) {
|
|
3385
|
+
return hasCapability(declaration, capability);
|
|
3386
|
+
}
|
|
3387
|
+
var DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
3388
|
+
var DEFAULT_PRESENCE_TTL_MS = 9e4;
|
|
3389
|
+
var DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS = 5e3;
|
|
3390
|
+
function assertPresenceHeartbeatCadence(cadence) {
|
|
3391
|
+
const { intervalMs, ttlMs, minimumIntervalMs } = cadence;
|
|
3392
|
+
if (!(minimumIntervalMs < intervalMs && intervalMs < ttlMs)) {
|
|
3393
|
+
throw new Error(
|
|
3394
|
+
`presence heartbeat interval must satisfy minimumIntervalMs < intervalMs < ttlMs \u2014 got ${minimumIntervalMs} < ${intervalMs} < ${ttlMs}`
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
var PresencePublisher = class {
|
|
3399
|
+
constructor(opts) {
|
|
3400
|
+
this.opts = opts;
|
|
3401
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS;
|
|
3402
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_PRESENCE_TTL_MS;
|
|
3403
|
+
const minimumIntervalMs = opts.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS;
|
|
3404
|
+
assertPresenceHeartbeatCadence({ intervalMs, ttlMs, minimumIntervalMs });
|
|
3405
|
+
this.intervalMs = intervalMs;
|
|
3406
|
+
this.url = new URL(BYOK_PRESENCE_PATH, toHttpBase(opts.serverUrl));
|
|
3407
|
+
}
|
|
3408
|
+
opts;
|
|
3409
|
+
url;
|
|
3410
|
+
intervalMs;
|
|
3411
|
+
timer;
|
|
3412
|
+
running = false;
|
|
3413
|
+
/** Set once a revoked device is observed. Terminal: `start()` will not restart this instance. */
|
|
3414
|
+
stoppedPermanently = false;
|
|
3415
|
+
/** Publishes immediately, then every `intervalMs`. Idempotent; a no-op after a permanent stop. */
|
|
3416
|
+
start() {
|
|
3417
|
+
if (this.running || this.stoppedPermanently) return;
|
|
3418
|
+
this.running = true;
|
|
3419
|
+
void this.beat();
|
|
3420
|
+
}
|
|
3421
|
+
/** Stops the cadence. Idempotent, and the only "offline" signal this producer emits — the hint's TTL does the rest. */
|
|
3422
|
+
stop() {
|
|
3423
|
+
this.running = false;
|
|
3424
|
+
if (this.timer !== void 0) {
|
|
3425
|
+
clearTimeout(this.timer);
|
|
3426
|
+
this.timer = void 0;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
schedule() {
|
|
3430
|
+
if (!this.running) return;
|
|
3431
|
+
this.timer = setTimeout(() => {
|
|
3432
|
+
this.timer = void 0;
|
|
3433
|
+
void this.beat();
|
|
3434
|
+
}, this.intervalMs);
|
|
3435
|
+
this.timer.unref?.();
|
|
3436
|
+
}
|
|
3437
|
+
async beat() {
|
|
3438
|
+
if (!this.running) return;
|
|
3439
|
+
try {
|
|
3440
|
+
const response = await authedFetch(
|
|
3441
|
+
this.url,
|
|
3442
|
+
{
|
|
3443
|
+
method: "PUT",
|
|
3444
|
+
headers: { "content-type": "application/json" },
|
|
3445
|
+
body: JSON.stringify({ level: "online" })
|
|
3446
|
+
},
|
|
3447
|
+
this.opts.auth
|
|
3448
|
+
);
|
|
3449
|
+
if (!response.ok) {
|
|
3450
|
+
if (response.status === 401) {
|
|
3451
|
+
this.stopPermanently(`presence heartbeat unauthorized after token renewal (HTTP 401)`);
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
this.opts.onDegraded?.(`presence heartbeat failed: HTTP ${response.status}`);
|
|
3455
|
+
}
|
|
3456
|
+
} catch (err) {
|
|
3457
|
+
if (err instanceof DeviceRevokedError) {
|
|
3458
|
+
this.stopPermanently("presence heartbeat stopped: device has been revoked; re-pair required");
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
this.opts.onDegraded?.(`presence heartbeat failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3462
|
+
}
|
|
3463
|
+
this.schedule();
|
|
3464
|
+
}
|
|
3465
|
+
stopPermanently(reason) {
|
|
3466
|
+
this.stoppedPermanently = true;
|
|
3467
|
+
this.stop();
|
|
3468
|
+
this.opts.onDegraded?.(reason);
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
function freshJti() {
|
|
3472
|
+
return randomBytes(16).toString("base64url");
|
|
3473
|
+
}
|
|
3474
|
+
function mintDeviceAssertion(input) {
|
|
3475
|
+
const issuedAtMs = input.now.getTime();
|
|
3476
|
+
const expiresAt = new Date(issuedAtMs + input.ttlMs).toISOString();
|
|
3477
|
+
const claims = DeviceAssertionClaimsSchema.parse({
|
|
3478
|
+
version: 1,
|
|
3479
|
+
issuer: input.issuer,
|
|
3480
|
+
productId: input.productId,
|
|
3481
|
+
deviceId: input.record.deviceId,
|
|
3482
|
+
audience: input.audience,
|
|
3483
|
+
jti: freshJti(),
|
|
3484
|
+
issuedAt: new Date(issuedAtMs).toISOString(),
|
|
3485
|
+
expiresAt
|
|
3486
|
+
});
|
|
3487
|
+
const privateKey = importPrivateKeyPem(input.record.devicePrivateKeyPem);
|
|
3488
|
+
const signature = sign(null, deviceAssertionSigningInput(claims), privateKey).toString("base64url");
|
|
3489
|
+
return {
|
|
3490
|
+
envelope: {
|
|
3491
|
+
schema: DEVICE_ASSERTION_SCHEMA_ID,
|
|
3492
|
+
algorithm: "ed25519",
|
|
3493
|
+
protected: claims,
|
|
3494
|
+
signature
|
|
3495
|
+
},
|
|
3496
|
+
claims,
|
|
3497
|
+
expiresAt
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3149
3500
|
var CONTROL_PROTOCOL_VERSION = 1;
|
|
3150
3501
|
var HANDSHAKE_TIMEOUT_MS = 3e3;
|
|
3151
3502
|
var UNIX_SOCKET_PATH_SOFT_LIMIT = 100;
|
|
3503
|
+
var CONTROL_SOCKET_FALLBACK_ROOT = "/tmp";
|
|
3152
3504
|
function shortHash(input) {
|
|
3153
3505
|
return createHash("sha256").update(input, "utf8").digest("hex").slice(0, 16);
|
|
3154
3506
|
}
|
|
3155
3507
|
function controlSocketPath(storeDir) {
|
|
3156
|
-
const candidate =
|
|
3508
|
+
const candidate = path17.join(storeDir, "control.sock");
|
|
3157
3509
|
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT) return candidate;
|
|
3158
|
-
return
|
|
3510
|
+
return path17.join(CONTROL_SOCKET_FALLBACK_ROOT, `byok-${shortHash(storeDir)}`, "sock");
|
|
3159
3511
|
}
|
|
3160
3512
|
function controlPipeName(productId, storeDir) {
|
|
3161
|
-
const id = shortHash(`${productId}|${
|
|
3513
|
+
const id = shortHash(`${productId}|${path17.resolve(storeDir)}`);
|
|
3162
3514
|
return `\\\\.\\pipe\\byok-${id}`;
|
|
3163
3515
|
}
|
|
3164
3516
|
function controlEndpointPath(productId, storeDir, platform = process.platform) {
|
|
3165
3517
|
return platform === "win32" ? controlPipeName(productId, storeDir) : controlSocketPath(storeDir);
|
|
3166
3518
|
}
|
|
3167
3519
|
function controlTokenPath(storeDir) {
|
|
3168
|
-
return
|
|
3520
|
+
return path17.join(storeDir, "control.token");
|
|
3169
3521
|
}
|
|
3170
3522
|
var SERVER_PROOF_LABEL = "byok-control-server|";
|
|
3171
3523
|
var CLIENT_AUTH_LABEL = "byok-control-client|";
|
|
@@ -3195,11 +3547,23 @@ function parseClientHello(value) {
|
|
|
3195
3547
|
if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "client" || typeof value.nonce !== "string") return void 0;
|
|
3196
3548
|
return { v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: value.nonce };
|
|
3197
3549
|
}
|
|
3550
|
+
function parseServerHello(value) {
|
|
3551
|
+
if (!isRecord2(value)) return void 0;
|
|
3552
|
+
if (value.v !== CONTROL_PROTOCOL_VERSION || value.hello !== "server" || typeof value.proof !== "string" || typeof value.nonce !== "string") {
|
|
3553
|
+
return void 0;
|
|
3554
|
+
}
|
|
3555
|
+
return { v: CONTROL_PROTOCOL_VERSION, hello: "server", proof: value.proof, nonce: value.nonce };
|
|
3556
|
+
}
|
|
3198
3557
|
function parseClientAuth(value) {
|
|
3199
3558
|
if (!isRecord2(value)) return void 0;
|
|
3200
3559
|
if (value.v !== CONTROL_PROTOCOL_VERSION || typeof value.auth !== "string") return void 0;
|
|
3201
3560
|
return { v: CONTROL_PROTOCOL_VERSION, auth: value.auth };
|
|
3202
3561
|
}
|
|
3562
|
+
function parseServerReady(value) {
|
|
3563
|
+
if (!isRecord2(value)) return void 0;
|
|
3564
|
+
if (value.v !== CONTROL_PROTOCOL_VERSION || value.ready !== true) return void 0;
|
|
3565
|
+
return { v: CONTROL_PROTOCOL_VERSION, ready: true };
|
|
3566
|
+
}
|
|
3203
3567
|
function parseRawControlRequest(value) {
|
|
3204
3568
|
if (!isRecord2(value)) return void 0;
|
|
3205
3569
|
if (typeof value.id !== "string" || typeof value.method !== "string") return void 0;
|
|
@@ -3249,6 +3613,16 @@ function parseApprovalsRequestParams(value) {
|
|
|
3249
3613
|
if (typeof value.summary !== "string") return void 0;
|
|
3250
3614
|
return { taskId: value.taskId, summary: value.summary };
|
|
3251
3615
|
}
|
|
3616
|
+
var ASSERTION_AUDIENCE_MAX_BYTES = 256;
|
|
3617
|
+
function parseAssertionIssueParams(value) {
|
|
3618
|
+
if (!isRecord2(value)) return void 0;
|
|
3619
|
+
const keys = Object.keys(value);
|
|
3620
|
+
if (keys.length !== 1 || keys[0] !== "audience") return void 0;
|
|
3621
|
+
const { audience } = value;
|
|
3622
|
+
if (typeof audience !== "string" || audience.length === 0) return void 0;
|
|
3623
|
+
if (Buffer.byteLength(audience, "utf8") > ASSERTION_AUDIENCE_MAX_BYTES) return void 0;
|
|
3624
|
+
return { audience };
|
|
3625
|
+
}
|
|
3252
3626
|
function parseShutdownParams(value) {
|
|
3253
3627
|
if (!isRecord2(value)) return {};
|
|
3254
3628
|
return value.reason === "unpair" || value.reason === "operator" ? { reason: value.reason } : {};
|
|
@@ -3310,7 +3684,7 @@ async function assertOwnedPrivateDir(dir) {
|
|
|
3310
3684
|
}
|
|
3311
3685
|
async function bindControlEndpoint(server, endpoint) {
|
|
3312
3686
|
if (process.platform !== "win32") {
|
|
3313
|
-
const endpointDir =
|
|
3687
|
+
const endpointDir = path17.dirname(endpoint);
|
|
3314
3688
|
await promises.mkdir(endpointDir, { recursive: true, mode: 448 });
|
|
3315
3689
|
await promises.chmod(endpointDir, 448).catch(() => {
|
|
3316
3690
|
});
|
|
@@ -3581,7 +3955,7 @@ var LongPollClient = class {
|
|
|
3581
3955
|
try {
|
|
3582
3956
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3583
3957
|
const res = await authedFetch(
|
|
3584
|
-
new URL(
|
|
3958
|
+
new URL(BYOK_MESSAGES_PATH, base),
|
|
3585
3959
|
{
|
|
3586
3960
|
method: "POST",
|
|
3587
3961
|
headers: { "content-type": "application/json" },
|
|
@@ -3605,7 +3979,7 @@ var LongPollClient = class {
|
|
|
3605
3979
|
while (this.running) {
|
|
3606
3980
|
try {
|
|
3607
3981
|
const base = toHttpBase(this.opts.serverUrl);
|
|
3608
|
-
const url = new URL(
|
|
3982
|
+
const url = new URL(BYOK_EVENTS_PATH, base);
|
|
3609
3983
|
const cursor = this.opts.getCursor();
|
|
3610
3984
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
3611
3985
|
const res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
@@ -4591,7 +4965,7 @@ function sameFileState2(left, right) {
|
|
|
4591
4965
|
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
4592
4966
|
}
|
|
4593
4967
|
async function openOperationalHealthFile(storeDir) {
|
|
4594
|
-
const filePath =
|
|
4968
|
+
const filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
4595
4969
|
let namedBefore;
|
|
4596
4970
|
try {
|
|
4597
4971
|
namedBefore = await promises.lstat(filePath, { bigint: true });
|
|
@@ -4633,7 +5007,7 @@ var OperationalHealthTracker = class {
|
|
|
4633
5007
|
#writeTail = Promise.resolve();
|
|
4634
5008
|
#started = false;
|
|
4635
5009
|
constructor(storeDir, options = {}) {
|
|
4636
|
-
this.#filePath =
|
|
5010
|
+
this.#filePath = path17.join(storeDir, OPERATIONAL_HEALTH_FILENAME);
|
|
4637
5011
|
this.#windowMs = options.windowMs ?? 6e4;
|
|
4638
5012
|
this.#failureThreshold = options.failureThreshold ?? 3;
|
|
4639
5013
|
this.#maxFailures = options.maxFailures ?? 128;
|
|
@@ -4720,7 +5094,7 @@ var OperationalHealthTracker = class {
|
|
|
4720
5094
|
async #load() {
|
|
4721
5095
|
let opened;
|
|
4722
5096
|
try {
|
|
4723
|
-
opened = await openOperationalHealthFile(
|
|
5097
|
+
opened = await openOperationalHealthFile(path17.dirname(this.#filePath));
|
|
4724
5098
|
} catch (err) {
|
|
4725
5099
|
throw new Error("operational health state could not be read");
|
|
4726
5100
|
}
|
|
@@ -4756,7 +5130,7 @@ var OperationalHealthTracker = class {
|
|
|
4756
5130
|
if (!this.#state) return;
|
|
4757
5131
|
const body = JSON.stringify(this.#state, null, 2);
|
|
4758
5132
|
this.#writeTail = this.#writeTail.then(async () => {
|
|
4759
|
-
await ensureSecureDir(
|
|
5133
|
+
await ensureSecureDir(path17.dirname(this.#filePath));
|
|
4760
5134
|
await atomicWriteFile(this.#filePath, body, { mode: 384, fsync: true });
|
|
4761
5135
|
});
|
|
4762
5136
|
try {
|
|
@@ -4831,19 +5205,19 @@ var RECLAIM_FILENAME = `${DAEMON_OWNER_FILENAME}.reclaim`;
|
|
|
4831
5205
|
var MAX_OWNER_BYTES = 4096;
|
|
4832
5206
|
var RECLAIM_MALFORMED_GRACE_MS = 3e4;
|
|
4833
5207
|
var SELF_PROCESS_STARTED_AT = new Date(Date.now() - process.uptime() * 1e3).toISOString();
|
|
4834
|
-
var STORE_MUTEX_PORT_BASE = 1e4;
|
|
4835
|
-
var STORE_MUTEX_PORT_COUNT = 2e4;
|
|
4836
|
-
var STORE_MUTEX_PORT_CANDIDATES = 32;
|
|
4837
5208
|
var STORE_MUTEX_ID_PREFIX = "byok-store-mutex-v1:";
|
|
4838
5209
|
var STORE_MUTEX_PROBE_TIMEOUT_MS = 1e3;
|
|
5210
|
+
var STORE_MUTEX_SOCKET_FILENAME = "mutex.sock";
|
|
5211
|
+
var UNIX_SOCKET_PATH_SOFT_LIMIT2 = 100;
|
|
5212
|
+
var STORE_MUTEX_FALLBACK_ROOT = "/tmp";
|
|
4839
5213
|
function storeMutexIdentity(canonicalStoreDir) {
|
|
4840
5214
|
return createHash("sha256").update(canonicalStoreDir).digest("hex");
|
|
4841
5215
|
}
|
|
4842
|
-
function
|
|
4843
|
-
|
|
4844
|
-
const
|
|
4845
|
-
|
|
4846
|
-
return
|
|
5216
|
+
function storeMutexEndpoint(canonicalStoreDir, identity, platform = process.platform) {
|
|
5217
|
+
if (platform === "win32") return `\\\\.\\pipe\\byok-store-mutex-${identity.slice(0, 16)}`;
|
|
5218
|
+
const candidate = path17.join(canonicalStoreDir, STORE_MUTEX_SOCKET_FILENAME);
|
|
5219
|
+
if (Buffer.byteLength(candidate, "utf8") <= UNIX_SOCKET_PATH_SOFT_LIMIT2) return candidate;
|
|
5220
|
+
return path17.join(STORE_MUTEX_FALLBACK_ROOT, `byok-store-mutex-${identity.slice(0, 16)}`, "sock");
|
|
4847
5221
|
}
|
|
4848
5222
|
var DaemonOwnerActiveError = class extends Error {
|
|
4849
5223
|
constructor(role) {
|
|
@@ -4964,70 +5338,90 @@ async function createLivenessListener() {
|
|
|
4964
5338
|
})
|
|
4965
5339
|
};
|
|
4966
5340
|
}
|
|
4967
|
-
async function probeStoreMutex(
|
|
5341
|
+
async function probeStoreMutex(endpoint, identity) {
|
|
4968
5342
|
return new Promise((resolve) => {
|
|
4969
|
-
const socket = createConnection(
|
|
5343
|
+
const socket = createConnection(endpoint);
|
|
4970
5344
|
let settled = false;
|
|
4971
5345
|
let raw = "";
|
|
4972
5346
|
const finish = (result) => {
|
|
4973
5347
|
if (settled) return;
|
|
4974
5348
|
settled = true;
|
|
5349
|
+
clearTimeout(timer);
|
|
5350
|
+
socket.removeAllListeners();
|
|
4975
5351
|
socket.destroy();
|
|
4976
5352
|
resolve(result);
|
|
4977
5353
|
};
|
|
5354
|
+
const timer = setTimeout(() => finish({ kind: "occupied" }), STORE_MUTEX_PROBE_TIMEOUT_MS);
|
|
4978
5355
|
socket.setEncoding("utf8");
|
|
4979
|
-
socket.setTimeout(STORE_MUTEX_PROBE_TIMEOUT_MS, () => finish({ kind: "uncertain" }));
|
|
4980
5356
|
socket.on("data", (chunk) => {
|
|
4981
5357
|
raw += chunk;
|
|
4982
|
-
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "
|
|
5358
|
+
if (raw.length > STORE_MUTEX_ID_PREFIX.length + 64 + 1) finish({ kind: "occupied" });
|
|
4983
5359
|
});
|
|
4984
|
-
socket.once("end", () => {
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
finish(
|
|
4988
|
-
|
|
4989
|
-
);
|
|
4990
|
-
});
|
|
4991
|
-
socket.once("error", () => finish({ kind: "foreign-or-gone" }));
|
|
5360
|
+
socket.once("end", () => finish(raw.trimEnd() === `${STORE_MUTEX_ID_PREFIX}${identity}` ? { kind: "holder" } : { kind: "occupied" }));
|
|
5361
|
+
socket.once(
|
|
5362
|
+
"error",
|
|
5363
|
+
(err) => finish(err.code === "ECONNREFUSED" || err.code === "ENOENT" ? { kind: "unbound" } : { kind: "occupied" })
|
|
5364
|
+
);
|
|
4992
5365
|
});
|
|
4993
5366
|
}
|
|
5367
|
+
async function clearStaleStoreMutexSocket(endpoint, identity) {
|
|
5368
|
+
let stat;
|
|
5369
|
+
try {
|
|
5370
|
+
stat = await promises.lstat(endpoint);
|
|
5371
|
+
} catch (err) {
|
|
5372
|
+
if (err.code === "ENOENT") return;
|
|
5373
|
+
throw err;
|
|
5374
|
+
}
|
|
5375
|
+
if (!stat.isSocket()) throw new Error("store mutation lock path exists but is not a socket");
|
|
5376
|
+
if ((await probeStoreMutex(endpoint, identity)).kind !== "unbound") throw new DaemonOwnerActiveError("unknown");
|
|
5377
|
+
await promises.rm(endpoint, { force: true });
|
|
5378
|
+
}
|
|
5379
|
+
async function assertOwnedPrivateDir2(dir) {
|
|
5380
|
+
const uid = process.getuid?.();
|
|
5381
|
+
if (uid === void 0) return;
|
|
5382
|
+
const stat = await promises.lstat(dir);
|
|
5383
|
+
if (stat.isSymbolicLink() || stat.uid !== uid) {
|
|
5384
|
+
throw new Error(`refusing to bind the store mutation lock under "${dir}": not a real directory owned by this process's own uid`);
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
4994
5387
|
async function acquireStoreMutex(canonicalStoreDir) {
|
|
4995
5388
|
const identity = storeMutexIdentity(canonicalStoreDir);
|
|
4996
|
-
|
|
4997
|
-
|
|
5389
|
+
const endpoint = storeMutexEndpoint(canonicalStoreDir, identity);
|
|
5390
|
+
const isPipe = process.platform === "win32";
|
|
5391
|
+
if (!isPipe) {
|
|
5392
|
+
const endpointDir = path17.dirname(endpoint);
|
|
5393
|
+
if (endpointDir !== canonicalStoreDir) {
|
|
5394
|
+
await ensureSecureDir(endpointDir);
|
|
5395
|
+
await assertOwnedPrivateDir2(endpointDir);
|
|
5396
|
+
}
|
|
5397
|
+
await clearStaleStoreMutexSocket(endpoint, identity);
|
|
5398
|
+
}
|
|
5399
|
+
const server = createServer((socket) => socket.end(`${STORE_MUTEX_ID_PREFIX}${identity}
|
|
4998
5400
|
`));
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
server.
|
|
5004
|
-
|
|
5005
|
-
resolve();
|
|
5006
|
-
});
|
|
5401
|
+
try {
|
|
5402
|
+
await new Promise((resolve, reject) => {
|
|
5403
|
+
server.once("error", reject);
|
|
5404
|
+
server.listen(endpoint, () => {
|
|
5405
|
+
server.removeListener("error", reject);
|
|
5406
|
+
resolve();
|
|
5007
5407
|
});
|
|
5008
|
-
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
throw new DaemonOwnerActiveError("unknown");
|
|
5013
|
-
}
|
|
5014
|
-
continue;
|
|
5015
|
-
}
|
|
5016
|
-
server.unref();
|
|
5017
|
-
let closed = false;
|
|
5018
|
-
return {
|
|
5019
|
-
port,
|
|
5020
|
-
close: () => new Promise((resolve, reject) => {
|
|
5021
|
-
if (closed) {
|
|
5022
|
-
resolve();
|
|
5023
|
-
return;
|
|
5024
|
-
}
|
|
5025
|
-
closed = true;
|
|
5026
|
-
server.close((err) => err ? reject(err) : resolve());
|
|
5027
|
-
})
|
|
5028
|
-
};
|
|
5408
|
+
});
|
|
5409
|
+
} catch (err) {
|
|
5410
|
+
if (err.code === "EADDRINUSE") throw new DaemonOwnerActiveError("unknown");
|
|
5411
|
+
throw err;
|
|
5029
5412
|
}
|
|
5030
|
-
|
|
5413
|
+
if (!isPipe) await promises.chmod(endpoint, 384).catch(() => void 0);
|
|
5414
|
+
server.unref();
|
|
5415
|
+
let closed = false;
|
|
5416
|
+
return {
|
|
5417
|
+
endpoint,
|
|
5418
|
+
close: async () => {
|
|
5419
|
+
if (closed) return;
|
|
5420
|
+
closed = true;
|
|
5421
|
+
await new Promise((resolve, reject) => server.close((err) => err ? reject(err) : resolve()));
|
|
5422
|
+
if (!isPipe) await promises.rm(endpoint, { force: true }).catch(() => void 0);
|
|
5423
|
+
}
|
|
5424
|
+
};
|
|
5031
5425
|
}
|
|
5032
5426
|
async function reclaimExistsAndIsActive(reclaimPath) {
|
|
5033
5427
|
let stat;
|
|
@@ -5075,8 +5469,8 @@ async function acquireDaemonOwner(storeDir, role, clock = () => /* @__PURE__ */
|
|
|
5075
5469
|
await mutex.close().catch(() => void 0);
|
|
5076
5470
|
throw err;
|
|
5077
5471
|
}
|
|
5078
|
-
const ownerPath =
|
|
5079
|
-
const reclaimPath =
|
|
5472
|
+
const ownerPath = path17.join(storeDir, DAEMON_OWNER_FILENAME);
|
|
5473
|
+
const reclaimPath = path17.join(storeDir, RECLAIM_FILENAME);
|
|
5080
5474
|
const record = {
|
|
5081
5475
|
version: 2,
|
|
5082
5476
|
pid: process.pid,
|
|
@@ -5143,6 +5537,7 @@ function toRuntimeInfoCapabilities(caps) {
|
|
|
5143
5537
|
steer: caps.steer,
|
|
5144
5538
|
resume: caps.resume,
|
|
5145
5539
|
approvalInteractive: caps.approvalInteractive,
|
|
5540
|
+
...caps.mcpToolsets === void 0 ? {} : { mcpToolsets: caps.mcpToolsets },
|
|
5146
5541
|
permissionModes: caps.permissionModes
|
|
5147
5542
|
};
|
|
5148
5543
|
}
|
|
@@ -5153,7 +5548,7 @@ var CursorStore = class {
|
|
|
5153
5548
|
storeDir;
|
|
5154
5549
|
fileFor(serverUrl, deviceId) {
|
|
5155
5550
|
const key = createHash("sha256").update(`${serverUrl}::${deviceId}`).digest("hex").slice(0, 32);
|
|
5156
|
-
return
|
|
5551
|
+
return path17.join(this.storeDir, `cursor-${key}.json`);
|
|
5157
5552
|
}
|
|
5158
5553
|
async load(serverUrl, deviceId) {
|
|
5159
5554
|
let raw;
|
|
@@ -5173,7 +5568,7 @@ var CursorStore = class {
|
|
|
5173
5568
|
}
|
|
5174
5569
|
async save(serverUrl, deviceId, cursor) {
|
|
5175
5570
|
const file = this.fileFor(serverUrl, deviceId);
|
|
5176
|
-
await promises.mkdir(
|
|
5571
|
+
await promises.mkdir(path17.dirname(file), { recursive: true, mode: 448 });
|
|
5177
5572
|
await atomicWriteFile(file, JSON.stringify({ cursor }));
|
|
5178
5573
|
}
|
|
5179
5574
|
/** Remove any persisted cursor for (serverUrl, deviceId) — a no-op if none exists. Called from `pair()` (finding F5) so a device that's about to be replaced never leaves a cursor a future, unrelated device could somehow inherit. */
|
|
@@ -5216,7 +5611,7 @@ var DaemonObserver = class {
|
|
|
5216
5611
|
}
|
|
5217
5612
|
/**
|
|
5218
5613
|
* Feed a raw INBOUND (server -> daemon) envelope. Deliberately narrow: only
|
|
5219
|
-
*
|
|
5614
|
+
* either offer variant produces a local event here — every other inbound type
|
|
5220
5615
|
* (`task.cancel`/`task.steer`/`task.approve`/`task.reject`) is a
|
|
5221
5616
|
* best-effort notification whose OWN observable effect already surfaces
|
|
5222
5617
|
* through the daemon's outbound envelopes (`task.cancelled`, `task.progress`
|
|
@@ -5224,7 +5619,7 @@ var DaemonObserver = class {
|
|
|
5224
5619
|
* where those are actually reported from.
|
|
5225
5620
|
*/
|
|
5226
5621
|
handleInboundEnvelope(envelope) {
|
|
5227
|
-
if (envelope.type !== "task.offer") return;
|
|
5622
|
+
if (envelope.type !== "task.offer" && envelope.type !== "task.offer_with_toolsets") return;
|
|
5228
5623
|
const taskId = envelope.task_id;
|
|
5229
5624
|
if (this.taskInfo.has(taskId)) return;
|
|
5230
5625
|
this.upsertTask(taskId, { state: "Offered", runtime: envelope.payload.runtime });
|
|
@@ -5345,6 +5740,37 @@ var DaemonObserver = class {
|
|
|
5345
5740
|
noteShutdownComplete(reason, undeliveredOutboxCount) {
|
|
5346
5741
|
this.emit({ kind: "shutdown-complete", ts: nowIso(), reason, undeliveredOutboxCount });
|
|
5347
5742
|
}
|
|
5743
|
+
/**
|
|
5744
|
+
* Plan `device-assertion-broker`: see the `device-assertion` `DaemonEvent`
|
|
5745
|
+
* variant's own doc comment. The parameter type is what keeps the signature
|
|
5746
|
+
* out — there is no field to pass one through.
|
|
5747
|
+
*
|
|
5748
|
+
* codex round-2 F4: the DENIED caller can pass its raw `audience` here, but
|
|
5749
|
+
* it is converted to a byte SIZE the instant the event is constructed and the
|
|
5750
|
+
* raw string is dropped — it is never placed on the emitted `DaemonEvent`, so
|
|
5751
|
+
* it cannot reach a subscriber, `format.ts`, stdout, or the audit file. The
|
|
5752
|
+
* ISSUED `audience` came from the allowlist and is kept verbatim.
|
|
5753
|
+
*/
|
|
5754
|
+
noteDeviceAssertion(event) {
|
|
5755
|
+
if (event.result === "issued") {
|
|
5756
|
+
this.emit({
|
|
5757
|
+
kind: "device-assertion",
|
|
5758
|
+
ts: nowIso(),
|
|
5759
|
+
result: "issued",
|
|
5760
|
+
audience: event.audience,
|
|
5761
|
+
jti: event.jti,
|
|
5762
|
+
expiresAt: event.expiresAt
|
|
5763
|
+
});
|
|
5764
|
+
return;
|
|
5765
|
+
}
|
|
5766
|
+
this.emit({
|
|
5767
|
+
kind: "device-assertion",
|
|
5768
|
+
ts: nowIso(),
|
|
5769
|
+
result: "denied",
|
|
5770
|
+
reason: event.reason,
|
|
5771
|
+
audienceSize: event.audience === void 0 ? void 0 : Buffer.byteLength(event.audience, "utf8")
|
|
5772
|
+
});
|
|
5773
|
+
}
|
|
5348
5774
|
/** M4 Phase 3 hardening: see the `stale-approval-decision` `DaemonEvent` variant's own doc comment. */
|
|
5349
5775
|
noteStaleApprovalDecision(taskId, decision, reason) {
|
|
5350
5776
|
this.emit({ kind: "stale-approval-decision", ts: nowIso(), taskId, decision, reason });
|
|
@@ -5469,7 +5895,7 @@ var SessionWorkspaceStore = class {
|
|
|
5469
5895
|
*/
|
|
5470
5896
|
queue = Promise.resolve();
|
|
5471
5897
|
constructor(storeDir) {
|
|
5472
|
-
this.filePath =
|
|
5898
|
+
this.filePath = path17.join(storeDir, "session-workspaces.json");
|
|
5473
5899
|
}
|
|
5474
5900
|
async get(sessionRef) {
|
|
5475
5901
|
return this.enqueue(async () => {
|
|
@@ -5527,7 +5953,7 @@ var SessionWorkspaceStore = class {
|
|
|
5527
5953
|
}
|
|
5528
5954
|
}
|
|
5529
5955
|
async save(all) {
|
|
5530
|
-
const dir =
|
|
5956
|
+
const dir = path17.dirname(this.filePath);
|
|
5531
5957
|
await promises.mkdir(dir, { recursive: true, mode: 448 });
|
|
5532
5958
|
const tmpPath = `${this.filePath}.${process.pid}-${tmpSeq2++}.tmp`;
|
|
5533
5959
|
try {
|
|
@@ -5632,9 +6058,9 @@ function isSqliteAvailable() {
|
|
|
5632
6058
|
}
|
|
5633
6059
|
}
|
|
5634
6060
|
var SECURE_FILE_MODE = 384;
|
|
5635
|
-
function openJournalDatabase(
|
|
6061
|
+
function openJournalDatabase(path21, busyTimeoutMs, faults) {
|
|
5636
6062
|
const { DatabaseSync } = loadSqliteModule();
|
|
5637
|
-
const db = new DatabaseSync(
|
|
6063
|
+
const db = new DatabaseSync(path21, { timeout: busyTimeoutMs });
|
|
5638
6064
|
try {
|
|
5639
6065
|
faults?.onStep?.("after-open");
|
|
5640
6066
|
db.exec("PRAGMA auto_vacuum = INCREMENTAL;");
|
|
@@ -5777,9 +6203,9 @@ var RECEIVED_STATE = "received";
|
|
|
5777
6203
|
function byteLength(value) {
|
|
5778
6204
|
return Buffer.byteLength(value, "utf8");
|
|
5779
6205
|
}
|
|
5780
|
-
function fileBytes(
|
|
6206
|
+
function fileBytes(path21) {
|
|
5781
6207
|
try {
|
|
5782
|
-
return statSync(
|
|
6208
|
+
return statSync(path21).size;
|
|
5783
6209
|
} catch {
|
|
5784
6210
|
return 0;
|
|
5785
6211
|
}
|
|
@@ -6986,6 +7412,21 @@ var MAX_INLINE_ARTIFACT_BYTES = 64 * 1024;
|
|
|
6986
7412
|
var MAX_TRACKED_TASK_IDS = 2e3;
|
|
6987
7413
|
var MAX_DURATION_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxDurationMs";
|
|
6988
7414
|
var MAX_OUTPUT_BYTES_EXCEEDED_REASON_PREFIX = "resource limit exceeded: maxTaskOutputBytes";
|
|
7415
|
+
var RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX = "result document undeliverable";
|
|
7416
|
+
function resultDocumentRejectionDetail(check) {
|
|
7417
|
+
switch (check.reason) {
|
|
7418
|
+
case "over-cap":
|
|
7419
|
+
return `${check.bytes} bytes as canonical JSON, over the ${RESULT_DOCUMENT_MAX_BYTES}-byte limit (it is never truncated \u2014 a truncated JSON document is not valid JSON; use artifactRefs for a result this size)`;
|
|
7420
|
+
case "not-serializable":
|
|
7421
|
+
return "not JSON-serializable (JSON.stringify threw, or produced no output at all)";
|
|
7422
|
+
case "not-plain-json":
|
|
7423
|
+
return "not plain JSON data: it does not equal its own JSON round trip, so serializing it would silently change it (an undefined-valued key, NaN, a function or symbol value, a Date, a toJSON that rewrites the value, or a getter that answers differently on a second read)";
|
|
7424
|
+
default: {
|
|
7425
|
+
const exhaustive = check;
|
|
7426
|
+
throw new Error(`unhandled result document rejection: ${JSON.stringify(exhaustive)}`);
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
6989
7430
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
6990
7431
|
function isKnownRuntimeId(id) {
|
|
6991
7432
|
return RuntimeIdSchema.safeParse(id).success;
|
|
@@ -6998,6 +7439,14 @@ function orderByPreference(candidates, preference) {
|
|
|
6998
7439
|
function adapterSupportsMode(adapter, mode) {
|
|
6999
7440
|
return adapter.capabilities().permissionModes.includes(mode);
|
|
7000
7441
|
}
|
|
7442
|
+
function adapterSupportsMcpToolsets(adapter) {
|
|
7443
|
+
return adapter.capabilities().mcpToolsets === true;
|
|
7444
|
+
}
|
|
7445
|
+
function withoutRequiredToolsets(payload) {
|
|
7446
|
+
if (!("requiredToolsets" in payload)) return payload;
|
|
7447
|
+
const { requiredToolsets, ...offer } = payload;
|
|
7448
|
+
return offer;
|
|
7449
|
+
}
|
|
7001
7450
|
function errorMessage3(err) {
|
|
7002
7451
|
return err instanceof Error ? err.message : String(err);
|
|
7003
7452
|
}
|
|
@@ -7033,8 +7482,8 @@ function estimateEventBytes(event) {
|
|
|
7033
7482
|
}
|
|
7034
7483
|
async function openArtifact(workspaceDir, name) {
|
|
7035
7484
|
const realWorkspaceDir = await promises.realpath(workspaceDir).catch(() => workspaceDir);
|
|
7036
|
-
const candidate =
|
|
7037
|
-
const prefix = realWorkspaceDir.endsWith(
|
|
7485
|
+
const candidate = path17.resolve(realWorkspaceDir, name);
|
|
7486
|
+
const prefix = realWorkspaceDir.endsWith(path17.sep) ? realWorkspaceDir : realWorkspaceDir + path17.sep;
|
|
7038
7487
|
if (candidate !== realWorkspaceDir && !candidate.startsWith(prefix)) {
|
|
7039
7488
|
return { ok: false, reason: `artifact name "${name}" resolves outside the task workspace \u2014 rejected` };
|
|
7040
7489
|
}
|
|
@@ -7353,6 +7802,9 @@ var TaskRunner = class {
|
|
|
7353
7802
|
case "task.offer":
|
|
7354
7803
|
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7355
7804
|
return;
|
|
7805
|
+
case "task.offer_with_toolsets":
|
|
7806
|
+
await this.handleOffer(envelope.task_id, envelope.payload);
|
|
7807
|
+
return;
|
|
7356
7808
|
case "task.cancel":
|
|
7357
7809
|
await this.handleCancel(envelope.task_id, envelope.payload.reason);
|
|
7358
7810
|
return;
|
|
@@ -7406,7 +7858,22 @@ var TaskRunner = class {
|
|
|
7406
7858
|
);
|
|
7407
7859
|
return;
|
|
7408
7860
|
}
|
|
7409
|
-
|
|
7861
|
+
if (payload.dispatchSelection !== void 0 && payload.runtime !== void 0 && payload.runtime !== payload.dispatchSelection.runtimeId) {
|
|
7862
|
+
this.decline(
|
|
7863
|
+
taskId,
|
|
7864
|
+
`offer runtime ${payload.runtime} does not match dispatchSelection.runtimeId ${payload.dispatchSelection.runtimeId}`,
|
|
7865
|
+
false
|
|
7866
|
+
);
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7869
|
+
const requiredToolsets = "requiredToolsets" in payload ? payload.requiredToolsets : void 0;
|
|
7870
|
+
const resolvedMcp = requiredToolsets ? this.resolveMcpServers(requiredToolsets) : void 0;
|
|
7871
|
+
if (resolvedMcp && !resolvedMcp.ok) {
|
|
7872
|
+
this.decline(taskId, resolvedMcp.reason, true);
|
|
7873
|
+
return;
|
|
7874
|
+
}
|
|
7875
|
+
const requestedRuntime = payload.dispatchSelection?.runtimeId ?? payload.runtime;
|
|
7876
|
+
const pick = await this.pickAdapter(requestedRuntime, payload.policy.mode, requiredToolsets !== void 0);
|
|
7410
7877
|
if (!pick.ok) {
|
|
7411
7878
|
this.decline(taskId, pick.reason, pick.retryable);
|
|
7412
7879
|
return;
|
|
@@ -7432,7 +7899,7 @@ var TaskRunner = class {
|
|
|
7432
7899
|
const sameProtocolTask = ledger?.taskId === taskId;
|
|
7433
7900
|
const interruptedOldTask = ledger?.phase === "interrupted" && sameProtocolTask;
|
|
7434
7901
|
const activeDifferentTask = ledger !== void 0 && ledger.taskId !== taskId && (ledger.phase === "preparing" || ledger.phase === "active");
|
|
7435
|
-
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef ||
|
|
7902
|
+
if (!known || known.workspaceKind !== "git" || !known.gitWorkspaceId || !ledger || ledger.workspaceId !== known.gitWorkspaceId || ledger.sessionRef !== payload.sessionRef || path17.resolve(ledger.workspaceDir) !== path17.resolve(known.workspaceDir) || interruptedOldTask || activeDifferentTask) {
|
|
7436
7903
|
this.decline(taskId, "session is incompatible with Git workspace mode", true);
|
|
7437
7904
|
return;
|
|
7438
7905
|
}
|
|
@@ -7447,7 +7914,7 @@ var TaskRunner = class {
|
|
|
7447
7914
|
return;
|
|
7448
7915
|
}
|
|
7449
7916
|
} else {
|
|
7450
|
-
workspaceDir =
|
|
7917
|
+
workspaceDir = path17.join(this.deps.workspaceRoot, taskId);
|
|
7451
7918
|
}
|
|
7452
7919
|
try {
|
|
7453
7920
|
gitLease = await gitManager.acquireLease(workspaceDir, payload.sessionRef);
|
|
@@ -7457,7 +7924,7 @@ var TaskRunner = class {
|
|
|
7457
7924
|
}
|
|
7458
7925
|
} else if (!this.deps.gitWorkspaceManager && !this.deps.gitWorkspaceStore) {
|
|
7459
7926
|
known = payload.sessionRef ? await this.deps.sessionWorkspaces.get(payload.sessionRef) : void 0;
|
|
7460
|
-
workspaceDir = known?.workspaceDir ??
|
|
7927
|
+
workspaceDir = known?.workspaceDir ?? path17.join(this.deps.workspaceRoot, taskId);
|
|
7461
7928
|
plainWorkspaceNeedsResolve = true;
|
|
7462
7929
|
} else {
|
|
7463
7930
|
this.decline(taskId, "workspace mode is unavailable", true);
|
|
@@ -7569,6 +8036,7 @@ var TaskRunner = class {
|
|
|
7569
8036
|
const ctx = {
|
|
7570
8037
|
workspaceDir,
|
|
7571
8038
|
policy: decision.policy,
|
|
8039
|
+
...resolvedMcp?.ok ? { mcpServers: resolvedMcp.servers } : {},
|
|
7572
8040
|
...gitWorkspaceId ? { gitWorkspace: { workspaceId: gitWorkspaceId, baseline: gitBaseline } } : {},
|
|
7573
8041
|
// M5: no longer `process.env` verbatim (see `environment.ts`'s own
|
|
7574
8042
|
// module doc comment for the credential-leak gap that closed) —
|
|
@@ -7605,7 +8073,7 @@ var TaskRunner = class {
|
|
|
7605
8073
|
}
|
|
7606
8074
|
};
|
|
7607
8075
|
const effectiveOffer = {
|
|
7608
|
-
...payload,
|
|
8076
|
+
...withoutRequiredToolsets(payload),
|
|
7609
8077
|
instruction: gitWorkspaceId ? prependGitWorkspaceGuidance(resolvedInstruction) : resolvedInstruction,
|
|
7610
8078
|
// Never forward a sessionRef this device has no recorded workspace
|
|
7611
8079
|
// for (stale, from another device, or simply made up) — an adapter
|
|
@@ -7683,6 +8151,36 @@ var TaskRunner = class {
|
|
|
7683
8151
|
if (typeof instruction === "string") return instruction;
|
|
7684
8152
|
return this.deps.blobClient.resolveInstruction(instruction.blobRef);
|
|
7685
8153
|
}
|
|
8154
|
+
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
8155
|
+
resolveMcpServers(requiredToolsets) {
|
|
8156
|
+
const registry = this.deps.mcpToolsets;
|
|
8157
|
+
if (!registry) {
|
|
8158
|
+
return { ok: false, reason: "offer requires MCP toolsets, but this device has no local mcpToolsets registry" };
|
|
8159
|
+
}
|
|
8160
|
+
const servers = /* @__PURE__ */ Object.create(null);
|
|
8161
|
+
for (const toolsetId of requiredToolsets) {
|
|
8162
|
+
const toolset = registry.get(toolsetId);
|
|
8163
|
+
if (!toolset) {
|
|
8164
|
+
return { ok: false, reason: `required MCP toolset "${toolsetId}" is not configured on this device` };
|
|
8165
|
+
}
|
|
8166
|
+
for (const [serverName, server] of Object.entries(toolset.mcpServers)) {
|
|
8167
|
+
if (Object.prototype.hasOwnProperty.call(servers, serverName)) {
|
|
8168
|
+
return {
|
|
8169
|
+
ok: false,
|
|
8170
|
+
reason: `required MCP toolsets collide on server name "${serverName}"; refusing ambiguous projection`
|
|
8171
|
+
};
|
|
8172
|
+
}
|
|
8173
|
+
servers[serverName] = Object.freeze({
|
|
8174
|
+
command: server.command,
|
|
8175
|
+
...server.args ? { args: Object.freeze([...server.args]) } : {}
|
|
8176
|
+
});
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
if (Object.keys(servers).length === 0) {
|
|
8180
|
+
return { ok: false, reason: "required MCP toolsets resolved to no servers; refusing to run without tools" };
|
|
8181
|
+
}
|
|
8182
|
+
return { ok: true, servers: Object.freeze(servers) };
|
|
8183
|
+
}
|
|
7686
8184
|
async pump(active) {
|
|
7687
8185
|
try {
|
|
7688
8186
|
for await (const event of active.session.events) {
|
|
@@ -7716,11 +8214,38 @@ var TaskRunner = class {
|
|
|
7716
8214
|
if (event.type === "turn_end") {
|
|
7717
8215
|
active.batcher.push(event);
|
|
7718
8216
|
active.batcher.flush();
|
|
8217
|
+
const finalOutput = active.summaryParts.join("");
|
|
8218
|
+
const outcome = await this.resolveResultDocument(active, finalOutput);
|
|
8219
|
+
if (!outcome.deliver) return;
|
|
7719
8220
|
await this.observeGit(active, "completed");
|
|
8221
|
+
if (outcome.document !== void 0 && !this.hasResultDocumentCapability()) {
|
|
8222
|
+
await this.fail(
|
|
8223
|
+
active.taskId,
|
|
8224
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server stopped advertising the result-document capability before this completion could be sent (a reconnect to an older server), so it would silently discard this document`,
|
|
8225
|
+
false
|
|
8226
|
+
);
|
|
8227
|
+
return;
|
|
8228
|
+
}
|
|
7720
8229
|
this.deps.send(
|
|
7721
8230
|
createEnvelope(
|
|
7722
8231
|
"task.complete",
|
|
7723
|
-
{
|
|
8232
|
+
{
|
|
8233
|
+
summary: finalOutput,
|
|
8234
|
+
sessionRef: active.session.sessionRef,
|
|
8235
|
+
// Spread rather than `document: outcome.document`, so a
|
|
8236
|
+
// completion with no document is the exact same payload it
|
|
8237
|
+
// was before this field existed — not one carrying an
|
|
8238
|
+
// explicit `document: undefined` key.
|
|
8239
|
+
//
|
|
8240
|
+
// `outcome.document` is the protocol's CANONICAL SNAPSHOT
|
|
8241
|
+
// (`checkResultDocument`), never the object the extractor
|
|
8242
|
+
// returned: pure data serializes identically at the root
|
|
8243
|
+
// (where it was measured) and nested inside this payload
|
|
8244
|
+
// (where the codec actually serializes it), so a contextual
|
|
8245
|
+
// `toJSON(key)` or an unstable getter cannot make the wire
|
|
8246
|
+
// bytes differ from what the cap gate approved.
|
|
8247
|
+
...outcome.document !== void 0 ? { document: outcome.document } : {}
|
|
8248
|
+
},
|
|
7724
8249
|
{ taskId: active.taskId, sessionRef: active.session.sessionRef }
|
|
7725
8250
|
)
|
|
7726
8251
|
);
|
|
@@ -8258,21 +8783,117 @@ var TaskRunner = class {
|
|
|
8258
8783
|
this.deps.send(createEnvelope("task.fail", { reason, retryable }, { taskId }));
|
|
8259
8784
|
await this.finish(taskId);
|
|
8260
8785
|
}
|
|
8261
|
-
|
|
8262
|
-
|
|
8786
|
+
/**
|
|
8787
|
+
* additive-minor (`task.complete.document`): the whole daemon-side gate
|
|
8788
|
+
* between a configured {@link ResultDocumentExtractor} and the wire —
|
|
8789
|
+
* called once, from the `turn_end` completion path, immediately before
|
|
8790
|
+
* `task.complete` is built.
|
|
8791
|
+
*
|
|
8792
|
+
* `{deliver: true}` means "go on and send `task.complete`", carrying the
|
|
8793
|
+
* document when there is one. `{deliver: false}` means this method has
|
|
8794
|
+
* ALREADY reported `task.fail` and finished the task; the caller must
|
|
8795
|
+
* return without sending anything further.
|
|
8796
|
+
*
|
|
8797
|
+
* Four fail-closed branches, all `retryable: false` (see
|
|
8798
|
+
* {@link RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX} for why none of them
|
|
8799
|
+
* can succeed on a retry):
|
|
8800
|
+
*
|
|
8801
|
+
* 1. The extractor threw — its error is surfaced, never swallowed.
|
|
8802
|
+
* 2. The extractor returned a thenable, violating the synchronous
|
|
8803
|
+
* contract in the one way that would otherwise ship a wrong answer.
|
|
8804
|
+
* 3. The document is over the cap, not JSON-serializable, or not plain
|
|
8805
|
+
* JSON data, per `checkResultDocument` — the protocol's OWN check,
|
|
8806
|
+
* imported rather than reimplemented, so this gate and the server's
|
|
8807
|
+
* schema validation can never disagree about what is legal.
|
|
8808
|
+
* 4. The connected server never advertised `result-document`. Its
|
|
8809
|
+
* tolerant `z.object()` would silently strip the field on arrival
|
|
8810
|
+
* (`version.ts`'s own flag doc comment), so "send anyway" is not a
|
|
8811
|
+
* degraded-but-working path — it is the task's primary structured
|
|
8812
|
+
* result being deleted in transit with nothing reported anywhere.
|
|
8813
|
+
*
|
|
8814
|
+
* The capability is checked LAST, deliberately: a document that is itself
|
|
8815
|
+
* invalid is the host's own bug and is worth reporting as such even when
|
|
8816
|
+
* the connected server could not have accepted any document at all. It is
|
|
8817
|
+
* then re-checked once more by the caller after its own last await, since
|
|
8818
|
+
* a reconnect can invalidate this answer in between (F3).
|
|
8819
|
+
*
|
|
8820
|
+
* **Residual window (bounded, deliberately not hacked around).** Even the
|
|
8821
|
+
* caller's re-check happens before `ConnectionManager.send` hands the
|
|
8822
|
+
* envelope to a transport, and a queued envelope can outlive the
|
|
8823
|
+
* connection it was queued for: a reconnect between `send()` and the
|
|
8824
|
+
* outbox actually draining could still deliver this `task.complete` to a
|
|
8825
|
+
* rolled-back N-1 server that strips the document. Closing that would
|
|
8826
|
+
* mean teaching the transport outbox to inspect payload semantics and
|
|
8827
|
+
* mint a substitute `task.fail` for a task this runner already finished —
|
|
8828
|
+
* a second authority over terminal outcomes living in the queue, which is
|
|
8829
|
+
* worse than the window it closes. Documented instead, here and in
|
|
8830
|
+
* docs/protocol.md §7.2.
|
|
8831
|
+
*/
|
|
8832
|
+
async resolveResultDocument(active, finalOutput) {
|
|
8833
|
+
const extract = this.deps.resultDocument?.extract;
|
|
8834
|
+
if (!extract) return { deliver: true };
|
|
8835
|
+
let document;
|
|
8263
8836
|
try {
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8837
|
+
document = extract(finalOutput, { taskId: active.taskId, sessionRef: active.session.sessionRef });
|
|
8838
|
+
} catch (err) {
|
|
8839
|
+
await this.fail(
|
|
8840
|
+
active.taskId,
|
|
8841
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract threw: ${errorMessage3(err)}`,
|
|
8842
|
+
false
|
|
8843
|
+
);
|
|
8844
|
+
return { deliver: false };
|
|
8845
|
+
}
|
|
8846
|
+
if (typeof document?.then === "function") {
|
|
8847
|
+
await this.fail(
|
|
8848
|
+
active.taskId,
|
|
8849
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the configured resultDocument.extract returned a promise; the contract is synchronous (an awaited value is never read, and a promise encodes to an empty document)`,
|
|
8850
|
+
false
|
|
8851
|
+
);
|
|
8852
|
+
return { deliver: false };
|
|
8853
|
+
}
|
|
8854
|
+
if (document === void 0) return { deliver: true };
|
|
8855
|
+
const check = checkResultDocument(document);
|
|
8856
|
+
if (!check.ok) {
|
|
8857
|
+
const detail = resultDocumentRejectionDetail(check);
|
|
8858
|
+
await this.fail(active.taskId, `${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: ${detail}`, false);
|
|
8859
|
+
return { deliver: false };
|
|
8860
|
+
}
|
|
8861
|
+
if (!this.hasResultDocumentCapability()) {
|
|
8862
|
+
await this.fail(
|
|
8863
|
+
active.taskId,
|
|
8864
|
+
`${RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX}: the connected server did not advertise the result-document capability, so it would silently discard this ${check.bytes}-byte document`,
|
|
8865
|
+
false
|
|
8866
|
+
);
|
|
8867
|
+
return { deliver: false };
|
|
8868
|
+
}
|
|
8869
|
+
return { deliver: true, document: check.canonical };
|
|
8870
|
+
}
|
|
8871
|
+
/**
|
|
8872
|
+
* Whether the CURRENTLY connected server advertised `result-document` —
|
|
8873
|
+
* read fresh on every call, never captured, because the answer changes
|
|
8874
|
+
* across a reconnect (`ConnectionManager.getServerCapabilities` returns
|
|
8875
|
+
* `[]` from the moment an acked connection closes until a fresh
|
|
8876
|
+
* `conn.ack` repopulates it). An absent `getServerCapabilities` seam is
|
|
8877
|
+
* "no capabilities", the fail-closed reading.
|
|
8878
|
+
*/
|
|
8879
|
+
hasResultDocumentCapability() {
|
|
8880
|
+
return (this.deps.getServerCapabilities?.() ?? []).includes("result-document");
|
|
8881
|
+
}
|
|
8882
|
+
async observeGit(active, phase) {
|
|
8883
|
+
if (!active.gitWorkspaceId || !this.deps.gitWorkspaceManager || !this.deps.gitWorkspaceStore) return;
|
|
8884
|
+
try {
|
|
8885
|
+
const observation = await new Promise((resolve, reject) => {
|
|
8886
|
+
const timer = setTimeout(() => reject(new Error("Git workspace observation timed out")), GIT_OBSERVATION_TIMEOUT_MS);
|
|
8887
|
+
timer.unref?.();
|
|
8888
|
+
void this.deps.gitWorkspaceManager.observe(active.workspaceDir, active.gitBaseline).then(
|
|
8889
|
+
(value) => {
|
|
8890
|
+
clearTimeout(timer);
|
|
8891
|
+
resolve(value);
|
|
8892
|
+
},
|
|
8893
|
+
(error) => {
|
|
8894
|
+
clearTimeout(timer);
|
|
8895
|
+
reject(error);
|
|
8896
|
+
}
|
|
8276
8897
|
);
|
|
8277
8898
|
});
|
|
8278
8899
|
await this.deps.gitWorkspaceStore.updateObservation(active.gitWorkspaceId, observation, phase);
|
|
@@ -8329,7 +8950,7 @@ var TaskRunner = class {
|
|
|
8329
8950
|
}
|
|
8330
8951
|
/** `reuseDir`, when set (a known sessionRef's recorded workspace), is used verbatim instead of a fresh `workspaceRoot/<taskId>` directory — `mkdir recursive` is idempotent either way, so ensuring-exists is safe to do unconditionally. */
|
|
8331
8952
|
async resolveWorkspaceDir(taskId, reuseDir) {
|
|
8332
|
-
const dir = reuseDir ??
|
|
8953
|
+
const dir = reuseDir ?? path17.join(this.deps.workspaceRoot, taskId);
|
|
8333
8954
|
await promises.mkdir(dir, { recursive: true });
|
|
8334
8955
|
return dir;
|
|
8335
8956
|
}
|
|
@@ -8360,7 +8981,7 @@ var TaskRunner = class {
|
|
|
8360
8981
|
* is device-specific (which runtimes happen to be installed here), so a
|
|
8361
8982
|
* different device's installed runtime set might satisfy it.
|
|
8362
8983
|
*/
|
|
8363
|
-
async pickAdapter(requestedRuntime, policyMode) {
|
|
8984
|
+
async pickAdapter(requestedRuntime, policyMode, requiresMcpToolsets) {
|
|
8364
8985
|
const allowlist = this.deps.runtimeAllowlist;
|
|
8365
8986
|
if (requestedRuntime) {
|
|
8366
8987
|
if (allowlist && !allowlist.includes(requestedRuntime)) {
|
|
@@ -8381,6 +9002,13 @@ var TaskRunner = class {
|
|
|
8381
9002
|
retryable: false
|
|
8382
9003
|
};
|
|
8383
9004
|
}
|
|
9005
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) {
|
|
9006
|
+
return {
|
|
9007
|
+
ok: false,
|
|
9008
|
+
reason: `runtime "${requestedRuntime}" cannot project required MCP toolsets`,
|
|
9009
|
+
retryable: false
|
|
9010
|
+
};
|
|
9011
|
+
}
|
|
8384
9012
|
const detected = await adapter.detect();
|
|
8385
9013
|
if (!detected.present) {
|
|
8386
9014
|
return {
|
|
@@ -8395,12 +9023,13 @@ var TaskRunner = class {
|
|
|
8395
9023
|
const candidates = orderByPreference(eligible, this.deps.runtimePreference ?? DEFAULT_RUNTIME_PREFERENCE);
|
|
8396
9024
|
for (const adapter of candidates) {
|
|
8397
9025
|
if (!adapterSupportsMode(adapter, policyMode)) continue;
|
|
9026
|
+
if (requiresMcpToolsets && !adapterSupportsMcpToolsets(adapter)) continue;
|
|
8398
9027
|
const detected = await adapter.detect();
|
|
8399
9028
|
if (detected.present) return { ok: true, adapter };
|
|
8400
9029
|
}
|
|
8401
9030
|
return {
|
|
8402
9031
|
ok: false,
|
|
8403
|
-
reason: `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
9032
|
+
reason: requiresMcpToolsets ? `no available runtime on this device can express permission mode "${policyMode}" with required MCP toolsets` : `no available runtime on this device can express permission mode "${policyMode}"`,
|
|
8404
9033
|
retryable: true
|
|
8405
9034
|
};
|
|
8406
9035
|
}
|
|
@@ -8431,7 +9060,7 @@ function toJournalEnvelopeRecord(envelope, identity) {
|
|
|
8431
9060
|
bytes,
|
|
8432
9061
|
bytesHash: journalHash(bytes),
|
|
8433
9062
|
receivedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8434
|
-
opensTask: envelope.type === "task.offer"
|
|
9063
|
+
opensTask: envelope.type === "task.offer" || envelope.type === "task.offer_with_toolsets"
|
|
8435
9064
|
};
|
|
8436
9065
|
}
|
|
8437
9066
|
function isRuntimeId(id) {
|
|
@@ -8455,29 +9084,216 @@ function computeCapabilities(adapters) {
|
|
|
8455
9084
|
if (adapters.some((adapter) => adapter.capabilities().steer)) flags.push("steer");
|
|
8456
9085
|
flags.push("blob-upload");
|
|
8457
9086
|
flags.push("approval-targeting");
|
|
9087
|
+
const selectionAdapters = adapters.filter(
|
|
9088
|
+
(adapter) => ALL_RUNTIME_IDS.includes(adapter.id)
|
|
9089
|
+
);
|
|
9090
|
+
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.supportsDispatchSelection === true)) {
|
|
9091
|
+
flags.push("dispatch-selection");
|
|
9092
|
+
}
|
|
9093
|
+
if (adapters.some((adapter) => adapter.capabilities().mcpToolsets === true)) {
|
|
9094
|
+
flags.push("toolset-selection");
|
|
9095
|
+
}
|
|
8458
9096
|
return flags;
|
|
8459
9097
|
}
|
|
8460
9098
|
var ALL_RUNTIME_IDS = ["pi", "claude", "codex"];
|
|
8461
|
-
function buildAdapter(id) {
|
|
9099
|
+
function buildAdapter(id, config) {
|
|
8462
9100
|
switch (id) {
|
|
8463
9101
|
case "pi":
|
|
8464
|
-
return new PiAdapter();
|
|
9102
|
+
return new PiAdapter({ byokLauncher: config.piByokLauncher });
|
|
8465
9103
|
case "claude":
|
|
8466
9104
|
return new ClaudeAdapter();
|
|
8467
9105
|
case "codex":
|
|
8468
9106
|
return new CodexAdapter();
|
|
8469
9107
|
}
|
|
8470
9108
|
}
|
|
8471
|
-
function buildDefaultAdapters(
|
|
8472
|
-
const ids = runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => runtimeAllowlist
|
|
8473
|
-
return ids.map(buildAdapter);
|
|
9109
|
+
function buildDefaultAdapters(config) {
|
|
9110
|
+
const ids = config.runtimeAllowlist ? ALL_RUNTIME_IDS.filter((id) => config.runtimeAllowlist?.includes(id)) : ALL_RUNTIME_IDS;
|
|
9111
|
+
return ids.map((id) => buildAdapter(id, config));
|
|
9112
|
+
}
|
|
9113
|
+
function validatePiByokLauncherConfig(launcher) {
|
|
9114
|
+
for (const [field, value] of [
|
|
9115
|
+
["command", launcher.command],
|
|
9116
|
+
["profileDbPath", launcher.profileDbPath],
|
|
9117
|
+
["sessionDir", launcher.sessionDir]
|
|
9118
|
+
]) {
|
|
9119
|
+
if (value.trim().length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
9120
|
+
throw new Error(`DaemonConfig.piByokLauncher.${field} must be a non-empty single-line string`);
|
|
9121
|
+
}
|
|
9122
|
+
}
|
|
9123
|
+
if (!isAbsolute(launcher.profileDbPath) || !isAbsolute(launcher.sessionDir)) {
|
|
9124
|
+
throw new Error(
|
|
9125
|
+
"DaemonConfig.piByokLauncher profileDbPath and sessionDir must be absolute paths"
|
|
9126
|
+
);
|
|
9127
|
+
}
|
|
9128
|
+
if (launcher.secretServicePrefix !== void 0 && (launcher.secretServicePrefix.trim().length === 0 || /[\u0000\r\n]/u.test(launcher.secretServicePrefix))) {
|
|
9129
|
+
throw new Error(
|
|
9130
|
+
"DaemonConfig.piByokLauncher.secretServicePrefix must be a non-empty single-line string"
|
|
9131
|
+
);
|
|
9132
|
+
}
|
|
9133
|
+
const reserved = /* @__PURE__ */ new Set([
|
|
9134
|
+
"--",
|
|
9135
|
+
"--pi-bin",
|
|
9136
|
+
"--profile-db",
|
|
9137
|
+
"--session-dir",
|
|
9138
|
+
"--secret-service-prefix",
|
|
9139
|
+
"--provider",
|
|
9140
|
+
"--model"
|
|
9141
|
+
]);
|
|
9142
|
+
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
9143
|
+
if (conflicting !== void 0) {
|
|
9144
|
+
throw new Error(
|
|
9145
|
+
`DaemonConfig.piByokLauncher.args must not override reserved launcher argument ${conflicting}`
|
|
9146
|
+
);
|
|
9147
|
+
}
|
|
9148
|
+
const invalidArg = launcher.args?.find((arg) => arg.length === 0 || /[\u0000\r\n]/u.test(arg));
|
|
9149
|
+
if (invalidArg !== void 0) {
|
|
9150
|
+
throw new Error("DaemonConfig.piByokLauncher.args must contain only non-empty single-line strings");
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
var MAX_LOCAL_MCP_TOOLSETS = 64;
|
|
9154
|
+
var MAX_LOCAL_MCP_SERVERS_PER_TOOLSET = 16;
|
|
9155
|
+
var MAX_LOCAL_MCP_ARGS = 64;
|
|
9156
|
+
var MAX_LOCAL_MCP_TOKEN_CHARS = 4096;
|
|
9157
|
+
function isNonEmptySingleLine(value) {
|
|
9158
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_LOCAL_MCP_TOKEN_CHARS && !/[\u0000\r\n]/u.test(value);
|
|
9159
|
+
}
|
|
9160
|
+
function resolveMcpToolsets(configured) {
|
|
9161
|
+
if (configured === void 0) return void 0;
|
|
9162
|
+
if (configured === null || typeof configured !== "object" || Array.isArray(configured)) {
|
|
9163
|
+
throw new Error("DaemonConfig.mcpToolsets must be an object keyed by logical toolset id");
|
|
9164
|
+
}
|
|
9165
|
+
const toolsetEntries = Object.entries(configured);
|
|
9166
|
+
if (toolsetEntries.length > MAX_LOCAL_MCP_TOOLSETS) {
|
|
9167
|
+
throw new Error(`DaemonConfig.mcpToolsets may contain at most ${MAX_LOCAL_MCP_TOOLSETS} toolsets`);
|
|
9168
|
+
}
|
|
9169
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
9170
|
+
for (const [toolsetId, rawToolset] of toolsetEntries) {
|
|
9171
|
+
const parsedId = ToolsetIdSchema.safeParse(toolsetId);
|
|
9172
|
+
if (!parsedId.success) {
|
|
9173
|
+
throw new Error(`DaemonConfig.mcpToolsets contains invalid toolset id ${JSON.stringify(toolsetId)}`);
|
|
9174
|
+
}
|
|
9175
|
+
if (rawToolset === null || typeof rawToolset !== "object" || Array.isArray(rawToolset)) {
|
|
9176
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} must be an object`);
|
|
9177
|
+
}
|
|
9178
|
+
const toolsetKeys = Object.keys(rawToolset);
|
|
9179
|
+
if (toolsetKeys.some((key) => key !== "mcpServers")) {
|
|
9180
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId} accepts only the mcpServers field`);
|
|
9181
|
+
}
|
|
9182
|
+
const rawServers = rawToolset.mcpServers;
|
|
9183
|
+
if (rawServers === null || typeof rawServers !== "object" || Array.isArray(rawServers)) {
|
|
9184
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must be an object`);
|
|
9185
|
+
}
|
|
9186
|
+
const serverEntries = Object.entries(rawServers);
|
|
9187
|
+
if (serverEntries.length === 0 || serverEntries.length > MAX_LOCAL_MCP_SERVERS_PER_TOOLSET) {
|
|
9188
|
+
throw new Error(
|
|
9189
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers must contain 1-${MAX_LOCAL_MCP_SERVERS_PER_TOOLSET} servers`
|
|
9190
|
+
);
|
|
9191
|
+
}
|
|
9192
|
+
const servers = {};
|
|
9193
|
+
for (const [serverName, rawServer] of serverEntries) {
|
|
9194
|
+
if (!ToolsetIdSchema.safeParse(serverName).success) {
|
|
9195
|
+
throw new Error(
|
|
9196
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers contains invalid server name ${JSON.stringify(serverName)}`
|
|
9197
|
+
);
|
|
9198
|
+
}
|
|
9199
|
+
if (serverName === APPROVAL_MCP_SERVER_NAME) {
|
|
9200
|
+
throw new Error(
|
|
9201
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} uses a server name reserved by the daemon`
|
|
9202
|
+
);
|
|
9203
|
+
}
|
|
9204
|
+
if (rawServer === null || typeof rawServer !== "object" || Array.isArray(rawServer)) {
|
|
9205
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} must be an object`);
|
|
9206
|
+
}
|
|
9207
|
+
const serverKeys = Object.keys(rawServer);
|
|
9208
|
+
if (serverKeys.some((key) => key !== "command" && key !== "args")) {
|
|
9209
|
+
throw new Error(
|
|
9210
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName} accepts only command and args; env, headers, and remote task data are not supported`
|
|
9211
|
+
);
|
|
9212
|
+
}
|
|
9213
|
+
const server = rawServer;
|
|
9214
|
+
if (!isNonEmptySingleLine(server.command)) {
|
|
9215
|
+
throw new Error(
|
|
9216
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.command must be a non-empty single-line string no longer than ${MAX_LOCAL_MCP_TOKEN_CHARS} characters`
|
|
9217
|
+
);
|
|
9218
|
+
}
|
|
9219
|
+
if (server.args !== void 0 && !Array.isArray(server.args)) {
|
|
9220
|
+
throw new Error(`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must be an array`);
|
|
9221
|
+
}
|
|
9222
|
+
const args = server.args ?? [];
|
|
9223
|
+
if (args.length > MAX_LOCAL_MCP_ARGS || args.some((arg) => !isNonEmptySingleLine(arg))) {
|
|
9224
|
+
throw new Error(
|
|
9225
|
+
`DaemonConfig.mcpToolsets.${toolsetId}.mcpServers.${serverName}.args must contain at most ${MAX_LOCAL_MCP_ARGS} non-empty single-line strings`
|
|
9226
|
+
);
|
|
9227
|
+
}
|
|
9228
|
+
servers[serverName] = Object.freeze({
|
|
9229
|
+
command: server.command,
|
|
9230
|
+
...args.length > 0 ? { args: Object.freeze([...args]) } : {}
|
|
9231
|
+
});
|
|
9232
|
+
}
|
|
9233
|
+
resolved.set(toolsetId, Object.freeze({ mcpServers: Object.freeze(servers) }));
|
|
9234
|
+
}
|
|
9235
|
+
return resolved;
|
|
9236
|
+
}
|
|
9237
|
+
function resolveDeviceAssertionAudiences(config) {
|
|
9238
|
+
if (config === void 0) return void 0;
|
|
9239
|
+
if (!Array.isArray(config.audiences)) {
|
|
9240
|
+
throw new Error(
|
|
9241
|
+
`DaemonConfig.deviceAssertion.audiences must be an array of exact audience strings \u2014 got ${JSON.stringify(config.audiences)}. Omit the deviceAssertion section (or pass an empty array) to leave the assertion broker disabled.`
|
|
9242
|
+
);
|
|
9243
|
+
}
|
|
9244
|
+
if (config.audiences.length === 0) return void 0;
|
|
9245
|
+
const audiences = /* @__PURE__ */ new Set();
|
|
9246
|
+
for (const audience of config.audiences) {
|
|
9247
|
+
if (typeof audience !== "string" || audience.length === 0) {
|
|
9248
|
+
throw new Error(
|
|
9249
|
+
`DaemonConfig.deviceAssertion.audiences entries must be non-empty strings \u2014 got ${JSON.stringify(audience)}`
|
|
9250
|
+
);
|
|
9251
|
+
}
|
|
9252
|
+
if (Buffer.byteLength(audience, "utf8") > DEVICE_ASSERTION_AUDIENCE_MAX_BYTES) {
|
|
9253
|
+
throw new Error(
|
|
9254
|
+
`DaemonConfig.deviceAssertion.audiences entry ${JSON.stringify(audience)} exceeds ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
9255
|
+
);
|
|
9256
|
+
}
|
|
9257
|
+
if (audiences.has(audience)) {
|
|
9258
|
+
throw new Error(
|
|
9259
|
+
`DaemonConfig.deviceAssertion.audiences contains ${JSON.stringify(audience)} twice \u2014 rejected rather than de-duplicated, because a duplicate is usually a copy-paste that hid a typo in the entry that was meant to be different`
|
|
9260
|
+
);
|
|
9261
|
+
}
|
|
9262
|
+
audiences.add(audience);
|
|
9263
|
+
}
|
|
9264
|
+
return audiences;
|
|
9265
|
+
}
|
|
9266
|
+
function resolveDeviceAssertionTtlMs(config) {
|
|
9267
|
+
const ttlMs = config?.ttlMs ?? DEVICE_ASSERTION_DEFAULT_TTL_MS;
|
|
9268
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs <= 0 || ttlMs > DEVICE_ASSERTION_MAX_TTL_MS) {
|
|
9269
|
+
throw new Error(
|
|
9270
|
+
`DaemonConfig.deviceAssertion.ttlMs must be a positive integer no greater than ${DEVICE_ASSERTION_MAX_TTL_MS} ms \u2014 got ${JSON.stringify(config?.ttlMs)}`
|
|
9271
|
+
);
|
|
9272
|
+
}
|
|
9273
|
+
return ttlMs;
|
|
8474
9274
|
}
|
|
8475
9275
|
function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
9276
|
+
return buildDaemonWithAdapters(config, adapters, overrides);
|
|
9277
|
+
}
|
|
9278
|
+
function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProbe) {
|
|
9279
|
+
const mcpToolsets = resolveMcpToolsets(config.mcpToolsets);
|
|
9280
|
+
if (config.piByokLauncher !== void 0) {
|
|
9281
|
+
validatePiByokLauncherConfig(config.piByokLauncher);
|
|
9282
|
+
}
|
|
8476
9283
|
if (config.maxTaskOutputBytes !== void 0 && !(config.maxTaskOutputBytes > 0)) {
|
|
8477
9284
|
throw new Error(
|
|
8478
9285
|
`DaemonConfig.maxTaskOutputBytes must be a positive number (or omitted to use the ${DEFAULT_MAX_TASK_OUTPUT_BYTES}-byte default) \u2014 got ${config.maxTaskOutputBytes}. Pass Number.POSITIVE_INFINITY to explicitly disable the cap; 0 or a negative number is rejected rather than silently treated as "disabled".`
|
|
8479
9286
|
);
|
|
8480
9287
|
}
|
|
9288
|
+
const presenceCadence = {
|
|
9289
|
+
intervalMs: config.presence?.intervalMs ?? DEFAULT_PRESENCE_HEARTBEAT_INTERVAL_MS,
|
|
9290
|
+
ttlMs: config.presence?.ttlMs ?? DEFAULT_PRESENCE_TTL_MS,
|
|
9291
|
+
minimumIntervalMs: config.presence?.minimumIntervalMs ?? DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS
|
|
9292
|
+
};
|
|
9293
|
+
assertPresenceHeartbeatCadence(presenceCadence);
|
|
9294
|
+
const deviceAssertionAudiences = resolveDeviceAssertionAudiences(config.deviceAssertion);
|
|
9295
|
+
const deviceAssertionTtlMs = resolveDeviceAssertionTtlMs(config.deviceAssertion);
|
|
9296
|
+
let shuttingDown = false;
|
|
8481
9297
|
const storeDir = DeviceStore.resolveDir(config.productId, config.storeDir);
|
|
8482
9298
|
const store = new DeviceStore(storeDir);
|
|
8483
9299
|
const operationalHealth = new OperationalHealthTracker(storeDir);
|
|
@@ -8568,6 +9384,9 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8568
9384
|
let runner;
|
|
8569
9385
|
let controlServerHandle;
|
|
8570
9386
|
let daemonOwnerLease;
|
|
9387
|
+
let presencePublisher;
|
|
9388
|
+
let presenceDiscovery;
|
|
9389
|
+
let presenceDiscoveryInFlight = false;
|
|
8571
9390
|
let shutdownPromise;
|
|
8572
9391
|
const pendingLateMutationBarriers = /* @__PURE__ */ new Set();
|
|
8573
9392
|
let startedAt;
|
|
@@ -8740,6 +9559,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8740
9559
|
deviceId: record.deviceId,
|
|
8741
9560
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
8742
9561
|
runtimeEnvironment: config.runtimeEnvironment,
|
|
9562
|
+
...mcpToolsets ? { mcpToolsets } : {},
|
|
8743
9563
|
// M3-2a: `send` is already this file's OWN closure (not something
|
|
8744
9564
|
// `TaskRunner` builds) — every `task.claim`/`task.started`/
|
|
8745
9565
|
// `task.progress`/`task.artifact`/`task.await_approval`/
|
|
@@ -8779,6 +9599,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8779
9599
|
shutdownInterruptTimeoutMs: overrides.shutdown?.taskInterruptTimeoutMs,
|
|
8780
9600
|
// M5 batch-3 (workstream 2): see DaemonConfig.maxTaskOutputBytes's own doc comment — already validated above.
|
|
8781
9601
|
maxTaskOutputBytes: config.maxTaskOutputBytes,
|
|
9602
|
+
// additive-minor (`task.complete.document`): passed through verbatim,
|
|
9603
|
+
// absent when unconfigured — see `DaemonConfig.resultDocument`'s own
|
|
9604
|
+
// doc comment. Spread rather than assigned so an unconfigured daemon
|
|
9605
|
+
// builds the exact `deps` object it did before this seam existed.
|
|
9606
|
+
...config.resultDocument ? { resultDocument: config.resultDocument } : {},
|
|
8782
9607
|
// M4 Phase 3 hardening: bridges TaskRunner's stale-approval-race
|
|
8783
9608
|
// finding out to the SAME local observability seam every other
|
|
8784
9609
|
// daemon-local event already uses (see observer.ts's own module doc
|
|
@@ -8854,8 +9679,10 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8854
9679
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
8855
9680
|
},
|
|
8856
9681
|
onStateChange: (state) => {
|
|
9682
|
+
const wasSettled = connectionState === "open" || connectionState === "degraded";
|
|
8857
9683
|
connectionState = state;
|
|
8858
9684
|
observer.noteConnectionState(state);
|
|
9685
|
+
if (!wasSettled && (state === "open" || state === "degraded")) runPresenceDiscovery();
|
|
8859
9686
|
},
|
|
8860
9687
|
backoff: overrides.backoff,
|
|
8861
9688
|
liveness: overrides.liveness,
|
|
@@ -8873,6 +9700,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8873
9700
|
});
|
|
8874
9701
|
await connection.start();
|
|
8875
9702
|
await connection.waitForAck();
|
|
9703
|
+
startPresenceProducer();
|
|
8876
9704
|
} catch (err) {
|
|
8877
9705
|
try {
|
|
8878
9706
|
await runShutdownSequence("startup failed", { drainTimeoutMs: 0 });
|
|
@@ -8882,7 +9710,41 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8882
9710
|
throw err;
|
|
8883
9711
|
}
|
|
8884
9712
|
}
|
|
9713
|
+
function startPresenceProducer() {
|
|
9714
|
+
presenceDiscovery = new AbortController();
|
|
9715
|
+
runPresenceDiscovery();
|
|
9716
|
+
}
|
|
9717
|
+
function runPresenceDiscovery() {
|
|
9718
|
+
const discovery = presenceDiscovery;
|
|
9719
|
+
if (!discovery || presenceDiscoveryInFlight) return;
|
|
9720
|
+
presenceDiscoveryInFlight = true;
|
|
9721
|
+
void (async () => {
|
|
9722
|
+
try {
|
|
9723
|
+
const declaration = await fetchCapabilityDeclaration(config.serverUrl, { signal: discovery.signal });
|
|
9724
|
+
if (discovery.signal.aborted) return;
|
|
9725
|
+
if (declares(declaration, PRESENCE_HINTS_CAPABILITY)) {
|
|
9726
|
+
presencePublisher ??= new PresencePublisher({
|
|
9727
|
+
serverUrl: config.serverUrl,
|
|
9728
|
+
auth,
|
|
9729
|
+
...presenceCadence,
|
|
9730
|
+
onDegraded: (reason) => console.warn(`[byok/client] ${reason}`)
|
|
9731
|
+
});
|
|
9732
|
+
presencePublisher.start();
|
|
9733
|
+
} else {
|
|
9734
|
+
presencePublisher?.stop();
|
|
9735
|
+
}
|
|
9736
|
+
} catch (err) {
|
|
9737
|
+
if (discovery.signal.aborted) return;
|
|
9738
|
+
console.warn(
|
|
9739
|
+
`[byok/client] capability discovery failed; presence publishing stays off until the next reconnect: ${err instanceof Error ? err.message : String(err)}`
|
|
9740
|
+
);
|
|
9741
|
+
} finally {
|
|
9742
|
+
presenceDiscoveryInFlight = false;
|
|
9743
|
+
}
|
|
9744
|
+
})();
|
|
9745
|
+
}
|
|
8885
9746
|
async function runShutdownSequence(reason, opts = {}) {
|
|
9747
|
+
shuttingDown = true;
|
|
8886
9748
|
const errors = [];
|
|
8887
9749
|
let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
|
|
8888
9750
|
if (!hostedStorageInitializationBarrierComplete) {
|
|
@@ -8911,6 +9773,11 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8911
9773
|
errors.push(new Error("a prior active task teardown remains unsettled; ownership lease retained"));
|
|
8912
9774
|
}
|
|
8913
9775
|
}
|
|
9776
|
+
presenceDiscovery?.abort();
|
|
9777
|
+
presenceDiscovery = void 0;
|
|
9778
|
+
presenceDiscoveryInFlight = false;
|
|
9779
|
+
presencePublisher?.stop();
|
|
9780
|
+
presencePublisher = void 0;
|
|
8914
9781
|
const stoppingOwnedPressureEngine = ownedPressureEngine;
|
|
8915
9782
|
const stoppingOwnedJournal = ownedJournal;
|
|
8916
9783
|
const maintenanceStopped = stoppingOwnedPressureEngine?.stop() ?? Promise.resolve();
|
|
@@ -8971,6 +9838,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8971
9838
|
}
|
|
8972
9839
|
}
|
|
8973
9840
|
async function stop(opts = {}) {
|
|
9841
|
+
shuttingDown = true;
|
|
8974
9842
|
await requestShutdown(opts.reason ?? "operator", { drainTimeoutMs: opts.drainTimeoutMs });
|
|
8975
9843
|
}
|
|
8976
9844
|
function requestShutdown(reason, opts = {}) {
|
|
@@ -8983,6 +9851,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
8983
9851
|
return current;
|
|
8984
9852
|
}
|
|
8985
9853
|
async function unpair() {
|
|
9854
|
+
shuttingDown = true;
|
|
8986
9855
|
await runLifecycleMutation(unpairUnderLease);
|
|
8987
9856
|
}
|
|
8988
9857
|
async function unpairUnderLease() {
|
|
@@ -9061,6 +9930,7 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9061
9930
|
}
|
|
9062
9931
|
async function performControlShutdown(reason) {
|
|
9063
9932
|
const effectiveReason = reason ?? "operator";
|
|
9933
|
+
shuttingDown = true;
|
|
9064
9934
|
observer.noteShutdownRequested(effectiveReason);
|
|
9065
9935
|
try {
|
|
9066
9936
|
await requestShutdown(`control socket shutdown (${effectiveReason})`);
|
|
@@ -9098,7 +9968,107 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9098
9968
|
if (!runner) throw new ControlError("not_found", "daemon is not started");
|
|
9099
9969
|
return runner.requestApproval(parsed.taskId, parsed.summary);
|
|
9100
9970
|
},
|
|
9971
|
+
/**
|
|
9972
|
+
* Plan `device-assertion-broker`: mint one short-lived, audience-scoped
|
|
9973
|
+
* device assertion for a sibling local process.
|
|
9974
|
+
*
|
|
9975
|
+
* SIX fail-closed gates, in this exact order, none of which signs
|
|
9976
|
+
* anything on the way out. The order is part of the contract, not an
|
|
9977
|
+
* implementation detail:
|
|
9978
|
+
*
|
|
9979
|
+
* 1. `assertion_disabled` — before anything else, because a daemon that
|
|
9980
|
+
* was never configured for this must not reveal, by answering
|
|
9981
|
+
* differently for different inputs, that it even validates params.
|
|
9982
|
+
* 2. `bad_request` — shape/length, checked before the allowlist so a
|
|
9983
|
+
* malformed request cannot be used to probe membership.
|
|
9984
|
+
* 3. `audience_denied` — EXACT `Set.has`, never a prefix/suffix rule
|
|
9985
|
+
* (`salesko-api.evil.com` and `salesko-ap` both fail against an entry
|
|
9986
|
+
* of `salesko-api`). The message deliberately does not echo the
|
|
9987
|
+
* allowlist: a refusal must not be an enumeration oracle.
|
|
9988
|
+
* 4. `shutting_down` — see `performControlShutdown`'s own comment for
|
|
9989
|
+
* the minting window this closes.
|
|
9990
|
+
* 5. `revoked` — the server-side revocation this daemon already knows
|
|
9991
|
+
* about.
|
|
9992
|
+
* 6. `not_paired` — the on-disk record, re-read on EVERY call (never
|
|
9993
|
+
* cached), so clearing `device.json` removes local signing authority
|
|
9994
|
+
* immediately.
|
|
9995
|
+
*
|
|
9996
|
+
* Only after all six does the private key get imported, used once, and
|
|
9997
|
+
* dropped (`device-assertion-signer.ts`).
|
|
9998
|
+
*
|
|
9999
|
+
* Honest limit, and it must stay in the docs as well as here: gates 4-6
|
|
10000
|
+
* are only HALF of revocation. They make this daemon stop minting
|
|
10001
|
+
* promptly, but an assertion already in a caller's hands is not recalled
|
|
10002
|
+
* by any of them. The other half is the host's own recheck at exchange
|
|
10003
|
+
* time, which is why core's `verifyDeviceAssertion` makes the device
|
|
10004
|
+
* row's `revoked` state a REQUIRED parameter. Nothing here entitles
|
|
10005
|
+
* anyone to claim this daemon delivers synchronous invalidation on its
|
|
10006
|
+
* own.
|
|
10007
|
+
*/
|
|
10008
|
+
"assertion.issue": async (params) => {
|
|
10009
|
+
if (deviceAssertionAudiences === void 0) {
|
|
10010
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "assertion_disabled" });
|
|
10011
|
+
throw new ControlError(
|
|
10012
|
+
"assertion_disabled",
|
|
10013
|
+
"this daemon is not configured to issue device assertions (DaemonConfig.deviceAssertion.audiences is absent or empty)"
|
|
10014
|
+
);
|
|
10015
|
+
}
|
|
10016
|
+
const parsed = parseAssertionIssueParams(params);
|
|
10017
|
+
if (!parsed) {
|
|
10018
|
+
const rawAudience = typeof params === "object" && params !== null && typeof params.audience === "string" ? params.audience : void 0;
|
|
10019
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "bad_request", audience: rawAudience });
|
|
10020
|
+
throw new ControlError(
|
|
10021
|
+
"bad_request",
|
|
10022
|
+
`assertion.issue requires exactly {audience} where audience is a non-empty string of at most ${DEVICE_ASSERTION_AUDIENCE_MAX_BYTES} UTF-8 bytes`
|
|
10023
|
+
);
|
|
10024
|
+
}
|
|
10025
|
+
if (!deviceAssertionAudiences.has(parsed.audience)) {
|
|
10026
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "audience_denied", audience: parsed.audience });
|
|
10027
|
+
throw new ControlError("audience_denied", "the requested audience is not allowed by this daemon");
|
|
10028
|
+
}
|
|
10029
|
+
if (shuttingDown) {
|
|
10030
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10031
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10032
|
+
}
|
|
10033
|
+
if (auth.isRevoked()) {
|
|
10034
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10035
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10036
|
+
}
|
|
10037
|
+
const record = await store.load();
|
|
10038
|
+
if (record === void 0) {
|
|
10039
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "not_paired", audience: parsed.audience });
|
|
10040
|
+
throw new ControlError("not_paired", "this device is not paired; nothing can be asserted about it");
|
|
10041
|
+
}
|
|
10042
|
+
if (shuttingDown) {
|
|
10043
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "shutting_down", audience: parsed.audience });
|
|
10044
|
+
throw new ControlError("shutting_down", "this daemon is shutting down and will not issue new assertions");
|
|
10045
|
+
}
|
|
10046
|
+
if (auth.isRevoked()) {
|
|
10047
|
+
observer.noteDeviceAssertion({ result: "denied", reason: "revoked", audience: parsed.audience });
|
|
10048
|
+
throw new ControlError("revoked", "this device has been revoked by the server; re-pair required");
|
|
10049
|
+
}
|
|
10050
|
+
const minted = mintDeviceAssertion({
|
|
10051
|
+
record,
|
|
10052
|
+
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
10053
|
+
// (ws:->http:, wss:->https:, path stripped), so an operator who
|
|
10054
|
+
// configured the websocket spelling and one who configured the HTTP
|
|
10055
|
+
// spelling of the same deployment produce the same issuer.
|
|
10056
|
+
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
10057
|
+
productId: config.productId,
|
|
10058
|
+
audience: parsed.audience,
|
|
10059
|
+
ttlMs: deviceAssertionTtlMs,
|
|
10060
|
+
now: /* @__PURE__ */ new Date()
|
|
10061
|
+
});
|
|
10062
|
+
observer.noteDeviceAssertion({
|
|
10063
|
+
result: "issued",
|
|
10064
|
+
audience: minted.claims.audience,
|
|
10065
|
+
jti: minted.claims.jti,
|
|
10066
|
+
expiresAt: minted.expiresAt
|
|
10067
|
+
});
|
|
10068
|
+
return { assertion: minted.envelope, expiresAt: minted.expiresAt };
|
|
10069
|
+
},
|
|
9101
10070
|
shutdown: (params) => {
|
|
10071
|
+
shuttingDown = true;
|
|
9102
10072
|
const { reason } = parseShutdownParams(params);
|
|
9103
10073
|
setImmediate(() => {
|
|
9104
10074
|
void performControlShutdown(reason).catch((err) => {
|
|
@@ -9143,7 +10113,275 @@ function createDaemonWithAdapters(config, adapters, overrides = {}) {
|
|
|
9143
10113
|
return { pair, start, stop, status, subscribe, tasks, unpair, approve, reject };
|
|
9144
10114
|
}
|
|
9145
10115
|
function createDaemon(config) {
|
|
9146
|
-
return createDaemonWithAdapters(config, buildDefaultAdapters(config
|
|
10116
|
+
return createDaemonWithAdapters(config, buildDefaultAdapters(config));
|
|
10117
|
+
}
|
|
10118
|
+
var MAX_CONTROL_TOKEN_BYTES = 256;
|
|
10119
|
+
function errorMessage4(err) {
|
|
10120
|
+
return err instanceof Error ? err.message : String(err);
|
|
10121
|
+
}
|
|
10122
|
+
function sameFileState3(left, right) {
|
|
10123
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
10124
|
+
}
|
|
10125
|
+
async function readControlToken(tokenPath) {
|
|
10126
|
+
let namedBefore;
|
|
10127
|
+
try {
|
|
10128
|
+
namedBefore = await promises.lstat(tokenPath, { bigint: true });
|
|
10129
|
+
} catch (err) {
|
|
10130
|
+
if (err.code === "ENOENT") return void 0;
|
|
10131
|
+
throw err;
|
|
10132
|
+
}
|
|
10133
|
+
if (!namedBefore.isFile() || namedBefore.isSymbolicLink()) {
|
|
10134
|
+
throw new Error("control token is not a real regular file");
|
|
10135
|
+
}
|
|
10136
|
+
const handle = await promises.open(
|
|
10137
|
+
tokenPath,
|
|
10138
|
+
constants.O_RDONLY | (constants.O_NONBLOCK ?? 0) | (constants.O_NOFOLLOW ?? 0)
|
|
10139
|
+
);
|
|
10140
|
+
try {
|
|
10141
|
+
const opened = await handle.stat({ bigint: true });
|
|
10142
|
+
const namedAfterOpen = await promises.lstat(tokenPath, { bigint: true });
|
|
10143
|
+
if (!opened.isFile() || !namedAfterOpen.isFile() || namedAfterOpen.isSymbolicLink() || !sameFileState3(namedBefore, opened) || !sameFileState3(opened, namedAfterOpen)) {
|
|
10144
|
+
throw new Error("control token pathname changed before safe open");
|
|
10145
|
+
}
|
|
10146
|
+
if (opened.size < 0 || opened.size > BigInt(MAX_CONTROL_TOKEN_BYTES)) {
|
|
10147
|
+
throw new Error("control token exceeds the bounded read limit");
|
|
10148
|
+
}
|
|
10149
|
+
const size = Number(opened.size);
|
|
10150
|
+
const bytes = Buffer.alloc(size);
|
|
10151
|
+
const { bytesRead } = await handle.read(bytes, 0, size, 0);
|
|
10152
|
+
const afterRead = await handle.stat({ bigint: true });
|
|
10153
|
+
const namedAfterRead = await promises.lstat(tokenPath, { bigint: true });
|
|
10154
|
+
if (bytesRead !== size || namedAfterRead.isSymbolicLink() || !sameFileState3(opened, afterRead) || !sameFileState3(afterRead, namedAfterRead)) {
|
|
10155
|
+
throw new Error("control token changed during bounded read");
|
|
10156
|
+
}
|
|
10157
|
+
return bytes.toString("utf8").trim();
|
|
10158
|
+
} finally {
|
|
10159
|
+
await handle.close();
|
|
10160
|
+
}
|
|
10161
|
+
}
|
|
10162
|
+
async function connectControlClient(opts) {
|
|
10163
|
+
const tokenPath = controlTokenPath(opts.storeDir);
|
|
10164
|
+
let token;
|
|
10165
|
+
try {
|
|
10166
|
+
const read = await readControlToken(tokenPath);
|
|
10167
|
+
if (read === void 0) {
|
|
10168
|
+
return { ok: false, reason: "daemon is not running (no control.token found)" };
|
|
10169
|
+
}
|
|
10170
|
+
token = read;
|
|
10171
|
+
} catch (err) {
|
|
10172
|
+
return { ok: false, reason: `could not read the control token: ${errorMessage4(err)}` };
|
|
10173
|
+
}
|
|
10174
|
+
if (!token) {
|
|
10175
|
+
return { ok: false, reason: "control token file is empty" };
|
|
10176
|
+
}
|
|
10177
|
+
const endpoint = controlEndpointPath(opts.productId, opts.storeDir);
|
|
10178
|
+
try {
|
|
10179
|
+
const client = await connectAndHandshake(endpoint, token, opts);
|
|
10180
|
+
return { ok: true, client };
|
|
10181
|
+
} catch (err) {
|
|
10182
|
+
return { ok: false, reason: `daemon control socket not reachable: ${errorMessage4(err)}` };
|
|
10183
|
+
}
|
|
10184
|
+
}
|
|
10185
|
+
function connectAndHandshake(endpoint, token, opts) {
|
|
10186
|
+
return new Promise((resolve, reject) => {
|
|
10187
|
+
const socket = net.createConnection(endpoint);
|
|
10188
|
+
const reader = new NdjsonLineReader();
|
|
10189
|
+
let phase = "server-hello";
|
|
10190
|
+
let settled = false;
|
|
10191
|
+
const clientNonce = randomNonceHex();
|
|
10192
|
+
const timer = setTimeout(() => {
|
|
10193
|
+
fail(new Error("handshake timed out"));
|
|
10194
|
+
}, opts.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS);
|
|
10195
|
+
timer.unref?.();
|
|
10196
|
+
function fail(err) {
|
|
10197
|
+
if (settled) return;
|
|
10198
|
+
settled = true;
|
|
10199
|
+
clearTimeout(timer);
|
|
10200
|
+
socket.removeAllListeners();
|
|
10201
|
+
socket.destroy();
|
|
10202
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
10203
|
+
}
|
|
10204
|
+
function succeed() {
|
|
10205
|
+
settled = true;
|
|
10206
|
+
clearTimeout(timer);
|
|
10207
|
+
socket.removeListener("error", onError);
|
|
10208
|
+
socket.removeListener("data", onData);
|
|
10209
|
+
resolve(createControlClient(socket, reader, opts));
|
|
10210
|
+
}
|
|
10211
|
+
function onData(chunk) {
|
|
10212
|
+
let lines;
|
|
10213
|
+
try {
|
|
10214
|
+
lines = reader.push(chunk);
|
|
10215
|
+
} catch (err) {
|
|
10216
|
+
fail(err);
|
|
10217
|
+
return;
|
|
10218
|
+
}
|
|
10219
|
+
for (const line of lines) {
|
|
10220
|
+
let parsed;
|
|
10221
|
+
try {
|
|
10222
|
+
parsed = JSON.parse(line);
|
|
10223
|
+
} catch {
|
|
10224
|
+
fail(new Error("malformed handshake frame"));
|
|
10225
|
+
return;
|
|
10226
|
+
}
|
|
10227
|
+
if (phase === "server-hello") {
|
|
10228
|
+
const hello = parseServerHello(parsed);
|
|
10229
|
+
if (!hello) {
|
|
10230
|
+
fail(new Error("malformed or unexpected server hello"));
|
|
10231
|
+
return;
|
|
10232
|
+
}
|
|
10233
|
+
if (!timingSafeEqualHex(hello.proof, computeServerProof(token, clientNonce))) {
|
|
10234
|
+
fail(new Error("server failed to prove it holds the control token"));
|
|
10235
|
+
return;
|
|
10236
|
+
}
|
|
10237
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, auth: computeClientAuth(token, hello.nonce) }));
|
|
10238
|
+
phase = "ready";
|
|
10239
|
+
continue;
|
|
10240
|
+
}
|
|
10241
|
+
if (!parseServerReady(parsed)) {
|
|
10242
|
+
fail(new Error("server did not confirm readiness"));
|
|
10243
|
+
return;
|
|
10244
|
+
}
|
|
10245
|
+
succeed();
|
|
10246
|
+
return;
|
|
10247
|
+
}
|
|
10248
|
+
}
|
|
10249
|
+
function onError(err) {
|
|
10250
|
+
fail(err);
|
|
10251
|
+
}
|
|
10252
|
+
socket.once("error", onError);
|
|
10253
|
+
socket.once("connect", () => {
|
|
10254
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, hello: "client", nonce: clientNonce }));
|
|
10255
|
+
socket.on("data", onData);
|
|
10256
|
+
});
|
|
10257
|
+
});
|
|
10258
|
+
}
|
|
10259
|
+
function withTimeout(promise, ms, message) {
|
|
10260
|
+
return new Promise((resolve, reject) => {
|
|
10261
|
+
const timer = setTimeout(() => reject(new Error(message)), ms);
|
|
10262
|
+
timer.unref?.();
|
|
10263
|
+
promise.then(
|
|
10264
|
+
(value) => {
|
|
10265
|
+
clearTimeout(timer);
|
|
10266
|
+
resolve(value);
|
|
10267
|
+
},
|
|
10268
|
+
(err) => {
|
|
10269
|
+
clearTimeout(timer);
|
|
10270
|
+
reject(err);
|
|
10271
|
+
}
|
|
10272
|
+
);
|
|
10273
|
+
});
|
|
10274
|
+
}
|
|
10275
|
+
function createControlClient(socket, reader, opts) {
|
|
10276
|
+
const pending = /* @__PURE__ */ new Map();
|
|
10277
|
+
let idSeq = 0;
|
|
10278
|
+
let closed = false;
|
|
10279
|
+
function handleFrame(parsed) {
|
|
10280
|
+
if (!isRecord2(parsed) || typeof parsed.id !== "string") return;
|
|
10281
|
+
const entry = pending.get(parsed.id);
|
|
10282
|
+
if (!entry) return;
|
|
10283
|
+
if ("event" in parsed) {
|
|
10284
|
+
entry.onEvent?.(parsed.event);
|
|
10285
|
+
return;
|
|
10286
|
+
}
|
|
10287
|
+
if (parsed.ok === true) {
|
|
10288
|
+
pending.delete(parsed.id);
|
|
10289
|
+
entry.resolve(parsed.done === true ? void 0 : parsed.result);
|
|
10290
|
+
return;
|
|
10291
|
+
}
|
|
10292
|
+
pending.delete(parsed.id);
|
|
10293
|
+
const shape = parsed.error;
|
|
10294
|
+
entry.reject(
|
|
10295
|
+
new ControlError(
|
|
10296
|
+
typeof shape?.code === "string" ? shape.code : "internal_error",
|
|
10297
|
+
typeof shape?.message === "string" ? shape.message : "unknown control error"
|
|
10298
|
+
)
|
|
10299
|
+
);
|
|
10300
|
+
}
|
|
10301
|
+
socket.on("data", (chunk) => {
|
|
10302
|
+
let lines;
|
|
10303
|
+
try {
|
|
10304
|
+
lines = reader.push(chunk);
|
|
10305
|
+
} catch {
|
|
10306
|
+
socket.destroy();
|
|
10307
|
+
return;
|
|
10308
|
+
}
|
|
10309
|
+
for (const line of lines) {
|
|
10310
|
+
let parsed;
|
|
10311
|
+
try {
|
|
10312
|
+
parsed = JSON.parse(line);
|
|
10313
|
+
} catch {
|
|
10314
|
+
continue;
|
|
10315
|
+
}
|
|
10316
|
+
handleFrame(parsed);
|
|
10317
|
+
}
|
|
10318
|
+
});
|
|
10319
|
+
socket.on("close", () => {
|
|
10320
|
+
closed = true;
|
|
10321
|
+
for (const entry of pending.values()) entry.reject(new Error("control connection closed"));
|
|
10322
|
+
pending.clear();
|
|
10323
|
+
});
|
|
10324
|
+
socket.on("error", () => {
|
|
10325
|
+
});
|
|
10326
|
+
function send(method, params, onEvent) {
|
|
10327
|
+
const id = `c${++idSeq}`;
|
|
10328
|
+
const promise = new Promise((resolve, reject) => {
|
|
10329
|
+
pending.set(id, { resolve, reject, onEvent });
|
|
10330
|
+
});
|
|
10331
|
+
socket.write(encodeFrame({ v: CONTROL_PROTOCOL_VERSION, id, method, params }));
|
|
10332
|
+
return { id, promise };
|
|
10333
|
+
}
|
|
10334
|
+
return {
|
|
10335
|
+
async request(method, params) {
|
|
10336
|
+
if (closed) throw new Error("control connection is closed");
|
|
10337
|
+
const { promise } = send(method, params);
|
|
10338
|
+
const result = await withTimeout(promise, opts.requestTimeoutMs ?? 1e4, `control request "${method}" timed out`);
|
|
10339
|
+
return result;
|
|
10340
|
+
},
|
|
10341
|
+
subscribe(method, params, onEvent) {
|
|
10342
|
+
const { id, promise } = send(method, params, onEvent);
|
|
10343
|
+
promise.catch(() => {
|
|
10344
|
+
});
|
|
10345
|
+
return {
|
|
10346
|
+
close: () => {
|
|
10347
|
+
pending.delete(id);
|
|
10348
|
+
socket.destroy();
|
|
10349
|
+
}
|
|
10350
|
+
};
|
|
10351
|
+
},
|
|
10352
|
+
close() {
|
|
10353
|
+
socket.destroy();
|
|
10354
|
+
}
|
|
10355
|
+
};
|
|
10356
|
+
}
|
|
10357
|
+
|
|
10358
|
+
// src/daemon/assertion-client.ts
|
|
10359
|
+
function errorMessage5(err) {
|
|
10360
|
+
return err instanceof Error ? err.message : String(err);
|
|
10361
|
+
}
|
|
10362
|
+
async function requestDeviceAssertion(options) {
|
|
10363
|
+
const storeDir = DeviceStore.resolveDir(options.productId, options.storeDir);
|
|
10364
|
+
const connected = await connectControlClient({
|
|
10365
|
+
storeDir,
|
|
10366
|
+
productId: options.productId,
|
|
10367
|
+
...options.timeoutMs === void 0 ? {} : { requestTimeoutMs: options.timeoutMs }
|
|
10368
|
+
});
|
|
10369
|
+
if (!connected.ok) return { ok: false, code: "unavailable", reason: connected.reason };
|
|
10370
|
+
try {
|
|
10371
|
+
const result = await connected.client.request("assertion.issue", {
|
|
10372
|
+
audience: options.audience
|
|
10373
|
+
});
|
|
10374
|
+
if (result === null || typeof result !== "object" || typeof result.expiresAt !== "string") {
|
|
10375
|
+
return { ok: false, code: "bad_response", reason: "daemon returned a malformed assertion.issue result" };
|
|
10376
|
+
}
|
|
10377
|
+
const assertion = parseDeviceAssertionEnvelope(result.assertion);
|
|
10378
|
+
return { ok: true, assertion, expiresAt: result.expiresAt };
|
|
10379
|
+
} catch (err) {
|
|
10380
|
+
if (err instanceof ControlError) return { ok: false, code: err.code, reason: err.message };
|
|
10381
|
+
return { ok: false, code: "bad_response", reason: errorMessage5(err) };
|
|
10382
|
+
} finally {
|
|
10383
|
+
connected.client.close();
|
|
10384
|
+
}
|
|
9147
10385
|
}
|
|
9148
10386
|
var StoredDeviceProofSigner = class {
|
|
9149
10387
|
constructor(options) {
|
|
@@ -9202,9 +10440,359 @@ function requireNonEmpty(name, value) {
|
|
|
9202
10440
|
function sha256(bytes) {
|
|
9203
10441
|
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
9204
10442
|
}
|
|
10443
|
+
var SKILL_PACKS_CAPABILITY = "skills.pack";
|
|
10444
|
+
var SKILL_PACKS_DIRNAME = "skill-packs";
|
|
10445
|
+
var SKILL_PACK_LOCK_FILENAME = "lock.json";
|
|
10446
|
+
var SKILL_PACK_AUDIT_FILENAME = "audit.jsonl";
|
|
10447
|
+
var SKILL_PACK_LOCK_SCHEMA = "byok-skill-pack-lock-v1";
|
|
10448
|
+
var DIR_MODE = 448;
|
|
10449
|
+
var FILE_MODE = 384;
|
|
10450
|
+
var SKILL_PACK_RESPONSE_MAX_BYTES = SKILL_PACK_MAX_BYTES * 2;
|
|
10451
|
+
var SKILL_PACK_INSTALL_ERROR_CODES = [
|
|
10452
|
+
"capability_unavailable",
|
|
10453
|
+
"transport_failed",
|
|
10454
|
+
"response_invalid",
|
|
10455
|
+
"response_too_large",
|
|
10456
|
+
"manifest_invalid",
|
|
10457
|
+
"content_rejected",
|
|
10458
|
+
"store_unsafe"
|
|
10459
|
+
];
|
|
10460
|
+
var SkillPackInstallError = class extends Error {
|
|
10461
|
+
code;
|
|
10462
|
+
packName;
|
|
10463
|
+
constructor(code, message, options = {}) {
|
|
10464
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
10465
|
+
this.name = "SkillPackInstallError";
|
|
10466
|
+
this.code = code;
|
|
10467
|
+
this.packName = options.packName;
|
|
10468
|
+
}
|
|
10469
|
+
};
|
|
10470
|
+
function hashBytes(bytes) {
|
|
10471
|
+
return contentHash(`sha256:${createHash("sha256").update(bytes).digest("hex")}`);
|
|
10472
|
+
}
|
|
10473
|
+
function originOf(serverUrl) {
|
|
10474
|
+
return new URL(toHttpBase(serverUrl)).origin;
|
|
10475
|
+
}
|
|
10476
|
+
async function readBoundedJson(response, what) {
|
|
10477
|
+
const declaredLength = response.headers.get("content-length");
|
|
10478
|
+
if (declaredLength !== null) {
|
|
10479
|
+
const parsed = Number(declaredLength);
|
|
10480
|
+
if (Number.isSafeInteger(parsed) && parsed > SKILL_PACK_RESPONSE_MAX_BYTES) {
|
|
10481
|
+
throw new SkillPackInstallError(
|
|
10482
|
+
"response_too_large",
|
|
10483
|
+
`${what} declared ${parsed} bytes, over the ${SKILL_PACK_RESPONSE_MAX_BYTES} byte response limit.`
|
|
10484
|
+
);
|
|
10485
|
+
}
|
|
10486
|
+
}
|
|
10487
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
10488
|
+
if (bytes.byteLength > SKILL_PACK_RESPONSE_MAX_BYTES) {
|
|
10489
|
+
throw new SkillPackInstallError(
|
|
10490
|
+
"response_too_large",
|
|
10491
|
+
`${what} delivered ${bytes.byteLength} bytes, over the ${SKILL_PACK_RESPONSE_MAX_BYTES} byte response limit.`
|
|
10492
|
+
);
|
|
10493
|
+
}
|
|
10494
|
+
try {
|
|
10495
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
10496
|
+
} catch (cause) {
|
|
10497
|
+
throw new SkillPackInstallError("response_invalid", `${what} is not valid JSON.`, { cause });
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
async function getJson(url, auth, signal, what) {
|
|
10501
|
+
let response;
|
|
10502
|
+
try {
|
|
10503
|
+
response = await authedFetch(url, { method: "GET", ...signal === void 0 ? {} : { signal } }, auth);
|
|
10504
|
+
} catch (cause) {
|
|
10505
|
+
throw new SkillPackInstallError("transport_failed", `${what} could not be fetched from ${url.toString()}.`, {
|
|
10506
|
+
cause
|
|
10507
|
+
});
|
|
10508
|
+
}
|
|
10509
|
+
if (!response.ok) {
|
|
10510
|
+
throw new SkillPackInstallError("transport_failed", `${what} answered HTTP ${response.status}.`);
|
|
10511
|
+
}
|
|
10512
|
+
return readBoundedJson(response, what);
|
|
10513
|
+
}
|
|
10514
|
+
function skillPacksRoot(dataDir) {
|
|
10515
|
+
return path17.join(dataDir, SKILL_PACKS_DIRNAME);
|
|
10516
|
+
}
|
|
10517
|
+
function resolveInside(baseDir, relative) {
|
|
10518
|
+
if (!isSkillPackPathSafe(relative)) {
|
|
10519
|
+
throw new SkillPackInstallError("store_unsafe", `${JSON.stringify(relative)} is not a safe pack-relative path.`);
|
|
10520
|
+
}
|
|
10521
|
+
const resolved = path17.resolve(baseDir, relative);
|
|
10522
|
+
const prefix = path17.resolve(baseDir) + path17.sep;
|
|
10523
|
+
if (!resolved.startsWith(prefix)) {
|
|
10524
|
+
throw new SkillPackInstallError(
|
|
10525
|
+
"store_unsafe",
|
|
10526
|
+
`${JSON.stringify(relative)} resolves outside ${baseDir}.`
|
|
10527
|
+
);
|
|
10528
|
+
}
|
|
10529
|
+
return resolved;
|
|
10530
|
+
}
|
|
10531
|
+
async function assertNotSymlink(target) {
|
|
10532
|
+
let stats;
|
|
10533
|
+
try {
|
|
10534
|
+
stats = await promises.lstat(target);
|
|
10535
|
+
} catch (err) {
|
|
10536
|
+
if (err.code === "ENOENT") return;
|
|
10537
|
+
throw err;
|
|
10538
|
+
}
|
|
10539
|
+
if (stats.isSymbolicLink()) {
|
|
10540
|
+
throw new SkillPackInstallError("store_unsafe", `${target} is a symbolic link; refusing to follow it.`);
|
|
10541
|
+
}
|
|
10542
|
+
}
|
|
10543
|
+
async function appendAuditLine(dataDir, record) {
|
|
10544
|
+
const root = skillPacksRoot(dataDir);
|
|
10545
|
+
await promises.mkdir(root, { recursive: true, mode: DIR_MODE });
|
|
10546
|
+
await promises.chmod(root, DIR_MODE).catch(() => {
|
|
10547
|
+
});
|
|
10548
|
+
const filePath = path17.join(root, SKILL_PACK_AUDIT_FILENAME);
|
|
10549
|
+
const handle = await promises.open(filePath, "a", FILE_MODE);
|
|
10550
|
+
try {
|
|
10551
|
+
await handle.chmod(FILE_MODE);
|
|
10552
|
+
await handle.appendFile(`${JSON.stringify(record)}
|
|
10553
|
+
`, "utf8");
|
|
10554
|
+
} finally {
|
|
10555
|
+
await handle.close();
|
|
10556
|
+
}
|
|
10557
|
+
}
|
|
10558
|
+
async function installSkillPacks(options) {
|
|
10559
|
+
if (!hasCapability(options.declaration, SKILL_PACKS_CAPABILITY)) {
|
|
10560
|
+
throw new SkillPackInstallError(
|
|
10561
|
+
"capability_unavailable",
|
|
10562
|
+
`This deployment does not declare ${SKILL_PACKS_CAPABILITY}; refusing to fetch skill packs.`
|
|
10563
|
+
);
|
|
10564
|
+
}
|
|
10565
|
+
const base = toHttpBase(options.serverUrl);
|
|
10566
|
+
const source = originOf(options.serverUrl);
|
|
10567
|
+
const body = await getJson(
|
|
10568
|
+
new URL(BYOK_SKILL_PACKS_PATH, base),
|
|
10569
|
+
options.auth,
|
|
10570
|
+
options.signal,
|
|
10571
|
+
"the skill pack manifest list"
|
|
10572
|
+
);
|
|
10573
|
+
const rawPacks = body?.packs;
|
|
10574
|
+
if (!Array.isArray(rawPacks)) {
|
|
10575
|
+
throw new SkillPackInstallError("response_invalid", "the skill pack manifest list has no `packs` array.");
|
|
10576
|
+
}
|
|
10577
|
+
const installed = [];
|
|
10578
|
+
const unchanged = [];
|
|
10579
|
+
for (const raw of rawPacks) {
|
|
10580
|
+
const manifest = parseManifest(raw);
|
|
10581
|
+
const existing = await readLock(options.dataDir, manifest.name);
|
|
10582
|
+
if (existing?.lock.content_hash === manifest.contentHash) {
|
|
10583
|
+
unchanged.push(manifest.name);
|
|
10584
|
+
continue;
|
|
10585
|
+
}
|
|
10586
|
+
installed.push(await installOne(options, manifest, source, base));
|
|
10587
|
+
}
|
|
10588
|
+
return { installed, unchanged };
|
|
10589
|
+
}
|
|
10590
|
+
function parseManifest(raw) {
|
|
10591
|
+
let manifest;
|
|
10592
|
+
try {
|
|
10593
|
+
manifest = parseSkillPackManifest(raw);
|
|
10594
|
+
} catch (cause) {
|
|
10595
|
+
throw new SkillPackInstallError("manifest_invalid", "a published skill pack manifest is not valid.", { cause });
|
|
10596
|
+
}
|
|
10597
|
+
const structural = checkSkillPackManifest(manifest);
|
|
10598
|
+
if (!structural.ok) {
|
|
10599
|
+
throw new SkillPackInstallError(
|
|
10600
|
+
"manifest_invalid",
|
|
10601
|
+
`skill pack ${JSON.stringify(manifest.name)} was refused: ${structural.reason} \u2014 ${structural.detail}`,
|
|
10602
|
+
{ packName: manifest.name }
|
|
10603
|
+
);
|
|
10604
|
+
}
|
|
10605
|
+
return manifest;
|
|
10606
|
+
}
|
|
10607
|
+
async function installOne(options, manifest, source, base) {
|
|
10608
|
+
const refuse = async (code, message) => {
|
|
10609
|
+
await appendAuditLine(options.dataDir, {
|
|
10610
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10611
|
+
event: "skill-pack-rejected",
|
|
10612
|
+
name: manifest.name,
|
|
10613
|
+
contentHash: manifest.contentHash,
|
|
10614
|
+
source,
|
|
10615
|
+
code,
|
|
10616
|
+
reason: message
|
|
10617
|
+
});
|
|
10618
|
+
throw new SkillPackInstallError(code, message, { packName: manifest.name });
|
|
10619
|
+
};
|
|
10620
|
+
const expectedPackHash = hashBytes(new TextEncoder().encode(skillPackContentHashInput(manifest)));
|
|
10621
|
+
if (expectedPackHash !== manifest.contentHash) {
|
|
10622
|
+
await refuse(
|
|
10623
|
+
"manifest_invalid",
|
|
10624
|
+
`skill pack ${JSON.stringify(manifest.name)} declares ${manifest.contentHash} but its file rows address ${expectedPackHash}.`
|
|
10625
|
+
);
|
|
10626
|
+
}
|
|
10627
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
10628
|
+
let totalBytes = 0;
|
|
10629
|
+
for (const declared of manifest.files) {
|
|
10630
|
+
const url = new URL(byokSkillPackFilePath(manifest.name, declared.path), base);
|
|
10631
|
+
const raw = await getJson(url, options.auth, options.signal, `skill pack file ${JSON.stringify(declared.path)}`);
|
|
10632
|
+
const content = raw?.content;
|
|
10633
|
+
if (typeof content !== "string") {
|
|
10634
|
+
await refuse("response_invalid", `skill pack file ${JSON.stringify(declared.path)} carried no string content.`);
|
|
10635
|
+
}
|
|
10636
|
+
const bytes = new TextEncoder().encode(content);
|
|
10637
|
+
const check = checkSkillPackFileContent(declared, {
|
|
10638
|
+
byteSize: bytes.byteLength,
|
|
10639
|
+
contentHash: hashBytes(bytes)
|
|
10640
|
+
});
|
|
10641
|
+
if (!check.ok) {
|
|
10642
|
+
await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: ${check.reason} \u2014 ${check.detail}`);
|
|
10643
|
+
}
|
|
10644
|
+
totalBytes += bytes.byteLength;
|
|
10645
|
+
bodies.set(declared.path, content);
|
|
10646
|
+
}
|
|
10647
|
+
const entryText = bodies.get(SKILL_PACK_ENTRY_PATH);
|
|
10648
|
+
if (entryText === void 0) {
|
|
10649
|
+
await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: entry-missing \u2014 ${SKILL_PACK_ENTRY_PATH} was not delivered.`);
|
|
10650
|
+
}
|
|
10651
|
+
let entryCheck;
|
|
10652
|
+
try {
|
|
10653
|
+
entryCheck = checkSkillPackEntry(manifest, entryText);
|
|
10654
|
+
} catch (cause) {
|
|
10655
|
+
await refuse(
|
|
10656
|
+
"content_rejected",
|
|
10657
|
+
`skill pack ${JSON.stringify(manifest.name)}: ${SKILL_PACK_ENTRY_PATH} frontmatter was refused (${cause instanceof Error ? cause.message : String(cause)}).`
|
|
10658
|
+
);
|
|
10659
|
+
}
|
|
10660
|
+
if (!entryCheck.ok) {
|
|
10661
|
+
await refuse("content_rejected", `skill pack ${JSON.stringify(manifest.name)}: ${entryCheck.reason} \u2014 ${entryCheck.detail}`);
|
|
10662
|
+
}
|
|
10663
|
+
const packRoot = path17.join(skillPacksRoot(options.dataDir), manifest.name);
|
|
10664
|
+
const revisionDir = path17.join(packRoot, manifest.contentHash.slice("sha256:".length));
|
|
10665
|
+
await promises.mkdir(revisionDir, { recursive: true, mode: DIR_MODE });
|
|
10666
|
+
for (const [relative, content] of bodies) {
|
|
10667
|
+
const target = resolveInside(revisionDir, relative);
|
|
10668
|
+
await promises.mkdir(path17.dirname(target), { recursive: true, mode: DIR_MODE });
|
|
10669
|
+
await assertNotSymlink(target);
|
|
10670
|
+
await atomicWriteFile(target, content, { mode: FILE_MODE });
|
|
10671
|
+
}
|
|
10672
|
+
const lock = {
|
|
10673
|
+
schema: SKILL_PACK_LOCK_SCHEMA,
|
|
10674
|
+
name: manifest.name,
|
|
10675
|
+
version: manifest.version,
|
|
10676
|
+
description: manifest.description,
|
|
10677
|
+
content_hash: manifest.contentHash,
|
|
10678
|
+
source,
|
|
10679
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10680
|
+
files: manifest.files.map((file) => ({
|
|
10681
|
+
path: file.path,
|
|
10682
|
+
sha256: file.contentHash,
|
|
10683
|
+
bytes: file.byteSize
|
|
10684
|
+
}))
|
|
10685
|
+
};
|
|
10686
|
+
await atomicWriteFile(path17.join(packRoot, SKILL_PACK_LOCK_FILENAME), `${JSON.stringify(lock, null, 2)}
|
|
10687
|
+
`, {
|
|
10688
|
+
mode: FILE_MODE
|
|
10689
|
+
});
|
|
10690
|
+
await appendAuditLine(options.dataDir, {
|
|
10691
|
+
ts: lock.installed_at,
|
|
10692
|
+
event: "skill-pack-installed",
|
|
10693
|
+
name: lock.name,
|
|
10694
|
+
version: lock.version,
|
|
10695
|
+
contentHash: lock.content_hash,
|
|
10696
|
+
source: lock.source,
|
|
10697
|
+
files: lock.files.length,
|
|
10698
|
+
bytes: totalBytes
|
|
10699
|
+
});
|
|
10700
|
+
return { name: manifest.name, lock, directory: revisionDir };
|
|
10701
|
+
}
|
|
10702
|
+
function isLockShaped(value) {
|
|
10703
|
+
const lock = value;
|
|
10704
|
+
return lock !== null && typeof lock === "object" && lock.schema === SKILL_PACK_LOCK_SCHEMA && typeof lock.name === "string" && typeof lock.content_hash === "string" && CONTENT_HASH_PATTERN.test(lock.content_hash) && Array.isArray(lock.files);
|
|
10705
|
+
}
|
|
10706
|
+
async function readLock(dataDir, name) {
|
|
10707
|
+
if (!isSkillPackPathSafe(name)) return void 0;
|
|
10708
|
+
const packRoot = path17.join(skillPacksRoot(dataDir), name);
|
|
10709
|
+
let raw;
|
|
10710
|
+
try {
|
|
10711
|
+
raw = await promises.readFile(path17.join(packRoot, SKILL_PACK_LOCK_FILENAME), "utf8");
|
|
10712
|
+
} catch (err) {
|
|
10713
|
+
if (err.code === "ENOENT") return void 0;
|
|
10714
|
+
throw err;
|
|
10715
|
+
}
|
|
10716
|
+
let parsed;
|
|
10717
|
+
try {
|
|
10718
|
+
parsed = JSON.parse(raw);
|
|
10719
|
+
} catch {
|
|
10720
|
+
return void 0;
|
|
10721
|
+
}
|
|
10722
|
+
if (!isLockShaped(parsed) || parsed.name !== name) return void 0;
|
|
10723
|
+
return {
|
|
10724
|
+
name,
|
|
10725
|
+
lock: parsed,
|
|
10726
|
+
directory: path17.join(packRoot, parsed.content_hash.slice("sha256:".length))
|
|
10727
|
+
};
|
|
10728
|
+
}
|
|
10729
|
+
async function listInstalledSkillPacks(dataDir) {
|
|
10730
|
+
let entries;
|
|
10731
|
+
try {
|
|
10732
|
+
entries = await promises.readdir(skillPacksRoot(dataDir), { withFileTypes: true });
|
|
10733
|
+
} catch (err) {
|
|
10734
|
+
if (err.code === "ENOENT") return [];
|
|
10735
|
+
throw err;
|
|
10736
|
+
}
|
|
10737
|
+
const packs = [];
|
|
10738
|
+
for (const entry of entries) {
|
|
10739
|
+
if (!entry.isDirectory()) continue;
|
|
10740
|
+
const installed = await readLock(dataDir, entry.name);
|
|
10741
|
+
if (installed !== void 0) packs.push(installed);
|
|
10742
|
+
}
|
|
10743
|
+
packs.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
10744
|
+
return packs;
|
|
10745
|
+
}
|
|
10746
|
+
async function projectSkillPack(dataDir, name, targetDir) {
|
|
10747
|
+
const installed = await readLock(dataDir, name);
|
|
10748
|
+
if (installed === void 0) {
|
|
10749
|
+
throw new SkillPackInstallError("store_unsafe", `no installed skill pack named ${JSON.stringify(name)}.`, {
|
|
10750
|
+
packName: name
|
|
10751
|
+
});
|
|
10752
|
+
}
|
|
10753
|
+
const copied = [];
|
|
10754
|
+
for (const file of installed.lock.files) {
|
|
10755
|
+
const sourcePath = resolveInside(installed.directory, file.path);
|
|
10756
|
+
await assertNotSymlink(sourcePath);
|
|
10757
|
+
let bytes;
|
|
10758
|
+
try {
|
|
10759
|
+
bytes = await promises.readFile(sourcePath);
|
|
10760
|
+
} catch (cause) {
|
|
10761
|
+
throw new SkillPackInstallError(
|
|
10762
|
+
"store_unsafe",
|
|
10763
|
+
`installed skill pack ${JSON.stringify(name)} is missing ${JSON.stringify(file.path)}.`,
|
|
10764
|
+
{ cause, packName: name }
|
|
10765
|
+
);
|
|
10766
|
+
}
|
|
10767
|
+
if (hashBytes(bytes) !== file.sha256 || bytes.byteLength !== file.bytes) {
|
|
10768
|
+
throw new SkillPackInstallError(
|
|
10769
|
+
"store_unsafe",
|
|
10770
|
+
`installed skill pack ${JSON.stringify(name)} no longer matches its lock at ${JSON.stringify(file.path)}.`,
|
|
10771
|
+
{ packName: name }
|
|
10772
|
+
);
|
|
10773
|
+
}
|
|
10774
|
+
const destination = resolveInside(targetDir, file.path);
|
|
10775
|
+
await promises.mkdir(path17.dirname(destination), { recursive: true, mode: DIR_MODE });
|
|
10776
|
+
await assertNotSymlink(destination);
|
|
10777
|
+
await atomicWriteFile(destination, bytes, { mode: FILE_MODE });
|
|
10778
|
+
copied.push(file.path);
|
|
10779
|
+
}
|
|
10780
|
+
await appendAuditLine(dataDir, {
|
|
10781
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10782
|
+
event: "skill-pack-projected",
|
|
10783
|
+
name: installed.name,
|
|
10784
|
+
contentHash: installed.lock.content_hash,
|
|
10785
|
+
files: copied.length
|
|
10786
|
+
});
|
|
10787
|
+
return {
|
|
10788
|
+
name: installed.name,
|
|
10789
|
+
contentHash: installed.lock.content_hash,
|
|
10790
|
+
targetDir: path17.resolve(targetDir),
|
|
10791
|
+
files: copied
|
|
10792
|
+
};
|
|
10793
|
+
}
|
|
9205
10794
|
var EMPTY_BODY = new Uint8Array();
|
|
9206
10795
|
var LARGE_SNAPSHOT_THRESHOLD_BYTES = 1024 * 1024;
|
|
9207
|
-
var DEVICE_PROOF_HEADER = "x-byok-device-proof";
|
|
9208
10796
|
var TruthMemoryClientError = class extends Error {
|
|
9209
10797
|
constructor(code, message, status) {
|
|
9210
10798
|
super(message);
|
|
@@ -9231,8 +10819,8 @@ var TruthMemoryClient = class {
|
|
|
9231
10819
|
#requestId;
|
|
9232
10820
|
#allowedObjectDownloadOrigins;
|
|
9233
10821
|
async listManifest(query = {}) {
|
|
9234
|
-
const
|
|
9235
|
-
const response = await this.#proofFetch(
|
|
10822
|
+
const path21 = manifestPath(query);
|
|
10823
|
+
const response = await this.#proofFetch(path21, {
|
|
9236
10824
|
method: "GET",
|
|
9237
10825
|
operation: "truth.list",
|
|
9238
10826
|
resource: "records",
|
|
@@ -9339,9 +10927,9 @@ var TruthMemoryClient = class {
|
|
|
9339
10927
|
}
|
|
9340
10928
|
async #write(kind, recordKey, requestId, payload, expectedPrimary, expectedSnapshots) {
|
|
9341
10929
|
assertDistinctExpectedWrites([expectedPrimary, ...expectedSnapshots]);
|
|
9342
|
-
const
|
|
10930
|
+
const path21 = recordPath(kind, recordKey);
|
|
9343
10931
|
const body = new TextEncoder().encode(JSON.stringify(payload));
|
|
9344
|
-
const response = await this.#proofFetch(
|
|
10932
|
+
const response = await this.#proofFetch(path21, {
|
|
9345
10933
|
method: "PUT",
|
|
9346
10934
|
operation: "truth.write",
|
|
9347
10935
|
resource: `${kind}/${recordKey}`,
|
|
@@ -9367,8 +10955,8 @@ var TruthMemoryClient = class {
|
|
|
9367
10955
|
};
|
|
9368
10956
|
}
|
|
9369
10957
|
async #readVerified(listed) {
|
|
9370
|
-
const
|
|
9371
|
-
const response = await this.#proofFetch(
|
|
10958
|
+
const path21 = recordPath(listed.kind, listed.recordKey);
|
|
10959
|
+
const response = await this.#proofFetch(path21, {
|
|
9372
10960
|
method: "GET",
|
|
9373
10961
|
operation: "truth.read",
|
|
9374
10962
|
resource: `${listed.kind}/${listed.recordKey}`,
|
|
@@ -9429,16 +11017,16 @@ var TruthMemoryClient = class {
|
|
|
9429
11017
|
}
|
|
9430
11018
|
return { ...listed, bytes };
|
|
9431
11019
|
}
|
|
9432
|
-
async #proofFetch(
|
|
11020
|
+
async #proofFetch(path21, request) {
|
|
9433
11021
|
const proof = await this.options.signer.sign({
|
|
9434
11022
|
method: request.method,
|
|
9435
|
-
path:
|
|
11023
|
+
path: path21,
|
|
9436
11024
|
operation: request.operation,
|
|
9437
11025
|
resource: request.resource,
|
|
9438
11026
|
requestId: request.requestId,
|
|
9439
11027
|
body: request.body
|
|
9440
11028
|
});
|
|
9441
|
-
const response = await this.#fetch(new URL(
|
|
11029
|
+
const response = await this.#fetch(new URL(path21, this.#base), {
|
|
9442
11030
|
method: request.method,
|
|
9443
11031
|
headers: {
|
|
9444
11032
|
...request.headers,
|
|
@@ -9449,7 +11037,7 @@ var TruthMemoryClient = class {
|
|
|
9449
11037
|
if (!response.ok) {
|
|
9450
11038
|
throw new TruthMemoryClientError(
|
|
9451
11039
|
"truth_http_failed",
|
|
9452
|
-
`truth request ${request.method} ${
|
|
11040
|
+
`truth request ${request.method} ${path21} failed with HTTP ${response.status}`,
|
|
9453
11041
|
response.status
|
|
9454
11042
|
);
|
|
9455
11043
|
}
|
|
@@ -9462,10 +11050,10 @@ function manifestPath(query) {
|
|
|
9462
11050
|
if (query.keyPrefix !== void 0) search.set("prefix", query.keyPrefix);
|
|
9463
11051
|
if (query.limit !== void 0) search.set("limit", String(query.limit));
|
|
9464
11052
|
const encoded = search.toString();
|
|
9465
|
-
return encoded.length === 0 ?
|
|
11053
|
+
return encoded.length === 0 ? BYOK_RECORDS_PATH : `${BYOK_RECORDS_PATH}?${encoded}`;
|
|
9466
11054
|
}
|
|
9467
11055
|
function recordPath(kind, recordKey) {
|
|
9468
|
-
return
|
|
11056
|
+
return byokRecordPath(kind, recordKey);
|
|
9469
11057
|
}
|
|
9470
11058
|
function prepareTransportBody(body) {
|
|
9471
11059
|
if (body.kind === "inline") {
|
|
@@ -9707,9 +11295,9 @@ function plistString(value) {
|
|
|
9707
11295
|
function generateLaunchdPlist(def) {
|
|
9708
11296
|
const { label, program, logDir } = def;
|
|
9709
11297
|
const args = [program.command, ...program.args];
|
|
9710
|
-
const cwd = program.cwd ??
|
|
9711
|
-
const outLog =
|
|
9712
|
-
const errLog =
|
|
11298
|
+
const cwd = program.cwd ?? os5.homedir();
|
|
11299
|
+
const outLog = path17.join(logDir, `${label}.out.log`);
|
|
11300
|
+
const errLog = path17.join(logDir, `${label}.err.log`);
|
|
9713
11301
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
9714
11302
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
9715
11303
|
<plist version="1.0">
|
|
@@ -9741,8 +11329,8 @@ ${args.map((a) => ` ${plistString(a)}`).join("\n")}
|
|
|
9741
11329
|
}
|
|
9742
11330
|
function createLaunchdLifecycle(def, deps = {}) {
|
|
9743
11331
|
const run = deps.run ?? defaultRunner;
|
|
9744
|
-
const
|
|
9745
|
-
const homedir = deps.homedir ?? (() =>
|
|
11332
|
+
const fs17 = deps.fs ?? promises;
|
|
11333
|
+
const homedir = deps.homedir ?? (() => os5.homedir());
|
|
9746
11334
|
const getuid = deps.getuid ?? (() => {
|
|
9747
11335
|
if (typeof process.getuid !== "function") {
|
|
9748
11336
|
throw new Error("launchd lifecycle requires a POSIX uid (process.getuid unavailable) \u2014 this module only runs on macOS");
|
|
@@ -9750,12 +11338,12 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
9750
11338
|
return process.getuid();
|
|
9751
11339
|
});
|
|
9752
11340
|
const label = sanitizeServiceName(def.name);
|
|
9753
|
-
const plistPath = () =>
|
|
11341
|
+
const plistPath = () => path17.join(homedir(), "Library", "LaunchAgents", `${label}.plist`);
|
|
9754
11342
|
const domainTarget = () => `gui/${getuid()}`;
|
|
9755
11343
|
const serviceTarget = () => `${domainTarget()}/${label}`;
|
|
9756
11344
|
async function fileExists(p) {
|
|
9757
11345
|
try {
|
|
9758
|
-
await
|
|
11346
|
+
await fs17.stat(p);
|
|
9759
11347
|
return true;
|
|
9760
11348
|
} catch {
|
|
9761
11349
|
return false;
|
|
@@ -9763,9 +11351,9 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
9763
11351
|
}
|
|
9764
11352
|
async function writePlist(program) {
|
|
9765
11353
|
const xml = generateLaunchdPlist({ label, program, logDir: def.logDir });
|
|
9766
|
-
await
|
|
9767
|
-
await
|
|
9768
|
-
await
|
|
11354
|
+
await fs17.mkdir(path17.dirname(plistPath()), { recursive: true });
|
|
11355
|
+
await fs17.mkdir(def.logDir, { recursive: true });
|
|
11356
|
+
await fs17.writeFile(plistPath(), xml, "utf8");
|
|
9769
11357
|
}
|
|
9770
11358
|
async function install(opts = {}) {
|
|
9771
11359
|
await writePlist(opts.program ?? def.program);
|
|
@@ -9776,7 +11364,7 @@ function createLaunchdLifecycle(def, deps = {}) {
|
|
|
9776
11364
|
}
|
|
9777
11365
|
async function uninstall() {
|
|
9778
11366
|
await runIdempotent(run, "launchctl", ["bootout", serviceTarget()], "launchctl bootout", LAUNCHD_NOT_LOADED);
|
|
9779
|
-
await
|
|
11367
|
+
await fs17.rm(plistPath(), { force: true });
|
|
9780
11368
|
}
|
|
9781
11369
|
async function start() {
|
|
9782
11370
|
if (!await fileExists(plistPath())) {
|
|
@@ -9835,10 +11423,10 @@ function generateSystemdUnit(def) {
|
|
|
9835
11423
|
const { name, displayName, program, logDir } = def;
|
|
9836
11424
|
assertNoControlChars(name, "name");
|
|
9837
11425
|
assertNoControlChars(displayName, "displayName");
|
|
9838
|
-
const cwd = program.cwd ??
|
|
11426
|
+
const cwd = program.cwd ?? os5.homedir();
|
|
9839
11427
|
assertNoControlChars(cwd, "program.cwd");
|
|
9840
|
-
const outLog =
|
|
9841
|
-
const errLog =
|
|
11428
|
+
const outLog = path17.join(logDir, `${name}.out.log`);
|
|
11429
|
+
const errLog = path17.join(logDir, `${name}.err.log`);
|
|
9842
11430
|
assertNoControlChars(outLog, "logDir");
|
|
9843
11431
|
assertNoControlChars(errLog, "logDir");
|
|
9844
11432
|
const execStart = [program.command, ...program.args].map(quoteSystemdArg).join(" ");
|
|
@@ -9860,14 +11448,14 @@ WantedBy=default.target
|
|
|
9860
11448
|
}
|
|
9861
11449
|
function createSystemdLifecycle(def, deps = {}) {
|
|
9862
11450
|
const run = deps.run ?? defaultRunner;
|
|
9863
|
-
const
|
|
9864
|
-
const homedir = deps.homedir ?? (() =>
|
|
11451
|
+
const fs17 = deps.fs ?? promises;
|
|
11452
|
+
const homedir = deps.homedir ?? (() => os5.homedir());
|
|
9865
11453
|
const name = sanitizeServiceName(def.name);
|
|
9866
11454
|
const unitName = `${name}.service`;
|
|
9867
|
-
const unitPath = () =>
|
|
11455
|
+
const unitPath = () => path17.join(homedir(), ".config", "systemd", "user", unitName);
|
|
9868
11456
|
async function fileExists(p) {
|
|
9869
11457
|
try {
|
|
9870
|
-
await
|
|
11458
|
+
await fs17.stat(p);
|
|
9871
11459
|
return true;
|
|
9872
11460
|
} catch {
|
|
9873
11461
|
return false;
|
|
@@ -9875,9 +11463,9 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
9875
11463
|
}
|
|
9876
11464
|
async function writeUnit(program) {
|
|
9877
11465
|
const unit = generateSystemdUnit({ name, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
9878
|
-
await
|
|
9879
|
-
await
|
|
9880
|
-
await
|
|
11466
|
+
await fs17.mkdir(path17.dirname(unitPath()), { recursive: true });
|
|
11467
|
+
await fs17.mkdir(def.logDir, { recursive: true });
|
|
11468
|
+
await fs17.writeFile(unitPath(), unit, "utf8");
|
|
9881
11469
|
}
|
|
9882
11470
|
async function install(opts = {}) {
|
|
9883
11471
|
await writeUnit(opts.program ?? def.program);
|
|
@@ -9886,7 +11474,7 @@ function createSystemdLifecycle(def, deps = {}) {
|
|
|
9886
11474
|
}
|
|
9887
11475
|
async function uninstall() {
|
|
9888
11476
|
await runIdempotent(run, "systemctl", ["--user", "disable", "--now", unitName], "systemctl disable --now", SYSTEMD_NOT_LOADED);
|
|
9889
|
-
await
|
|
11477
|
+
await fs17.rm(unitPath(), { force: true });
|
|
9890
11478
|
await run("systemctl", ["--user", "daemon-reload"]);
|
|
9891
11479
|
}
|
|
9892
11480
|
async function start() {
|
|
@@ -9949,7 +11537,7 @@ ${argXml}${cwdXml}
|
|
|
9949
11537
|
}
|
|
9950
11538
|
function createWinswLifecycle(def, deps = {}) {
|
|
9951
11539
|
const run = deps.run ?? defaultRunner;
|
|
9952
|
-
const
|
|
11540
|
+
const fs17 = deps.fs ?? promises;
|
|
9953
11541
|
const windows = def.windows;
|
|
9954
11542
|
if (!windows) {
|
|
9955
11543
|
throw new Error("WinSW service lifecycle requires `ServiceDefinition.windows.winswBin` (the product-bundled WinSW executable path)");
|
|
@@ -9957,11 +11545,11 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
9957
11545
|
const winswBin = windows.winswBin;
|
|
9958
11546
|
const id = sanitizeServiceName(def.name);
|
|
9959
11547
|
const installDir = windows.installDir ?? def.logDir;
|
|
9960
|
-
const exePath =
|
|
9961
|
-
const xmlPath =
|
|
11548
|
+
const exePath = path17.join(installDir, `${id}.exe`);
|
|
11549
|
+
const xmlPath = path17.join(installDir, `${id}.xml`);
|
|
9962
11550
|
async function fileExists(p) {
|
|
9963
11551
|
try {
|
|
9964
|
-
await
|
|
11552
|
+
await fs17.stat(p);
|
|
9965
11553
|
return true;
|
|
9966
11554
|
} catch {
|
|
9967
11555
|
return false;
|
|
@@ -9969,10 +11557,10 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
9969
11557
|
}
|
|
9970
11558
|
async function writeFiles(program) {
|
|
9971
11559
|
const xml = generateWinswXml({ id, displayName: def.displayName ?? def.name, program, logDir: def.logDir });
|
|
9972
|
-
await
|
|
9973
|
-
await
|
|
9974
|
-
await
|
|
9975
|
-
await
|
|
11560
|
+
await fs17.mkdir(installDir, { recursive: true });
|
|
11561
|
+
await fs17.mkdir(def.logDir, { recursive: true });
|
|
11562
|
+
await fs17.copyFile(winswBin, exePath);
|
|
11563
|
+
await fs17.writeFile(xmlPath, xml, "utf8");
|
|
9976
11564
|
}
|
|
9977
11565
|
async function install(opts = {}) {
|
|
9978
11566
|
await writeFiles(opts.program ?? def.program);
|
|
@@ -9982,8 +11570,8 @@ function createWinswLifecycle(def, deps = {}) {
|
|
|
9982
11570
|
async function uninstall() {
|
|
9983
11571
|
await runIdempotent(run, exePath, ["stop"], "winsw stop", WINSW_NOT_INSTALLED);
|
|
9984
11572
|
await runIdempotent(run, exePath, ["uninstall"], "winsw uninstall", WINSW_NOT_INSTALLED);
|
|
9985
|
-
await
|
|
9986
|
-
await
|
|
11573
|
+
await fs17.rm(exePath, { force: true });
|
|
11574
|
+
await fs17.rm(xmlPath, { force: true });
|
|
9987
11575
|
}
|
|
9988
11576
|
async function start() {
|
|
9989
11577
|
if (!await fileExists(xmlPath)) {
|
|
@@ -10026,6 +11614,6 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
10026
11614
|
}
|
|
10027
11615
|
}
|
|
10028
11616
|
|
|
10029
|
-
export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, SecureDirHardeningError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, isGitWorkspaceConfig, journalHash, nodeAgentProgram, prependGitWorkspaceGuidance, resolveLocalStoragePolicy, sanitizeServiceName };
|
|
11617
|
+
export { AuthManager, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, DeviceRevokedError, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, StoredDeviceProofSigner, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, ensureSecureDir, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isGitWorkspaceConfig, journalHash, listInstalledSkillPacks, nodeAgentProgram, prependGitWorkspaceGuidance, projectSkillPack, requestDeviceAssertion, resolveLocalStoragePolicy, sanitizeServiceName, skillPacksRoot };
|
|
10030
11618
|
//# sourceMappingURL=index.js.map
|
|
10031
11619
|
//# sourceMappingURL=index.js.map
|