@byok-sdk/client 0.11.0 → 0.13.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 +28 -3
- package/dist/adapters/index.js +74 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +3 -0
- package/dist/adapters/pi/subagents-policy-extension.js +1 -1
- package/dist/adapters/pi/subagents-policy-extension.js.map +1 -1
- package/dist/agent-home.d.ts +107 -0
- package/dist/agent-memory/index.d.ts +1 -1
- package/dist/agent-memory/index.js +8 -5
- package/dist/agent-memory/index.js.map +1 -1
- package/dist/bin/byok-agent-memory-mcp.js.map +1 -1
- package/dist/bin/byok-agent-message-mcp.js.map +1 -1
- package/dist/bin/byok-agent-team-mcp.js.map +1 -1
- package/dist/bin/byok-agent.js +1279 -642
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/daemon/agent-egress-controller.d.ts +4 -0
- package/dist/daemon/auth-manager.d.ts +20 -0
- package/dist/daemon/blob-client.d.ts +24 -6
- package/dist/daemon/connection-manager.d.ts +69 -126
- package/dist/daemon/control-protocol.d.ts +8 -1
- package/dist/daemon/create-daemon.d.ts +28 -13
- package/dist/daemon/device-credential-store.d.ts +16 -0
- package/dist/daemon/long-poll-transport.d.ts +32 -23
- package/dist/daemon/observer.d.ts +1 -1
- package/dist/daemon/replay-cursor.d.ts +9 -0
- package/dist/daemon/task-runner.d.ts +22 -0
- package/dist/daemon/url.d.ts +17 -13
- package/dist/diagnostics/support-bundle.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1278 -641
- package/dist/index.js.map +1 -1
- package/package.json +5 -7
- package/dist/daemon/ws-transport.d.ts +0 -139
package/dist/bin/byok-agent.js
CHANGED
|
@@ -3,7 +3,7 @@ import { randomUUID, createHash, randomBytes, timingSafeEqual, createHmac, creat
|
|
|
3
3
|
import { readFileSync, promises, linkSync, fstatSync, lstatSync, unlinkSync, constants, readSync, openSync, writeFileSync, fchmodSync, fsyncSync, closeSync, opendirSync, existsSync, realpathSync, mkdirSync, renameSync, chmodSync, statSync, readdirSync } from 'fs';
|
|
4
4
|
import * as path3 from 'path';
|
|
5
5
|
import path3__default, { isAbsolute, join } from 'path';
|
|
6
|
-
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, AgentHomeProjectionPayloadSchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, TERMINAL_PROJECTION_SELECTION_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES,
|
|
6
|
+
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, TASK_STATES, AgentEgressPolicySchema, AgentHomeProjectionPayloadSchema, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentRefSchema, AgentContentReceiptPayloadSchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, partitionAgentEvents, TASK_TRANSITIONS, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, TERMINAL_PROJECTION_SELECTION_CAPABILITY, PROVIDER_PROFILE_BINDING_CAPABILITY, STRICT_AGENT_ONLY_CAPABILITY, AGENT_HOME_PROJECTION_CAPABILITY, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_FRESH_SESSION_CAPABILITY, AGENT_MESSAGE_EGRESS_CAPABILITY, AgentHomeProjectionCompletionRequestSchema, byokAgentHomeProjectionCompletionPath, AgentHomeProjectionReadbackSchema, parseMessage, checkResultDocument, MAX_MESSAGES_PER_BATCH, BYOK_CAPABILITIES_PATH, BYOK_PRESENCE_PATH, AgentMessagePublishPayloadSchema, AgentMessageDispositionPayloadSchema, RuntimeIdSchema, AGENT_MEMORY_PROJECTION_CAPABILITY, TERMINAL_INFERENCE_USAGE_MAX_DURATION_MS, TERMINAL_INFERENCE_USAGE_MAX_TOKENS, RESULT_DOCUMENT_MAX_BYTES, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendResponseSchema, UnknownMessageTypeError, MESSAGE_TYPES, AGENT_MEMORY_PROJECTION_MAX_ORDERING_VALUE, AgentMemoryProjectionMutationSchema, AGENT_MEMORY_PROJECTION_MAX_REDACTED_BYTES } from '@byok-sdk/protocol';
|
|
7
7
|
import net, { createServer, createConnection } from 'net';
|
|
8
8
|
import * as os from 'os';
|
|
9
9
|
import os__default from 'os';
|
|
@@ -13,7 +13,6 @@ import 'readline';
|
|
|
13
13
|
import { isTenantId, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, nonceSigningBytes, CapabilityDeclarationSchema, hasCapability, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import * as fs13 from 'fs/promises';
|
|
16
|
-
import { WebSocket } from 'ws';
|
|
17
16
|
import { createRequire } from 'module';
|
|
18
17
|
import { createInterface } from 'readline/promises';
|
|
19
18
|
|
|
@@ -445,6 +444,23 @@ async function resolveExistingAncestor(inputPath) {
|
|
|
445
444
|
}
|
|
446
445
|
}
|
|
447
446
|
}
|
|
447
|
+
async function canonicalPath(inputPath) {
|
|
448
|
+
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
449
|
+
return path3__default.resolve(canonical2, ...tail);
|
|
450
|
+
}
|
|
451
|
+
async function assertRealDirectoryIfPresent(target) {
|
|
452
|
+
let stat;
|
|
453
|
+
try {
|
|
454
|
+
stat = await promises.lstat(target);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
const code = error.code;
|
|
457
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
461
|
+
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${target}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
448
464
|
async function materializeDirectory(inputPath) {
|
|
449
465
|
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
450
466
|
let cursor = canonical2;
|
|
@@ -539,6 +555,39 @@ var AgentHomeLayout = class {
|
|
|
539
555
|
await gate.release();
|
|
540
556
|
}
|
|
541
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Pure canonical-home derivation for read-only callers, such as the
|
|
560
|
+
* pre-admission single-writer count. It validates the AgentRef and joins
|
|
561
|
+
* exactly the same `<hostStorageRoot>/agents/<agentId>` segments
|
|
562
|
+
* {@link AgentHomeLayout.resolve} would, canonicalizing only the components
|
|
563
|
+
* that already exist.
|
|
564
|
+
*
|
|
565
|
+
* It deliberately creates no directory, takes no cross-process mutation
|
|
566
|
+
* gate and records no Agent binding, so an offer the host vetoes after the
|
|
567
|
+
* count leaves nothing behind on disk. `resolve()` stays the only path that
|
|
568
|
+
* may materialize a home or bind it to an Agent identity.
|
|
569
|
+
*
|
|
570
|
+
* An `agents` root or `agents/<agentId>` leaf that already exists but is a
|
|
571
|
+
* symlink (or any non-directory) is rejected here with the same error class
|
|
572
|
+
* and message `resolve()` raises for it, so an in-root `two -> one` link
|
|
573
|
+
* fails closed instead of silently keying the count of `one`. A leaf that
|
|
574
|
+
* does not exist yet is not an error: this derivation runs before the home
|
|
575
|
+
* is materialized. A home canonicalizing outside the `agents` root stays
|
|
576
|
+
* rejected as before.
|
|
577
|
+
*/
|
|
578
|
+
async canonicalHomePath(agentRefInput) {
|
|
579
|
+
const agentRef = validateAgentRef(agentRefInput);
|
|
580
|
+
const hostStorageRoot = this.canonicalRoot ?? await canonicalPath(this.hostStorageRootInput);
|
|
581
|
+
const agentsRoot = path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY);
|
|
582
|
+
await assertRealDirectoryIfPresent(agentsRoot);
|
|
583
|
+
const lexicalHome = path3__default.join(agentsRoot, agentRef.agentId);
|
|
584
|
+
await assertRealDirectoryIfPresent(lexicalHome);
|
|
585
|
+
const canonicalHome = await canonicalPath(lexicalHome);
|
|
586
|
+
if (canonicalHome === agentsRoot || !isWithin(agentsRoot, canonicalHome)) {
|
|
587
|
+
throw new AgentHomeResolutionError(`Agent home resolves outside the Agent home root: ${canonicalHome}`);
|
|
588
|
+
}
|
|
589
|
+
return canonicalHome;
|
|
590
|
+
}
|
|
542
591
|
/**
|
|
543
592
|
* Prove the canonical root is materializable and writable before the daemon
|
|
544
593
|
* advertises Agent-home capability. No Agent identity or persistent Agent
|
|
@@ -768,6 +817,158 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
|
|
|
768
817
|
}
|
|
769
818
|
}
|
|
770
819
|
};
|
|
820
|
+
function executionKey(input) {
|
|
821
|
+
const value = input.sessionRef === void 0 ? input.taskId : input.sessionRef;
|
|
822
|
+
const label = input.sessionRef === void 0 ? "taskId" : "sessionRef";
|
|
823
|
+
if (typeof value !== "string" || value.length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
824
|
+
throw new AgentHomeResolutionError(`Agent execution ${label} must be a non-empty single-line string`);
|
|
825
|
+
}
|
|
826
|
+
return `${input.sessionRef === void 0 ? "task" : "session"}\0${value}`;
|
|
827
|
+
}
|
|
828
|
+
var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
|
|
829
|
+
constructor(manager) {
|
|
830
|
+
this.manager = manager;
|
|
831
|
+
}
|
|
832
|
+
manager;
|
|
833
|
+
static groups = /* @__PURE__ */ new Map();
|
|
834
|
+
static queues = /* @__PURE__ */ new Map();
|
|
835
|
+
async acquire(resolution, input) {
|
|
836
|
+
const initialKey = executionKey(input);
|
|
837
|
+
return this.exclusive(resolution.canonicalHome, async () => {
|
|
838
|
+
let group = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
|
|
839
|
+
if (group === void 0) {
|
|
840
|
+
const baseLease = await this.manager.acquire(resolution);
|
|
841
|
+
group = {
|
|
842
|
+
manager: this.manager,
|
|
843
|
+
baseLease,
|
|
844
|
+
agentId: resolution.agentRef.agentId,
|
|
845
|
+
leasesByKey: /* @__PURE__ */ new Map()
|
|
846
|
+
};
|
|
847
|
+
_AgentHomeExecutionLeaseManager.groups.set(resolution.canonicalHome, group);
|
|
848
|
+
} else if (group.manager !== this.manager || group.agentId !== resolution.agentRef.agentId) {
|
|
849
|
+
throw new AgentHomeBusyError(`Agent home ${resolution.canonicalHome} is active under another execution owner`);
|
|
850
|
+
}
|
|
851
|
+
if (group.leasesByKey.has(initialKey)) {
|
|
852
|
+
throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
|
|
853
|
+
}
|
|
854
|
+
const leaseId = randomUUID();
|
|
855
|
+
group.leasesByKey.set(initialKey, leaseId);
|
|
856
|
+
let currentKey = initialKey;
|
|
857
|
+
let sessionBound = input.sessionRef !== void 0;
|
|
858
|
+
let released = false;
|
|
859
|
+
return Object.freeze({
|
|
860
|
+
leaseId,
|
|
861
|
+
agentRef: resolution.agentRef,
|
|
862
|
+
canonicalHome: resolution.canonicalHome,
|
|
863
|
+
cwd: resolution.canonicalHome,
|
|
864
|
+
homeIdentity: group.baseLease.homeIdentity,
|
|
865
|
+
bindSession: async (sessionRef) => {
|
|
866
|
+
const nextKey = executionKey({ taskId: input.taskId, sessionRef });
|
|
867
|
+
await this.exclusive(resolution.canonicalHome, async () => {
|
|
868
|
+
if (released) throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is already released`);
|
|
869
|
+
const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
|
|
870
|
+
if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
|
|
871
|
+
throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
|
|
872
|
+
}
|
|
873
|
+
if (nextKey === currentKey) return;
|
|
874
|
+
if (sessionBound) {
|
|
875
|
+
throw new AgentHomeBusyError(
|
|
876
|
+
`Agent session execution lease ${leaseId} cannot rebind to a different runtime session`
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (currentGroup.leasesByKey.has(nextKey)) {
|
|
880
|
+
throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
|
|
881
|
+
}
|
|
882
|
+
currentGroup.leasesByKey.set(nextKey, leaseId);
|
|
883
|
+
currentGroup.leasesByKey.delete(currentKey);
|
|
884
|
+
currentKey = nextKey;
|
|
885
|
+
sessionBound = true;
|
|
886
|
+
});
|
|
887
|
+
},
|
|
888
|
+
release: async () => {
|
|
889
|
+
await this.exclusive(resolution.canonicalHome, async () => {
|
|
890
|
+
if (released) return;
|
|
891
|
+
const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
|
|
892
|
+
if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
|
|
893
|
+
released = true;
|
|
894
|
+
throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
|
|
895
|
+
}
|
|
896
|
+
currentGroup.leasesByKey.delete(currentKey);
|
|
897
|
+
released = true;
|
|
898
|
+
if (currentGroup.leasesByKey.size === 0) {
|
|
899
|
+
_AgentHomeExecutionLeaseManager.groups.delete(resolution.canonicalHome);
|
|
900
|
+
await currentGroup.baseLease.release();
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* WP0: Attempts currently holding an execution lease on this exact
|
|
909
|
+
* canonical home, across every lane and every session. This is the number
|
|
910
|
+
* the daemon's admission gate reads before any side effect — see
|
|
911
|
+
* `TaskRunner.handleOffer`'s per-home busy gate.
|
|
912
|
+
*
|
|
913
|
+
* Derived from the one lease registry above rather than a second tally, so
|
|
914
|
+
* it inherits the lease lifecycle exactly: an entry appears at `acquire()`,
|
|
915
|
+
* survives `bindSession()` (which rekeys in place), and disappears only at
|
|
916
|
+
* `release()`, which the task runner calls after the attempt is terminal
|
|
917
|
+
* AND `Session.close()` resolved. A failed disposal never reaches
|
|
918
|
+
* `release()`, so the slot stays held — fail closed, the same posture as
|
|
919
|
+
* `runtime-disposal-failed`. Crash residue needs nothing extra here: a
|
|
920
|
+
* restarted daemon starts with an empty registry and reclaims the on-disk
|
|
921
|
+
* marker only under the same stable owner identity (`openLeaseMarker`).
|
|
922
|
+
*
|
|
923
|
+
* Counted regardless of which lease manager owns the group: the invariant
|
|
924
|
+
* being protected is the filesystem path (`MEMORY.md`, `notes/`, `.git`),
|
|
925
|
+
* not the owner identity.
|
|
926
|
+
*/
|
|
927
|
+
activeAttemptCount(canonicalHome) {
|
|
928
|
+
return _AgentHomeExecutionLeaseManager.groups.get(canonicalHome)?.leasesByKey.size ?? 0;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Counts-only readback for daemon/control status. Scoped to this manager's
|
|
932
|
+
* own leases, so the number describes this daemon rather than every home
|
|
933
|
+
* any manager in the process happens to hold. Never exposes a home path.
|
|
934
|
+
*/
|
|
935
|
+
activeAttemptSummary() {
|
|
936
|
+
let homes = 0;
|
|
937
|
+
let attempts = 0;
|
|
938
|
+
for (const group of _AgentHomeExecutionLeaseManager.groups.values()) {
|
|
939
|
+
if (group.manager !== this.manager) continue;
|
|
940
|
+
homes += 1;
|
|
941
|
+
attempts += group.leasesByKey.size;
|
|
942
|
+
}
|
|
943
|
+
return { homes, attempts };
|
|
944
|
+
}
|
|
945
|
+
async mutate(binding, operation) {
|
|
946
|
+
return this.exclusive(binding.resolution.canonicalHome, async () => {
|
|
947
|
+
const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
|
|
948
|
+
if (group === void 0 || group.manager !== this.manager || ![...group.leasesByKey.values()].includes(binding.lease.leaseId)) {
|
|
949
|
+
throw new AgentHomeBusyError("Agent execution lease does not own this home mutation");
|
|
950
|
+
}
|
|
951
|
+
return operation();
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
async exclusive(canonicalHome, operation) {
|
|
955
|
+
const prior = _AgentHomeExecutionLeaseManager.queues.get(canonicalHome) ?? Promise.resolve();
|
|
956
|
+
let release;
|
|
957
|
+
const tail = new Promise((resolve) => {
|
|
958
|
+
release = resolve;
|
|
959
|
+
});
|
|
960
|
+
_AgentHomeExecutionLeaseManager.queues.set(canonicalHome, tail);
|
|
961
|
+
await prior;
|
|
962
|
+
try {
|
|
963
|
+
return await operation();
|
|
964
|
+
} finally {
|
|
965
|
+
release();
|
|
966
|
+
if (_AgentHomeExecutionLeaseManager.queues.get(canonicalHome) === tail) {
|
|
967
|
+
_AgentHomeExecutionLeaseManager.queues.delete(canonicalHome);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
};
|
|
771
972
|
async function initializeAgentHome(resolution) {
|
|
772
973
|
await ensureDirectoryNoSymlink(
|
|
773
974
|
resolution.canonicalHome,
|
|
@@ -843,10 +1044,12 @@ var AgentHomeManager = class {
|
|
|
843
1044
|
layout;
|
|
844
1045
|
projection;
|
|
845
1046
|
leaseManager;
|
|
1047
|
+
executionLeaseManager;
|
|
846
1048
|
constructor(options) {
|
|
847
1049
|
this.layout = new AgentHomeLayout(options.hostStorageRoot);
|
|
848
1050
|
this.projection = options.projection;
|
|
849
1051
|
this.leaseManager = options.leaseManager ?? new AgentHomeLeaseManager();
|
|
1052
|
+
this.executionLeaseManager = new AgentHomeExecutionLeaseManager(this.leaseManager);
|
|
850
1053
|
}
|
|
851
1054
|
async prepare(agentRef) {
|
|
852
1055
|
const binding = await this.acquire(agentRef);
|
|
@@ -873,12 +1076,25 @@ var AgentHomeManager = class {
|
|
|
873
1076
|
const lease = await this.leaseManager.acquire(resolution);
|
|
874
1077
|
return Object.freeze({ resolution, lease });
|
|
875
1078
|
}
|
|
1079
|
+
async acquireExecution(agentRef, input) {
|
|
1080
|
+
const resolution = await this.layout.resolve(agentRef);
|
|
1081
|
+
const lease = await this.executionLeaseManager.acquire(resolution, input);
|
|
1082
|
+
return Object.freeze({ resolution, lease });
|
|
1083
|
+
}
|
|
876
1084
|
/** Initialize only after any requested session exact-match has succeeded. */
|
|
877
1085
|
async initialize(binding) {
|
|
878
|
-
|
|
1086
|
+
await this.initializeResolved(binding.resolution, binding.lease.cwd);
|
|
1087
|
+
}
|
|
1088
|
+
async initializeExecution(binding) {
|
|
1089
|
+
await this.mutateExecution(binding, () => this.initializeResolved(binding.resolution, binding.lease.cwd));
|
|
1090
|
+
}
|
|
1091
|
+
async mutateExecution(binding, operation) {
|
|
1092
|
+
return this.executionLeaseManager.mutate(binding, operation);
|
|
1093
|
+
}
|
|
1094
|
+
async initializeResolved(resolution, cwd) {
|
|
879
1095
|
await initializeAgentHome(resolution);
|
|
880
1096
|
const prepare = this.projection?.prepare;
|
|
881
|
-
if (prepare !== void 0) await prepare({ ...resolution, cwd
|
|
1097
|
+
if (prepare !== void 0) await prepare({ ...resolution, cwd });
|
|
882
1098
|
if (await promises.realpath(resolution.homeDir) !== resolution.canonicalHome) {
|
|
883
1099
|
throw new AgentHomeResolutionError("Agent projection changed the canonical home path");
|
|
884
1100
|
}
|
|
@@ -1689,13 +1905,22 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
|
1689
1905
|
});
|
|
1690
1906
|
}
|
|
1691
1907
|
function sealRuntimeOperationManifest(manifest) {
|
|
1908
|
+
const dispatchSelection = manifest.dispatchSelection === void 0 ? void 0 : manifest.dispatchSelection.lane === "byok-profile" ? Object.freeze({
|
|
1909
|
+
...manifest.dispatchSelection,
|
|
1910
|
+
providerProfile: Object.freeze({
|
|
1911
|
+
...manifest.dispatchSelection.providerProfile,
|
|
1912
|
+
requiredCapabilities: Object.freeze([
|
|
1913
|
+
...manifest.dispatchSelection.providerProfile.requiredCapabilities
|
|
1914
|
+
])
|
|
1915
|
+
})
|
|
1916
|
+
}) : Object.freeze({ ...manifest.dispatchSelection });
|
|
1692
1917
|
return Object.freeze({
|
|
1693
1918
|
taskId: manifest.taskId,
|
|
1694
1919
|
runtimeId: manifest.runtimeId,
|
|
1695
1920
|
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
1696
1921
|
policy: frozenPolicy(manifest.policy),
|
|
1697
1922
|
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
1698
|
-
...
|
|
1923
|
+
...dispatchSelection === void 0 ? {} : { dispatchSelection },
|
|
1699
1924
|
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
1700
1925
|
...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
|
|
1701
1926
|
cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
|
|
@@ -3723,7 +3948,11 @@ function validatePiByokLauncherConfig(launcher) {
|
|
|
3723
3948
|
"--macos-keychain-path",
|
|
3724
3949
|
"--secret-service-prefix",
|
|
3725
3950
|
"--provider",
|
|
3726
|
-
"--model"
|
|
3951
|
+
"--model",
|
|
3952
|
+
"--profile-revision",
|
|
3953
|
+
"--profile-hash",
|
|
3954
|
+
"--required-capabilities",
|
|
3955
|
+
"--validate-only"
|
|
3727
3956
|
]);
|
|
3728
3957
|
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
3729
3958
|
if (conflicting !== void 0) {
|
|
@@ -3785,11 +4014,17 @@ var PiAdapter = class {
|
|
|
3785
4014
|
const bin = this.resolveBin();
|
|
3786
4015
|
const extensions = (this.options.resolveExtensions ?? resolvePiExtensions)();
|
|
3787
4016
|
const selection = input.offer.dispatchSelection;
|
|
3788
|
-
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({
|
|
4017
|
+
const pinnedSelection = selection === void 0 ? void 0 : selection.lane === "byok-profile" ? Object.freeze({
|
|
4018
|
+
...selection,
|
|
4019
|
+
providerProfile: Object.freeze({
|
|
4020
|
+
...selection.providerProfile,
|
|
4021
|
+
requiredCapabilities: Object.freeze([...selection.providerProfile.requiredCapabilities])
|
|
4022
|
+
})
|
|
4023
|
+
}) : Object.freeze({ ...selection });
|
|
3789
4024
|
let command = bin.command;
|
|
3790
4025
|
let launcherArgs;
|
|
3791
4026
|
if (pinnedSelection !== void 0) {
|
|
3792
|
-
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
4027
|
+
if (pinnedSelection.lane !== "byok" && pinnedSelection.lane !== "byok-profile" || pinnedSelection.runtimeId !== "pi") {
|
|
3793
4028
|
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
3794
4029
|
}
|
|
3795
4030
|
const launcher = this.options.byokLauncher;
|
|
@@ -3797,6 +4032,21 @@ var PiAdapter = class {
|
|
|
3797
4032
|
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
3798
4033
|
}
|
|
3799
4034
|
command = launcher.command;
|
|
4035
|
+
const providerProfile = pinnedSelection.lane === "byok-profile" ? pinnedSelection.providerProfile : void 0;
|
|
4036
|
+
if (providerProfile !== void 0) {
|
|
4037
|
+
try {
|
|
4038
|
+
await (this.options.validateProviderProfileBinding ?? validateProviderProfileBindingWithLauncher)(
|
|
4039
|
+
providerProfile,
|
|
4040
|
+
launcher
|
|
4041
|
+
);
|
|
4042
|
+
} catch (error) {
|
|
4043
|
+
return {
|
|
4044
|
+
kind: "reject",
|
|
4045
|
+
reason: `provider profile admission failed: ${errorMessage2(error)}`,
|
|
4046
|
+
retryable: false
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
3800
4050
|
launcherArgs = [
|
|
3801
4051
|
...launcher.args ?? [],
|
|
3802
4052
|
"--pi-bin",
|
|
@@ -3808,9 +4058,19 @@ var PiAdapter = class {
|
|
|
3808
4058
|
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
3809
4059
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
3810
4060
|
"--provider",
|
|
3811
|
-
pinnedSelection.providerId,
|
|
4061
|
+
providerProfile?.profileRef ?? (pinnedSelection.lane === "byok" ? pinnedSelection.providerId : ""),
|
|
3812
4062
|
"--model",
|
|
3813
|
-
pinnedSelection.modelId
|
|
4063
|
+
providerProfile?.modelId ?? (pinnedSelection.lane === "byok" ? pinnedSelection.modelId : ""),
|
|
4064
|
+
...providerProfile === void 0 ? [] : [
|
|
4065
|
+
"--profile-revision",
|
|
4066
|
+
providerProfile.profileRevision,
|
|
4067
|
+
"--profile-hash",
|
|
4068
|
+
providerProfile.profileHash,
|
|
4069
|
+
"--required-capabilities",
|
|
4070
|
+
JSON.stringify(providerProfile.requiredCapabilities),
|
|
4071
|
+
"--validate-only",
|
|
4072
|
+
"false"
|
|
4073
|
+
]
|
|
3814
4074
|
];
|
|
3815
4075
|
}
|
|
3816
4076
|
return {
|
|
@@ -3953,8 +4213,42 @@ var PiAdapter = class {
|
|
|
3953
4213
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
3954
4214
|
}
|
|
3955
4215
|
};
|
|
4216
|
+
async function validateProviderProfileBindingWithLauncher(binding, launcher) {
|
|
4217
|
+
await execFileAsync(launcher.command, [
|
|
4218
|
+
...launcher.args ?? [],
|
|
4219
|
+
"--pi-bin",
|
|
4220
|
+
process.execPath,
|
|
4221
|
+
"--profile-db",
|
|
4222
|
+
launcher.profileDbPath,
|
|
4223
|
+
"--session-dir",
|
|
4224
|
+
launcher.sessionDir,
|
|
4225
|
+
"--provider",
|
|
4226
|
+
binding.profileRef,
|
|
4227
|
+
"--model",
|
|
4228
|
+
binding.modelId,
|
|
4229
|
+
"--profile-revision",
|
|
4230
|
+
binding.profileRevision,
|
|
4231
|
+
"--profile-hash",
|
|
4232
|
+
binding.profileHash,
|
|
4233
|
+
"--required-capabilities",
|
|
4234
|
+
JSON.stringify(binding.requiredCapabilities),
|
|
4235
|
+
"--validate-only",
|
|
4236
|
+
"true",
|
|
4237
|
+
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
4238
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : []
|
|
4239
|
+
], { timeout: DETECT_TIMEOUT_MS });
|
|
4240
|
+
}
|
|
3956
4241
|
function sameDispatchSelection(left, right) {
|
|
3957
4242
|
if (left === void 0 || right === void 0) return left === right;
|
|
4243
|
+
if (left.lane === "byok-profile" || right.lane === "byok-profile") {
|
|
4244
|
+
if (left.lane !== "byok-profile" || right.lane !== "byok-profile") return false;
|
|
4245
|
+
if (left.runtimeId !== right.runtimeId) return false;
|
|
4246
|
+
const leftProfile = left.providerProfile;
|
|
4247
|
+
const rightProfile = right.providerProfile;
|
|
4248
|
+
return leftProfile.profileRef === rightProfile.profileRef && leftProfile.profileRevision === rightProfile.profileRevision && leftProfile.profileHash === rightProfile.profileHash && leftProfile.modelId === rightProfile.modelId && leftProfile.requiredCapabilities.length === rightProfile.requiredCapabilities.length && leftProfile.requiredCapabilities.every(
|
|
4249
|
+
(capability, index) => capability === rightProfile.requiredCapabilities[index]
|
|
4250
|
+
);
|
|
4251
|
+
}
|
|
3958
4252
|
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
3959
4253
|
}
|
|
3960
4254
|
async function resolveAuthoritativeSessionId(rpc) {
|
|
@@ -6371,9 +6665,18 @@ function assertRecord(value) {
|
|
|
6371
6665
|
throw new DeviceCredentialStoreError("OS credential entry has an incomplete device credential");
|
|
6372
6666
|
}
|
|
6373
6667
|
}
|
|
6374
|
-
function
|
|
6375
|
-
|
|
6376
|
-
|
|
6668
|
+
function isFirstPairingAttempt(value) {
|
|
6669
|
+
return "kind" in value && value.kind === "first-pairing-attempt-v1";
|
|
6670
|
+
}
|
|
6671
|
+
function assertFirstPairingAttempt(value) {
|
|
6672
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value.kind !== "first-pairing-attempt-v1" || typeof value.deviceName !== "string" || typeof value.devicePublicKey !== "string" || typeof value.devicePrivateKeyPem !== "string" || value.machineId !== void 0 && typeof value.machineId !== "string" || value.deviceName.length === 0 || value.devicePublicKey.length === 0 || value.devicePrivateKeyPem.length === 0) {
|
|
6673
|
+
throw new DeviceCredentialStoreError("OS credential entry has an invalid first-pairing attempt shape");
|
|
6674
|
+
}
|
|
6675
|
+
}
|
|
6676
|
+
function encode(authority) {
|
|
6677
|
+
if (isFirstPairingAttempt(authority)) assertFirstPairingAttempt(authority);
|
|
6678
|
+
else assertRecord(authority);
|
|
6679
|
+
const encoded = Buffer.from(JSON.stringify(authority), "utf8").toString("base64");
|
|
6377
6680
|
const value = `${ENCODED_PREFIX}${encoded}`;
|
|
6378
6681
|
if (Buffer.byteLength(value, "utf8") > 2400) {
|
|
6379
6682
|
throw new DeviceCredentialStoreError("device credential exceeds the OS credential entry bound");
|
|
@@ -6402,6 +6705,16 @@ function decode(value) {
|
|
|
6402
6705
|
} catch {
|
|
6403
6706
|
throw new DeviceCredentialStoreError("OS credential entry is not valid JSON");
|
|
6404
6707
|
}
|
|
6708
|
+
if (typeof parsed === "object" && parsed !== null && parsed.kind === "first-pairing-attempt-v1") {
|
|
6709
|
+
assertFirstPairingAttempt(parsed);
|
|
6710
|
+
return Object.freeze({
|
|
6711
|
+
kind: parsed.kind,
|
|
6712
|
+
deviceName: parsed.deviceName,
|
|
6713
|
+
devicePublicKey: parsed.devicePublicKey,
|
|
6714
|
+
devicePrivateKeyPem: parsed.devicePrivateKeyPem,
|
|
6715
|
+
...parsed.machineId === void 0 ? {} : { machineId: parsed.machineId }
|
|
6716
|
+
});
|
|
6717
|
+
}
|
|
6405
6718
|
assertRecord(parsed);
|
|
6406
6719
|
return Object.freeze({
|
|
6407
6720
|
deviceId: parsed.deviceId,
|
|
@@ -6432,6 +6745,18 @@ var DeviceCredentialStore = class {
|
|
|
6432
6745
|
this.#run = options.commandRunner ?? runDeviceCommand;
|
|
6433
6746
|
}
|
|
6434
6747
|
async read() {
|
|
6748
|
+
const authority = await this.#readAuthority();
|
|
6749
|
+
return authority === void 0 || isFirstPairingAttempt(authority) ? void 0 : authority;
|
|
6750
|
+
}
|
|
6751
|
+
async readFirstPairingAttempt() {
|
|
6752
|
+
const authority = await this.#readAuthority();
|
|
6753
|
+
return authority !== void 0 && isFirstPairingAttempt(authority) ? authority : void 0;
|
|
6754
|
+
}
|
|
6755
|
+
async saveFirstPairingAttempt(attempt) {
|
|
6756
|
+
assertFirstPairingAttempt(attempt);
|
|
6757
|
+
await this.#replaceAuthority(attempt);
|
|
6758
|
+
}
|
|
6759
|
+
async #readAuthority() {
|
|
6435
6760
|
const result = await this.#invoke("read");
|
|
6436
6761
|
if (result.exitCode === NOT_FOUND || this.#platform === "linux" && result.exitCode === 1 && result.stderr.trim().length === 0) return void 0;
|
|
6437
6762
|
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
@@ -6444,6 +6769,12 @@ var DeviceCredentialStore = class {
|
|
|
6444
6769
|
}
|
|
6445
6770
|
async replace(record) {
|
|
6446
6771
|
const encoded = encode(record);
|
|
6772
|
+
await this.#replaceEncoded(encoded);
|
|
6773
|
+
}
|
|
6774
|
+
async #replaceAuthority(authority) {
|
|
6775
|
+
await this.#replaceEncoded(encode(authority));
|
|
6776
|
+
}
|
|
6777
|
+
async #replaceEncoded(encoded) {
|
|
6447
6778
|
const result = await this.#invoke("replace", encoded);
|
|
6448
6779
|
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
6449
6780
|
if (result.exitCode !== 0) {
|
|
@@ -6454,7 +6785,7 @@ var DeviceCredentialStore = class {
|
|
|
6454
6785
|
}
|
|
6455
6786
|
/** Returns true only after the sole secret authority is confirmed absent. */
|
|
6456
6787
|
async clear() {
|
|
6457
|
-
const before = await this
|
|
6788
|
+
const before = await this.#readAuthority();
|
|
6458
6789
|
if (before === void 0) return false;
|
|
6459
6790
|
const result = await this.#invoke("clear");
|
|
6460
6791
|
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
@@ -6463,7 +6794,7 @@ var DeviceCredentialStore = class {
|
|
|
6463
6794
|
`operating-system credential provider could not clear device credentials${providerDiagnostic(result.stderr)}`
|
|
6464
6795
|
);
|
|
6465
6796
|
}
|
|
6466
|
-
if (await this
|
|
6797
|
+
if (await this.#readAuthority() !== void 0) {
|
|
6467
6798
|
throw new DeviceCredentialStoreError("operating-system credential provider reported deletion but device credentials remain");
|
|
6468
6799
|
}
|
|
6469
6800
|
return true;
|
|
@@ -6538,17 +6869,24 @@ var DeviceCredentialStore = class {
|
|
|
6538
6869
|
}
|
|
6539
6870
|
};
|
|
6540
6871
|
var InMemoryDeviceCredentialStore = class {
|
|
6541
|
-
#
|
|
6872
|
+
#authority;
|
|
6542
6873
|
async read() {
|
|
6543
|
-
return this.#
|
|
6874
|
+
return this.#authority === void 0 || isFirstPairingAttempt(this.#authority) ? void 0 : Object.freeze({ ...this.#authority });
|
|
6875
|
+
}
|
|
6876
|
+
async readFirstPairingAttempt() {
|
|
6877
|
+
return this.#authority !== void 0 && isFirstPairingAttempt(this.#authority) ? Object.freeze({ ...this.#authority }) : void 0;
|
|
6878
|
+
}
|
|
6879
|
+
async saveFirstPairingAttempt(attempt) {
|
|
6880
|
+
assertFirstPairingAttempt(attempt);
|
|
6881
|
+
this.#authority = Object.freeze({ ...attempt });
|
|
6544
6882
|
}
|
|
6545
6883
|
async replace(record) {
|
|
6546
6884
|
assertRecord(record);
|
|
6547
|
-
this.#
|
|
6885
|
+
this.#authority = Object.freeze({ ...record });
|
|
6548
6886
|
}
|
|
6549
6887
|
async clear() {
|
|
6550
|
-
const had = this.#
|
|
6551
|
-
this.#
|
|
6888
|
+
const had = this.#authority !== void 0;
|
|
6889
|
+
this.#authority = void 0;
|
|
6552
6890
|
return had;
|
|
6553
6891
|
}
|
|
6554
6892
|
};
|
|
@@ -6803,23 +7141,22 @@ function signNonce(privateKey, nonce) {
|
|
|
6803
7141
|
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
6804
7142
|
return signature.toString("base64url");
|
|
6805
7143
|
}
|
|
7144
|
+
|
|
7145
|
+
// src/daemon/url.ts
|
|
6806
7146
|
function toHttpBase(serverUrl) {
|
|
6807
7147
|
const url = new URL(serverUrl);
|
|
6808
|
-
if (url.protocol === "ws:") url.protocol = "http:";
|
|
6809
|
-
else if (url.protocol === "wss:") url.protocol = "https:";
|
|
6810
7148
|
url.pathname = "/";
|
|
6811
7149
|
url.search = "";
|
|
6812
7150
|
return url.toString();
|
|
6813
7151
|
}
|
|
6814
|
-
function toWsUrl(serverUrl) {
|
|
6815
|
-
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
6816
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
6817
|
-
return url.toString();
|
|
6818
|
-
}
|
|
6819
7152
|
function describeEndpoint(transport, url) {
|
|
6820
7153
|
const parsed = typeof url === "string" ? new URL(url) : url;
|
|
6821
7154
|
return { transport, host: parsed.host, path: parsed.pathname };
|
|
6822
7155
|
}
|
|
7156
|
+
function formatServerUrl(value) {
|
|
7157
|
+
const url = typeof value === "string" ? new URL(value) : value;
|
|
7158
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
7159
|
+
}
|
|
6823
7160
|
var InsecureServerUrlError = class extends Error {
|
|
6824
7161
|
constructor(message) {
|
|
6825
7162
|
super(message);
|
|
@@ -6838,22 +7175,21 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
|
|
|
6838
7175
|
let url;
|
|
6839
7176
|
try {
|
|
6840
7177
|
url = new URL(rawUrl);
|
|
6841
|
-
} catch
|
|
6842
|
-
throw new InsecureServerUrlError(
|
|
7178
|
+
} catch {
|
|
7179
|
+
throw new InsecureServerUrlError("invalid server URL");
|
|
6843
7180
|
}
|
|
7181
|
+
const endpoint = formatServerUrl(url);
|
|
6844
7182
|
switch (url.protocol) {
|
|
6845
7183
|
case "https:":
|
|
6846
|
-
case "wss:":
|
|
6847
7184
|
return;
|
|
6848
7185
|
case "http:":
|
|
6849
|
-
case "ws:":
|
|
6850
7186
|
if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
|
|
6851
7187
|
throw new InsecureServerUrlError(
|
|
6852
|
-
`refusing to connect to "${
|
|
7188
|
+
`refusing to connect to "${endpoint}" over plaintext ${url.protocol.replace(":", "")} \u2014 "${url.hostname}" is not a loopback host. Use https: for any non-loopback server, or pass { dangerouslyAllowInsecureRemote: true } (DaemonConfig) if you understand and accept the risk of sending device credentials over an unencrypted connection.`
|
|
6853
7189
|
);
|
|
6854
7190
|
default:
|
|
6855
7191
|
throw new InsecureServerUrlError(
|
|
6856
|
-
`refusing to connect to "${
|
|
7192
|
+
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http: or https:).`
|
|
6857
7193
|
);
|
|
6858
7194
|
}
|
|
6859
7195
|
}
|
|
@@ -6865,12 +7201,22 @@ var DeviceRevokedError = class extends Error {
|
|
|
6865
7201
|
this.name = "DeviceRevokedError";
|
|
6866
7202
|
}
|
|
6867
7203
|
};
|
|
7204
|
+
var AuthRequestAbortedError = class extends Error {
|
|
7205
|
+
constructor(reason) {
|
|
7206
|
+
super(reason === "deadline" ? "authentication request exceeded its deadline" : "authentication request was cancelled during shutdown");
|
|
7207
|
+
this.reason = reason;
|
|
7208
|
+
this.name = "AuthRequestAbortedError";
|
|
7209
|
+
}
|
|
7210
|
+
reason;
|
|
7211
|
+
};
|
|
6868
7212
|
var ASSUMED_PAIR_TOKEN_TTL_MS = 45 * 60 * 1e3;
|
|
6869
7213
|
var RENEW_MARGIN_MS = 60 * 1e3;
|
|
7214
|
+
var DEFAULT_AUTH_REQUEST_DEADLINE_MS = 15e3;
|
|
6870
7215
|
var AuthManager = class {
|
|
6871
7216
|
constructor(opts) {
|
|
6872
7217
|
this.opts = opts;
|
|
6873
7218
|
this.credentials = opts.credentials ?? opts.store.credentials;
|
|
7219
|
+
this.requestDeadlineMs = resolveRequestDeadlineMs(opts.authRequestDeadlineMs);
|
|
6874
7220
|
}
|
|
6875
7221
|
opts;
|
|
6876
7222
|
record;
|
|
@@ -6880,7 +7226,10 @@ var AuthManager = class {
|
|
|
6880
7226
|
stopped = false;
|
|
6881
7227
|
pairing = false;
|
|
6882
7228
|
credentialMutationTail = Promise.resolve();
|
|
7229
|
+
/** The sole cancellation authority for the request currently inside the serialized credential mutation. */
|
|
7230
|
+
activeRequest;
|
|
6883
7231
|
credentials;
|
|
7232
|
+
requestDeadlineMs;
|
|
6884
7233
|
get deviceId() {
|
|
6885
7234
|
return this.record?.deviceId;
|
|
6886
7235
|
}
|
|
@@ -6916,23 +7265,51 @@ var AuthManager = class {
|
|
|
6916
7265
|
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
6917
7266
|
}
|
|
6918
7267
|
}
|
|
6919
|
-
const
|
|
7268
|
+
const observedDeviceName = this.opts.deviceName ?? os__default.hostname();
|
|
7269
|
+
const observedMachineId = await this.opts.machineId?.();
|
|
7270
|
+
let keyPair;
|
|
7271
|
+
let pairingDeviceName = observedDeviceName;
|
|
7272
|
+
let pairingMachineId = observedMachineId;
|
|
7273
|
+
if (existing) {
|
|
7274
|
+
keyPair = { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey };
|
|
7275
|
+
} else {
|
|
7276
|
+
const firstAttempt = await this.credentials.readFirstPairingAttempt();
|
|
7277
|
+
if (firstAttempt) {
|
|
7278
|
+
keyPair = {
|
|
7279
|
+
privateKey: importPrivateKeyPem(firstAttempt.devicePrivateKeyPem),
|
|
7280
|
+
publicKeyBase64Url: firstAttempt.devicePublicKey
|
|
7281
|
+
};
|
|
7282
|
+
pairingDeviceName = firstAttempt.deviceName;
|
|
7283
|
+
pairingMachineId = firstAttempt.machineId;
|
|
7284
|
+
} else {
|
|
7285
|
+
keyPair = generateDeviceKeyPair();
|
|
7286
|
+
await this.credentials.saveFirstPairingAttempt({
|
|
7287
|
+
kind: "first-pairing-attempt-v1",
|
|
7288
|
+
deviceName: pairingDeviceName,
|
|
7289
|
+
devicePublicKey: keyPair.publicKeyBase64Url,
|
|
7290
|
+
devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey),
|
|
7291
|
+
...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
|
|
7292
|
+
});
|
|
7293
|
+
}
|
|
7294
|
+
}
|
|
6920
7295
|
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
6921
|
-
const
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
7296
|
+
const body = await this.runRequest(async (signal) => {
|
|
7297
|
+
const res = await fetch(url, {
|
|
7298
|
+
method: "POST",
|
|
7299
|
+
headers: { "content-type": "application/json" },
|
|
7300
|
+
body: JSON.stringify({
|
|
7301
|
+
pairingCode,
|
|
7302
|
+
deviceName: pairingDeviceName,
|
|
7303
|
+
devicePublicKey: keyPair.publicKeyBase64Url,
|
|
7304
|
+
...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
|
|
7305
|
+
}),
|
|
7306
|
+
signal
|
|
7307
|
+
});
|
|
7308
|
+
if (!res.ok) {
|
|
7309
|
+
throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
|
|
7310
|
+
}
|
|
7311
|
+
return PairResponseSchema.parse(await res.json());
|
|
6931
7312
|
});
|
|
6932
|
-
if (!res.ok) {
|
|
6933
|
-
throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
|
|
6934
|
-
}
|
|
6935
|
-
const body = PairResponseSchema.parse(await res.json());
|
|
6936
7313
|
const metadata = {
|
|
6937
7314
|
deviceId: body.deviceId,
|
|
6938
7315
|
tenantId: body.tenantId,
|
|
@@ -6971,6 +7348,7 @@ var AuthManager = class {
|
|
|
6971
7348
|
this.stopped = true;
|
|
6972
7349
|
if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
|
|
6973
7350
|
this.proactiveTimer = void 0;
|
|
7351
|
+
this.activeRequest?.abort();
|
|
6974
7352
|
await this.credentialMutationTail;
|
|
6975
7353
|
}
|
|
6976
7354
|
async renew() {
|
|
@@ -6987,29 +7365,35 @@ var AuthManager = class {
|
|
|
6987
7365
|
const record = this.record;
|
|
6988
7366
|
const base = toHttpBase(this.opts.serverUrl);
|
|
6989
7367
|
const privateKey = importPrivateKeyPem(record.devicePrivateKeyPem);
|
|
6990
|
-
const
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
7368
|
+
const { nonce } = await this.runRequest(async (signal) => {
|
|
7369
|
+
const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
|
|
7370
|
+
method: "POST",
|
|
7371
|
+
headers: { "content-type": "application/json" },
|
|
7372
|
+
body: JSON.stringify({ deviceId: record.deviceId }),
|
|
7373
|
+
signal
|
|
7374
|
+
});
|
|
7375
|
+
if (challengeRes.status === 401) this.markRevoked();
|
|
7376
|
+
if (!challengeRes.ok) {
|
|
7377
|
+
throw new Error(
|
|
7378
|
+
`token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
|
|
7379
|
+
);
|
|
7380
|
+
}
|
|
7381
|
+
return await challengeRes.json();
|
|
6994
7382
|
});
|
|
6995
|
-
if (challengeRes.status === 401) this.markRevoked();
|
|
6996
|
-
if (!challengeRes.ok) {
|
|
6997
|
-
throw new Error(
|
|
6998
|
-
`token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
|
|
6999
|
-
);
|
|
7000
|
-
}
|
|
7001
|
-
const { nonce } = await challengeRes.json();
|
|
7002
7383
|
const signature = signNonce(privateKey, nonce);
|
|
7003
|
-
const
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7384
|
+
const body = await this.runRequest(async (signal) => {
|
|
7385
|
+
const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
|
|
7386
|
+
method: "POST",
|
|
7387
|
+
headers: { "content-type": "application/json" },
|
|
7388
|
+
body: JSON.stringify({ deviceId: record.deviceId, nonce, signature }),
|
|
7389
|
+
signal
|
|
7390
|
+
});
|
|
7391
|
+
if (tokenRes.status === 401) this.markRevoked();
|
|
7392
|
+
if (!tokenRes.ok) {
|
|
7393
|
+
throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
|
|
7394
|
+
}
|
|
7395
|
+
return await tokenRes.json();
|
|
7007
7396
|
});
|
|
7008
|
-
if (tokenRes.status === 401) this.markRevoked();
|
|
7009
|
-
if (!tokenRes.ok) {
|
|
7010
|
-
throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
|
|
7011
|
-
}
|
|
7012
|
-
const body = await tokenRes.json();
|
|
7013
7397
|
const updated = {
|
|
7014
7398
|
...record,
|
|
7015
7399
|
accessToken: body.accessToken,
|
|
@@ -7039,6 +7423,37 @@ var AuthManager = class {
|
|
|
7039
7423
|
timer.unref?.();
|
|
7040
7424
|
this.proactiveTimer = timer;
|
|
7041
7425
|
}
|
|
7426
|
+
/**
|
|
7427
|
+
* Bounds one complete auth exchange rather than fetch alone. Keeping the
|
|
7428
|
+
* controller active through `json()`/`text()` makes a non-cooperative or
|
|
7429
|
+
* partial response body cancellable by the same authority that owns fetch.
|
|
7430
|
+
*/
|
|
7431
|
+
async runRequest(operation) {
|
|
7432
|
+
if (this.stopped) throw new AuthRequestAbortedError("stopped");
|
|
7433
|
+
const controller = new AbortController();
|
|
7434
|
+
this.activeRequest = controller;
|
|
7435
|
+
let deadlineElapsed = false;
|
|
7436
|
+
const deadline = setTimeout(() => {
|
|
7437
|
+
deadlineElapsed = true;
|
|
7438
|
+
controller.abort();
|
|
7439
|
+
}, this.requestDeadlineMs);
|
|
7440
|
+
let rejectOnAbort;
|
|
7441
|
+
const aborted = new Promise((_, reject) => {
|
|
7442
|
+
rejectOnAbort = reject;
|
|
7443
|
+
});
|
|
7444
|
+
const onAbort = () => rejectOnAbort(new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped"));
|
|
7445
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
7446
|
+
try {
|
|
7447
|
+
return await Promise.race([operation(controller.signal), aborted]);
|
|
7448
|
+
} catch (error) {
|
|
7449
|
+
if (controller.signal.aborted) throw new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped");
|
|
7450
|
+
throw error;
|
|
7451
|
+
} finally {
|
|
7452
|
+
clearTimeout(deadline);
|
|
7453
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
7454
|
+
if (this.activeRequest === controller) this.activeRequest = void 0;
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7042
7457
|
async runCredentialMutation(operation) {
|
|
7043
7458
|
const predecessor = this.credentialMutationTail;
|
|
7044
7459
|
let release;
|
|
@@ -7081,6 +7496,13 @@ function resolvePairExpiry(refreshHint) {
|
|
|
7081
7496
|
}
|
|
7082
7497
|
return new Date(Date.now() + ASSUMED_PAIR_TOKEN_TTL_MS).toISOString();
|
|
7083
7498
|
}
|
|
7499
|
+
function resolveRequestDeadlineMs(value) {
|
|
7500
|
+
const deadline = value ?? DEFAULT_AUTH_REQUEST_DEADLINE_MS;
|
|
7501
|
+
if (!Number.isSafeInteger(deadline) || deadline <= 0) {
|
|
7502
|
+
throw new Error("authRequestDeadlineMs must be a positive safe integer");
|
|
7503
|
+
}
|
|
7504
|
+
return deadline;
|
|
7505
|
+
}
|
|
7084
7506
|
async function safeErrorText(res) {
|
|
7085
7507
|
try {
|
|
7086
7508
|
return await res.text();
|
|
@@ -7102,37 +7524,48 @@ function withAuth(init, token) {
|
|
|
7102
7524
|
}
|
|
7103
7525
|
|
|
7104
7526
|
// src/daemon/blob-client.ts
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7527
|
+
var BlobRequestAbortedError = class extends Error {
|
|
7528
|
+
constructor(reason) {
|
|
7529
|
+
super(reason === "deadline" ? "blob request deadline elapsed" : "blob request cancelled");
|
|
7530
|
+
this.reason = reason;
|
|
7531
|
+
this.name = "BlobRequestAbortedError";
|
|
7110
7532
|
}
|
|
7111
|
-
|
|
7533
|
+
reason;
|
|
7534
|
+
};
|
|
7112
7535
|
var BlobClient = class {
|
|
7113
|
-
constructor(serverUrl, auth) {
|
|
7536
|
+
constructor(serverUrl, auth, options = {}) {
|
|
7114
7537
|
this.serverUrl = serverUrl;
|
|
7115
7538
|
this.auth = auth;
|
|
7539
|
+
this.options = options;
|
|
7540
|
+
const requestDeadlineMs = options.requestDeadlineMs ?? 15e3;
|
|
7541
|
+
if (!Number.isSafeInteger(requestDeadlineMs) || requestDeadlineMs <= 0) {
|
|
7542
|
+
throw new Error("BlobClient requestDeadlineMs must be a positive safe integer");
|
|
7543
|
+
}
|
|
7544
|
+
this.requestDeadlineMs = requestDeadlineMs;
|
|
7116
7545
|
}
|
|
7117
7546
|
serverUrl;
|
|
7118
7547
|
auth;
|
|
7548
|
+
options;
|
|
7549
|
+
requestDeadlineMs;
|
|
7119
7550
|
/** `blobRef` -> `GET /byok/blobs/:id/url` -> fetch the presigned download URL -> text content. Always resolves fresh rather than trusting any inlined `BlobRef.url`, per docs/protocol.md §7. */
|
|
7120
|
-
async resolveInstruction(blobRef) {
|
|
7551
|
+
async resolveInstruction(blobRef, options = {}) {
|
|
7121
7552
|
const base = toHttpBase(this.serverUrl);
|
|
7122
|
-
const urlRes = await
|
|
7123
|
-
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
7124
|
-
|
|
7125
|
-
this.auth
|
|
7553
|
+
const urlRes = await this.#request(
|
|
7554
|
+
(signal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal }, this.auth),
|
|
7555
|
+
options.signal
|
|
7126
7556
|
);
|
|
7127
7557
|
if (!urlRes.ok) {
|
|
7128
|
-
throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await
|
|
7558
|
+
throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, options.signal)}`.trimEnd());
|
|
7559
|
+
}
|
|
7560
|
+
const { downloadUrl } = await this.#readJson(urlRes, options.signal);
|
|
7561
|
+
if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
|
|
7562
|
+
throw new Error("failed to resolve blob download url: response omitted downloadUrl");
|
|
7129
7563
|
}
|
|
7130
|
-
const
|
|
7131
|
-
const contentRes = await fetch(new URL(downloadUrl, base));
|
|
7564
|
+
const contentRes = await this.#request((signal) => fetch(new URL(downloadUrl, base), { signal }), options.signal);
|
|
7132
7565
|
if (!contentRes.ok) {
|
|
7133
7566
|
throw new Error(`failed to download blob content: HTTP ${contentRes.status}`);
|
|
7134
7567
|
}
|
|
7135
|
-
const bytes =
|
|
7568
|
+
const bytes = await this.#readBody(contentRes, options.signal);
|
|
7136
7569
|
const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7137
7570
|
if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
|
|
7138
7571
|
throw new Error(
|
|
@@ -7141,63 +7574,71 @@ var BlobClient = class {
|
|
|
7141
7574
|
}
|
|
7142
7575
|
return new TextDecoder().decode(bytes);
|
|
7143
7576
|
}
|
|
7144
|
-
/** `POST /byok/blobs`
|
|
7577
|
+
/** `POST /byok/blobs` -> PUT the bytes to the presigned URL -> finalize into a `BlobRef`. */
|
|
7145
7578
|
async uploadArtifact(content, contentType, options = {}) {
|
|
7146
7579
|
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
7147
7580
|
const contentHash2 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7148
7581
|
const base = toHttpBase(this.serverUrl);
|
|
7149
7582
|
const reservationId = options.idempotencyKey ?? `blob_${randomUUID()}`;
|
|
7150
|
-
const createRes = await
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
"content-type": "application/json",
|
|
7156
|
-
|
|
7583
|
+
const createRes = await this.#request(
|
|
7584
|
+
(signal) => authedFetch(
|
|
7585
|
+
new URL(BYOK_BLOBS_PATH, base),
|
|
7586
|
+
{
|
|
7587
|
+
method: "POST",
|
|
7588
|
+
headers: { "content-type": "application/json", "idempotency-key": reservationId },
|
|
7589
|
+
body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash2 }),
|
|
7590
|
+
signal
|
|
7157
7591
|
},
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7592
|
+
this.auth
|
|
7593
|
+
),
|
|
7594
|
+
options.signal
|
|
7161
7595
|
);
|
|
7162
7596
|
if (!createRes.ok) {
|
|
7163
|
-
throw new Error(`failed to create blob: HTTP ${createRes.status} ${await
|
|
7597
|
+
throw new Error(`failed to create blob: HTTP ${createRes.status} ${await this.#safeErrorText(createRes, options.signal)}`.trimEnd());
|
|
7598
|
+
}
|
|
7599
|
+
const { blobId, uploadUrl } = await this.#readJson(createRes, options.signal);
|
|
7600
|
+
if (typeof blobId !== "string" || blobId.length === 0 || typeof uploadUrl !== "string" || uploadUrl.length === 0) {
|
|
7601
|
+
throw new Error("failed to create blob: response omitted blobId or uploadUrl");
|
|
7164
7602
|
}
|
|
7165
|
-
const { blobId, uploadUrl } = await createRes.json();
|
|
7166
7603
|
const blobRef = { blobId, contentHash: contentHash2, size: bytes.length, contentType };
|
|
7167
|
-
if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef)) {
|
|
7604
|
+
if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef, options.signal)) {
|
|
7168
7605
|
return blobRef;
|
|
7169
7606
|
}
|
|
7170
|
-
const putRes = await
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7607
|
+
const putRes = await this.#request(
|
|
7608
|
+
(signal) => fetch(new URL(uploadUrl, base), {
|
|
7609
|
+
method: "PUT",
|
|
7610
|
+
headers: { "content-type": contentType },
|
|
7611
|
+
body: bytes,
|
|
7612
|
+
signal
|
|
7613
|
+
}),
|
|
7614
|
+
options.signal
|
|
7615
|
+
);
|
|
7175
7616
|
if (!putRes.ok) {
|
|
7176
7617
|
throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
|
|
7177
7618
|
}
|
|
7178
|
-
|
|
7619
|
+
this.#throwIfAborted(options.signal);
|
|
7620
|
+
await this.#finalize(base, blobId, reservationId, options.signal);
|
|
7179
7621
|
return blobRef;
|
|
7180
7622
|
}
|
|
7181
|
-
async #hasExactCommittedBlob(base, blobRef) {
|
|
7182
|
-
const urlRes = await
|
|
7183
|
-
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
7184
|
-
|
|
7185
|
-
this.auth
|
|
7623
|
+
async #hasExactCommittedBlob(base, blobRef, signal) {
|
|
7624
|
+
const urlRes = await this.#request(
|
|
7625
|
+
(requestSignal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal: requestSignal }, this.auth),
|
|
7626
|
+
signal
|
|
7186
7627
|
);
|
|
7187
7628
|
if (urlRes.status === 404) return false;
|
|
7188
7629
|
if (!urlRes.ok) {
|
|
7189
|
-
throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await
|
|
7630
|
+
throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, signal)}`.trimEnd());
|
|
7190
7631
|
}
|
|
7191
|
-
const { downloadUrl } = await urlRes
|
|
7632
|
+
const { downloadUrl } = await this.#readJson(urlRes, signal);
|
|
7192
7633
|
if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
|
|
7193
7634
|
throw new Error("failed to read back idempotent blob: response omitted downloadUrl");
|
|
7194
7635
|
}
|
|
7195
|
-
const contentRes = await fetch(new URL(downloadUrl, base));
|
|
7636
|
+
const contentRes = await this.#request((requestSignal) => fetch(new URL(downloadUrl, base), { signal: requestSignal }), signal);
|
|
7196
7637
|
if (contentRes.status === 404) return false;
|
|
7197
7638
|
if (!contentRes.ok) {
|
|
7198
7639
|
throw new Error(`failed to read back idempotent blob content: HTTP ${contentRes.status}`);
|
|
7199
7640
|
}
|
|
7200
|
-
const bytes =
|
|
7641
|
+
const bytes = await this.#readBody(contentRes, signal);
|
|
7201
7642
|
const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7202
7643
|
if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
|
|
7203
7644
|
throw new Error(
|
|
@@ -7206,18 +7647,19 @@ var BlobClient = class {
|
|
|
7206
7647
|
}
|
|
7207
7648
|
return true;
|
|
7208
7649
|
}
|
|
7209
|
-
async #finalize(base, blobId, reservationId) {
|
|
7650
|
+
async #finalize(base, blobId, reservationId, signal) {
|
|
7210
7651
|
let lastFailure;
|
|
7211
7652
|
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
7653
|
+
this.#throwIfAborted(signal);
|
|
7212
7654
|
let response;
|
|
7213
7655
|
try {
|
|
7214
|
-
response = await
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
method: "POST",
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7656
|
+
response = await this.#request(
|
|
7657
|
+
(requestSignal) => authedFetch(
|
|
7658
|
+
new URL(byokBlobFinalizePath(blobId), base),
|
|
7659
|
+
{ method: "POST", headers: { "idempotency-key": reservationId }, signal: requestSignal },
|
|
7660
|
+
this.auth
|
|
7661
|
+
),
|
|
7662
|
+
signal
|
|
7221
7663
|
);
|
|
7222
7664
|
} catch (error) {
|
|
7223
7665
|
lastFailure = error;
|
|
@@ -7227,13 +7669,105 @@ var BlobClient = class {
|
|
|
7227
7669
|
if (response.ok) return;
|
|
7228
7670
|
if (response.status < 500 || attempt === 2) {
|
|
7229
7671
|
throw new Error(
|
|
7230
|
-
`failed to finalize blob: HTTP ${response.status} ${await
|
|
7672
|
+
`failed to finalize blob: HTTP ${response.status} ${await this.#safeErrorText(response, signal)}`.trimEnd()
|
|
7231
7673
|
);
|
|
7232
7674
|
}
|
|
7233
7675
|
lastFailure = new Error(`failed to finalize blob: HTTP ${response.status}`);
|
|
7234
7676
|
}
|
|
7235
7677
|
throw lastFailure;
|
|
7236
7678
|
}
|
|
7679
|
+
async #readJson(res, signal) {
|
|
7680
|
+
return JSON.parse(await this.#readText(res, signal));
|
|
7681
|
+
}
|
|
7682
|
+
async #safeErrorText(res, signal) {
|
|
7683
|
+
try {
|
|
7684
|
+
return await this.#readText(res, signal);
|
|
7685
|
+
} catch (error) {
|
|
7686
|
+
if (error instanceof BlobRequestAbortedError) throw error;
|
|
7687
|
+
return "";
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
async #readText(res, signal) {
|
|
7691
|
+
return new TextDecoder().decode(await this.#readBody(res, signal));
|
|
7692
|
+
}
|
|
7693
|
+
/**
|
|
7694
|
+
* `Response.arrayBuffer()` only leaves cancellation observable through the
|
|
7695
|
+
* Fetch implementation. Read the body directly so our lifecycle/deadline
|
|
7696
|
+
* authority cancels the actual stream even when a fetch implementation has
|
|
7697
|
+
* already resolved at headers.
|
|
7698
|
+
*/
|
|
7699
|
+
async #readBody(res, signal) {
|
|
7700
|
+
return this.#request(async (requestSignal) => {
|
|
7701
|
+
if (res.body === null) return new Uint8Array();
|
|
7702
|
+
const reader = res.body.getReader();
|
|
7703
|
+
const cancelBody = () => {
|
|
7704
|
+
void reader.cancel().catch(() => void 0);
|
|
7705
|
+
};
|
|
7706
|
+
requestSignal.addEventListener("abort", cancelBody, { once: true });
|
|
7707
|
+
try {
|
|
7708
|
+
const chunks = [];
|
|
7709
|
+
let length = 0;
|
|
7710
|
+
for (; ; ) {
|
|
7711
|
+
const { done, value } = await reader.read();
|
|
7712
|
+
if (done) break;
|
|
7713
|
+
chunks.push(value);
|
|
7714
|
+
length += value.byteLength;
|
|
7715
|
+
}
|
|
7716
|
+
const bytes = new Uint8Array(length);
|
|
7717
|
+
let offset = 0;
|
|
7718
|
+
for (const chunk of chunks) {
|
|
7719
|
+
bytes.set(chunk, offset);
|
|
7720
|
+
offset += chunk.byteLength;
|
|
7721
|
+
}
|
|
7722
|
+
return bytes;
|
|
7723
|
+
} finally {
|
|
7724
|
+
requestSignal.removeEventListener("abort", cancelBody);
|
|
7725
|
+
reader.releaseLock();
|
|
7726
|
+
}
|
|
7727
|
+
}, signal);
|
|
7728
|
+
}
|
|
7729
|
+
#throwIfAborted(signal) {
|
|
7730
|
+
if (this.options.signal?.aborted || signal?.aborted) {
|
|
7731
|
+
throw new BlobRequestAbortedError("cancelled");
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
async #request(request, signal) {
|
|
7735
|
+
this.#throwIfAborted(signal);
|
|
7736
|
+
const controller = new AbortController();
|
|
7737
|
+
let abortReason = "cancelled";
|
|
7738
|
+
const abort = (reason) => {
|
|
7739
|
+
if (controller.signal.aborted) return;
|
|
7740
|
+
abortReason = reason;
|
|
7741
|
+
controller.abort();
|
|
7742
|
+
};
|
|
7743
|
+
const inheritedSignals = [this.options.signal, signal].filter((value) => value !== void 0);
|
|
7744
|
+
const abortForCancellation = () => abort("cancelled");
|
|
7745
|
+
for (const inheritedSignal of inheritedSignals) {
|
|
7746
|
+
inheritedSignal.addEventListener("abort", abortForCancellation, { once: true });
|
|
7747
|
+
}
|
|
7748
|
+
const deadline = setTimeout(() => abort("deadline"), this.requestDeadlineMs);
|
|
7749
|
+
deadline.unref?.();
|
|
7750
|
+
let rejectAbort = () => {
|
|
7751
|
+
};
|
|
7752
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
7753
|
+
rejectAbort = reject;
|
|
7754
|
+
});
|
|
7755
|
+
const rejectOnAbort = () => rejectAbort(new BlobRequestAbortedError(abortReason));
|
|
7756
|
+
controller.signal.addEventListener("abort", rejectOnAbort, { once: true });
|
|
7757
|
+
try {
|
|
7758
|
+
return await Promise.race([request(controller.signal), aborted]);
|
|
7759
|
+
} catch (error) {
|
|
7760
|
+
if (error instanceof BlobRequestAbortedError) throw error;
|
|
7761
|
+
if (controller.signal.aborted) throw new BlobRequestAbortedError(abortReason);
|
|
7762
|
+
throw error;
|
|
7763
|
+
} finally {
|
|
7764
|
+
clearTimeout(deadline);
|
|
7765
|
+
controller.signal.removeEventListener("abort", rejectOnAbort);
|
|
7766
|
+
for (const inheritedSignal of inheritedSignals) {
|
|
7767
|
+
inheritedSignal.removeEventListener("abort", abortForCancellation);
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7237
7771
|
};
|
|
7238
7772
|
var DEFAULT_TIMEOUT_MS2 = 2e3;
|
|
7239
7773
|
var DARWIN_UUID_RE = /"IOPlatformUUID"\s*=\s*"([^"]+)"/u;
|
|
@@ -7480,6 +8014,7 @@ var AnotherControlServerRunningError = class extends Error {
|
|
|
7480
8014
|
}
|
|
7481
8015
|
};
|
|
7482
8016
|
var MAX_HALF_OPEN_CONNECTIONS = 8;
|
|
8017
|
+
var MAX_OUTBOUND_QUEUE_BYTES = 1024 * 1024;
|
|
7483
8018
|
function errorMessage4(err) {
|
|
7484
8019
|
return err instanceof Error ? err.message : String(err);
|
|
7485
8020
|
}
|
|
@@ -7556,11 +8091,12 @@ async function bindControlEndpoint(server, endpoint) {
|
|
|
7556
8091
|
}
|
|
7557
8092
|
function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshakeSettled) {
|
|
7558
8093
|
const reader = new NdjsonLineReader();
|
|
7559
|
-
const
|
|
8094
|
+
const activeRequests = /* @__PURE__ */ new Map();
|
|
7560
8095
|
let phase = "client-hello";
|
|
7561
8096
|
let serverNonce = "";
|
|
7562
8097
|
let destroyed = false;
|
|
7563
8098
|
let handshakeSettled = false;
|
|
8099
|
+
let disposeOutboundWriter;
|
|
7564
8100
|
function settleHandshake() {
|
|
7565
8101
|
if (handshakeSettled) return;
|
|
7566
8102
|
handshakeSettled = true;
|
|
@@ -7570,8 +8106,83 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
|
|
|
7570
8106
|
if (phase !== "ready") socket.destroy();
|
|
7571
8107
|
}, handshakeTimeoutMs);
|
|
7572
8108
|
handshakeTimer.unref?.();
|
|
8109
|
+
function terminateConnection() {
|
|
8110
|
+
if (destroyed) return;
|
|
8111
|
+
destroyed = true;
|
|
8112
|
+
clearTimeout(handshakeTimer);
|
|
8113
|
+
settleHandshake();
|
|
8114
|
+
disposeOutboundWriter?.();
|
|
8115
|
+
const requests = [...activeRequests.values()];
|
|
8116
|
+
activeRequests.clear();
|
|
8117
|
+
for (const request of requests) {
|
|
8118
|
+
if (request.kind === "stream" && !request.controller.signal.aborted) request.controller.abort();
|
|
8119
|
+
}
|
|
8120
|
+
if (!socket.destroyed) socket.destroy();
|
|
8121
|
+
}
|
|
8122
|
+
const queuedFrames = [];
|
|
8123
|
+
let queuedBytes = 0;
|
|
8124
|
+
let blocked = false;
|
|
8125
|
+
let terminal = false;
|
|
8126
|
+
let drainListening = false;
|
|
8127
|
+
function disposeQueuedFrames() {
|
|
8128
|
+
terminal = true;
|
|
8129
|
+
blocked = false;
|
|
8130
|
+
queuedFrames.length = 0;
|
|
8131
|
+
queuedBytes = 0;
|
|
8132
|
+
if (drainListening) {
|
|
8133
|
+
socket.removeListener("drain", onDrain);
|
|
8134
|
+
drainListening = false;
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
function waitForDrain() {
|
|
8138
|
+
if (drainListening || terminal) return;
|
|
8139
|
+
drainListening = true;
|
|
8140
|
+
socket.once("drain", onDrain);
|
|
8141
|
+
}
|
|
8142
|
+
function writeEncodedFrame(encoded) {
|
|
8143
|
+
if (terminal) return;
|
|
8144
|
+
if (!socket.writable) {
|
|
8145
|
+
terminateConnection();
|
|
8146
|
+
return;
|
|
8147
|
+
}
|
|
8148
|
+
try {
|
|
8149
|
+
if (!socket.write(encoded)) {
|
|
8150
|
+
blocked = true;
|
|
8151
|
+
waitForDrain();
|
|
8152
|
+
}
|
|
8153
|
+
} catch {
|
|
8154
|
+
terminateConnection();
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
8157
|
+
function onDrain() {
|
|
8158
|
+
drainListening = false;
|
|
8159
|
+
if (terminal) return;
|
|
8160
|
+
blocked = false;
|
|
8161
|
+
while (!blocked && queuedFrames.length > 0) {
|
|
8162
|
+
const frame = queuedFrames.shift();
|
|
8163
|
+
queuedBytes -= frame.bytes;
|
|
8164
|
+
writeEncodedFrame(frame.encoded);
|
|
8165
|
+
}
|
|
8166
|
+
}
|
|
8167
|
+
disposeOutboundWriter = disposeQueuedFrames;
|
|
7573
8168
|
function sendFrame(frame) {
|
|
7574
|
-
if (
|
|
8169
|
+
if (destroyed || terminal) return;
|
|
8170
|
+
const encoded = encodeFrame(frame);
|
|
8171
|
+
const bytes = Buffer.byteLength(encoded);
|
|
8172
|
+
if (bytes > MAX_OUTBOUND_QUEUE_BYTES) {
|
|
8173
|
+
terminateConnection();
|
|
8174
|
+
return;
|
|
8175
|
+
}
|
|
8176
|
+
if (blocked) {
|
|
8177
|
+
if (queuedBytes + bytes > MAX_OUTBOUND_QUEUE_BYTES) {
|
|
8178
|
+
terminateConnection();
|
|
8179
|
+
return;
|
|
8180
|
+
}
|
|
8181
|
+
queuedFrames.push({ encoded, bytes });
|
|
8182
|
+
queuedBytes += bytes;
|
|
8183
|
+
return;
|
|
8184
|
+
}
|
|
8185
|
+
writeEncodedFrame(encoded);
|
|
7575
8186
|
}
|
|
7576
8187
|
function handleClientHello(parsed) {
|
|
7577
8188
|
const hello = parseClientHello(parsed);
|
|
@@ -7594,21 +8205,51 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
|
|
|
7594
8205
|
settleHandshake();
|
|
7595
8206
|
sendFrame({ v: 1, ready: true });
|
|
7596
8207
|
}
|
|
8208
|
+
function rejectDuplicateRequest(id) {
|
|
8209
|
+
sendFrame({
|
|
8210
|
+
v: 1,
|
|
8211
|
+
id,
|
|
8212
|
+
ok: false,
|
|
8213
|
+
error: { code: "duplicate_request_id", message: `request id "${id}" is already active` }
|
|
8214
|
+
});
|
|
8215
|
+
}
|
|
8216
|
+
function registerRequest(id, record) {
|
|
8217
|
+
if (activeRequests.has(id)) {
|
|
8218
|
+
rejectDuplicateRequest(id);
|
|
8219
|
+
return false;
|
|
8220
|
+
}
|
|
8221
|
+
activeRequests.set(id, record);
|
|
8222
|
+
return true;
|
|
8223
|
+
}
|
|
8224
|
+
function releaseRequest(id, record) {
|
|
8225
|
+
if (activeRequests.get(id) !== record) return false;
|
|
8226
|
+
activeRequests.delete(id);
|
|
8227
|
+
return true;
|
|
8228
|
+
}
|
|
7597
8229
|
function dispatch(id, method, params) {
|
|
7598
8230
|
const unary = methods.unary[method];
|
|
7599
8231
|
if (unary) {
|
|
7600
|
-
|
|
8232
|
+
const record = { kind: "unary" };
|
|
8233
|
+
if (!registerRequest(id, record)) return;
|
|
8234
|
+
Promise.resolve().then(() => unary(params)).then((result) => {
|
|
8235
|
+
if (!releaseRequest(id, record)) return;
|
|
8236
|
+
sendFrame({ v: 1, id, ok: true, result });
|
|
8237
|
+
}).catch((err) => {
|
|
8238
|
+
if (!releaseRequest(id, record)) return;
|
|
8239
|
+
sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
|
|
8240
|
+
});
|
|
7601
8241
|
return;
|
|
7602
8242
|
}
|
|
7603
8243
|
const stream = methods.stream[method];
|
|
7604
8244
|
if (stream) {
|
|
7605
8245
|
const controller = new AbortController();
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
8246
|
+
const record = { kind: "stream", controller };
|
|
8247
|
+
if (!registerRequest(id, record)) return;
|
|
8248
|
+
Promise.resolve().then(() => stream(params, { emit: (event) => sendFrame({ v: 1, id, event }), signal: controller.signal })).then(() => {
|
|
8249
|
+
if (!releaseRequest(id, record)) return;
|
|
7609
8250
|
if (!controller.signal.aborted) sendFrame({ v: 1, id, ok: true, done: true });
|
|
7610
8251
|
}).catch((err) => {
|
|
7611
|
-
|
|
8252
|
+
if (!releaseRequest(id, record)) return;
|
|
7612
8253
|
sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
|
|
7613
8254
|
});
|
|
7614
8255
|
return;
|
|
@@ -7650,13 +8291,10 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
|
|
|
7650
8291
|
for (const line of lines) handleLine(line);
|
|
7651
8292
|
});
|
|
7652
8293
|
socket.on("error", () => {
|
|
8294
|
+
terminateConnection();
|
|
7653
8295
|
});
|
|
7654
8296
|
socket.on("close", () => {
|
|
7655
|
-
|
|
7656
|
-
clearTimeout(handshakeTimer);
|
|
7657
|
-
settleHandshake();
|
|
7658
|
-
for (const controller of activeStreams.values()) controller.abort();
|
|
7659
|
-
activeStreams.clear();
|
|
8297
|
+
terminateConnection();
|
|
7660
8298
|
});
|
|
7661
8299
|
}
|
|
7662
8300
|
async function startControlServer(opts) {
|
|
@@ -8574,6 +9212,20 @@ function createFleetJitter(productId, deviceId) {
|
|
|
8574
9212
|
delay: (domain, sequence, baseMs) => deterministicJitterMs({ seed, domain, sequence, baseMs })
|
|
8575
9213
|
};
|
|
8576
9214
|
}
|
|
9215
|
+
|
|
9216
|
+
// src/daemon/replay-cursor.ts
|
|
9217
|
+
var ReplayCursorTooOldError = class extends Error {
|
|
9218
|
+
constructor(recoverableFrom) {
|
|
9219
|
+
super(
|
|
9220
|
+
recoverableFrom === void 0 ? "server cannot replay the acknowledged cursor; re-pair or operator recovery is required" : `server cannot replay the acknowledged cursor; retained history starts at ${recoverableFrom}`
|
|
9221
|
+
);
|
|
9222
|
+
this.recoverableFrom = recoverableFrom;
|
|
9223
|
+
this.name = "ReplayCursorTooOldError";
|
|
9224
|
+
}
|
|
9225
|
+
recoverableFrom;
|
|
9226
|
+
};
|
|
9227
|
+
|
|
9228
|
+
// src/daemon/long-poll-transport.ts
|
|
8577
9229
|
var LongPollRouteError = class extends Error {
|
|
8578
9230
|
constructor(endpoint, status, cause) {
|
|
8579
9231
|
super(
|
|
@@ -8589,7 +9241,7 @@ var LongPollRouteError = class extends Error {
|
|
|
8589
9241
|
};
|
|
8590
9242
|
var MAX_TRACKED_VALIDATION_FAILURE_WARNINGS = 1e3;
|
|
8591
9243
|
var MAX_TRACKED_ROUTE_FAILURE_WARNINGS = 1e3;
|
|
8592
|
-
function parseLooseEventsPollResponse(raw) {
|
|
9244
|
+
function parseLooseEventsPollResponse(raw, requestedCursor) {
|
|
8593
9245
|
if (typeof raw !== "object" || raw === null) {
|
|
8594
9246
|
throw new Error("events poll response is not an object");
|
|
8595
9247
|
}
|
|
@@ -8597,19 +9249,48 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
8597
9249
|
if (!Array.isArray(events)) {
|
|
8598
9250
|
throw new Error("events poll response.events is not an array");
|
|
8599
9251
|
}
|
|
8600
|
-
if (
|
|
8601
|
-
throw new Error("events poll response.cursor is not
|
|
9252
|
+
if (!isSafeNonnegativeInteger(cursor)) {
|
|
9253
|
+
throw new Error("events poll response.cursor is not a safe nonnegative integer");
|
|
9254
|
+
}
|
|
9255
|
+
if (cursor < requestedCursor) {
|
|
9256
|
+
throw new Error("events poll response.cursor regressed below the requested cursor");
|
|
8602
9257
|
}
|
|
8603
9258
|
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
8604
9259
|
throw new Error("events poll response.capabilities is not an array of strings");
|
|
8605
9260
|
}
|
|
8606
9261
|
return { events, cursor, capabilities: capabilities ?? [] };
|
|
8607
9262
|
}
|
|
9263
|
+
function isSafeNonnegativeInteger(value) {
|
|
9264
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
9265
|
+
}
|
|
9266
|
+
function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
|
|
9267
|
+
let previousTaskSeq;
|
|
9268
|
+
for (const raw of events) {
|
|
9269
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
9270
|
+
const { type, seq } = raw;
|
|
9271
|
+
if (typeof type !== "string" || !type.startsWith("task.")) continue;
|
|
9272
|
+
if (!isSafeNonnegativeInteger(seq)) {
|
|
9273
|
+
throw new Error("events poll task seq is not a safe nonnegative integer");
|
|
9274
|
+
}
|
|
9275
|
+
if (seq <= requestedCursor || seq > pageCursor) {
|
|
9276
|
+
throw new Error("events poll task seq lies outside the trusted page cursor range");
|
|
9277
|
+
}
|
|
9278
|
+
if (previousTaskSeq !== void 0 && seq <= previousTaskSeq) {
|
|
9279
|
+
throw new Error("events poll task seq is not strictly page ordered");
|
|
9280
|
+
}
|
|
9281
|
+
const knownTaskType = MESSAGE_TYPES.includes(type);
|
|
9282
|
+
const predecessor = previousTaskSeq ?? requestedCursor;
|
|
9283
|
+
if (!knownTaskType && seq !== predecessor + 1) {
|
|
9284
|
+
throw new Error("events poll unknown task seq would cross an untrusted gap");
|
|
9285
|
+
}
|
|
9286
|
+
previousTaskSeq = seq;
|
|
9287
|
+
}
|
|
9288
|
+
}
|
|
8608
9289
|
function extractSkippableSeq(raw) {
|
|
8609
9290
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
8610
9291
|
const { type, seq } = raw;
|
|
8611
9292
|
if (typeof type !== "string" || !type.startsWith("task.")) return void 0;
|
|
8612
|
-
return
|
|
9293
|
+
return isSafeNonnegativeInteger(seq) ? seq : void 0;
|
|
8613
9294
|
}
|
|
8614
9295
|
var LongPollClient = class {
|
|
8615
9296
|
constructor(opts) {
|
|
@@ -8620,6 +9301,8 @@ var LongPollClient = class {
|
|
|
8620
9301
|
}
|
|
8621
9302
|
opts;
|
|
8622
9303
|
running = false;
|
|
9304
|
+
/** Owns exactly one active loop generation, including its held GET and retry delays. */
|
|
9305
|
+
loopAbortController;
|
|
8623
9306
|
/**
|
|
8624
9307
|
* Finding R1: seqs this loop has already `console.warn`'d about for a
|
|
8625
9308
|
* validation-failed (recognized-type, invalid-payload) entry — a poison
|
|
@@ -8679,16 +9362,22 @@ var LongPollClient = class {
|
|
|
8679
9362
|
noteRevoked(err) {
|
|
8680
9363
|
if (!(err instanceof DeviceRevokedError)) return false;
|
|
8681
9364
|
this.running = false;
|
|
9365
|
+
this.loopAbortController?.abort();
|
|
8682
9366
|
this.opts.onRevoked?.();
|
|
8683
9367
|
return true;
|
|
8684
9368
|
}
|
|
8685
9369
|
start() {
|
|
8686
9370
|
if (this.running) return;
|
|
8687
9371
|
this.running = true;
|
|
8688
|
-
|
|
9372
|
+
const controller = new AbortController();
|
|
9373
|
+
this.loopAbortController = controller;
|
|
9374
|
+
void this.loop(controller.signal).finally(() => {
|
|
9375
|
+
if (this.loopAbortController === controller) this.loopAbortController = void 0;
|
|
9376
|
+
});
|
|
8689
9377
|
}
|
|
8690
9378
|
stop() {
|
|
8691
9379
|
this.running = false;
|
|
9380
|
+
this.loopAbortController?.abort();
|
|
8692
9381
|
}
|
|
8693
9382
|
/**
|
|
8694
9383
|
* POST one batch of envelopes to `/byok/messages` (finding F6/protocol
|
|
@@ -8697,14 +9386,14 @@ var LongPollClient = class {
|
|
|
8697
9386
|
* (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
|
|
8698
9387
|
* envelope `id`s — the caller must never rebuild them) is deduped
|
|
8699
9388
|
* server-side into a safe no-op rather than reprocessed (§9). Returns
|
|
8700
|
-
*
|
|
9389
|
+
* validated frozen-v1 counts only after a readable response body.
|
|
8701
9390
|
*/
|
|
8702
9391
|
async postBatch(envelopes) {
|
|
8703
9392
|
try {
|
|
8704
9393
|
await this.opts.auth.getValidAccessToken();
|
|
8705
9394
|
} catch (err) {
|
|
8706
9395
|
this.noteRevoked(err);
|
|
8707
|
-
return
|
|
9396
|
+
return void 0;
|
|
8708
9397
|
}
|
|
8709
9398
|
let res;
|
|
8710
9399
|
try {
|
|
@@ -8720,23 +9409,26 @@ var LongPollClient = class {
|
|
|
8720
9409
|
);
|
|
8721
9410
|
} catch (err) {
|
|
8722
9411
|
if (!this.noteRevoked(err)) this.warnRouteFailure(this.messagesEndpoint, void 0, err);
|
|
8723
|
-
return
|
|
9412
|
+
return void 0;
|
|
8724
9413
|
}
|
|
8725
9414
|
if (!res.ok) {
|
|
8726
9415
|
this.warnRouteFailure(this.messagesEndpoint, res.status, void 0);
|
|
8727
|
-
return
|
|
9416
|
+
return void 0;
|
|
8728
9417
|
}
|
|
8729
9418
|
try {
|
|
8730
|
-
MessagesSendResponseSchema.parse(await res.json());
|
|
9419
|
+
const response = MessagesSendResponseSchema.parse(await res.json());
|
|
9420
|
+
if (response.accepted + (response.rejected ?? 0) !== envelopes.length) {
|
|
9421
|
+
throw new Error("messages response counts do not match the posted batch length");
|
|
9422
|
+
}
|
|
9423
|
+
return response;
|
|
8731
9424
|
} catch (err) {
|
|
8732
9425
|
this.warnRouteFailure(this.messagesEndpoint, res.status, err);
|
|
8733
|
-
return
|
|
9426
|
+
return void 0;
|
|
8734
9427
|
}
|
|
8735
|
-
return true;
|
|
8736
9428
|
}
|
|
8737
|
-
async loop() {
|
|
9429
|
+
async loop(signal) {
|
|
8738
9430
|
let retryAttempt = 0;
|
|
8739
|
-
while (this.running) {
|
|
9431
|
+
while (this.running && !signal.aborted) {
|
|
8740
9432
|
try {
|
|
8741
9433
|
await this.opts.auth.getValidAccessToken();
|
|
8742
9434
|
const base = toHttpBase(this.opts.serverUrl);
|
|
@@ -8745,30 +9437,40 @@ var LongPollClient = class {
|
|
|
8745
9437
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
8746
9438
|
let res;
|
|
8747
9439
|
try {
|
|
8748
|
-
res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
9440
|
+
res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
|
|
8749
9441
|
} catch (err) {
|
|
9442
|
+
if (signal.aborted) return;
|
|
8750
9443
|
if (!(err instanceof DeviceRevokedError)) {
|
|
8751
9444
|
this.warnRouteFailure(this.eventsEndpoint, void 0, err);
|
|
8752
9445
|
}
|
|
8753
9446
|
throw err;
|
|
8754
9447
|
}
|
|
8755
9448
|
if (!res.ok) {
|
|
9449
|
+
const replayCursorTooOld = await parseReplayCursorTooOld(res);
|
|
9450
|
+
if (replayCursorTooOld) {
|
|
9451
|
+
this.running = false;
|
|
9452
|
+
this.opts.onReplayCursorTooOld?.(replayCursorTooOld);
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
8756
9455
|
this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
|
|
8757
|
-
this.opts.
|
|
9456
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
9457
|
+
this.opts.onPollFailure?.();
|
|
8758
9458
|
this.opts.onOperationalOutcome?.("failure");
|
|
8759
9459
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
8760
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
9460
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
8761
9461
|
continue;
|
|
8762
9462
|
}
|
|
8763
9463
|
let parsed;
|
|
8764
9464
|
try {
|
|
8765
|
-
parsed = parseLooseEventsPollResponse(await res.json());
|
|
9465
|
+
parsed = parseLooseEventsPollResponse(await res.json(), cursor ?? 0);
|
|
9466
|
+
validateTrustedEventsPage(parsed.events, cursor ?? 0, parsed.cursor);
|
|
8766
9467
|
} catch (err) {
|
|
8767
9468
|
this.warnRouteFailure(this.eventsEndpoint, res.status, err);
|
|
8768
9469
|
throw err;
|
|
8769
9470
|
}
|
|
8770
9471
|
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
8771
9472
|
let hadValidationFailureThisBatch = false;
|
|
9473
|
+
let acceptedAnyEntry = false;
|
|
8772
9474
|
for (const raw of parsed.events) {
|
|
8773
9475
|
let envelope;
|
|
8774
9476
|
try {
|
|
@@ -8776,7 +9478,10 @@ var LongPollClient = class {
|
|
|
8776
9478
|
} catch (err) {
|
|
8777
9479
|
if (err instanceof UnknownMessageTypeError) {
|
|
8778
9480
|
const skippableSeq = extractSkippableSeq(raw);
|
|
8779
|
-
if (skippableSeq !== void 0)
|
|
9481
|
+
if (skippableSeq !== void 0) {
|
|
9482
|
+
this.opts.onSkippedSeq?.(skippableSeq);
|
|
9483
|
+
acceptedAnyEntry = true;
|
|
9484
|
+
}
|
|
8780
9485
|
} else {
|
|
8781
9486
|
const failedSeq = extractSkippableSeq(raw);
|
|
8782
9487
|
if (failedSeq !== void 0) {
|
|
@@ -8796,45 +9501,65 @@ var LongPollClient = class {
|
|
|
8796
9501
|
}
|
|
8797
9502
|
continue;
|
|
8798
9503
|
}
|
|
8799
|
-
this.opts.onEnvelope(envelope);
|
|
9504
|
+
if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
|
|
8800
9505
|
}
|
|
8801
9506
|
if (parsed.events.length === 0) {
|
|
8802
9507
|
retryAttempt = 0;
|
|
8803
9508
|
this.opts.onOperationalOutcome?.("success");
|
|
8804
|
-
await sleep(this.opts.idleDelayMs ?? 250);
|
|
8805
|
-
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch) {
|
|
9509
|
+
await sleep(this.opts.idleDelayMs ?? 250, signal);
|
|
9510
|
+
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor) {
|
|
8806
9511
|
this.opts.onOperationalOutcome?.("failure");
|
|
8807
9512
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
8808
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
9513
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
8809
9514
|
} else {
|
|
8810
9515
|
retryAttempt = 0;
|
|
8811
9516
|
this.opts.onOperationalOutcome?.("success");
|
|
8812
9517
|
}
|
|
8813
9518
|
} catch (err) {
|
|
8814
|
-
this.opts.
|
|
9519
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
8815
9520
|
if (this.noteRevoked(err)) return;
|
|
8816
|
-
if (!this.running) return;
|
|
9521
|
+
if (!this.running || signal.aborted) return;
|
|
9522
|
+
this.opts.onPollFailure?.();
|
|
8817
9523
|
this.opts.onOperationalOutcome?.("failure");
|
|
8818
9524
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
8819
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
9525
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
8820
9526
|
}
|
|
8821
9527
|
}
|
|
8822
9528
|
}
|
|
8823
9529
|
};
|
|
8824
|
-
function
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
this.endpoint = endpoint;
|
|
8832
|
-
this.name = "WsUnexpectedStatusError";
|
|
9530
|
+
async function parseReplayCursorTooOld(res) {
|
|
9531
|
+
if (res.status !== 409) return void 0;
|
|
9532
|
+
let body;
|
|
9533
|
+
try {
|
|
9534
|
+
body = await res.json();
|
|
9535
|
+
} catch {
|
|
9536
|
+
return void 0;
|
|
8833
9537
|
}
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
9538
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
9539
|
+
const { error, recoverableFrom } = body;
|
|
9540
|
+
if (error !== "cursor_too_old" || typeof recoverableFrom !== "number" || !Number.isSafeInteger(recoverableFrom) || recoverableFrom < 0) {
|
|
9541
|
+
return void 0;
|
|
9542
|
+
}
|
|
9543
|
+
return new ReplayCursorTooOldError(recoverableFrom);
|
|
9544
|
+
}
|
|
9545
|
+
function sleep(ms, signal) {
|
|
9546
|
+
if (signal.aborted) return Promise.resolve();
|
|
9547
|
+
return new Promise((resolve) => {
|
|
9548
|
+
const finish = () => {
|
|
9549
|
+
clearTimeout(timer);
|
|
9550
|
+
signal.removeEventListener("abort", finish);
|
|
9551
|
+
resolve();
|
|
9552
|
+
};
|
|
9553
|
+
const timer = setTimeout(finish, ms);
|
|
9554
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
9555
|
+
});
|
|
9556
|
+
}
|
|
9557
|
+
|
|
9558
|
+
// src/daemon/connection-manager.ts
|
|
9559
|
+
function isCursorEnvelopeType(type) {
|
|
9560
|
+
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
9561
|
+
}
|
|
9562
|
+
function createConnectionHelloEnvelope(opts, getCursor) {
|
|
8838
9563
|
const configuredToolsets = opts.getConfiguredToolsets?.();
|
|
8839
9564
|
return createEnvelope("conn.hello", {
|
|
8840
9565
|
protocolVersions: [PROTOCOL_VERSION],
|
|
@@ -8844,222 +9569,34 @@ function createConnectionHelloEnvelope(opts) {
|
|
|
8844
9569
|
clientVersion: opts.clientVersion,
|
|
8845
9570
|
runtimes: opts.runtimes,
|
|
8846
9571
|
configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
|
|
8847
|
-
cursor:
|
|
9572
|
+
cursor: getCursor()
|
|
8848
9573
|
});
|
|
8849
9574
|
}
|
|
8850
|
-
var
|
|
8851
|
-
constructor(opts) {
|
|
8852
|
-
this.opts = opts;
|
|
8853
|
-
}
|
|
8854
|
-
opts;
|
|
8855
|
-
socket;
|
|
8856
|
-
closedByUser = false;
|
|
8857
|
-
autoReconnect = true;
|
|
8858
|
-
acked = false;
|
|
8859
|
-
everAckedThisAttempt = false;
|
|
8860
|
-
reconnectAttempt = 0;
|
|
8861
|
-
reconnectTimer;
|
|
8862
|
-
livenessTimer;
|
|
8863
|
-
lastActivity = 0;
|
|
8864
|
-
lastUnexpectedStatus;
|
|
8865
|
-
ackWaiters = [];
|
|
8866
|
-
connect(opts = {}) {
|
|
8867
|
-
this.autoReconnect = opts.auto ?? true;
|
|
8868
|
-
this.closedByUser = false;
|
|
8869
|
-
void this.openSocket();
|
|
8870
|
-
}
|
|
8871
|
-
/** Stop scheduling further reconnect attempts (any pending one is cancelled too); the current socket, if any, is left alone. Used when handing retry ownership to a slower external cadence (e.g. the long-poll fallback's periodic WS probe). */
|
|
8872
|
-
stopAutoReconnect() {
|
|
8873
|
-
this.autoReconnect = false;
|
|
8874
|
-
if (this.reconnectTimer) {
|
|
8875
|
-
clearTimeout(this.reconnectTimer);
|
|
8876
|
-
this.reconnectTimer = void 0;
|
|
8877
|
-
}
|
|
8878
|
-
}
|
|
8879
|
-
/** Resume normal auto-reconnect-on-close behavior (does not itself trigger a connect — only affects future closes). */
|
|
8880
|
-
resumeAutoReconnect() {
|
|
8881
|
-
this.autoReconnect = true;
|
|
8882
|
-
}
|
|
8883
|
-
get isOpen() {
|
|
8884
|
-
return this.acked;
|
|
8885
|
-
}
|
|
8886
|
-
/**
|
|
8887
|
-
* Attempt to send ONE envelope right now; returns whether it actually went
|
|
8888
|
-
* out. `false` means the socket isn't currently open+acked — the caller
|
|
8889
|
-
* (`ConnectionManager.drainOutbox`, Design B/finding N4) owns re-queueing
|
|
8890
|
-
* and retrying later (e.g. on the next `onAcked`), since this transport no
|
|
8891
|
-
* longer buffers anything itself.
|
|
8892
|
-
*/
|
|
8893
|
-
sendNow(envelope) {
|
|
8894
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN && this.acked) {
|
|
8895
|
-
this.socket.send(encodeEnvelope(envelope));
|
|
8896
|
-
return true;
|
|
8897
|
-
}
|
|
8898
|
-
return false;
|
|
8899
|
-
}
|
|
8900
|
-
waitForAck(timeoutMs = 1e4) {
|
|
8901
|
-
if (this.acked) return Promise.resolve();
|
|
8902
|
-
return new Promise((resolve, reject) => {
|
|
8903
|
-
const timer = setTimeout(() => reject(new Error("Timed out waiting for conn.ack")), timeoutMs);
|
|
8904
|
-
this.ackWaiters.push({
|
|
8905
|
-
resolve: () => {
|
|
8906
|
-
clearTimeout(timer);
|
|
8907
|
-
resolve();
|
|
8908
|
-
},
|
|
8909
|
-
reject: (err) => {
|
|
8910
|
-
clearTimeout(timer);
|
|
8911
|
-
reject(err);
|
|
8912
|
-
}
|
|
8913
|
-
});
|
|
8914
|
-
});
|
|
8915
|
-
}
|
|
8916
|
-
close() {
|
|
8917
|
-
this.closedByUser = true;
|
|
8918
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
8919
|
-
this.socket?.close();
|
|
8920
|
-
}
|
|
8921
|
-
async openSocket() {
|
|
8922
|
-
const url = toWsUrl(this.opts.serverUrl);
|
|
8923
|
-
const endpoint = describeEndpoint("ws", url);
|
|
8924
|
-
this.acked = false;
|
|
8925
|
-
this.everAckedThisAttempt = false;
|
|
8926
|
-
this.opts.onStateChange?.("connecting");
|
|
8927
|
-
let token;
|
|
8928
|
-
try {
|
|
8929
|
-
token = await this.opts.getToken();
|
|
8930
|
-
} catch (err) {
|
|
8931
|
-
this.opts.onStateChange?.("closed");
|
|
8932
|
-
this.opts.onConnectOutcome?.(false, err, endpoint);
|
|
8933
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
8934
|
-
return;
|
|
8935
|
-
}
|
|
8936
|
-
const socket = new WebSocket(url, {
|
|
8937
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
8938
|
-
});
|
|
8939
|
-
this.socket = socket;
|
|
8940
|
-
this.lastActivity = Date.now();
|
|
8941
|
-
this.startLivenessCheck(socket);
|
|
8942
|
-
socket.on("open", () => {
|
|
8943
|
-
const hello = createConnectionHelloEnvelope(this.opts);
|
|
8944
|
-
socket.send(encodeEnvelope(hello));
|
|
8945
|
-
});
|
|
8946
|
-
socket.on("ping", () => {
|
|
8947
|
-
this.lastActivity = Date.now();
|
|
8948
|
-
});
|
|
8949
|
-
socket.on("message", (data, isBinary) => {
|
|
8950
|
-
this.lastActivity = Date.now();
|
|
8951
|
-
const bytes = toBytes(data);
|
|
8952
|
-
let envelope;
|
|
8953
|
-
try {
|
|
8954
|
-
envelope = decodeEnvelope(bytes);
|
|
8955
|
-
} catch {
|
|
8956
|
-
return;
|
|
8957
|
-
}
|
|
8958
|
-
if (envelope.type === "conn.ack") {
|
|
8959
|
-
this.reconnectAttempt = 0;
|
|
8960
|
-
this.acked = true;
|
|
8961
|
-
this.everAckedThisAttempt = true;
|
|
8962
|
-
this.opts.onStateChange?.("open");
|
|
8963
|
-
for (const waiter of this.ackWaiters.splice(0)) waiter.resolve();
|
|
8964
|
-
this.opts.onAcked?.(envelope.payload.capabilities);
|
|
8965
|
-
}
|
|
8966
|
-
this.opts.onEnvelope(envelope);
|
|
8967
|
-
});
|
|
8968
|
-
socket.on("close", () => {
|
|
8969
|
-
this.socket = void 0;
|
|
8970
|
-
this.stopLivenessCheck();
|
|
8971
|
-
this.opts.onStateChange?.("closed");
|
|
8972
|
-
const acked = this.everAckedThisAttempt;
|
|
8973
|
-
const status = this.lastUnexpectedStatus;
|
|
8974
|
-
this.lastUnexpectedStatus = void 0;
|
|
8975
|
-
this.opts.onConnectOutcome?.(
|
|
8976
|
-
acked,
|
|
8977
|
-
status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0,
|
|
8978
|
-
endpoint
|
|
8979
|
-
);
|
|
8980
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
8981
|
-
});
|
|
8982
|
-
socket.on("error", () => {
|
|
8983
|
-
});
|
|
8984
|
-
socket.on("unexpected-response", (_req, res) => {
|
|
8985
|
-
this.lastUnexpectedStatus = res.statusCode;
|
|
8986
|
-
res.resume();
|
|
8987
|
-
socket.terminate();
|
|
8988
|
-
});
|
|
8989
|
-
}
|
|
8990
|
-
scheduleReconnect() {
|
|
8991
|
-
const { baseMs = 1e3, maxMs = 3e4, factor = 2 } = this.opts.backoff ?? {};
|
|
8992
|
-
const delay5 = Math.min(maxMs, baseMs * factor ** this.reconnectAttempt);
|
|
8993
|
-
const jitter = this.opts.reconnectDelayMs?.(this.reconnectAttempt, delay5) ?? delay5;
|
|
8994
|
-
this.reconnectAttempt += 1;
|
|
8995
|
-
this.reconnectTimer = setTimeout(() => void this.openSocket(), jitter);
|
|
8996
|
-
}
|
|
8997
|
-
startLivenessCheck(socket) {
|
|
8998
|
-
const { timeoutMs = 75e3, checkIntervalMs = Math.max(1e3, Math.floor(timeoutMs / 3)) } = this.opts.liveness ?? {};
|
|
8999
|
-
this.livenessTimer = setInterval(() => {
|
|
9000
|
-
if (Date.now() - this.lastActivity > timeoutMs) {
|
|
9001
|
-
socket.terminate();
|
|
9002
|
-
}
|
|
9003
|
-
}, checkIntervalMs);
|
|
9004
|
-
this.livenessTimer.unref?.();
|
|
9005
|
-
}
|
|
9006
|
-
stopLivenessCheck() {
|
|
9007
|
-
if (this.livenessTimer) {
|
|
9008
|
-
clearInterval(this.livenessTimer);
|
|
9009
|
-
this.livenessTimer = void 0;
|
|
9010
|
-
}
|
|
9011
|
-
}
|
|
9012
|
-
};
|
|
9013
|
-
function toBytes(data, _isBinary) {
|
|
9014
|
-
if (Buffer.isBuffer(data)) return data;
|
|
9015
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
9016
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
9017
|
-
return Buffer.from(String(data), "utf8");
|
|
9018
|
-
}
|
|
9019
|
-
|
|
9020
|
-
// src/daemon/connection-manager.ts
|
|
9021
|
-
function isCursorEnvelopeType(type) {
|
|
9022
|
-
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
9023
|
-
}
|
|
9575
|
+
var MAX_REJECTED_OUTBOX_ENTRIES = 1e3;
|
|
9024
9576
|
var ConnectionManager = class {
|
|
9025
9577
|
constructor(opts) {
|
|
9026
9578
|
this.opts = opts;
|
|
9027
9579
|
this.fleetJitter = opts.fleetJitter ?? createFleetJitter(opts.productId, opts.deviceId);
|
|
9028
|
-
this.ws = new WsTransport({
|
|
9029
|
-
serverUrl: opts.serverUrl,
|
|
9030
|
-
getToken: () => opts.auth.getValidAccessToken(),
|
|
9031
|
-
deviceId: opts.deviceId,
|
|
9032
|
-
productId: opts.productId,
|
|
9033
|
-
capabilities: opts.capabilities,
|
|
9034
|
-
clientVersion: opts.clientVersion,
|
|
9035
|
-
runtimes: opts.runtimes,
|
|
9036
|
-
getConfiguredToolsets: opts.getConfiguredToolsets,
|
|
9037
|
-
getCursor: () => this.cursor,
|
|
9038
|
-
onEnvelope: (envelope) => this.deliver(envelope),
|
|
9039
|
-
onStateChange: (state) => {
|
|
9040
|
-
if (!this.stopped && !this.revoked) this.opts.onStateChange?.(state);
|
|
9041
|
-
},
|
|
9042
|
-
onAcked: (capabilities) => this.onAcked(capabilities),
|
|
9043
|
-
onConnectOutcome: (acked, err) => this.onWsOutcome(acked, err),
|
|
9044
|
-
backoff: opts.backoff,
|
|
9045
|
-
liveness: opts.liveness,
|
|
9046
|
-
reconnectDelayMs: (attempt, baseMs) => this.fleetJitter.delay("reconnect", attempt, baseMs)
|
|
9047
|
-
});
|
|
9048
9580
|
this.longPoll = new LongPollClient({
|
|
9049
9581
|
serverUrl: opts.serverUrl,
|
|
9050
9582
|
auth: opts.auth,
|
|
9051
|
-
//
|
|
9052
|
-
//
|
|
9053
|
-
//
|
|
9054
|
-
//
|
|
9055
|
-
//
|
|
9056
|
-
|
|
9057
|
-
getCursor: () => this.dedupWatermark(),
|
|
9583
|
+
// The long-poll query cursor is also the kernel's irreversible ack.
|
|
9584
|
+
// Only report the successfully processed cursor here. `deliveredSeq`
|
|
9585
|
+
// remains a local dedup watermark; using it on the wire would ack an
|
|
9586
|
+
// in-flight envelope before its handler settles and make a later
|
|
9587
|
+
// failure impossible to redeliver.
|
|
9588
|
+
getCursor: () => this.cursor,
|
|
9058
9589
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
9059
9590
|
onServerCapabilities: (capabilities) => {
|
|
9060
|
-
|
|
9591
|
+
this.serverCapabilities = capabilities;
|
|
9592
|
+
this.noteConnected();
|
|
9061
9593
|
},
|
|
9594
|
+
onServerCapabilitiesInvalidated: () => {
|
|
9595
|
+
this.serverCapabilities = [];
|
|
9596
|
+
},
|
|
9597
|
+
onPollFailure: () => this.noteDisconnected(),
|
|
9062
9598
|
onRevoked: () => this.enterRevoked(),
|
|
9599
|
+
onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
|
|
9063
9600
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
9064
9601
|
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
9065
9602
|
// unrecognized message type) still needs its cursor/watermark
|
|
@@ -9085,13 +9622,10 @@ var ConnectionManager = class {
|
|
|
9085
9622
|
}
|
|
9086
9623
|
opts;
|
|
9087
9624
|
fleetJitter;
|
|
9088
|
-
ws;
|
|
9089
9625
|
longPoll;
|
|
9090
|
-
mode = "ws";
|
|
9091
|
-
consecutiveFailures = 0;
|
|
9092
|
-
wsRetryTimer;
|
|
9093
|
-
wsProbeSequence = 0;
|
|
9094
9626
|
uploadRetryAttempt = 0;
|
|
9627
|
+
started = false;
|
|
9628
|
+
connected = false;
|
|
9095
9629
|
cursor;
|
|
9096
9630
|
/**
|
|
9097
9631
|
* Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
|
|
@@ -9113,10 +9647,9 @@ var ConnectionManager = class {
|
|
|
9113
9647
|
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
9114
9648
|
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
9115
9649
|
* dedup (see `deliver`/`noteDelivered`), independent of whether its
|
|
9116
|
-
* handler has even started, let alone succeeded. It exists so a
|
|
9117
|
-
*
|
|
9118
|
-
*
|
|
9119
|
-
* `handleOffer` is NOT idempotent and must never be re-pulled while a
|
|
9650
|
+
* handler has even started, let alone succeeded. It exists so a repeated
|
|
9651
|
+
* read at the durable cursor does not re-dispatch an envelope already in
|
|
9652
|
+
* flight — `handleOffer` must not start a second adapter session while a
|
|
9120
9653
|
* first attempt is still running. On WS this same field is written the
|
|
9121
9654
|
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
9122
9655
|
* once, it never has an observable effect there beyond mirroring
|
|
@@ -9127,18 +9660,18 @@ var ConnectionManager = class {
|
|
|
9127
9660
|
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
9128
9661
|
processingChain = Promise.resolve();
|
|
9129
9662
|
/**
|
|
9130
|
-
* Design B (finding N4): the ONE outbound queue
|
|
9131
|
-
*
|
|
9663
|
+
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
9664
|
+
* never re-encoded/rebuilt strings, so a
|
|
9132
9665
|
* resend after a failed send attempt is byte-identical to the original
|
|
9133
9666
|
* (same `id`), which is what lets the server's per-(deviceId,id) dedup
|
|
9134
9667
|
* (Wave 1) recognize it as a safe no-op retry rather than a second
|
|
9135
|
-
* application (protocol §9).
|
|
9136
|
-
* touches this queue — see `drainOutbox` — so nothing queued while one
|
|
9137
|
-
* transport was active is ever stranded when the other takes over.
|
|
9668
|
+
* application (protocol §9).
|
|
9138
9669
|
*/
|
|
9139
9670
|
outbox = [];
|
|
9671
|
+
/** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
|
|
9672
|
+
rejectedOutboundEnvelopes = [];
|
|
9140
9673
|
/**
|
|
9141
|
-
* Finding F5(b): how many envelopes `drainOutbox`
|
|
9674
|
+
* Finding F5(b): how many envelopes `drainOutbox` has
|
|
9142
9675
|
* currently spliced OUT of `this.outbox` for an in-flight (not yet
|
|
9143
9676
|
* confirmed delivered) `postBatch` call — 0 the rest of the time. See
|
|
9144
9677
|
* `outboxLength`'s own doc comment for why this needs to be tracked
|
|
@@ -9148,6 +9681,7 @@ var ConnectionManager = class {
|
|
|
9148
9681
|
draining = false;
|
|
9149
9682
|
stopped = false;
|
|
9150
9683
|
revoked = false;
|
|
9684
|
+
terminalError;
|
|
9151
9685
|
settledWaiters = [];
|
|
9152
9686
|
pendingCursorSave = Promise.resolve();
|
|
9153
9687
|
/**
|
|
@@ -9190,38 +9724,22 @@ var ConnectionManager = class {
|
|
|
9190
9724
|
*/
|
|
9191
9725
|
cancelPendingDrainRetry;
|
|
9192
9726
|
/**
|
|
9193
|
-
* The capabilities the
|
|
9194
|
-
* `string[]` for forward compatibility.
|
|
9195
|
-
* long-poll
|
|
9196
|
-
*
|
|
9197
|
-
*
|
|
9198
|
-
*
|
|
9199
|
-
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
9200
|
-
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
9201
|
-
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
9202
|
-
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
9203
|
-
* the transport that is still current.
|
|
9204
|
-
* The previous version of this doc comment claimed long-poll mode simply
|
|
9205
|
-
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
9206
|
-
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
9207
|
-
* session kept believing it applied to whatever it's connected to NOW,
|
|
9208
|
-
* even after a disconnect/degrade where nothing has actually confirmed
|
|
9209
|
-
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
9210
|
-
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
9211
|
-
* `task.approval_resolved` on this list — sending it to a server that
|
|
9212
|
-
* doesn't actually understand it over the long-poll path would get a
|
|
9213
|
-
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
9214
|
-
* `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
|
|
9215
|
-
* blocks EVERY envelope queued behind it on, permanently. Clearing this
|
|
9216
|
-
* eagerly means that gate reliably fails closed (falls back to the
|
|
9217
|
-
* pre-existing implicit-resume inference, unconditionally — see
|
|
9218
|
-
* `sendApprovalResolved`'s own doc comment) the moment the connection that
|
|
9219
|
-
* advertised the capability is no longer the one actually in use.
|
|
9727
|
+
* The capabilities the current server response advertised — untyped
|
|
9728
|
+
* `string[]` for forward compatibility. An advertisement is scoped to the
|
|
9729
|
+
* current long-poll response stream and is cleared after an HTTP failure,
|
|
9730
|
+
* terminal shutdown, or revocation. This keeps capability-gated outbound
|
|
9731
|
+
* messages fail-closed until the current server has explicitly advertised
|
|
9732
|
+
* support.
|
|
9220
9733
|
*/
|
|
9221
9734
|
serverCapabilities = [];
|
|
9222
9735
|
async start() {
|
|
9736
|
+
if (this.terminalError) throw this.terminalError;
|
|
9223
9737
|
this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
|
|
9224
|
-
this.
|
|
9738
|
+
this.started = true;
|
|
9739
|
+
this.opts.onStateChange?.("connecting");
|
|
9740
|
+
this.longPoll.start();
|
|
9741
|
+
this.outbox.unshift(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
9742
|
+
void this.drainOutbox();
|
|
9225
9743
|
}
|
|
9226
9744
|
/**
|
|
9227
9745
|
* Design B (finding N4): push onto the single shared outbox and try to
|
|
@@ -9232,17 +9750,13 @@ var ConnectionManager = class {
|
|
|
9232
9750
|
this.outbox.push(envelope);
|
|
9233
9751
|
void this.drainOutbox();
|
|
9234
9752
|
}
|
|
9753
|
+
/** Publish a fresh local configuration snapshot while this daemon is running. */
|
|
9754
|
+
refreshHello() {
|
|
9755
|
+
if (!this.started || this.stopped || this.revoked || this.terminalError) return;
|
|
9756
|
+
this.send(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
9757
|
+
}
|
|
9235
9758
|
/**
|
|
9236
|
-
*
|
|
9237
|
-
* transport is currently active, re-checking `this.mode` fresh on every
|
|
9238
|
-
* iteration so a transport switch mid-drain is picked up immediately
|
|
9239
|
-
* rather than fighting a stale decision made before the switch.
|
|
9240
|
-
*
|
|
9241
|
-
* WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
|
|
9242
|
-
* acked; stops (without dropping anything — the remainder stays queued)
|
|
9243
|
-
* the moment it isn't, and is re-invoked once `onAcked` fires.
|
|
9244
|
-
*
|
|
9245
|
-
* Long-poll: POSTs the outbox in chunks of at most
|
|
9759
|
+
* POSTs the outbox through long-poll in chunks of at most
|
|
9246
9760
|
* `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
|
|
9247
9761
|
* `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
|
|
9248
9762
|
* §8.2) and 400s the WHOLE request if it's exceeded, which — before this
|
|
@@ -9254,43 +9768,48 @@ var ConnectionManager = class {
|
|
|
9254
9768
|
* failure that SAME chunk is unshifted back (order-preserving, same
|
|
9255
9769
|
* Envelope objects/ids — never rebuilt, so a retry is exactly the resend
|
|
9256
9770
|
* Wave 1's server-side dedup expects) and retried after a short backoff,
|
|
9257
|
-
* re-reading `this.mode` each time so a WS recovery that happens
|
|
9258
|
-
* mid-retry is honored on the very next loop iteration instead of only
|
|
9259
|
-
* after this attempt's backoff chain gives up.
|
|
9260
|
-
*
|
|
9261
9771
|
* Re-entrancy is guarded by `draining`: a call arriving while a drain is
|
|
9262
9772
|
* already in progress just returns — the in-progress loop's own
|
|
9263
|
-
* `while (this.outbox.length > 0)` check
|
|
9264
|
-
* pushed (or left over after a mode switch) on its very next iteration.
|
|
9773
|
+
* `while (this.outbox.length > 0)` check picks up anything newly pushed.
|
|
9265
9774
|
*/
|
|
9266
9775
|
async drainOutbox() {
|
|
9267
|
-
if (this.draining) return;
|
|
9776
|
+
if (!this.started || this.draining) return;
|
|
9268
9777
|
this.draining = true;
|
|
9778
|
+
const isolateRejectedBatch = async (batch) => {
|
|
9779
|
+
const pending = [[...batch]];
|
|
9780
|
+
while (pending.length > 0) {
|
|
9781
|
+
if (this.stopped || this.revoked) return pending.flat();
|
|
9782
|
+
const segment = pending.shift();
|
|
9783
|
+
const result = await this.longPoll.postBatch(segment);
|
|
9784
|
+
if (result === void 0) return [...segment, ...pending.flat()];
|
|
9785
|
+
if ((result.rejected ?? 0) === 0) continue;
|
|
9786
|
+
if (segment.length === 1) {
|
|
9787
|
+
this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
|
|
9788
|
+
continue;
|
|
9789
|
+
}
|
|
9790
|
+
const midpoint = Math.floor(segment.length / 2);
|
|
9791
|
+
pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
|
|
9792
|
+
}
|
|
9793
|
+
return void 0;
|
|
9794
|
+
};
|
|
9269
9795
|
try {
|
|
9270
9796
|
while (this.outbox.length > 0) {
|
|
9271
9797
|
if (this.stopped || this.revoked) return;
|
|
9272
|
-
if (this.mode === "ws") {
|
|
9273
|
-
if (!this.ws.isOpen) return;
|
|
9274
|
-
const envelope = this.outbox[0];
|
|
9275
|
-
if (!this.ws.sendNow(envelope)) return;
|
|
9276
|
-
this.outbox.shift();
|
|
9277
|
-
continue;
|
|
9278
|
-
}
|
|
9279
9798
|
const batch = this.outbox.splice(0, MAX_MESSAGES_PER_BATCH);
|
|
9280
9799
|
this.inFlightBatchSize = batch.length;
|
|
9281
|
-
let
|
|
9800
|
+
let retrySegments;
|
|
9282
9801
|
try {
|
|
9283
|
-
|
|
9802
|
+
retrySegments = await isolateRejectedBatch(batch);
|
|
9284
9803
|
} finally {
|
|
9285
9804
|
this.inFlightBatchSize = 0;
|
|
9286
9805
|
}
|
|
9287
|
-
if (
|
|
9806
|
+
if (retrySegments === void 0) {
|
|
9288
9807
|
this.uploadRetryAttempt = 0;
|
|
9289
9808
|
this.opts.onOperationalOutcome?.("success", "upload");
|
|
9290
9809
|
continue;
|
|
9291
9810
|
}
|
|
9292
9811
|
this.opts.onOperationalOutcome?.("failure", "upload");
|
|
9293
|
-
this.outbox.unshift(...
|
|
9812
|
+
this.outbox.unshift(...retrySegments);
|
|
9294
9813
|
if (this.stopped || this.revoked) return;
|
|
9295
9814
|
const baseMs = this.opts.longPollRetryDelayMs ?? 2e3;
|
|
9296
9815
|
await this.drainRetryDelay(this.fleetJitter.delay("upload", this.uploadRetryAttempt++, baseMs));
|
|
@@ -9306,8 +9825,7 @@ var ConnectionManager = class {
|
|
|
9306
9825
|
* in-flight wait immediately instead of leaving `drainOutbox` parked here
|
|
9307
9826
|
* for up to the rest of the delay before it next checks `this.revoked` —
|
|
9308
9827
|
* and (b) unref'd, so the timer never keeps the Node process alive by
|
|
9309
|
-
* itself while nothing else (
|
|
9310
|
-
* legitimately is.
|
|
9828
|
+
* itself while nothing else (such as the live long-poll GET) legitimately is.
|
|
9311
9829
|
*/
|
|
9312
9830
|
drainRetryDelay(ms) {
|
|
9313
9831
|
return new Promise((resolve) => {
|
|
@@ -9323,45 +9841,41 @@ var ConnectionManager = class {
|
|
|
9323
9841
|
};
|
|
9324
9842
|
});
|
|
9325
9843
|
}
|
|
9326
|
-
isTransportDegraded() {
|
|
9327
|
-
return this.mode === "long-poll";
|
|
9328
|
-
}
|
|
9329
9844
|
/**
|
|
9330
|
-
* The capabilities the
|
|
9331
|
-
*
|
|
9332
|
-
* on long-poll. Empty before either transport has supplied its current
|
|
9333
|
-
* advertisement, and cleared across disconnect/switch boundaries.
|
|
9845
|
+
* The capabilities the latest successful `GET /byok/events` response
|
|
9846
|
+
* advertised. Empty before a successful response and after a failed one.
|
|
9334
9847
|
*/
|
|
9335
9848
|
getServerCapabilities() {
|
|
9336
9849
|
return this.serverCapabilities;
|
|
9337
9850
|
}
|
|
9851
|
+
getTerminalError() {
|
|
9852
|
+
return this.terminalError;
|
|
9853
|
+
}
|
|
9338
9854
|
isConnected() {
|
|
9339
|
-
return this.
|
|
9855
|
+
return this.connected;
|
|
9340
9856
|
}
|
|
9341
9857
|
isRevoked() {
|
|
9342
9858
|
return this.revoked;
|
|
9343
9859
|
}
|
|
9344
9860
|
/**
|
|
9345
|
-
* Resolves
|
|
9346
|
-
* connection
|
|
9347
|
-
* lets `daemon.start()` return promptly even when WS is unavailable from
|
|
9348
|
-
* the very first attempt, rather than hanging until a WS `conn.ack` that
|
|
9349
|
-
* may never come.
|
|
9861
|
+
* Resolves after the first successful long-poll response establishes the
|
|
9862
|
+
* authenticated connection.
|
|
9350
9863
|
*
|
|
9351
9864
|
* Rejects with {@link DeviceRevokedError} — instead of hanging until
|
|
9352
9865
|
* `timeoutMs` — if the device turns out to be revoked while settling (or
|
|
9353
9866
|
* already was): a cold `daemon.start()` against an already-revoked device
|
|
9354
9867
|
* must fail fast, not surface a generic timeout (protocol §6.3).
|
|
9355
9868
|
*/
|
|
9356
|
-
|
|
9357
|
-
if (this.
|
|
9869
|
+
waitForConnection(timeoutMs = 1e4) {
|
|
9870
|
+
if (this.connected) return Promise.resolve();
|
|
9871
|
+
if (this.terminalError) return Promise.reject(this.terminalError);
|
|
9358
9872
|
if (this.revoked) return Promise.reject(new DeviceRevokedError());
|
|
9359
9873
|
return new Promise((resolve, reject) => {
|
|
9360
9874
|
let settle = () => {
|
|
9361
9875
|
};
|
|
9362
9876
|
const timer = setTimeout(() => {
|
|
9363
9877
|
this.settledWaiters = this.settledWaiters.filter((w) => w !== settle);
|
|
9364
|
-
reject(new Error("Timed out waiting for the connection to settle
|
|
9878
|
+
reject(new Error("Timed out waiting for the long-poll connection to settle"));
|
|
9365
9879
|
}, timeoutMs);
|
|
9366
9880
|
settle = (err) => {
|
|
9367
9881
|
clearTimeout(timer);
|
|
@@ -9372,7 +9886,7 @@ var ConnectionManager = class {
|
|
|
9372
9886
|
});
|
|
9373
9887
|
}
|
|
9374
9888
|
/**
|
|
9375
|
-
* Stops
|
|
9889
|
+
* Stops the long-poll transport and waits for every in-flight envelope handler
|
|
9376
9890
|
* (the F3 FIFO chain) and the most recent cursor write to actually land on
|
|
9377
9891
|
* disk — otherwise a `stop()` racing a just-processed envelope's
|
|
9378
9892
|
* persistence could lose that cursor advance, or leave a handler running
|
|
@@ -9381,14 +9895,14 @@ var ConnectionManager = class {
|
|
|
9381
9895
|
* Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
|
|
9382
9896
|
* passed, bounds how long this waits for the shared outbox (`this.outbox`
|
|
9383
9897
|
* — Design B) to actually finish draining BEFORE flipping `this.stopped`
|
|
9384
|
-
* and
|
|
9898
|
+
* and stopping the transport. Before this fix, `stop()` set `stopped`
|
|
9385
9899
|
* synchronously and never waited for `drainOutbox` at all: an envelope
|
|
9386
9900
|
* `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
|
|
9387
9901
|
* own `task.fail`, sent right before `create-daemon.ts`'s
|
|
9388
9902
|
* `performControlShutdown` calls this) could still be sitting UNSENT in
|
|
9389
9903
|
* `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
|
|
9390
9904
|
* by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
|
|
9391
|
-
* method would happily proceed to `stopped = true`
|
|
9905
|
+
* method would happily proceed to `stopped = true` regardless,
|
|
9392
9906
|
* after which NOTHING ever drains it again: silently lost, even though
|
|
9393
9907
|
* `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
|
|
9394
9908
|
* default) preserves the EXACT prior behavior for every other existing
|
|
@@ -9405,9 +9919,9 @@ var ConnectionManager = class {
|
|
|
9405
9919
|
}
|
|
9406
9920
|
this.stopped = true;
|
|
9407
9921
|
this.serverCapabilities = [];
|
|
9408
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
9409
9922
|
this.longPoll.stop();
|
|
9410
|
-
this.
|
|
9923
|
+
this.connected = false;
|
|
9924
|
+
this.opts.onStateChange?.("closed");
|
|
9411
9925
|
await this.processingChain;
|
|
9412
9926
|
await this.pendingCursorSave;
|
|
9413
9927
|
}
|
|
@@ -9432,6 +9946,10 @@ var ConnectionManager = class {
|
|
|
9432
9946
|
outboxLength() {
|
|
9433
9947
|
return this.outbox.length + this.inFlightBatchSize;
|
|
9434
9948
|
}
|
|
9949
|
+
/** A bounded terminal quarantine for operator inspection; these entries are never retried. */
|
|
9950
|
+
rejectedOutbox() {
|
|
9951
|
+
return this.rejectedOutboundEnvelopes;
|
|
9952
|
+
}
|
|
9435
9953
|
/**
|
|
9436
9954
|
* Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
|
|
9437
9955
|
* alone — see that method's own doc comment for why a spliced-out,
|
|
@@ -9439,15 +9957,14 @@ var ConnectionManager = class {
|
|
|
9439
9957
|
* a single `drainOutbox()` promise directly — a drain in progress can
|
|
9440
9958
|
* itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
|
|
9441
9959
|
* while the server is unreachable, and a fresh, INDEPENDENT
|
|
9442
|
-
* `drainOutbox()` call can also be triggered concurrently (`send()
|
|
9443
|
-
*
|
|
9960
|
+
* `drainOutbox()` call can also be triggered concurrently (`send()`) —
|
|
9961
|
+
* polling the one thing
|
|
9444
9962
|
* that actually matters (is anything still undelivered) can never go
|
|
9445
9963
|
* stale the way capturing one specific in-flight promise reference
|
|
9446
9964
|
* could. Kicks off one more `drainOutbox()` attempt itself first
|
|
9447
9965
|
* (harmless no-op if one is already running — see its own re-entrancy
|
|
9448
|
-
* guard) in case nothing is currently actively retrying
|
|
9449
|
-
*
|
|
9450
|
-
* isn't just passively hoping something else happens to be making
|
|
9966
|
+
* guard) in case nothing is currently actively retrying, so this bounded
|
|
9967
|
+
* wait isn't just passively hoping something else happens to be making
|
|
9451
9968
|
* progress.
|
|
9452
9969
|
*/
|
|
9453
9970
|
waitForOutboxDrained(timeoutMs) {
|
|
@@ -9490,19 +10007,21 @@ var ConnectionManager = class {
|
|
|
9490
10007
|
deliver(envelope) {
|
|
9491
10008
|
const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
|
|
9492
10009
|
const watermark = this.dedupWatermark();
|
|
9493
|
-
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return;
|
|
10010
|
+
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
|
|
9494
10011
|
if (tracked) {
|
|
9495
10012
|
const seq = envelope.seq;
|
|
9496
|
-
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return;
|
|
10013
|
+
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return false;
|
|
9497
10014
|
this.inFlightSeqs.add(seq);
|
|
9498
10015
|
this.noteDelivered(seq);
|
|
9499
10016
|
}
|
|
9500
10017
|
this.processingChain = this.processingChain.then(() => this.process(envelope, tracked));
|
|
10018
|
+
return true;
|
|
9501
10019
|
}
|
|
9502
10020
|
/**
|
|
9503
|
-
*
|
|
9504
|
-
* against
|
|
9505
|
-
*
|
|
10021
|
+
* The local watermark `deliver()` dedupes inbound `task.*` envelopes
|
|
10022
|
+
* against. It is deliberately NOT the long-poll query cursor: that query
|
|
10023
|
+
* is the kernel acknowledgement and uses only the successfully processed
|
|
10024
|
+
* `cursor` (see the constructor). Normally this local watermark is
|
|
9506
10025
|
* `deliveredSeq` — which is always >= `cursor` (every envelope that
|
|
9507
10026
|
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
9508
10027
|
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
@@ -9518,11 +10037,9 @@ var ConnectionManager = class {
|
|
|
9518
10037
|
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
9519
10038
|
* reconnect" step is needed for this to be correct — collapsing to
|
|
9520
10039
|
* `cursor` exactly while stalled already produces the right answer on
|
|
9521
|
-
* every
|
|
9522
|
-
*
|
|
9523
|
-
*
|
|
9524
|
-
* re-pulling/re-dispatching something already in flight across a
|
|
9525
|
-
* reconnect that happens to land while a handler is still running.
|
|
10040
|
+
* every long-poll retry path. NOT resetting it unconditionally on every
|
|
10041
|
+
* retry lets `deliveredSeq` keep doing its job of not re-dispatching
|
|
10042
|
+
* something already in flight while a handler is still running.
|
|
9526
10043
|
*/
|
|
9527
10044
|
dedupWatermark() {
|
|
9528
10045
|
return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
|
|
@@ -9540,11 +10057,13 @@ var ConnectionManager = class {
|
|
|
9540
10057
|
}
|
|
9541
10058
|
await this.opts.onEnvelope(envelope);
|
|
9542
10059
|
if (!tracked) return;
|
|
9543
|
-
this.
|
|
9544
|
-
|
|
10060
|
+
if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) {
|
|
10061
|
+
this.processedSeqs.add(seq);
|
|
10062
|
+
return;
|
|
10063
|
+
}
|
|
9545
10064
|
this.stalledAtSeq = void 0;
|
|
10065
|
+
await this.advanceCursor(envelope.seq);
|
|
9546
10066
|
this.processedSeqs.clear();
|
|
9547
|
-
this.advanceCursor(envelope.seq);
|
|
9548
10067
|
} catch (err) {
|
|
9549
10068
|
if (tracked && this.stalledAtSeq === void 0) this.stalledAtSeq = envelope.seq;
|
|
9550
10069
|
console.error(
|
|
@@ -9558,9 +10077,8 @@ var ConnectionManager = class {
|
|
|
9558
10077
|
/**
|
|
9559
10078
|
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
9560
10079
|
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
9561
|
-
* unrecognized message type
|
|
9562
|
-
*
|
|
9563
|
-
* `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
10080
|
+
* unrecognized message type (see `long-poll-transport.ts`'s own doc
|
|
10081
|
+
* comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
9564
10082
|
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
9565
10083
|
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
9566
10084
|
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
@@ -9600,7 +10118,7 @@ var ConnectionManager = class {
|
|
|
9600
10118
|
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
9601
10119
|
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
9602
10120
|
* matching `deliver()`'s own eager, unconditional call for a real
|
|
9603
|
-
* envelope: its only job is "don't re-
|
|
10121
|
+
* envelope: its only job is "don't re-dispatch something already handed off,"
|
|
9604
10122
|
* independent of outcome, and that property does not depend on FIFO
|
|
9605
10123
|
* ordering the way the DURABLE cursor does.
|
|
9606
10124
|
*
|
|
@@ -9620,9 +10138,18 @@ var ConnectionManager = class {
|
|
|
9620
10138
|
*/
|
|
9621
10139
|
noteSkippedSeq(seq) {
|
|
9622
10140
|
this.noteDelivered(seq);
|
|
9623
|
-
this.processingChain = this.processingChain.then(() => {
|
|
10141
|
+
this.processingChain = this.processingChain.then(async () => {
|
|
9624
10142
|
if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
|
|
9625
|
-
|
|
10143
|
+
try {
|
|
10144
|
+
await this.advanceCursor(seq);
|
|
10145
|
+
if (this.stalledAtSeq === seq) this.stalledAtSeq = void 0;
|
|
10146
|
+
} catch (err) {
|
|
10147
|
+
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10148
|
+
console.error(
|
|
10149
|
+
`[byok/client] unknown task seq=${seq} could not durably persist its cursor; cursor left unadvanced for redelivery:`,
|
|
10150
|
+
err
|
|
10151
|
+
);
|
|
10152
|
+
}
|
|
9626
10153
|
}
|
|
9627
10154
|
});
|
|
9628
10155
|
}
|
|
@@ -9668,94 +10195,52 @@ var ConnectionManager = class {
|
|
|
9668
10195
|
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
9669
10196
|
});
|
|
9670
10197
|
}
|
|
9671
|
-
advanceCursor(seq) {
|
|
10198
|
+
async advanceCursor(seq) {
|
|
9672
10199
|
if (this.cursor !== void 0 && seq <= this.cursor) return;
|
|
10200
|
+
const save = this.pendingCursorSave.catch(() => void 0).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq));
|
|
10201
|
+
this.pendingCursorSave = save;
|
|
10202
|
+
await save;
|
|
9673
10203
|
this.cursor = seq;
|
|
9674
|
-
this.pendingCursorSave = this.pendingCursorSave.catch(() => {
|
|
9675
|
-
}).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq)).catch(() => {
|
|
9676
|
-
});
|
|
9677
|
-
}
|
|
9678
|
-
/**
|
|
9679
|
-
* Fires the moment a connection attempt reaches `conn.ack` — independent
|
|
9680
|
-
* of whether/when it later closes. This is the ONLY place that can
|
|
9681
|
-
* reliably detect "WS is back up" while long-polling: a healthy
|
|
9682
|
-
* connection stays open indefinitely, so it never reaches `onWsOutcome`
|
|
9683
|
-
* (which is close-only) at all.
|
|
9684
|
-
*/
|
|
9685
|
-
onAcked(capabilities) {
|
|
9686
|
-
this.serverCapabilities = capabilities;
|
|
9687
|
-
this.consecutiveFailures = 0;
|
|
9688
|
-
this.notifySettled();
|
|
9689
|
-
this.opts.onOperationalOutcome?.("success", "reconnect");
|
|
9690
|
-
if (this.mode === "long-poll") this.exitLongPoll();
|
|
9691
|
-
void this.drainOutbox();
|
|
9692
10204
|
}
|
|
9693
|
-
|
|
9694
|
-
if (this.
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
});
|
|
9700
|
-
}
|
|
9701
|
-
if (acked) return;
|
|
9702
|
-
this.opts.onOperationalOutcome?.("failure", "reconnect");
|
|
9703
|
-
this.consecutiveFailures += 1;
|
|
9704
|
-
if (this.mode === "ws" && this.consecutiveFailures >= (this.opts.wsFailureThreshold ?? 3)) {
|
|
9705
|
-
this.enterLongPoll();
|
|
9706
|
-
}
|
|
10205
|
+
quarantineRejectedOutbound(envelope, reason) {
|
|
10206
|
+
if (this.rejectedOutboundEnvelopes.length === MAX_REJECTED_OUTBOX_ENTRIES) this.rejectedOutboundEnvelopes.shift();
|
|
10207
|
+
this.rejectedOutboundEnvelopes.push({ envelope, reason });
|
|
10208
|
+
console.error(
|
|
10209
|
+
`[byok/client] outbound envelope ${envelope.id} was permanently rejected (${reason}) and moved to the terminal quarantine.`
|
|
10210
|
+
);
|
|
9707
10211
|
}
|
|
9708
10212
|
notifySettled(err) {
|
|
9709
10213
|
for (const waiter of this.settledWaiters.splice(0)) waiter(err);
|
|
9710
10214
|
}
|
|
9711
|
-
|
|
9712
|
-
this.
|
|
9713
|
-
this.
|
|
9714
|
-
this.
|
|
9715
|
-
this.opts.onStateChange?.("degraded");
|
|
9716
|
-
this.longPoll.start();
|
|
9717
|
-
this.outbox.unshift(createConnectionHelloEnvelope({
|
|
9718
|
-
deviceId: this.opts.deviceId,
|
|
9719
|
-
productId: this.opts.productId,
|
|
9720
|
-
capabilities: this.opts.capabilities,
|
|
9721
|
-
clientVersion: this.opts.clientVersion,
|
|
9722
|
-
runtimes: this.opts.runtimes,
|
|
9723
|
-
getConfiguredToolsets: this.opts.getConfiguredToolsets,
|
|
9724
|
-
getCursor: () => this.cursor
|
|
9725
|
-
}));
|
|
10215
|
+
noteConnected() {
|
|
10216
|
+
if (this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10217
|
+
this.connected = true;
|
|
10218
|
+
this.opts.onStateChange?.("open");
|
|
9726
10219
|
this.notifySettled();
|
|
9727
|
-
void this.drainOutbox();
|
|
9728
|
-
this.scheduleWsProbe();
|
|
9729
10220
|
}
|
|
9730
|
-
|
|
9731
|
-
if (this.
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
}
|
|
9735
|
-
this.longPoll.stop();
|
|
9736
|
-
this.mode = "ws";
|
|
9737
|
-
this.consecutiveFailures = 0;
|
|
9738
|
-
this.ws.resumeAutoReconnect();
|
|
9739
|
-
void this.drainOutbox();
|
|
10221
|
+
noteDisconnected() {
|
|
10222
|
+
if (!this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10223
|
+
this.connected = false;
|
|
10224
|
+
this.opts.onStateChange?.("connecting");
|
|
9740
10225
|
}
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
this.
|
|
10226
|
+
enterReplayCursorTooOld(error) {
|
|
10227
|
+
if (this.terminalError) return;
|
|
10228
|
+
this.terminalError = error;
|
|
10229
|
+
this.stopped = true;
|
|
10230
|
+
this.serverCapabilities = [];
|
|
10231
|
+
this.longPoll.stop();
|
|
10232
|
+
this.connected = false;
|
|
10233
|
+
this.cancelPendingDrainRetry?.();
|
|
10234
|
+
this.notifySettled(error);
|
|
10235
|
+
this.opts.onStateChange?.("closed");
|
|
10236
|
+
this.opts.onTerminalError?.(error);
|
|
9752
10237
|
}
|
|
9753
10238
|
enterRevoked() {
|
|
9754
10239
|
if (this.revoked) return;
|
|
9755
10240
|
this.revoked = true;
|
|
9756
|
-
|
|
10241
|
+
this.serverCapabilities = [];
|
|
9757
10242
|
this.longPoll.stop();
|
|
9758
|
-
this.
|
|
10243
|
+
this.connected = false;
|
|
9759
10244
|
this.cancelPendingDrainRetry?.();
|
|
9760
10245
|
this.opts.onStateChange?.("revoked");
|
|
9761
10246
|
this.notifySettled(new DeviceRevokedError());
|
|
@@ -12490,21 +12975,28 @@ async function recordAuditWarning(context, kind, values) {
|
|
|
12490
12975
|
}
|
|
12491
12976
|
}
|
|
12492
12977
|
var agentMemoryHomeQueues = /* @__PURE__ */ new Map();
|
|
12493
|
-
|
|
12494
|
-
|
|
12978
|
+
var agentMemoryProjectionTransactionQueues = /* @__PURE__ */ new Map();
|
|
12979
|
+
async function exclusiveAgentMemoryHomeQueue(queues2, home, fn) {
|
|
12980
|
+
const previous = queues2.get(home) ?? Promise.resolve();
|
|
12495
12981
|
let release;
|
|
12496
12982
|
const next = new Promise((resolve) => {
|
|
12497
12983
|
release = resolve;
|
|
12498
12984
|
});
|
|
12499
|
-
|
|
12985
|
+
queues2.set(home, next);
|
|
12500
12986
|
await previous;
|
|
12501
12987
|
try {
|
|
12502
12988
|
return await fn();
|
|
12503
12989
|
} finally {
|
|
12504
12990
|
release();
|
|
12505
|
-
if (
|
|
12991
|
+
if (queues2.get(home) === next) queues2.delete(home);
|
|
12506
12992
|
}
|
|
12507
12993
|
}
|
|
12994
|
+
async function exclusiveAgentMemoryHome(home, fn) {
|
|
12995
|
+
return exclusiveAgentMemoryHomeQueue(agentMemoryHomeQueues, home, fn);
|
|
12996
|
+
}
|
|
12997
|
+
async function exclusiveAgentMemoryProjectionTransaction(home, fn) {
|
|
12998
|
+
return exclusiveAgentMemoryHomeQueue(agentMemoryProjectionTransactionQueues, home, fn);
|
|
12999
|
+
}
|
|
12508
13000
|
var AgentMemoryService = class {
|
|
12509
13001
|
constructor(input) {
|
|
12510
13002
|
this.input = input;
|
|
@@ -12808,13 +13300,16 @@ var AgentMemoryRedactedOutbox = class _AgentMemoryRedactedOutbox {
|
|
|
12808
13300
|
async function snapshotAndProjectAgentMemory(input, projection) {
|
|
12809
13301
|
if (projection?.capability !== AGENT_MEMORY_PROJECTION_CAPABILITY || !projection.grant || !projection.redactor || !projection.port) return;
|
|
12810
13302
|
const context = taskContext(input);
|
|
12811
|
-
const
|
|
12812
|
-
|
|
12813
|
-
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
13303
|
+
const { grant, redactor, port } = projection;
|
|
13304
|
+
await exclusiveAgentMemoryProjectionTransaction(context.canonicalHome, async () => {
|
|
13305
|
+
const outbox = await AgentMemoryRedactedOutbox.open(context, grant);
|
|
13306
|
+
const initialReplay = await outbox.replay(port);
|
|
13307
|
+
if (initialReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(initialReplay);
|
|
13308
|
+
const snapshot = await captureAgentMemorySnapshot(context);
|
|
13309
|
+
await outbox.append(redactedBytes(snapshot, await redactor.redact(snapshot)));
|
|
13310
|
+
const trailingReplay = await outbox.replay(port);
|
|
13311
|
+
if (trailingReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(trailingReplay);
|
|
13312
|
+
});
|
|
12818
13313
|
}
|
|
12819
13314
|
var AGENT_MEMORY_FILESYSTEM_HELPER_PROTOCOL = 2;
|
|
12820
13315
|
var AGENT_MEMORY_FILESYSTEM_HELPER_VERSION = "2";
|
|
@@ -13113,6 +13608,7 @@ function resultDocumentRejectionDetail(check) {
|
|
|
13113
13608
|
}
|
|
13114
13609
|
}
|
|
13115
13610
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
13611
|
+
var DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
|
|
13116
13612
|
function isKnownRuntimeId(id) {
|
|
13117
13613
|
return RuntimeIdSchema.safeParse(id).success;
|
|
13118
13614
|
}
|
|
@@ -13222,6 +13718,7 @@ var TaskRunner = class {
|
|
|
13222
13718
|
deps;
|
|
13223
13719
|
tasks = /* @__PURE__ */ new Map();
|
|
13224
13720
|
pendingMessageTasks = /* @__PURE__ */ new Map();
|
|
13721
|
+
messageOutboxesByHome = /* @__PURE__ */ new Map();
|
|
13225
13722
|
messageContextByToken = /* @__PURE__ */ new Map();
|
|
13226
13723
|
messageContextByTask = /* @__PURE__ */ new Map();
|
|
13227
13724
|
memoryContextByToken = /* @__PURE__ */ new Map();
|
|
@@ -13282,6 +13779,8 @@ var TaskRunner = class {
|
|
|
13282
13779
|
* costs nothing.
|
|
13283
13780
|
*/
|
|
13284
13781
|
inFlightOffers = /* @__PURE__ */ new Set();
|
|
13782
|
+
/** Blob I/O before an offer becomes an active task still belongs to that offer's cancellation authority. */
|
|
13783
|
+
inFlightBlobAborts = /* @__PURE__ */ new Map();
|
|
13285
13784
|
/**
|
|
13286
13785
|
* Finding P2 (Fix 2c): taskIds that have reached a terminal outcome
|
|
13287
13786
|
* (Complete/Failed/Cancelled) this session — populated in `finish()`.
|
|
@@ -13356,6 +13855,10 @@ var TaskRunner = class {
|
|
|
13356
13855
|
get maxTaskOutputBytes() {
|
|
13357
13856
|
return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
|
|
13358
13857
|
}
|
|
13858
|
+
/** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
|
|
13859
|
+
get maxConcurrentMutableSessionsPerAgentHome() {
|
|
13860
|
+
return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
13861
|
+
}
|
|
13359
13862
|
/**
|
|
13360
13863
|
* M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
|
|
13361
13864
|
* for the control socket's `status` result — see
|
|
@@ -13408,6 +13911,7 @@ var TaskRunner = class {
|
|
|
13408
13911
|
async recoverAgentMessageOutboxes(agentsRoot) {
|
|
13409
13912
|
if (this.deps.tenantId === void 0) throw new Error("Agent message recovery requires authenticated tenant enrollment");
|
|
13410
13913
|
for (const outbox of await AgentMessageOutbox.recover(agentsRoot, this.deps.tenantId)) {
|
|
13914
|
+
this.messageOutboxesByHome.set(outbox.homeDir, Promise.resolve(outbox));
|
|
13411
13915
|
for (const record of outbox.retryableRecords()) {
|
|
13412
13916
|
if (record.sessionRef === void 0) continue;
|
|
13413
13917
|
const existing = this.recoveredMessageOutboxes.get(record.taskId);
|
|
@@ -13416,6 +13920,16 @@ var TaskRunner = class {
|
|
|
13416
13920
|
}
|
|
13417
13921
|
}
|
|
13418
13922
|
}
|
|
13923
|
+
agentMessageOutbox(homeDir) {
|
|
13924
|
+
const existing = this.messageOutboxesByHome.get(homeDir);
|
|
13925
|
+
if (existing !== void 0) return existing;
|
|
13926
|
+
const opened = AgentMessageOutbox.open(homeDir);
|
|
13927
|
+
this.messageOutboxesByHome.set(homeDir, opened);
|
|
13928
|
+
void opened.catch(() => {
|
|
13929
|
+
if (this.messageOutboxesByHome.get(homeDir) === opened) this.messageOutboxesByHome.delete(homeDir);
|
|
13930
|
+
});
|
|
13931
|
+
return opened;
|
|
13932
|
+
}
|
|
13419
13933
|
/** Retry stable recovered records after a transport handshake/re-handshake. */
|
|
13420
13934
|
retryRecoveredAgentMessages() {
|
|
13421
13935
|
for (const [taskId, outbox] of this.recoveredMessageOutboxes) {
|
|
@@ -13571,6 +14085,7 @@ var TaskRunner = class {
|
|
|
13571
14085
|
if (active.finalizationStarted) return this.finish(active.taskId);
|
|
13572
14086
|
if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
|
|
13573
14087
|
active.beingTornDown = true;
|
|
14088
|
+
active.blobAbort.abort();
|
|
13574
14089
|
await this.observeGit(active, "salvage");
|
|
13575
14090
|
const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
|
|
13576
14091
|
await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
|
|
@@ -13717,6 +14232,8 @@ var TaskRunner = class {
|
|
|
13717
14232
|
return;
|
|
13718
14233
|
}
|
|
13719
14234
|
this.inFlightOffers.add(taskId);
|
|
14235
|
+
const blobAbort = new AbortController();
|
|
14236
|
+
this.inFlightBlobAborts.set(taskId, blobAbort);
|
|
13720
14237
|
let agentBinding;
|
|
13721
14238
|
let agentLeaseTransferred = false;
|
|
13722
14239
|
try {
|
|
@@ -13731,6 +14248,24 @@ var TaskRunner = class {
|
|
|
13731
14248
|
decline("strict Agent-only daemon refuses legacy task offers", false);
|
|
13732
14249
|
return;
|
|
13733
14250
|
}
|
|
14251
|
+
if (agentRef !== void 0) {
|
|
14252
|
+
const limit = this.maxConcurrentMutableSessionsPerAgentHome;
|
|
14253
|
+
let canonicalHome;
|
|
14254
|
+
try {
|
|
14255
|
+
canonicalHome = await this.deps.agentHome.layout.canonicalHomePath(agentRef);
|
|
14256
|
+
} catch (error) {
|
|
14257
|
+
decline(
|
|
14258
|
+
`Agent home admission failed: ${errorMessage5(error)}`,
|
|
14259
|
+
!(error instanceof AgentHomeResolutionError)
|
|
14260
|
+
);
|
|
14261
|
+
return;
|
|
14262
|
+
}
|
|
14263
|
+
const active2 = this.deps.agentHome.executionLeaseManager.activeAttemptCount(canonicalHome);
|
|
14264
|
+
if (active2 >= limit) {
|
|
14265
|
+
decline(`agent home busy: ${active2} active attempt(s)`, true);
|
|
14266
|
+
return;
|
|
14267
|
+
}
|
|
14268
|
+
}
|
|
13734
14269
|
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
13735
14270
|
if (guarded !== void 0 && !guarded.admit) {
|
|
13736
14271
|
decline(guarded.reason, guarded.retryable);
|
|
@@ -13875,7 +14410,7 @@ var TaskRunner = class {
|
|
|
13875
14410
|
}
|
|
13876
14411
|
if (agentRef !== void 0) {
|
|
13877
14412
|
try {
|
|
13878
|
-
agentBinding = await this.deps.agentHome.
|
|
14413
|
+
agentBinding = await this.deps.agentHome.acquireExecution(agentRef, { taskId, sessionRef });
|
|
13879
14414
|
} catch (error) {
|
|
13880
14415
|
decline(
|
|
13881
14416
|
`Agent home admission failed: ${errorMessage5(error)}`,
|
|
@@ -13910,7 +14445,7 @@ var TaskRunner = class {
|
|
|
13910
14445
|
}
|
|
13911
14446
|
}
|
|
13912
14447
|
try {
|
|
13913
|
-
await this.deps.agentHome.
|
|
14448
|
+
await this.deps.agentHome.initializeExecution(agentBinding);
|
|
13914
14449
|
} catch (error) {
|
|
13915
14450
|
await agentBinding.lease.release().catch(() => {
|
|
13916
14451
|
});
|
|
@@ -13920,7 +14455,7 @@ var TaskRunner = class {
|
|
|
13920
14455
|
}
|
|
13921
14456
|
if (messageRequirement !== void 0) {
|
|
13922
14457
|
try {
|
|
13923
|
-
const outbox = await
|
|
14458
|
+
const outbox = await this.agentMessageOutbox(agentBinding.resolution.canonicalHome);
|
|
13924
14459
|
this.pendingMessageTasks.set(taskId, {
|
|
13925
14460
|
taskId,
|
|
13926
14461
|
agentRef: agentBinding.resolution.agentRef,
|
|
@@ -14039,7 +14574,7 @@ var TaskRunner = class {
|
|
|
14039
14574
|
);
|
|
14040
14575
|
let resolvedInstruction;
|
|
14041
14576
|
try {
|
|
14042
|
-
resolvedInstruction = await this.resolveInstruction(payload.instruction);
|
|
14577
|
+
resolvedInstruction = await this.resolveInstruction(payload.instruction, blobAbort.signal);
|
|
14043
14578
|
if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
|
|
14044
14579
|
} catch (err) {
|
|
14045
14580
|
gitLease?.release();
|
|
@@ -14152,9 +14687,29 @@ var TaskRunner = class {
|
|
|
14152
14687
|
}
|
|
14153
14688
|
return;
|
|
14154
14689
|
}
|
|
14690
|
+
if (agentBinding !== void 0) {
|
|
14691
|
+
try {
|
|
14692
|
+
await agentBinding.lease.bindSession(session.sessionRef);
|
|
14693
|
+
} catch (error) {
|
|
14694
|
+
await session.close().catch(() => {
|
|
14695
|
+
});
|
|
14696
|
+
await this.failClaimedAgent(
|
|
14697
|
+
taskId,
|
|
14698
|
+
`Agent session execution lease could not bind the runtime session: ${errorMessage5(error)}`,
|
|
14699
|
+
false,
|
|
14700
|
+
{
|
|
14701
|
+
binding: agentBinding,
|
|
14702
|
+
runtimeId: pick.descriptor.id,
|
|
14703
|
+
sessionRef: session.sessionRef
|
|
14704
|
+
}
|
|
14705
|
+
);
|
|
14706
|
+
return;
|
|
14707
|
+
}
|
|
14708
|
+
}
|
|
14155
14709
|
let active;
|
|
14156
14710
|
active = {
|
|
14157
14711
|
taskId,
|
|
14712
|
+
blobAbort,
|
|
14158
14713
|
egressEnabled: "egressPolicy" in payload,
|
|
14159
14714
|
adapter: pick.adapter,
|
|
14160
14715
|
session,
|
|
@@ -14195,15 +14750,16 @@ var TaskRunner = class {
|
|
|
14195
14750
|
...terminalProjection === void 0 ? {} : { terminalProjection }
|
|
14196
14751
|
};
|
|
14197
14752
|
if (agentBinding !== void 0) {
|
|
14753
|
+
const binding = agentBinding;
|
|
14198
14754
|
try {
|
|
14199
|
-
await this.deps.agentSessionHandoffs.record({
|
|
14200
|
-
agentRef:
|
|
14755
|
+
await this.deps.agentHome.mutateExecution(binding, () => this.deps.agentSessionHandoffs.record({
|
|
14756
|
+
agentRef: binding.resolution.agentRef,
|
|
14201
14757
|
taskId,
|
|
14202
14758
|
sessionRef: session.sessionRef,
|
|
14203
14759
|
runtimeId: pick.descriptor.id,
|
|
14204
14760
|
cwd: workspaceDir,
|
|
14205
|
-
leaseId:
|
|
14206
|
-
});
|
|
14761
|
+
leaseId: binding.lease.leaseId
|
|
14762
|
+
}));
|
|
14207
14763
|
} catch (error) {
|
|
14208
14764
|
await session.close().catch(() => {
|
|
14209
14765
|
});
|
|
@@ -14245,6 +14801,7 @@ var TaskRunner = class {
|
|
|
14245
14801
|
}
|
|
14246
14802
|
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
14247
14803
|
agentLeaseTransferred = agentBinding !== void 0;
|
|
14804
|
+
this.inFlightBlobAborts.delete(taskId);
|
|
14248
14805
|
this.tasks.set(taskId, active);
|
|
14249
14806
|
this.pendingMessageTasks.delete(taskId);
|
|
14250
14807
|
if (active.messageOutbox !== void 0 && activatedMessageRecord !== void 0) {
|
|
@@ -14276,6 +14833,7 @@ var TaskRunner = class {
|
|
|
14276
14833
|
await agentBinding.lease.release().catch(() => {
|
|
14277
14834
|
});
|
|
14278
14835
|
}
|
|
14836
|
+
this.inFlightBlobAborts.delete(taskId);
|
|
14279
14837
|
this.inFlightOffers.delete(taskId);
|
|
14280
14838
|
}
|
|
14281
14839
|
}
|
|
@@ -14427,9 +14985,9 @@ var TaskRunner = class {
|
|
|
14427
14985
|
void this.closeAgentMemoryFilesystem(taskId);
|
|
14428
14986
|
}
|
|
14429
14987
|
/** Protocol §7: an instruction too large to inline arrives as a `blobRef` — resolve it via the blob client rather than failing closed. */
|
|
14430
|
-
async resolveInstruction(instruction) {
|
|
14988
|
+
async resolveInstruction(instruction, signal) {
|
|
14431
14989
|
if (typeof instruction === "string") return instruction;
|
|
14432
|
-
return this.deps.blobClient.resolveInstruction(instruction.blobRef);
|
|
14990
|
+
return this.deps.blobClient.resolveInstruction(instruction.blobRef, { signal });
|
|
14433
14991
|
}
|
|
14434
14992
|
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
14435
14993
|
resolveMcpServers(requiredToolsets) {
|
|
@@ -14661,7 +15219,9 @@ var TaskRunner = class {
|
|
|
14661
15219
|
}
|
|
14662
15220
|
}
|
|
14663
15221
|
try {
|
|
14664
|
-
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType
|
|
15222
|
+
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType, {
|
|
15223
|
+
signal: active.blobAbort.signal
|
|
15224
|
+
});
|
|
14665
15225
|
this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
|
|
14666
15226
|
} catch (err) {
|
|
14667
15227
|
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage5(err)}`);
|
|
@@ -14676,6 +15236,7 @@ var TaskRunner = class {
|
|
|
14676
15236
|
const active = this.tasks.get(taskId);
|
|
14677
15237
|
if (!active) {
|
|
14678
15238
|
this.setPendingCancelled(taskId, reason);
|
|
15239
|
+
this.inFlightBlobAborts.get(taskId)?.abort();
|
|
14679
15240
|
return;
|
|
14680
15241
|
}
|
|
14681
15242
|
if (active.finalizationStarted) {
|
|
@@ -14686,6 +15247,7 @@ var TaskRunner = class {
|
|
|
14686
15247
|
await active.semanticTerminalSettled;
|
|
14687
15248
|
return;
|
|
14688
15249
|
}
|
|
15250
|
+
active.blobAbort.abort();
|
|
14689
15251
|
try {
|
|
14690
15252
|
await active.session.interrupt();
|
|
14691
15253
|
} catch {
|
|
@@ -15185,7 +15747,7 @@ var TaskRunner = class {
|
|
|
15185
15747
|
async failClaimedAgent(taskId, reason, retryable, context) {
|
|
15186
15748
|
const agentRef = context.binding.resolution.agentRef;
|
|
15187
15749
|
const result = await this.retryAgentTerminalEvidence(
|
|
15188
|
-
() => this.deps.agentSessionHandoffs.recordTaskTerminal({
|
|
15750
|
+
() => this.deps.agentHome.mutateExecution(context.binding, () => this.deps.agentSessionHandoffs.recordTaskTerminal({
|
|
15189
15751
|
agentRef,
|
|
15190
15752
|
taskId,
|
|
15191
15753
|
runtimeId: context.runtimeId,
|
|
@@ -15193,7 +15755,7 @@ var TaskRunner = class {
|
|
|
15193
15755
|
leaseId: context.binding.lease.leaseId,
|
|
15194
15756
|
...context.sessionRef === void 0 ? {} : { sessionRef: context.sessionRef },
|
|
15195
15757
|
terminalReason: reason
|
|
15196
|
-
})
|
|
15758
|
+
}))
|
|
15197
15759
|
);
|
|
15198
15760
|
if (!result.ok) {
|
|
15199
15761
|
this.reportAgentTerminalEvidenceFailure({
|
|
@@ -16182,6 +16744,8 @@ var AgentEgressController = class {
|
|
|
16182
16744
|
options;
|
|
16183
16745
|
latest = new AgentLatestValueState();
|
|
16184
16746
|
spools = /* @__PURE__ */ new Map();
|
|
16747
|
+
spoolOpens = /* @__PURE__ */ new Map();
|
|
16748
|
+
reliableAppendTail = Promise.resolve();
|
|
16185
16749
|
latestStatus = emptyLane();
|
|
16186
16750
|
reliableStatus = emptyLane();
|
|
16187
16751
|
drops = [];
|
|
@@ -16236,7 +16800,8 @@ var AgentEgressController = class {
|
|
|
16236
16800
|
return latest === void 0 ? [] : Object.freeze([latest]);
|
|
16237
16801
|
}
|
|
16238
16802
|
async appendReliable(input) {
|
|
16239
|
-
|
|
16803
|
+
const tenantId = this.options.tenantId;
|
|
16804
|
+
if (!this.active || tenantId === void 0) {
|
|
16240
16805
|
this.noteDrop("reliable", "policy_denied", input.agentRef);
|
|
16241
16806
|
return { ok: false, reason: "policy_denied" };
|
|
16242
16807
|
}
|
|
@@ -16251,17 +16816,21 @@ var AgentEgressController = class {
|
|
|
16251
16816
|
return { ok: false, reason: "sanitizer_rejected" };
|
|
16252
16817
|
}
|
|
16253
16818
|
try {
|
|
16254
|
-
const
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16819
|
+
const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
|
|
16820
|
+
return await this.withAppendTail(async () => {
|
|
16821
|
+
const spool = await spoolOpen;
|
|
16822
|
+
this.bindSpool(input.agentRef, spool, spoolOpen);
|
|
16823
|
+
const record = await spool.append({
|
|
16824
|
+
agentRef: input.agentRef,
|
|
16825
|
+
tenantId,
|
|
16826
|
+
policyRevision: this.options.policy.policyRevision,
|
|
16827
|
+
payload,
|
|
16828
|
+
sessionRef: input.sessionRef,
|
|
16829
|
+
...input.taskId === void 0 ? {} : { taskId: input.taskId },
|
|
16830
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId }
|
|
16831
|
+
}, this.options.policy, this.tenantPendingBytes());
|
|
16832
|
+
return { ok: true, record };
|
|
16833
|
+
});
|
|
16265
16834
|
} catch (error) {
|
|
16266
16835
|
const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
|
|
16267
16836
|
this.noteDrop("reliable", reason, input.agentRef);
|
|
@@ -16274,21 +16843,26 @@ var AgentEgressController = class {
|
|
|
16274
16843
|
* with `wireType: agent.content.receipt` before any transport attempt.
|
|
16275
16844
|
*/
|
|
16276
16845
|
async appendContentReceipt(input) {
|
|
16277
|
-
|
|
16846
|
+
const tenantId = this.options.tenantId;
|
|
16847
|
+
if (!this.active || tenantId === void 0) {
|
|
16278
16848
|
this.noteDrop("reliable", "policy_denied", input.agentRef);
|
|
16279
16849
|
return { ok: false, reason: "policy_denied" };
|
|
16280
16850
|
}
|
|
16281
16851
|
try {
|
|
16282
|
-
const
|
|
16283
|
-
|
|
16284
|
-
|
|
16285
|
-
|
|
16286
|
-
|
|
16287
|
-
|
|
16288
|
-
|
|
16289
|
-
|
|
16290
|
-
|
|
16291
|
-
|
|
16852
|
+
const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
|
|
16853
|
+
return await this.withAppendTail(async () => {
|
|
16854
|
+
const spool = await spoolOpen;
|
|
16855
|
+
this.bindSpool(input.agentRef, spool, spoolOpen);
|
|
16856
|
+
const record = await spool.appendContentReceipt({
|
|
16857
|
+
agentRef: input.agentRef,
|
|
16858
|
+
tenantId,
|
|
16859
|
+
policyRevision: this.options.policy.policyRevision,
|
|
16860
|
+
sessionRef: input.payload.sessionRef,
|
|
16861
|
+
payload: input.payload,
|
|
16862
|
+
...input.taskId === void 0 ? {} : { taskId: input.taskId }
|
|
16863
|
+
}, this.options.policy, this.tenantPendingBytes());
|
|
16864
|
+
return { ok: true, record };
|
|
16865
|
+
});
|
|
16292
16866
|
} catch (error) {
|
|
16293
16867
|
const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
|
|
16294
16868
|
this.noteDrop("reliable", reason, input.agentRef);
|
|
@@ -16363,20 +16937,59 @@ var AgentEgressController = class {
|
|
|
16363
16937
|
}
|
|
16364
16938
|
return this.reliableRecords();
|
|
16365
16939
|
}
|
|
16366
|
-
|
|
16940
|
+
spoolFor(homeDir, agentRef) {
|
|
16367
16941
|
const key = agentKey(agentRef);
|
|
16368
16942
|
const existing = this.spools.get(key);
|
|
16369
|
-
if (existing)
|
|
16370
|
-
|
|
16371
|
-
|
|
16372
|
-
|
|
16943
|
+
if (existing) {
|
|
16944
|
+
if (existing.homeDir !== homeDir) throw new Error("Agent reliable spool is already bound to a different home");
|
|
16945
|
+
return Promise.resolve(existing);
|
|
16946
|
+
}
|
|
16947
|
+
const inFlight = this.spoolOpens.get(key);
|
|
16948
|
+
if (inFlight !== void 0) {
|
|
16949
|
+
if (inFlight.homeDir !== homeDir) throw new Error("Agent reliable spool opening is already bound to a different home");
|
|
16950
|
+
return inFlight.promise;
|
|
16951
|
+
}
|
|
16952
|
+
const opened = (async () => {
|
|
16953
|
+
const spool = await AgentReliableSpool.open(homeDir);
|
|
16954
|
+
if (spool.records().some((record) => !sameAgent(record.agentRef, agentRef))) {
|
|
16955
|
+
throw new Error("Agent-local reliable spool has a different AgentRef");
|
|
16956
|
+
}
|
|
16957
|
+
return spool;
|
|
16958
|
+
})();
|
|
16959
|
+
const slot = { homeDir, promise: opened };
|
|
16960
|
+
this.spoolOpens.set(key, slot);
|
|
16961
|
+
void opened.catch(() => {
|
|
16962
|
+
if (this.spoolOpens.get(key) === slot) this.spoolOpens.delete(key);
|
|
16963
|
+
});
|
|
16964
|
+
return opened;
|
|
16965
|
+
}
|
|
16966
|
+
bindSpool(agentRef, spool, spoolOpen) {
|
|
16967
|
+
const key = agentKey(agentRef);
|
|
16968
|
+
const existing = this.spools.get(key);
|
|
16969
|
+
try {
|
|
16970
|
+
if (existing && existing !== spool) throw new Error("multiple Agent egress spools claim the same AgentRef");
|
|
16971
|
+
this.spools.set(key, spool);
|
|
16972
|
+
} finally {
|
|
16973
|
+
const slot = this.spoolOpens.get(key);
|
|
16974
|
+
if (slot?.promise === spoolOpen) this.spoolOpens.delete(key);
|
|
16373
16975
|
}
|
|
16374
|
-
this.spools.set(key, spool);
|
|
16375
|
-
return spool;
|
|
16376
16976
|
}
|
|
16377
16977
|
tenantPendingBytes() {
|
|
16378
16978
|
return this.reliableRecords().reduce((total, record) => total + record.byteCount, 0);
|
|
16379
16979
|
}
|
|
16980
|
+
async withAppendTail(operation) {
|
|
16981
|
+
const previous = this.reliableAppendTail;
|
|
16982
|
+
let release;
|
|
16983
|
+
this.reliableAppendTail = new Promise((resolve) => {
|
|
16984
|
+
release = resolve;
|
|
16985
|
+
});
|
|
16986
|
+
try {
|
|
16987
|
+
await previous;
|
|
16988
|
+
return await operation();
|
|
16989
|
+
} finally {
|
|
16990
|
+
release();
|
|
16991
|
+
}
|
|
16992
|
+
}
|
|
16380
16993
|
noteDrop(lane, reason, agentRef, countDrop = true) {
|
|
16381
16994
|
const laneStatus = lane === "latest-value" ? this.latestStatus : this.reliableStatus;
|
|
16382
16995
|
if (countDrop) laneStatus.dropped += 1;
|
|
@@ -17364,7 +17977,7 @@ async function detectRuntimes(adapters) {
|
|
|
17364
17977
|
}
|
|
17365
17978
|
return runtimes;
|
|
17366
17979
|
}
|
|
17367
|
-
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
|
|
17980
|
+
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies, providerProfileBindingConfigured = false) {
|
|
17368
17981
|
const flags = [];
|
|
17369
17982
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
17370
17983
|
flags.push("blob-upload");
|
|
@@ -17376,6 +17989,9 @@ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentO
|
|
|
17376
17989
|
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
17377
17990
|
flags.push("dispatch-selection");
|
|
17378
17991
|
}
|
|
17992
|
+
if (providerProfileBindingConfigured && adapters.some((adapter) => adapter.descriptor.id === "pi" && adapter.descriptor.supportsDispatchSelection)) {
|
|
17993
|
+
flags.push(PROVIDER_PROFILE_BINDING_CAPABILITY);
|
|
17994
|
+
}
|
|
17379
17995
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
17380
17996
|
flags.push("toolset-selection");
|
|
17381
17997
|
}
|
|
@@ -17538,6 +18154,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
17538
18154
|
if (config.strictAgentOnly === true && config.agentHome === void 0) {
|
|
17539
18155
|
throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
|
|
17540
18156
|
}
|
|
18157
|
+
if (config.maxConcurrentMutableSessionsPerAgentHome !== void 0 && !(Number.isSafeInteger(config.maxConcurrentMutableSessionsPerAgentHome) && config.maxConcurrentMutableSessionsPerAgentHome > 0)) {
|
|
18158
|
+
throw new Error(
|
|
18159
|
+
`DaemonConfig.maxConcurrentMutableSessionsPerAgentHome must be a positive safe integer (or omitted to use the default of ${DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}) \u2014 got ${config.maxConcurrentMutableSessionsPerAgentHome}. Raising it above 1 lets that many Attempts co-write one canonical Agent home; 0, a negative number, NaN, or a non-integer is rejected rather than silently treated as "uncapped".`
|
|
18160
|
+
);
|
|
18161
|
+
}
|
|
18162
|
+
const agentHomeAttemptLimit = config.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
17541
18163
|
const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
|
|
17542
18164
|
const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
|
|
17543
18165
|
...config.progressBatch,
|
|
@@ -17648,6 +18270,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
17648
18270
|
const observer = new DaemonObserver();
|
|
17649
18271
|
const approvalRegistry = new ApprovalRegistry();
|
|
17650
18272
|
let connection;
|
|
18273
|
+
let blobLifecycleAbort;
|
|
17651
18274
|
let connectionState = "closed";
|
|
17652
18275
|
let daemonStarted = false;
|
|
17653
18276
|
let tenantRebinding = false;
|
|
@@ -17666,6 +18289,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
17666
18289
|
function buildAuthManager() {
|
|
17667
18290
|
return new AuthManager({
|
|
17668
18291
|
serverUrl: config.serverUrl,
|
|
18292
|
+
authRequestDeadlineMs: config.authRequestDeadlineMs,
|
|
17669
18293
|
store,
|
|
17670
18294
|
deviceName: config.deviceName,
|
|
17671
18295
|
machineId: config.machineId ?? (() => resolveMachineId({ productId: config.productId })),
|
|
@@ -17683,7 +18307,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
17683
18307
|
assertServerUrlAllowed(config.serverUrl, { dangerouslyAllowInsecureRemote: true });
|
|
17684
18308
|
const reason = err instanceof Error ? err.message : String(err);
|
|
17685
18309
|
console.warn(
|
|
17686
|
-
`[byok/client] WARNING: dangerouslyAllowInsecureRemote is set \u2014 proceeding with an insecure server URL (${config.serverUrl}). Device credentials and task data will be sent in the clear to a non-loopback host. This should never be used in a real deployment. (${reason})`
|
|
18310
|
+
`[byok/client] WARNING: dangerouslyAllowInsecureRemote is set \u2014 proceeding with an insecure server URL (${formatServerUrl(config.serverUrl)}). Device credentials and task data will be sent in the clear to a non-loopback host. This should never be used in a real deployment. (${reason})`
|
|
17687
18311
|
);
|
|
17688
18312
|
}
|
|
17689
18313
|
}
|
|
@@ -17843,9 +18467,10 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
17843
18467
|
await gitWorkspaceStore?.initialize();
|
|
17844
18468
|
await gitWorkspaceStore?.reconcile();
|
|
17845
18469
|
}
|
|
18470
|
+
blobLifecycleAbort = new AbortController();
|
|
17846
18471
|
const [runtimes, blobClient] = await Promise.all([
|
|
17847
18472
|
detectRuntimes(adapters),
|
|
17848
|
-
Promise.resolve(new BlobClient(config.serverUrl, auth))
|
|
18473
|
+
Promise.resolve(new BlobClient(config.serverUrl, auth, { signal: blobLifecycleAbort.signal }))
|
|
17849
18474
|
]);
|
|
17850
18475
|
observer.noteRuntimesDetected(runtimes);
|
|
17851
18476
|
detectedRuntimeFacts = runtimes;
|
|
@@ -17855,7 +18480,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
17855
18480
|
config.strictAgentOnly === true,
|
|
17856
18481
|
agentHomeManager?.supportsTaskFreeProjection() === true,
|
|
17857
18482
|
config.agentEgress !== void 0,
|
|
17858
|
-
agentContentReadPolicies
|
|
18483
|
+
agentContentReadPolicies,
|
|
18484
|
+
config.piByokLauncher !== void 0
|
|
17859
18485
|
);
|
|
17860
18486
|
const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
|
|
17861
18487
|
serverUrl: config.serverUrl,
|
|
@@ -17914,6 +18540,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
17914
18540
|
workspaceRoot: config.workspaceRoot,
|
|
17915
18541
|
...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
|
|
17916
18542
|
...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
|
|
18543
|
+
// WP0: already validated up front — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`.
|
|
18544
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
17917
18545
|
...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
|
|
17918
18546
|
deviceId: record.deviceId,
|
|
17919
18547
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
@@ -18198,16 +18826,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18198
18826
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
18199
18827
|
},
|
|
18200
18828
|
onStateChange: (state) => {
|
|
18201
|
-
const wasSettled = connectionState === "open"
|
|
18829
|
+
const wasSettled = connectionState === "open";
|
|
18202
18830
|
connectionState = state;
|
|
18203
18831
|
observer.noteConnectionState(state);
|
|
18204
|
-
if (!wasSettled &&
|
|
18205
|
-
if (!wasSettled &&
|
|
18832
|
+
if (!wasSettled && state === "open") runner?.retryRecoveredAgentMessages();
|
|
18833
|
+
if (!wasSettled && state === "open") runPresenceDiscovery();
|
|
18206
18834
|
},
|
|
18207
|
-
backoff: overrides.backoff,
|
|
18208
|
-
liveness: overrides.liveness,
|
|
18209
|
-
wsFailureThreshold: overrides.longPoll?.wsFailureThreshold,
|
|
18210
|
-
wsRetryIntervalMs: overrides.longPoll?.wsRetryIntervalMs,
|
|
18211
18835
|
longPollRetryDelayMs: overrides.longPoll?.retryDelayMs,
|
|
18212
18836
|
longPollIdleDelayMs: overrides.longPoll?.idleDelayMs,
|
|
18213
18837
|
fleetJitter,
|
|
@@ -18219,7 +18843,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18219
18843
|
}
|
|
18220
18844
|
});
|
|
18221
18845
|
await connection.start();
|
|
18222
|
-
await connection.
|
|
18846
|
+
await connection.waitForConnection();
|
|
18223
18847
|
runner.retryRecoveredAgentMessages();
|
|
18224
18848
|
for (const record2 of agentEgress.retryableReliableRecords(connection.getServerCapabilities())) {
|
|
18225
18849
|
dispatchReliableRecord(record2);
|
|
@@ -18274,6 +18898,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18274
18898
|
}
|
|
18275
18899
|
async function runShutdownSequence(reason, opts = {}) {
|
|
18276
18900
|
shuttingDown = true;
|
|
18901
|
+
blobLifecycleAbort?.abort();
|
|
18902
|
+
blobLifecycleAbort = void 0;
|
|
18277
18903
|
const errors = [];
|
|
18278
18904
|
let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
|
|
18279
18905
|
if (!hostedStorageInitializationBarrierComplete) {
|
|
@@ -18476,7 +19102,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18476
19102
|
// fine.
|
|
18477
19103
|
...storageStatus === void 0 ? {} : { storage: storageStatus },
|
|
18478
19104
|
operationalHealth: operationalHealth.snapshot(),
|
|
18479
|
-
toolsets: toolsetRegistry.status()
|
|
19105
|
+
toolsets: toolsetRegistry.status(),
|
|
19106
|
+
// WP0: same counts `Daemon.status()` reports, from the same reader.
|
|
19107
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
18480
19108
|
};
|
|
18481
19109
|
}
|
|
18482
19110
|
async function performControlShutdown(reason) {
|
|
@@ -18711,9 +19339,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18711
19339
|
const minted = mintDeviceAssertion({
|
|
18712
19340
|
record,
|
|
18713
19341
|
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
18714
|
-
// (
|
|
18715
|
-
//
|
|
18716
|
-
// spelling of the same deployment produce the same issuer.
|
|
19342
|
+
// (path stripped), so the issuer ignores an operator-provided path
|
|
19343
|
+
// or query.
|
|
18717
19344
|
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
18718
19345
|
productId: config.productId,
|
|
18719
19346
|
audience: parsed.audience,
|
|
@@ -18828,23 +19455,33 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18828
19455
|
await binding.lease.release();
|
|
18829
19456
|
}
|
|
18830
19457
|
}
|
|
19458
|
+
function agentHomeExecutionStatus() {
|
|
19459
|
+
const summary = agentHomeManager?.executionLeaseManager.activeAttemptSummary();
|
|
19460
|
+
return {
|
|
19461
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
19462
|
+
activeHomes: summary?.homes ?? 0,
|
|
19463
|
+
activeAttempts: summary?.attempts ?? 0
|
|
19464
|
+
};
|
|
19465
|
+
}
|
|
18831
19466
|
function status() {
|
|
18832
19467
|
return {
|
|
18833
19468
|
localAgentRelease,
|
|
18834
19469
|
paired: auth.deviceId !== void 0,
|
|
18835
19470
|
connected: connectionState === "open",
|
|
18836
|
-
degraded: connection?.isTransportDegraded() ?? false,
|
|
18837
19471
|
revoked: connection?.isRevoked() ?? auth.isRevoked(),
|
|
18838
19472
|
deviceId: auth.deviceId,
|
|
18839
19473
|
activeTaskCount: runner?.activeTaskCount ?? 0,
|
|
18840
19474
|
branding: config.branding,
|
|
18841
19475
|
operationalHealth: operationalHealth.snapshot(),
|
|
18842
19476
|
toolsets: toolsetRegistry.status(),
|
|
18843
|
-
egress: agentEgress.status()
|
|
19477
|
+
egress: agentEgress.status(),
|
|
19478
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
18844
19479
|
};
|
|
18845
19480
|
}
|
|
18846
19481
|
function reloadMcpToolsets(mcpToolsets, expectedRevision) {
|
|
18847
|
-
|
|
19482
|
+
const receipt = toolsetRegistry.reload(mcpToolsets, expectedRevision);
|
|
19483
|
+
if (daemonStarted) connection?.refreshHello();
|
|
19484
|
+
return receipt;
|
|
18848
19485
|
}
|
|
18849
19486
|
function reportMcpToolsetObservation(toolsetId, expectedDefinitionRevision, observation) {
|
|
18850
19487
|
toolsetRegistry.report(toolsetId, expectedDefinitionRevision, observation);
|
|
@@ -19253,7 +19890,7 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
19253
19890
|
|
|
19254
19891
|
// src/bin/official-release.ts
|
|
19255
19892
|
var OFFICIAL_LOCAL_AGENT_RELEASE = resolveLocalAgentReleaseIdentity({
|
|
19256
|
-
version: "0.
|
|
19893
|
+
version: "0.13.0"
|
|
19257
19894
|
});
|
|
19258
19895
|
|
|
19259
19896
|
// src/bin/config.ts
|
|
@@ -21333,7 +21970,7 @@ async function recentAuditFacts(storeDir) {
|
|
|
21333
21970
|
}
|
|
21334
21971
|
function projectControl(control) {
|
|
21335
21972
|
if (control.status === "offline") return { status: "offline" };
|
|
21336
|
-
const transport = ["connecting", "open", "closed", "
|
|
21973
|
+
const transport = ["connecting", "open", "closed", "revoked"].includes(control.transport) ? control.transport : "unavailable";
|
|
21337
21974
|
const operationalHealth = control.operationalHealth.availability === "available" ? {
|
|
21338
21975
|
availability: "available",
|
|
21339
21976
|
state: control.operationalHealth.state,
|