@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/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID, createHash, randomBytes, createPrivateKey, generateKeyPairS
|
|
|
2
2
|
import { promises, mkdirSync, constants, existsSync, linkSync, fstatSync, lstatSync, unlinkSync, renameSync, writeFileSync, chmodSync, statSync, readdirSync, readFileSync, realpathSync } from 'fs';
|
|
3
3
|
import * as path3 from 'path';
|
|
4
4
|
import path3__default, { join, isAbsolute } from 'path';
|
|
5
|
-
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, 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,
|
|
5
|
+
import { AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, AgentRefSchema, AgentHomeProjectionPayloadSchema, byokBlobUrlPath, BYOK_BLOBS_PATH, byokBlobFinalizePath, partitionAgentEvents, BYOK_SKILL_PACKS_PATH, byokSkillPackFilePath, BYOK_RECORDS_PATH, byokRecordPath, TASK_TRANSITIONS, AgentEgressPolicySchema, BYOK_PAIR_PATH, PairResponseSchema, BYOK_CHALLENGE_PATH, BYOK_TOKEN_PATH, CONFIGURED_TOOLSETS_MAX_ITEMS, ToolsetIdSchema, AgentContentReceiptPayloadSchema, encodeEnvelope, PROTOCOL_VERSION, createEnvelope, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, 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';
|
|
6
6
|
import net2, { createServer, createConnection } from 'net';
|
|
7
7
|
import * as os4 from 'os';
|
|
8
8
|
import os4__default from 'os';
|
|
@@ -12,7 +12,6 @@ import { createInterface } from 'readline';
|
|
|
12
12
|
import { parseDeviceAssertionEnvelope, SKILL_PACK_MAX_BYTES, hasCapability, parseSkillPackManifest, checkSkillPackManifest, skillPackContentHashInput, checkSkillPackFileContent, SKILL_PACK_ENTRY_PATH, checkSkillPackEntry, isSkillPackPathSafe, DEVICE_PROOF_HEADER, contentHash as contentHash$1, TRUTH_RECORD_KINDS, DEVICE_ASSERTION_AUDIENCE_MAX_BYTES, DEVICE_ASSERTION_DEFAULT_TTL_MS, DEVICE_ASSERTION_MAX_TTL_MS, CONTENT_HASH_PATTERN, isTenantId, nonceSigningBytes, CapabilityDeclarationSchema, DeviceAssertionClaimsSchema, deviceAssertionSigningInput, DEVICE_ASSERTION_SCHEMA_ID } from '@byok-sdk/core';
|
|
13
13
|
import { promisify } from 'util';
|
|
14
14
|
import * as fs14 from 'fs/promises';
|
|
15
|
-
import { WebSocket } from 'ws';
|
|
16
15
|
import { createRequire } from 'module';
|
|
17
16
|
|
|
18
17
|
// src/agent-home.ts
|
|
@@ -423,6 +422,23 @@ async function resolveExistingAncestor(inputPath) {
|
|
|
423
422
|
}
|
|
424
423
|
}
|
|
425
424
|
}
|
|
425
|
+
async function canonicalPath(inputPath) {
|
|
426
|
+
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
427
|
+
return path3__default.resolve(canonical2, ...tail);
|
|
428
|
+
}
|
|
429
|
+
async function assertRealDirectoryIfPresent(target) {
|
|
430
|
+
let stat;
|
|
431
|
+
try {
|
|
432
|
+
stat = await promises.lstat(target);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
const code = error.code;
|
|
435
|
+
if (code === "ENOENT" || code === "ENOTDIR") return;
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
439
|
+
throw new AgentHomeResolutionError(`Agent home path component is not a real directory: ${target}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
426
442
|
async function materializeDirectory(inputPath) {
|
|
427
443
|
const { canonical: canonical2, tail } = await resolveExistingAncestor(inputPath);
|
|
428
444
|
let cursor = canonical2;
|
|
@@ -517,6 +533,39 @@ var AgentHomeLayout = class {
|
|
|
517
533
|
await gate.release();
|
|
518
534
|
}
|
|
519
535
|
}
|
|
536
|
+
/**
|
|
537
|
+
* Pure canonical-home derivation for read-only callers, such as the
|
|
538
|
+
* pre-admission single-writer count. It validates the AgentRef and joins
|
|
539
|
+
* exactly the same `<hostStorageRoot>/agents/<agentId>` segments
|
|
540
|
+
* {@link AgentHomeLayout.resolve} would, canonicalizing only the components
|
|
541
|
+
* that already exist.
|
|
542
|
+
*
|
|
543
|
+
* It deliberately creates no directory, takes no cross-process mutation
|
|
544
|
+
* gate and records no Agent binding, so an offer the host vetoes after the
|
|
545
|
+
* count leaves nothing behind on disk. `resolve()` stays the only path that
|
|
546
|
+
* may materialize a home or bind it to an Agent identity.
|
|
547
|
+
*
|
|
548
|
+
* An `agents` root or `agents/<agentId>` leaf that already exists but is a
|
|
549
|
+
* symlink (or any non-directory) is rejected here with the same error class
|
|
550
|
+
* and message `resolve()` raises for it, so an in-root `two -> one` link
|
|
551
|
+
* fails closed instead of silently keying the count of `one`. A leaf that
|
|
552
|
+
* does not exist yet is not an error: this derivation runs before the home
|
|
553
|
+
* is materialized. A home canonicalizing outside the `agents` root stays
|
|
554
|
+
* rejected as before.
|
|
555
|
+
*/
|
|
556
|
+
async canonicalHomePath(agentRefInput) {
|
|
557
|
+
const agentRef = validateAgentRef(agentRefInput);
|
|
558
|
+
const hostStorageRoot = this.canonicalRoot ?? await canonicalPath(this.hostStorageRootInput);
|
|
559
|
+
const agentsRoot = path3__default.join(hostStorageRoot, AGENT_HOME_DIRECTORY);
|
|
560
|
+
await assertRealDirectoryIfPresent(agentsRoot);
|
|
561
|
+
const lexicalHome = path3__default.join(agentsRoot, agentRef.agentId);
|
|
562
|
+
await assertRealDirectoryIfPresent(lexicalHome);
|
|
563
|
+
const canonicalHome = await canonicalPath(lexicalHome);
|
|
564
|
+
if (canonicalHome === agentsRoot || !isWithin(agentsRoot, canonicalHome)) {
|
|
565
|
+
throw new AgentHomeResolutionError(`Agent home resolves outside the Agent home root: ${canonicalHome}`);
|
|
566
|
+
}
|
|
567
|
+
return canonicalHome;
|
|
568
|
+
}
|
|
520
569
|
/**
|
|
521
570
|
* Prove the canonical root is materializable and writable before the daemon
|
|
522
571
|
* advertises Agent-home capability. No Agent identity or persistent Agent
|
|
@@ -746,6 +795,158 @@ var AgentHomeLeaseManager = class _AgentHomeLeaseManager {
|
|
|
746
795
|
}
|
|
747
796
|
}
|
|
748
797
|
};
|
|
798
|
+
function executionKey(input) {
|
|
799
|
+
const value = input.sessionRef === void 0 ? input.taskId : input.sessionRef;
|
|
800
|
+
const label = input.sessionRef === void 0 ? "taskId" : "sessionRef";
|
|
801
|
+
if (typeof value !== "string" || value.length === 0 || /[\u0000\r\n]/u.test(value)) {
|
|
802
|
+
throw new AgentHomeResolutionError(`Agent execution ${label} must be a non-empty single-line string`);
|
|
803
|
+
}
|
|
804
|
+
return `${input.sessionRef === void 0 ? "task" : "session"}\0${value}`;
|
|
805
|
+
}
|
|
806
|
+
var AgentHomeExecutionLeaseManager = class _AgentHomeExecutionLeaseManager {
|
|
807
|
+
constructor(manager) {
|
|
808
|
+
this.manager = manager;
|
|
809
|
+
}
|
|
810
|
+
manager;
|
|
811
|
+
static groups = /* @__PURE__ */ new Map();
|
|
812
|
+
static queues = /* @__PURE__ */ new Map();
|
|
813
|
+
async acquire(resolution, input) {
|
|
814
|
+
const initialKey = executionKey(input);
|
|
815
|
+
return this.exclusive(resolution.canonicalHome, async () => {
|
|
816
|
+
let group = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
|
|
817
|
+
if (group === void 0) {
|
|
818
|
+
const baseLease = await this.manager.acquire(resolution);
|
|
819
|
+
group = {
|
|
820
|
+
manager: this.manager,
|
|
821
|
+
baseLease,
|
|
822
|
+
agentId: resolution.agentRef.agentId,
|
|
823
|
+
leasesByKey: /* @__PURE__ */ new Map()
|
|
824
|
+
};
|
|
825
|
+
_AgentHomeExecutionLeaseManager.groups.set(resolution.canonicalHome, group);
|
|
826
|
+
} else if (group.manager !== this.manager || group.agentId !== resolution.agentRef.agentId) {
|
|
827
|
+
throw new AgentHomeBusyError(`Agent home ${resolution.canonicalHome} is active under another execution owner`);
|
|
828
|
+
}
|
|
829
|
+
if (group.leasesByKey.has(initialKey)) {
|
|
830
|
+
throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
|
|
831
|
+
}
|
|
832
|
+
const leaseId = randomUUID();
|
|
833
|
+
group.leasesByKey.set(initialKey, leaseId);
|
|
834
|
+
let currentKey = initialKey;
|
|
835
|
+
let sessionBound = input.sessionRef !== void 0;
|
|
836
|
+
let released = false;
|
|
837
|
+
return Object.freeze({
|
|
838
|
+
leaseId,
|
|
839
|
+
agentRef: resolution.agentRef,
|
|
840
|
+
canonicalHome: resolution.canonicalHome,
|
|
841
|
+
cwd: resolution.canonicalHome,
|
|
842
|
+
homeIdentity: group.baseLease.homeIdentity,
|
|
843
|
+
bindSession: async (sessionRef) => {
|
|
844
|
+
const nextKey = executionKey({ taskId: input.taskId, sessionRef });
|
|
845
|
+
await this.exclusive(resolution.canonicalHome, async () => {
|
|
846
|
+
if (released) throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is already released`);
|
|
847
|
+
const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
|
|
848
|
+
if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
|
|
849
|
+
throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
|
|
850
|
+
}
|
|
851
|
+
if (nextKey === currentKey) return;
|
|
852
|
+
if (sessionBound) {
|
|
853
|
+
throw new AgentHomeBusyError(
|
|
854
|
+
`Agent session execution lease ${leaseId} cannot rebind to a different runtime session`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (currentGroup.leasesByKey.has(nextKey)) {
|
|
858
|
+
throw new AgentHomeBusyError(`Agent session already has an active execution lease in ${resolution.canonicalHome}`);
|
|
859
|
+
}
|
|
860
|
+
currentGroup.leasesByKey.set(nextKey, leaseId);
|
|
861
|
+
currentGroup.leasesByKey.delete(currentKey);
|
|
862
|
+
currentKey = nextKey;
|
|
863
|
+
sessionBound = true;
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
release: async () => {
|
|
867
|
+
await this.exclusive(resolution.canonicalHome, async () => {
|
|
868
|
+
if (released) return;
|
|
869
|
+
const currentGroup = _AgentHomeExecutionLeaseManager.groups.get(resolution.canonicalHome);
|
|
870
|
+
if (currentGroup !== group || currentGroup.leasesByKey.get(currentKey) !== leaseId) {
|
|
871
|
+
released = true;
|
|
872
|
+
throw new AgentHomeBusyError(`Agent execution lease ${leaseId} is no longer owned by this process`);
|
|
873
|
+
}
|
|
874
|
+
currentGroup.leasesByKey.delete(currentKey);
|
|
875
|
+
released = true;
|
|
876
|
+
if (currentGroup.leasesByKey.size === 0) {
|
|
877
|
+
_AgentHomeExecutionLeaseManager.groups.delete(resolution.canonicalHome);
|
|
878
|
+
await currentGroup.baseLease.release();
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* WP0: Attempts currently holding an execution lease on this exact
|
|
887
|
+
* canonical home, across every lane and every session. This is the number
|
|
888
|
+
* the daemon's admission gate reads before any side effect — see
|
|
889
|
+
* `TaskRunner.handleOffer`'s per-home busy gate.
|
|
890
|
+
*
|
|
891
|
+
* Derived from the one lease registry above rather than a second tally, so
|
|
892
|
+
* it inherits the lease lifecycle exactly: an entry appears at `acquire()`,
|
|
893
|
+
* survives `bindSession()` (which rekeys in place), and disappears only at
|
|
894
|
+
* `release()`, which the task runner calls after the attempt is terminal
|
|
895
|
+
* AND `Session.close()` resolved. A failed disposal never reaches
|
|
896
|
+
* `release()`, so the slot stays held — fail closed, the same posture as
|
|
897
|
+
* `runtime-disposal-failed`. Crash residue needs nothing extra here: a
|
|
898
|
+
* restarted daemon starts with an empty registry and reclaims the on-disk
|
|
899
|
+
* marker only under the same stable owner identity (`openLeaseMarker`).
|
|
900
|
+
*
|
|
901
|
+
* Counted regardless of which lease manager owns the group: the invariant
|
|
902
|
+
* being protected is the filesystem path (`MEMORY.md`, `notes/`, `.git`),
|
|
903
|
+
* not the owner identity.
|
|
904
|
+
*/
|
|
905
|
+
activeAttemptCount(canonicalHome) {
|
|
906
|
+
return _AgentHomeExecutionLeaseManager.groups.get(canonicalHome)?.leasesByKey.size ?? 0;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Counts-only readback for daemon/control status. Scoped to this manager's
|
|
910
|
+
* own leases, so the number describes this daemon rather than every home
|
|
911
|
+
* any manager in the process happens to hold. Never exposes a home path.
|
|
912
|
+
*/
|
|
913
|
+
activeAttemptSummary() {
|
|
914
|
+
let homes = 0;
|
|
915
|
+
let attempts = 0;
|
|
916
|
+
for (const group of _AgentHomeExecutionLeaseManager.groups.values()) {
|
|
917
|
+
if (group.manager !== this.manager) continue;
|
|
918
|
+
homes += 1;
|
|
919
|
+
attempts += group.leasesByKey.size;
|
|
920
|
+
}
|
|
921
|
+
return { homes, attempts };
|
|
922
|
+
}
|
|
923
|
+
async mutate(binding, operation) {
|
|
924
|
+
return this.exclusive(binding.resolution.canonicalHome, async () => {
|
|
925
|
+
const group = _AgentHomeExecutionLeaseManager.groups.get(binding.resolution.canonicalHome);
|
|
926
|
+
if (group === void 0 || group.manager !== this.manager || ![...group.leasesByKey.values()].includes(binding.lease.leaseId)) {
|
|
927
|
+
throw new AgentHomeBusyError("Agent execution lease does not own this home mutation");
|
|
928
|
+
}
|
|
929
|
+
return operation();
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
async exclusive(canonicalHome, operation) {
|
|
933
|
+
const prior = _AgentHomeExecutionLeaseManager.queues.get(canonicalHome) ?? Promise.resolve();
|
|
934
|
+
let release;
|
|
935
|
+
const tail = new Promise((resolve) => {
|
|
936
|
+
release = resolve;
|
|
937
|
+
});
|
|
938
|
+
_AgentHomeExecutionLeaseManager.queues.set(canonicalHome, tail);
|
|
939
|
+
await prior;
|
|
940
|
+
try {
|
|
941
|
+
return await operation();
|
|
942
|
+
} finally {
|
|
943
|
+
release();
|
|
944
|
+
if (_AgentHomeExecutionLeaseManager.queues.get(canonicalHome) === tail) {
|
|
945
|
+
_AgentHomeExecutionLeaseManager.queues.delete(canonicalHome);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
};
|
|
749
950
|
async function initializeAgentHome(resolution) {
|
|
750
951
|
await ensureDirectoryNoSymlink(
|
|
751
952
|
resolution.canonicalHome,
|
|
@@ -821,10 +1022,12 @@ var AgentHomeManager = class {
|
|
|
821
1022
|
layout;
|
|
822
1023
|
projection;
|
|
823
1024
|
leaseManager;
|
|
1025
|
+
executionLeaseManager;
|
|
824
1026
|
constructor(options) {
|
|
825
1027
|
this.layout = new AgentHomeLayout(options.hostStorageRoot);
|
|
826
1028
|
this.projection = options.projection;
|
|
827
1029
|
this.leaseManager = options.leaseManager ?? new AgentHomeLeaseManager();
|
|
1030
|
+
this.executionLeaseManager = new AgentHomeExecutionLeaseManager(this.leaseManager);
|
|
828
1031
|
}
|
|
829
1032
|
async prepare(agentRef) {
|
|
830
1033
|
const binding = await this.acquire(agentRef);
|
|
@@ -851,12 +1054,25 @@ var AgentHomeManager = class {
|
|
|
851
1054
|
const lease = await this.leaseManager.acquire(resolution);
|
|
852
1055
|
return Object.freeze({ resolution, lease });
|
|
853
1056
|
}
|
|
1057
|
+
async acquireExecution(agentRef, input) {
|
|
1058
|
+
const resolution = await this.layout.resolve(agentRef);
|
|
1059
|
+
const lease = await this.executionLeaseManager.acquire(resolution, input);
|
|
1060
|
+
return Object.freeze({ resolution, lease });
|
|
1061
|
+
}
|
|
854
1062
|
/** Initialize only after any requested session exact-match has succeeded. */
|
|
855
1063
|
async initialize(binding) {
|
|
856
|
-
|
|
1064
|
+
await this.initializeResolved(binding.resolution, binding.lease.cwd);
|
|
1065
|
+
}
|
|
1066
|
+
async initializeExecution(binding) {
|
|
1067
|
+
await this.mutateExecution(binding, () => this.initializeResolved(binding.resolution, binding.lease.cwd));
|
|
1068
|
+
}
|
|
1069
|
+
async mutateExecution(binding, operation) {
|
|
1070
|
+
return this.executionLeaseManager.mutate(binding, operation);
|
|
1071
|
+
}
|
|
1072
|
+
async initializeResolved(resolution, cwd) {
|
|
857
1073
|
await initializeAgentHome(resolution);
|
|
858
1074
|
const prepare = this.projection?.prepare;
|
|
859
|
-
if (prepare !== void 0) await prepare({ ...resolution, cwd
|
|
1075
|
+
if (prepare !== void 0) await prepare({ ...resolution, cwd });
|
|
860
1076
|
if (await promises.realpath(resolution.homeDir) !== resolution.canonicalHome) {
|
|
861
1077
|
throw new AgentHomeResolutionError("Agent projection changed the canonical home path");
|
|
862
1078
|
}
|
|
@@ -1907,13 +2123,22 @@ function freezeRuntimeAdapterDescriptor(descriptor) {
|
|
|
1907
2123
|
});
|
|
1908
2124
|
}
|
|
1909
2125
|
function sealRuntimeOperationManifest(manifest) {
|
|
2126
|
+
const dispatchSelection = manifest.dispatchSelection === void 0 ? void 0 : manifest.dispatchSelection.lane === "byok-profile" ? Object.freeze({
|
|
2127
|
+
...manifest.dispatchSelection,
|
|
2128
|
+
providerProfile: Object.freeze({
|
|
2129
|
+
...manifest.dispatchSelection.providerProfile,
|
|
2130
|
+
requiredCapabilities: Object.freeze([
|
|
2131
|
+
...manifest.dispatchSelection.providerProfile.requiredCapabilities
|
|
2132
|
+
])
|
|
2133
|
+
})
|
|
2134
|
+
}) : Object.freeze({ ...manifest.dispatchSelection });
|
|
1910
2135
|
return Object.freeze({
|
|
1911
2136
|
taskId: manifest.taskId,
|
|
1912
2137
|
runtimeId: manifest.runtimeId,
|
|
1913
2138
|
descriptor: freezeRuntimeAdapterDescriptor(manifest.descriptor),
|
|
1914
2139
|
policy: frozenPolicy(manifest.policy),
|
|
1915
2140
|
requiredToolsetIds: Object.freeze([...manifest.requiredToolsetIds]),
|
|
1916
|
-
...
|
|
2141
|
+
...dispatchSelection === void 0 ? {} : { dispatchSelection },
|
|
1917
2142
|
...manifest.sessionRef === void 0 ? {} : { sessionRef: manifest.sessionRef },
|
|
1918
2143
|
...manifest.agentRef === void 0 ? {} : { agentRef: Object.freeze({ agentId: manifest.agentRef.agentId, profileRevision: manifest.agentRef.profileRevision }) },
|
|
1919
2144
|
cwd: manifest.cwd ?? manifest.workspace.workspaceDir,
|
|
@@ -4335,7 +4560,11 @@ function validatePiByokLauncherConfig(launcher) {
|
|
|
4335
4560
|
"--macos-keychain-path",
|
|
4336
4561
|
"--secret-service-prefix",
|
|
4337
4562
|
"--provider",
|
|
4338
|
-
"--model"
|
|
4563
|
+
"--model",
|
|
4564
|
+
"--profile-revision",
|
|
4565
|
+
"--profile-hash",
|
|
4566
|
+
"--required-capabilities",
|
|
4567
|
+
"--validate-only"
|
|
4339
4568
|
]);
|
|
4340
4569
|
const conflicting = launcher.args?.find((arg) => reserved.has(arg));
|
|
4341
4570
|
if (conflicting !== void 0) {
|
|
@@ -4397,11 +4626,17 @@ var PiAdapter = class {
|
|
|
4397
4626
|
const bin = this.resolveBin();
|
|
4398
4627
|
const extensions = (this.options.resolveExtensions ?? resolvePiExtensions)();
|
|
4399
4628
|
const selection = input.offer.dispatchSelection;
|
|
4400
|
-
const pinnedSelection = selection === void 0 ? void 0 : Object.freeze({
|
|
4629
|
+
const pinnedSelection = selection === void 0 ? void 0 : selection.lane === "byok-profile" ? Object.freeze({
|
|
4630
|
+
...selection,
|
|
4631
|
+
providerProfile: Object.freeze({
|
|
4632
|
+
...selection.providerProfile,
|
|
4633
|
+
requiredCapabilities: Object.freeze([...selection.providerProfile.requiredCapabilities])
|
|
4634
|
+
})
|
|
4635
|
+
}) : Object.freeze({ ...selection });
|
|
4401
4636
|
let command = bin.command;
|
|
4402
4637
|
let launcherArgs;
|
|
4403
4638
|
if (pinnedSelection !== void 0) {
|
|
4404
|
-
if (pinnedSelection.lane !== "byok" || pinnedSelection.runtimeId !== "pi") {
|
|
4639
|
+
if (pinnedSelection.lane !== "byok" && pinnedSelection.lane !== "byok-profile" || pinnedSelection.runtimeId !== "pi") {
|
|
4405
4640
|
return { kind: "reject", reason: `pi adapter cannot execute ${pinnedSelection.lane} selection for runtime ${pinnedSelection.runtimeId}`, retryable: false };
|
|
4406
4641
|
}
|
|
4407
4642
|
const launcher = this.options.byokLauncher;
|
|
@@ -4409,6 +4644,21 @@ var PiAdapter = class {
|
|
|
4409
4644
|
return { kind: "reject", reason: "pi BYOK selection requires a configured credential-custody launcher", retryable: false };
|
|
4410
4645
|
}
|
|
4411
4646
|
command = launcher.command;
|
|
4647
|
+
const providerProfile = pinnedSelection.lane === "byok-profile" ? pinnedSelection.providerProfile : void 0;
|
|
4648
|
+
if (providerProfile !== void 0) {
|
|
4649
|
+
try {
|
|
4650
|
+
await (this.options.validateProviderProfileBinding ?? validateProviderProfileBindingWithLauncher)(
|
|
4651
|
+
providerProfile,
|
|
4652
|
+
launcher
|
|
4653
|
+
);
|
|
4654
|
+
} catch (error) {
|
|
4655
|
+
return {
|
|
4656
|
+
kind: "reject",
|
|
4657
|
+
reason: `provider profile admission failed: ${errorMessage3(error)}`,
|
|
4658
|
+
retryable: false
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4412
4662
|
launcherArgs = [
|
|
4413
4663
|
...launcher.args ?? [],
|
|
4414
4664
|
"--pi-bin",
|
|
@@ -4420,9 +4670,19 @@ var PiAdapter = class {
|
|
|
4420
4670
|
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
4421
4671
|
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : [],
|
|
4422
4672
|
"--provider",
|
|
4423
|
-
pinnedSelection.providerId,
|
|
4673
|
+
providerProfile?.profileRef ?? (pinnedSelection.lane === "byok" ? pinnedSelection.providerId : ""),
|
|
4424
4674
|
"--model",
|
|
4425
|
-
pinnedSelection.modelId
|
|
4675
|
+
providerProfile?.modelId ?? (pinnedSelection.lane === "byok" ? pinnedSelection.modelId : ""),
|
|
4676
|
+
...providerProfile === void 0 ? [] : [
|
|
4677
|
+
"--profile-revision",
|
|
4678
|
+
providerProfile.profileRevision,
|
|
4679
|
+
"--profile-hash",
|
|
4680
|
+
providerProfile.profileHash,
|
|
4681
|
+
"--required-capabilities",
|
|
4682
|
+
JSON.stringify(providerProfile.requiredCapabilities),
|
|
4683
|
+
"--validate-only",
|
|
4684
|
+
"false"
|
|
4685
|
+
]
|
|
4426
4686
|
];
|
|
4427
4687
|
}
|
|
4428
4688
|
return {
|
|
@@ -4565,8 +4825,42 @@ var PiAdapter = class {
|
|
|
4565
4825
|
return (this.options.resolveBin ?? resolvePiBin)();
|
|
4566
4826
|
}
|
|
4567
4827
|
};
|
|
4828
|
+
async function validateProviderProfileBindingWithLauncher(binding, launcher) {
|
|
4829
|
+
await execFileAsync(launcher.command, [
|
|
4830
|
+
...launcher.args ?? [],
|
|
4831
|
+
"--pi-bin",
|
|
4832
|
+
process.execPath,
|
|
4833
|
+
"--profile-db",
|
|
4834
|
+
launcher.profileDbPath,
|
|
4835
|
+
"--session-dir",
|
|
4836
|
+
launcher.sessionDir,
|
|
4837
|
+
"--provider",
|
|
4838
|
+
binding.profileRef,
|
|
4839
|
+
"--model",
|
|
4840
|
+
binding.modelId,
|
|
4841
|
+
"--profile-revision",
|
|
4842
|
+
binding.profileRevision,
|
|
4843
|
+
"--profile-hash",
|
|
4844
|
+
binding.profileHash,
|
|
4845
|
+
"--required-capabilities",
|
|
4846
|
+
JSON.stringify(binding.requiredCapabilities),
|
|
4847
|
+
"--validate-only",
|
|
4848
|
+
"true",
|
|
4849
|
+
...launcher.macosKeychainPath !== void 0 ? ["--macos-keychain-path", launcher.macosKeychainPath] : [],
|
|
4850
|
+
...launcher.secretServicePrefix ? ["--secret-service-prefix", launcher.secretServicePrefix] : []
|
|
4851
|
+
], { timeout: DETECT_TIMEOUT_MS });
|
|
4852
|
+
}
|
|
4568
4853
|
function sameDispatchSelection(left, right) {
|
|
4569
4854
|
if (left === void 0 || right === void 0) return left === right;
|
|
4855
|
+
if (left.lane === "byok-profile" || right.lane === "byok-profile") {
|
|
4856
|
+
if (left.lane !== "byok-profile" || right.lane !== "byok-profile") return false;
|
|
4857
|
+
if (left.runtimeId !== right.runtimeId) return false;
|
|
4858
|
+
const leftProfile = left.providerProfile;
|
|
4859
|
+
const rightProfile = right.providerProfile;
|
|
4860
|
+
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(
|
|
4861
|
+
(capability, index) => capability === rightProfile.requiredCapabilities[index]
|
|
4862
|
+
);
|
|
4863
|
+
}
|
|
4570
4864
|
return left.lane === right.lane && left.runtimeId === right.runtimeId && left.providerId === right.providerId && left.modelId === right.modelId;
|
|
4571
4865
|
}
|
|
4572
4866
|
async function resolveAuthoritativeSessionId(rpc) {
|
|
@@ -6983,9 +7277,18 @@ function assertRecord(value) {
|
|
|
6983
7277
|
throw new DeviceCredentialStoreError("OS credential entry has an incomplete device credential");
|
|
6984
7278
|
}
|
|
6985
7279
|
}
|
|
6986
|
-
function
|
|
6987
|
-
|
|
6988
|
-
|
|
7280
|
+
function isFirstPairingAttempt(value) {
|
|
7281
|
+
return "kind" in value && value.kind === "first-pairing-attempt-v1";
|
|
7282
|
+
}
|
|
7283
|
+
function assertFirstPairingAttempt(value) {
|
|
7284
|
+
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) {
|
|
7285
|
+
throw new DeviceCredentialStoreError("OS credential entry has an invalid first-pairing attempt shape");
|
|
7286
|
+
}
|
|
7287
|
+
}
|
|
7288
|
+
function encode(authority) {
|
|
7289
|
+
if (isFirstPairingAttempt(authority)) assertFirstPairingAttempt(authority);
|
|
7290
|
+
else assertRecord(authority);
|
|
7291
|
+
const encoded = Buffer.from(JSON.stringify(authority), "utf8").toString("base64");
|
|
6989
7292
|
const value = `${ENCODED_PREFIX}${encoded}`;
|
|
6990
7293
|
if (Buffer.byteLength(value, "utf8") > 2400) {
|
|
6991
7294
|
throw new DeviceCredentialStoreError("device credential exceeds the OS credential entry bound");
|
|
@@ -7014,6 +7317,16 @@ function decode(value) {
|
|
|
7014
7317
|
} catch {
|
|
7015
7318
|
throw new DeviceCredentialStoreError("OS credential entry is not valid JSON");
|
|
7016
7319
|
}
|
|
7320
|
+
if (typeof parsed === "object" && parsed !== null && parsed.kind === "first-pairing-attempt-v1") {
|
|
7321
|
+
assertFirstPairingAttempt(parsed);
|
|
7322
|
+
return Object.freeze({
|
|
7323
|
+
kind: parsed.kind,
|
|
7324
|
+
deviceName: parsed.deviceName,
|
|
7325
|
+
devicePublicKey: parsed.devicePublicKey,
|
|
7326
|
+
devicePrivateKeyPem: parsed.devicePrivateKeyPem,
|
|
7327
|
+
...parsed.machineId === void 0 ? {} : { machineId: parsed.machineId }
|
|
7328
|
+
});
|
|
7329
|
+
}
|
|
7017
7330
|
assertRecord(parsed);
|
|
7018
7331
|
return Object.freeze({
|
|
7019
7332
|
deviceId: parsed.deviceId,
|
|
@@ -7044,6 +7357,18 @@ var DeviceCredentialStore = class {
|
|
|
7044
7357
|
this.#run = options.commandRunner ?? runDeviceCommand;
|
|
7045
7358
|
}
|
|
7046
7359
|
async read() {
|
|
7360
|
+
const authority = await this.#readAuthority();
|
|
7361
|
+
return authority === void 0 || isFirstPairingAttempt(authority) ? void 0 : authority;
|
|
7362
|
+
}
|
|
7363
|
+
async readFirstPairingAttempt() {
|
|
7364
|
+
const authority = await this.#readAuthority();
|
|
7365
|
+
return authority !== void 0 && isFirstPairingAttempt(authority) ? authority : void 0;
|
|
7366
|
+
}
|
|
7367
|
+
async saveFirstPairingAttempt(attempt) {
|
|
7368
|
+
assertFirstPairingAttempt(attempt);
|
|
7369
|
+
await this.#replaceAuthority(attempt);
|
|
7370
|
+
}
|
|
7371
|
+
async #readAuthority() {
|
|
7047
7372
|
const result = await this.#invoke("read");
|
|
7048
7373
|
if (result.exitCode === NOT_FOUND || this.#platform === "linux" && result.exitCode === 1 && result.stderr.trim().length === 0) return void 0;
|
|
7049
7374
|
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
@@ -7056,6 +7381,12 @@ var DeviceCredentialStore = class {
|
|
|
7056
7381
|
}
|
|
7057
7382
|
async replace(record3) {
|
|
7058
7383
|
const encoded = encode(record3);
|
|
7384
|
+
await this.#replaceEncoded(encoded);
|
|
7385
|
+
}
|
|
7386
|
+
async #replaceAuthority(authority) {
|
|
7387
|
+
await this.#replaceEncoded(encode(authority));
|
|
7388
|
+
}
|
|
7389
|
+
async #replaceEncoded(encoded) {
|
|
7059
7390
|
const result = await this.#invoke("replace", encoded);
|
|
7060
7391
|
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
7061
7392
|
if (result.exitCode !== 0) {
|
|
@@ -7066,7 +7397,7 @@ var DeviceCredentialStore = class {
|
|
|
7066
7397
|
}
|
|
7067
7398
|
/** Returns true only after the sole secret authority is confirmed absent. */
|
|
7068
7399
|
async clear() {
|
|
7069
|
-
const before = await this
|
|
7400
|
+
const before = await this.#readAuthority();
|
|
7070
7401
|
if (before === void 0) return false;
|
|
7071
7402
|
const result = await this.#invoke("clear");
|
|
7072
7403
|
if (result.exitCode === 127) throw new DeviceCredentialStoreUnavailableError();
|
|
@@ -7075,7 +7406,7 @@ var DeviceCredentialStore = class {
|
|
|
7075
7406
|
`operating-system credential provider could not clear device credentials${providerDiagnostic(result.stderr)}`
|
|
7076
7407
|
);
|
|
7077
7408
|
}
|
|
7078
|
-
if (await this
|
|
7409
|
+
if (await this.#readAuthority() !== void 0) {
|
|
7079
7410
|
throw new DeviceCredentialStoreError("operating-system credential provider reported deletion but device credentials remain");
|
|
7080
7411
|
}
|
|
7081
7412
|
return true;
|
|
@@ -7150,17 +7481,24 @@ var DeviceCredentialStore = class {
|
|
|
7150
7481
|
}
|
|
7151
7482
|
};
|
|
7152
7483
|
var InMemoryDeviceCredentialStore = class {
|
|
7153
|
-
#
|
|
7484
|
+
#authority;
|
|
7154
7485
|
async read() {
|
|
7155
|
-
return this.#
|
|
7486
|
+
return this.#authority === void 0 || isFirstPairingAttempt(this.#authority) ? void 0 : Object.freeze({ ...this.#authority });
|
|
7487
|
+
}
|
|
7488
|
+
async readFirstPairingAttempt() {
|
|
7489
|
+
return this.#authority !== void 0 && isFirstPairingAttempt(this.#authority) ? Object.freeze({ ...this.#authority }) : void 0;
|
|
7490
|
+
}
|
|
7491
|
+
async saveFirstPairingAttempt(attempt) {
|
|
7492
|
+
assertFirstPairingAttempt(attempt);
|
|
7493
|
+
this.#authority = Object.freeze({ ...attempt });
|
|
7156
7494
|
}
|
|
7157
7495
|
async replace(record3) {
|
|
7158
7496
|
assertRecord(record3);
|
|
7159
|
-
this.#
|
|
7497
|
+
this.#authority = Object.freeze({ ...record3 });
|
|
7160
7498
|
}
|
|
7161
7499
|
async clear() {
|
|
7162
|
-
const had = this.#
|
|
7163
|
-
this.#
|
|
7500
|
+
const had = this.#authority !== void 0;
|
|
7501
|
+
this.#authority = void 0;
|
|
7164
7502
|
return had;
|
|
7165
7503
|
}
|
|
7166
7504
|
};
|
|
@@ -7430,23 +7768,22 @@ function signNonce(privateKey, nonce) {
|
|
|
7430
7768
|
const signature = sign(null, nonceSigningBytes(nonce), privateKey);
|
|
7431
7769
|
return signature.toString("base64url");
|
|
7432
7770
|
}
|
|
7771
|
+
|
|
7772
|
+
// src/daemon/url.ts
|
|
7433
7773
|
function toHttpBase(serverUrl) {
|
|
7434
7774
|
const url = new URL(serverUrl);
|
|
7435
|
-
if (url.protocol === "ws:") url.protocol = "http:";
|
|
7436
|
-
else if (url.protocol === "wss:") url.protocol = "https:";
|
|
7437
7775
|
url.pathname = "/";
|
|
7438
7776
|
url.search = "";
|
|
7439
7777
|
return url.toString();
|
|
7440
7778
|
}
|
|
7441
|
-
function toWsUrl(serverUrl) {
|
|
7442
|
-
const url = new URL(BYOK_WS_PATH, toHttpBase(serverUrl));
|
|
7443
|
-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
7444
|
-
return url.toString();
|
|
7445
|
-
}
|
|
7446
7779
|
function describeEndpoint(transport, url) {
|
|
7447
7780
|
const parsed = typeof url === "string" ? new URL(url) : url;
|
|
7448
7781
|
return { transport, host: parsed.host, path: parsed.pathname };
|
|
7449
7782
|
}
|
|
7783
|
+
function formatServerUrl(value) {
|
|
7784
|
+
const url = typeof value === "string" ? new URL(value) : value;
|
|
7785
|
+
return `${url.protocol}//${url.host}${url.pathname}`;
|
|
7786
|
+
}
|
|
7450
7787
|
var InsecureServerUrlError = class extends Error {
|
|
7451
7788
|
constructor(message) {
|
|
7452
7789
|
super(message);
|
|
@@ -7465,22 +7802,21 @@ function assertServerUrlAllowed(rawUrl, opts = {}) {
|
|
|
7465
7802
|
let url;
|
|
7466
7803
|
try {
|
|
7467
7804
|
url = new URL(rawUrl);
|
|
7468
|
-
} catch
|
|
7469
|
-
throw new InsecureServerUrlError(
|
|
7805
|
+
} catch {
|
|
7806
|
+
throw new InsecureServerUrlError("invalid server URL");
|
|
7470
7807
|
}
|
|
7808
|
+
const endpoint = formatServerUrl(url);
|
|
7471
7809
|
switch (url.protocol) {
|
|
7472
7810
|
case "https:":
|
|
7473
|
-
case "wss:":
|
|
7474
7811
|
return;
|
|
7475
7812
|
case "http:":
|
|
7476
|
-
case "ws:":
|
|
7477
7813
|
if (opts.dangerouslyAllowInsecureRemote || isLoopbackHostname(url.hostname)) return;
|
|
7478
7814
|
throw new InsecureServerUrlError(
|
|
7479
|
-
`refusing to connect to "${
|
|
7815
|
+
`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.`
|
|
7480
7816
|
);
|
|
7481
7817
|
default:
|
|
7482
7818
|
throw new InsecureServerUrlError(
|
|
7483
|
-
`refusing to connect to "${
|
|
7819
|
+
`refusing to connect to "${endpoint}" \u2014 unsupported scheme "${url.protocol}" (expected http: or https:).`
|
|
7484
7820
|
);
|
|
7485
7821
|
}
|
|
7486
7822
|
}
|
|
@@ -7492,12 +7828,22 @@ var DeviceRevokedError = class extends Error {
|
|
|
7492
7828
|
this.name = "DeviceRevokedError";
|
|
7493
7829
|
}
|
|
7494
7830
|
};
|
|
7831
|
+
var AuthRequestAbortedError = class extends Error {
|
|
7832
|
+
constructor(reason) {
|
|
7833
|
+
super(reason === "deadline" ? "authentication request exceeded its deadline" : "authentication request was cancelled during shutdown");
|
|
7834
|
+
this.reason = reason;
|
|
7835
|
+
this.name = "AuthRequestAbortedError";
|
|
7836
|
+
}
|
|
7837
|
+
reason;
|
|
7838
|
+
};
|
|
7495
7839
|
var ASSUMED_PAIR_TOKEN_TTL_MS = 45 * 60 * 1e3;
|
|
7496
7840
|
var RENEW_MARGIN_MS = 60 * 1e3;
|
|
7841
|
+
var DEFAULT_AUTH_REQUEST_DEADLINE_MS = 15e3;
|
|
7497
7842
|
var AuthManager = class {
|
|
7498
7843
|
constructor(opts) {
|
|
7499
7844
|
this.opts = opts;
|
|
7500
7845
|
this.credentials = opts.credentials ?? opts.store.credentials;
|
|
7846
|
+
this.requestDeadlineMs = resolveRequestDeadlineMs(opts.authRequestDeadlineMs);
|
|
7501
7847
|
}
|
|
7502
7848
|
opts;
|
|
7503
7849
|
record;
|
|
@@ -7507,7 +7853,10 @@ var AuthManager = class {
|
|
|
7507
7853
|
stopped = false;
|
|
7508
7854
|
pairing = false;
|
|
7509
7855
|
credentialMutationTail = Promise.resolve();
|
|
7856
|
+
/** The sole cancellation authority for the request currently inside the serialized credential mutation. */
|
|
7857
|
+
activeRequest;
|
|
7510
7858
|
credentials;
|
|
7859
|
+
requestDeadlineMs;
|
|
7511
7860
|
get deviceId() {
|
|
7512
7861
|
return this.record?.deviceId;
|
|
7513
7862
|
}
|
|
@@ -7543,23 +7892,51 @@ var AuthManager = class {
|
|
|
7543
7892
|
if (!(error instanceof DeviceRecordRePairRequiredError)) throw error;
|
|
7544
7893
|
}
|
|
7545
7894
|
}
|
|
7546
|
-
const
|
|
7895
|
+
const observedDeviceName = this.opts.deviceName ?? os4__default.hostname();
|
|
7896
|
+
const observedMachineId = await this.opts.machineId?.();
|
|
7897
|
+
let keyPair;
|
|
7898
|
+
let pairingDeviceName = observedDeviceName;
|
|
7899
|
+
let pairingMachineId = observedMachineId;
|
|
7900
|
+
if (existing) {
|
|
7901
|
+
keyPair = { privateKey: importPrivateKeyPem(existing.devicePrivateKeyPem), publicKeyBase64Url: existing.devicePublicKey };
|
|
7902
|
+
} else {
|
|
7903
|
+
const firstAttempt = await this.credentials.readFirstPairingAttempt();
|
|
7904
|
+
if (firstAttempt) {
|
|
7905
|
+
keyPair = {
|
|
7906
|
+
privateKey: importPrivateKeyPem(firstAttempt.devicePrivateKeyPem),
|
|
7907
|
+
publicKeyBase64Url: firstAttempt.devicePublicKey
|
|
7908
|
+
};
|
|
7909
|
+
pairingDeviceName = firstAttempt.deviceName;
|
|
7910
|
+
pairingMachineId = firstAttempt.machineId;
|
|
7911
|
+
} else {
|
|
7912
|
+
keyPair = generateDeviceKeyPair();
|
|
7913
|
+
await this.credentials.saveFirstPairingAttempt({
|
|
7914
|
+
kind: "first-pairing-attempt-v1",
|
|
7915
|
+
deviceName: pairingDeviceName,
|
|
7916
|
+
devicePublicKey: keyPair.publicKeyBase64Url,
|
|
7917
|
+
devicePrivateKeyPem: exportPrivateKeyPem(keyPair.privateKey),
|
|
7918
|
+
...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
|
|
7919
|
+
});
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7547
7922
|
const url = new URL(BYOK_PAIR_PATH, toHttpBase(this.opts.serverUrl));
|
|
7548
|
-
const
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7923
|
+
const body = await this.runRequest(async (signal) => {
|
|
7924
|
+
const res = await fetch(url, {
|
|
7925
|
+
method: "POST",
|
|
7926
|
+
headers: { "content-type": "application/json" },
|
|
7927
|
+
body: JSON.stringify({
|
|
7928
|
+
pairingCode,
|
|
7929
|
+
deviceName: pairingDeviceName,
|
|
7930
|
+
devicePublicKey: keyPair.publicKeyBase64Url,
|
|
7931
|
+
...pairingMachineId === void 0 ? {} : { machineId: pairingMachineId }
|
|
7932
|
+
}),
|
|
7933
|
+
signal
|
|
7934
|
+
});
|
|
7935
|
+
if (!res.ok) {
|
|
7936
|
+
throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
|
|
7937
|
+
}
|
|
7938
|
+
return PairResponseSchema.parse(await res.json());
|
|
7558
7939
|
});
|
|
7559
|
-
if (!res.ok) {
|
|
7560
|
-
throw new Error(`pairing failed: HTTP ${res.status} ${await safeErrorText(res)}`.trimEnd());
|
|
7561
|
-
}
|
|
7562
|
-
const body = PairResponseSchema.parse(await res.json());
|
|
7563
7940
|
const metadata = {
|
|
7564
7941
|
deviceId: body.deviceId,
|
|
7565
7942
|
tenantId: body.tenantId,
|
|
@@ -7598,6 +7975,7 @@ var AuthManager = class {
|
|
|
7598
7975
|
this.stopped = true;
|
|
7599
7976
|
if (this.proactiveTimer) clearTimeout(this.proactiveTimer);
|
|
7600
7977
|
this.proactiveTimer = void 0;
|
|
7978
|
+
this.activeRequest?.abort();
|
|
7601
7979
|
await this.credentialMutationTail;
|
|
7602
7980
|
}
|
|
7603
7981
|
async renew() {
|
|
@@ -7614,29 +7992,35 @@ var AuthManager = class {
|
|
|
7614
7992
|
const record3 = this.record;
|
|
7615
7993
|
const base = toHttpBase(this.opts.serverUrl);
|
|
7616
7994
|
const privateKey = importPrivateKeyPem(record3.devicePrivateKeyPem);
|
|
7617
|
-
const
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7995
|
+
const { nonce } = await this.runRequest(async (signal) => {
|
|
7996
|
+
const challengeRes = await fetch(new URL(BYOK_CHALLENGE_PATH, base), {
|
|
7997
|
+
method: "POST",
|
|
7998
|
+
headers: { "content-type": "application/json" },
|
|
7999
|
+
body: JSON.stringify({ deviceId: record3.deviceId }),
|
|
8000
|
+
signal
|
|
8001
|
+
});
|
|
8002
|
+
if (challengeRes.status === 401) this.markRevoked();
|
|
8003
|
+
if (!challengeRes.ok) {
|
|
8004
|
+
throw new Error(
|
|
8005
|
+
`token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
|
|
8006
|
+
);
|
|
8007
|
+
}
|
|
8008
|
+
return await challengeRes.json();
|
|
7621
8009
|
});
|
|
7622
|
-
if (challengeRes.status === 401) this.markRevoked();
|
|
7623
|
-
if (!challengeRes.ok) {
|
|
7624
|
-
throw new Error(
|
|
7625
|
-
`token renewal (challenge) failed: HTTP ${challengeRes.status} ${await safeErrorText(challengeRes)}`.trimEnd()
|
|
7626
|
-
);
|
|
7627
|
-
}
|
|
7628
|
-
const { nonce } = await challengeRes.json();
|
|
7629
8010
|
const signature = signNonce(privateKey, nonce);
|
|
7630
|
-
const
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
8011
|
+
const body = await this.runRequest(async (signal) => {
|
|
8012
|
+
const tokenRes = await fetch(new URL(BYOK_TOKEN_PATH, base), {
|
|
8013
|
+
method: "POST",
|
|
8014
|
+
headers: { "content-type": "application/json" },
|
|
8015
|
+
body: JSON.stringify({ deviceId: record3.deviceId, nonce, signature }),
|
|
8016
|
+
signal
|
|
8017
|
+
});
|
|
8018
|
+
if (tokenRes.status === 401) this.markRevoked();
|
|
8019
|
+
if (!tokenRes.ok) {
|
|
8020
|
+
throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
|
|
8021
|
+
}
|
|
8022
|
+
return await tokenRes.json();
|
|
7634
8023
|
});
|
|
7635
|
-
if (tokenRes.status === 401) this.markRevoked();
|
|
7636
|
-
if (!tokenRes.ok) {
|
|
7637
|
-
throw new Error(`token renewal (token) failed: HTTP ${tokenRes.status} ${await safeErrorText(tokenRes)}`.trimEnd());
|
|
7638
|
-
}
|
|
7639
|
-
const body = await tokenRes.json();
|
|
7640
8024
|
const updated = {
|
|
7641
8025
|
...record3,
|
|
7642
8026
|
accessToken: body.accessToken,
|
|
@@ -7666,6 +8050,37 @@ var AuthManager = class {
|
|
|
7666
8050
|
timer.unref?.();
|
|
7667
8051
|
this.proactiveTimer = timer;
|
|
7668
8052
|
}
|
|
8053
|
+
/**
|
|
8054
|
+
* Bounds one complete auth exchange rather than fetch alone. Keeping the
|
|
8055
|
+
* controller active through `json()`/`text()` makes a non-cooperative or
|
|
8056
|
+
* partial response body cancellable by the same authority that owns fetch.
|
|
8057
|
+
*/
|
|
8058
|
+
async runRequest(operation) {
|
|
8059
|
+
if (this.stopped) throw new AuthRequestAbortedError("stopped");
|
|
8060
|
+
const controller = new AbortController();
|
|
8061
|
+
this.activeRequest = controller;
|
|
8062
|
+
let deadlineElapsed = false;
|
|
8063
|
+
const deadline = setTimeout(() => {
|
|
8064
|
+
deadlineElapsed = true;
|
|
8065
|
+
controller.abort();
|
|
8066
|
+
}, this.requestDeadlineMs);
|
|
8067
|
+
let rejectOnAbort;
|
|
8068
|
+
const aborted = new Promise((_, reject) => {
|
|
8069
|
+
rejectOnAbort = reject;
|
|
8070
|
+
});
|
|
8071
|
+
const onAbort = () => rejectOnAbort(new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped"));
|
|
8072
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
8073
|
+
try {
|
|
8074
|
+
return await Promise.race([operation(controller.signal), aborted]);
|
|
8075
|
+
} catch (error) {
|
|
8076
|
+
if (controller.signal.aborted) throw new AuthRequestAbortedError(deadlineElapsed ? "deadline" : "stopped");
|
|
8077
|
+
throw error;
|
|
8078
|
+
} finally {
|
|
8079
|
+
clearTimeout(deadline);
|
|
8080
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
8081
|
+
if (this.activeRequest === controller) this.activeRequest = void 0;
|
|
8082
|
+
}
|
|
8083
|
+
}
|
|
7669
8084
|
async runCredentialMutation(operation) {
|
|
7670
8085
|
const predecessor = this.credentialMutationTail;
|
|
7671
8086
|
let release;
|
|
@@ -7708,6 +8123,13 @@ function resolvePairExpiry(refreshHint) {
|
|
|
7708
8123
|
}
|
|
7709
8124
|
return new Date(Date.now() + ASSUMED_PAIR_TOKEN_TTL_MS).toISOString();
|
|
7710
8125
|
}
|
|
8126
|
+
function resolveRequestDeadlineMs(value) {
|
|
8127
|
+
const deadline = value ?? DEFAULT_AUTH_REQUEST_DEADLINE_MS;
|
|
8128
|
+
if (!Number.isSafeInteger(deadline) || deadline <= 0) {
|
|
8129
|
+
throw new Error("authRequestDeadlineMs must be a positive safe integer");
|
|
8130
|
+
}
|
|
8131
|
+
return deadline;
|
|
8132
|
+
}
|
|
7711
8133
|
async function safeErrorText(res) {
|
|
7712
8134
|
try {
|
|
7713
8135
|
return await res.text();
|
|
@@ -7729,37 +8151,48 @@ function withAuth(init, token) {
|
|
|
7729
8151
|
}
|
|
7730
8152
|
|
|
7731
8153
|
// src/daemon/blob-client.ts
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
8154
|
+
var BlobRequestAbortedError = class extends Error {
|
|
8155
|
+
constructor(reason) {
|
|
8156
|
+
super(reason === "deadline" ? "blob request deadline elapsed" : "blob request cancelled");
|
|
8157
|
+
this.reason = reason;
|
|
8158
|
+
this.name = "BlobRequestAbortedError";
|
|
7737
8159
|
}
|
|
7738
|
-
|
|
8160
|
+
reason;
|
|
8161
|
+
};
|
|
7739
8162
|
var BlobClient = class {
|
|
7740
|
-
constructor(serverUrl, auth) {
|
|
8163
|
+
constructor(serverUrl, auth, options = {}) {
|
|
7741
8164
|
this.serverUrl = serverUrl;
|
|
7742
8165
|
this.auth = auth;
|
|
8166
|
+
this.options = options;
|
|
8167
|
+
const requestDeadlineMs = options.requestDeadlineMs ?? 15e3;
|
|
8168
|
+
if (!Number.isSafeInteger(requestDeadlineMs) || requestDeadlineMs <= 0) {
|
|
8169
|
+
throw new Error("BlobClient requestDeadlineMs must be a positive safe integer");
|
|
8170
|
+
}
|
|
8171
|
+
this.requestDeadlineMs = requestDeadlineMs;
|
|
7743
8172
|
}
|
|
7744
8173
|
serverUrl;
|
|
7745
8174
|
auth;
|
|
8175
|
+
options;
|
|
8176
|
+
requestDeadlineMs;
|
|
7746
8177
|
/** `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. */
|
|
7747
|
-
async resolveInstruction(blobRef) {
|
|
8178
|
+
async resolveInstruction(blobRef, options = {}) {
|
|
7748
8179
|
const base = toHttpBase(this.serverUrl);
|
|
7749
|
-
const urlRes = await
|
|
7750
|
-
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
7751
|
-
|
|
7752
|
-
this.auth
|
|
8180
|
+
const urlRes = await this.#request(
|
|
8181
|
+
(signal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal }, this.auth),
|
|
8182
|
+
options.signal
|
|
7753
8183
|
);
|
|
7754
8184
|
if (!urlRes.ok) {
|
|
7755
|
-
throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await
|
|
8185
|
+
throw new Error(`failed to resolve blob download url: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, options.signal)}`.trimEnd());
|
|
7756
8186
|
}
|
|
7757
|
-
const { downloadUrl } = await urlRes.
|
|
7758
|
-
|
|
8187
|
+
const { downloadUrl } = await this.#readJson(urlRes, options.signal);
|
|
8188
|
+
if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
|
|
8189
|
+
throw new Error("failed to resolve blob download url: response omitted downloadUrl");
|
|
8190
|
+
}
|
|
8191
|
+
const contentRes = await this.#request((signal) => fetch(new URL(downloadUrl, base), { signal }), options.signal);
|
|
7759
8192
|
if (!contentRes.ok) {
|
|
7760
8193
|
throw new Error(`failed to download blob content: HTTP ${contentRes.status}`);
|
|
7761
8194
|
}
|
|
7762
|
-
const bytes =
|
|
8195
|
+
const bytes = await this.#readBody(contentRes, options.signal);
|
|
7763
8196
|
const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7764
8197
|
if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
|
|
7765
8198
|
throw new Error(
|
|
@@ -7768,63 +8201,71 @@ var BlobClient = class {
|
|
|
7768
8201
|
}
|
|
7769
8202
|
return new TextDecoder().decode(bytes);
|
|
7770
8203
|
}
|
|
7771
|
-
/** `POST /byok/blobs`
|
|
8204
|
+
/** `POST /byok/blobs` -> PUT the bytes to the presigned URL -> finalize into a `BlobRef`. */
|
|
7772
8205
|
async uploadArtifact(content, contentType, options = {}) {
|
|
7773
8206
|
const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
|
|
7774
8207
|
const contentHash4 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7775
8208
|
const base = toHttpBase(this.serverUrl);
|
|
7776
8209
|
const reservationId = options.idempotencyKey ?? `blob_${randomUUID()}`;
|
|
7777
|
-
const createRes = await
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
"content-type": "application/json",
|
|
7783
|
-
|
|
8210
|
+
const createRes = await this.#request(
|
|
8211
|
+
(signal) => authedFetch(
|
|
8212
|
+
new URL(BYOK_BLOBS_PATH, base),
|
|
8213
|
+
{
|
|
8214
|
+
method: "POST",
|
|
8215
|
+
headers: { "content-type": "application/json", "idempotency-key": reservationId },
|
|
8216
|
+
body: JSON.stringify({ size: bytes.length, contentType, contentHash: contentHash4 }),
|
|
8217
|
+
signal
|
|
7784
8218
|
},
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
8219
|
+
this.auth
|
|
8220
|
+
),
|
|
8221
|
+
options.signal
|
|
7788
8222
|
);
|
|
7789
8223
|
if (!createRes.ok) {
|
|
7790
|
-
throw new Error(`failed to create blob: HTTP ${createRes.status} ${await
|
|
8224
|
+
throw new Error(`failed to create blob: HTTP ${createRes.status} ${await this.#safeErrorText(createRes, options.signal)}`.trimEnd());
|
|
8225
|
+
}
|
|
8226
|
+
const { blobId, uploadUrl } = await this.#readJson(createRes, options.signal);
|
|
8227
|
+
if (typeof blobId !== "string" || blobId.length === 0 || typeof uploadUrl !== "string" || uploadUrl.length === 0) {
|
|
8228
|
+
throw new Error("failed to create blob: response omitted blobId or uploadUrl");
|
|
7791
8229
|
}
|
|
7792
|
-
const { blobId, uploadUrl } = await createRes.json();
|
|
7793
8230
|
const blobRef = { blobId, contentHash: contentHash4, size: bytes.length, contentType };
|
|
7794
|
-
if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef)) {
|
|
8231
|
+
if (options.idempotencyKey !== void 0 && await this.#hasExactCommittedBlob(base, blobRef, options.signal)) {
|
|
7795
8232
|
return blobRef;
|
|
7796
8233
|
}
|
|
7797
|
-
const putRes = await
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
8234
|
+
const putRes = await this.#request(
|
|
8235
|
+
(signal) => fetch(new URL(uploadUrl, base), {
|
|
8236
|
+
method: "PUT",
|
|
8237
|
+
headers: { "content-type": contentType },
|
|
8238
|
+
body: bytes,
|
|
8239
|
+
signal
|
|
8240
|
+
}),
|
|
8241
|
+
options.signal
|
|
8242
|
+
);
|
|
7802
8243
|
if (!putRes.ok) {
|
|
7803
8244
|
throw new Error(`failed to upload blob content: HTTP ${putRes.status}`);
|
|
7804
8245
|
}
|
|
7805
|
-
|
|
8246
|
+
this.#throwIfAborted(options.signal);
|
|
8247
|
+
await this.#finalize(base, blobId, reservationId, options.signal);
|
|
7806
8248
|
return blobRef;
|
|
7807
8249
|
}
|
|
7808
|
-
async #hasExactCommittedBlob(base, blobRef) {
|
|
7809
|
-
const urlRes = await
|
|
7810
|
-
new URL(byokBlobUrlPath(blobRef.blobId), base),
|
|
7811
|
-
|
|
7812
|
-
this.auth
|
|
8250
|
+
async #hasExactCommittedBlob(base, blobRef, signal) {
|
|
8251
|
+
const urlRes = await this.#request(
|
|
8252
|
+
(requestSignal) => authedFetch(new URL(byokBlobUrlPath(blobRef.blobId), base), { method: "GET", signal: requestSignal }, this.auth),
|
|
8253
|
+
signal
|
|
7813
8254
|
);
|
|
7814
8255
|
if (urlRes.status === 404) return false;
|
|
7815
8256
|
if (!urlRes.ok) {
|
|
7816
|
-
throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await
|
|
8257
|
+
throw new Error(`failed to read back idempotent blob: HTTP ${urlRes.status} ${await this.#safeErrorText(urlRes, signal)}`.trimEnd());
|
|
7817
8258
|
}
|
|
7818
|
-
const { downloadUrl } = await urlRes
|
|
8259
|
+
const { downloadUrl } = await this.#readJson(urlRes, signal);
|
|
7819
8260
|
if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
|
|
7820
8261
|
throw new Error("failed to read back idempotent blob: response omitted downloadUrl");
|
|
7821
8262
|
}
|
|
7822
|
-
const contentRes = await fetch(new URL(downloadUrl, base));
|
|
8263
|
+
const contentRes = await this.#request((requestSignal) => fetch(new URL(downloadUrl, base), { signal: requestSignal }), signal);
|
|
7823
8264
|
if (contentRes.status === 404) return false;
|
|
7824
8265
|
if (!contentRes.ok) {
|
|
7825
8266
|
throw new Error(`failed to read back idempotent blob content: HTTP ${contentRes.status}`);
|
|
7826
8267
|
}
|
|
7827
|
-
const bytes =
|
|
8268
|
+
const bytes = await this.#readBody(contentRes, signal);
|
|
7828
8269
|
const observedHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7829
8270
|
if (observedHash !== blobRef.contentHash || bytes.length !== blobRef.size) {
|
|
7830
8271
|
throw new Error(
|
|
@@ -7833,18 +8274,19 @@ var BlobClient = class {
|
|
|
7833
8274
|
}
|
|
7834
8275
|
return true;
|
|
7835
8276
|
}
|
|
7836
|
-
async #finalize(base, blobId, reservationId) {
|
|
8277
|
+
async #finalize(base, blobId, reservationId, signal) {
|
|
7837
8278
|
let lastFailure;
|
|
7838
8279
|
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
8280
|
+
this.#throwIfAborted(signal);
|
|
7839
8281
|
let response;
|
|
7840
8282
|
try {
|
|
7841
|
-
response = await
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
method: "POST",
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
8283
|
+
response = await this.#request(
|
|
8284
|
+
(requestSignal) => authedFetch(
|
|
8285
|
+
new URL(byokBlobFinalizePath(blobId), base),
|
|
8286
|
+
{ method: "POST", headers: { "idempotency-key": reservationId }, signal: requestSignal },
|
|
8287
|
+
this.auth
|
|
8288
|
+
),
|
|
8289
|
+
signal
|
|
7848
8290
|
);
|
|
7849
8291
|
} catch (error) {
|
|
7850
8292
|
lastFailure = error;
|
|
@@ -7854,13 +8296,105 @@ var BlobClient = class {
|
|
|
7854
8296
|
if (response.ok) return;
|
|
7855
8297
|
if (response.status < 500 || attempt === 2) {
|
|
7856
8298
|
throw new Error(
|
|
7857
|
-
`failed to finalize blob: HTTP ${response.status} ${await
|
|
8299
|
+
`failed to finalize blob: HTTP ${response.status} ${await this.#safeErrorText(response, signal)}`.trimEnd()
|
|
7858
8300
|
);
|
|
7859
8301
|
}
|
|
7860
8302
|
lastFailure = new Error(`failed to finalize blob: HTTP ${response.status}`);
|
|
7861
8303
|
}
|
|
7862
8304
|
throw lastFailure;
|
|
7863
8305
|
}
|
|
8306
|
+
async #readJson(res, signal) {
|
|
8307
|
+
return JSON.parse(await this.#readText(res, signal));
|
|
8308
|
+
}
|
|
8309
|
+
async #safeErrorText(res, signal) {
|
|
8310
|
+
try {
|
|
8311
|
+
return await this.#readText(res, signal);
|
|
8312
|
+
} catch (error) {
|
|
8313
|
+
if (error instanceof BlobRequestAbortedError) throw error;
|
|
8314
|
+
return "";
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
async #readText(res, signal) {
|
|
8318
|
+
return new TextDecoder().decode(await this.#readBody(res, signal));
|
|
8319
|
+
}
|
|
8320
|
+
/**
|
|
8321
|
+
* `Response.arrayBuffer()` only leaves cancellation observable through the
|
|
8322
|
+
* Fetch implementation. Read the body directly so our lifecycle/deadline
|
|
8323
|
+
* authority cancels the actual stream even when a fetch implementation has
|
|
8324
|
+
* already resolved at headers.
|
|
8325
|
+
*/
|
|
8326
|
+
async #readBody(res, signal) {
|
|
8327
|
+
return this.#request(async (requestSignal) => {
|
|
8328
|
+
if (res.body === null) return new Uint8Array();
|
|
8329
|
+
const reader = res.body.getReader();
|
|
8330
|
+
const cancelBody = () => {
|
|
8331
|
+
void reader.cancel().catch(() => void 0);
|
|
8332
|
+
};
|
|
8333
|
+
requestSignal.addEventListener("abort", cancelBody, { once: true });
|
|
8334
|
+
try {
|
|
8335
|
+
const chunks = [];
|
|
8336
|
+
let length = 0;
|
|
8337
|
+
for (; ; ) {
|
|
8338
|
+
const { done, value } = await reader.read();
|
|
8339
|
+
if (done) break;
|
|
8340
|
+
chunks.push(value);
|
|
8341
|
+
length += value.byteLength;
|
|
8342
|
+
}
|
|
8343
|
+
const bytes = new Uint8Array(length);
|
|
8344
|
+
let offset = 0;
|
|
8345
|
+
for (const chunk of chunks) {
|
|
8346
|
+
bytes.set(chunk, offset);
|
|
8347
|
+
offset += chunk.byteLength;
|
|
8348
|
+
}
|
|
8349
|
+
return bytes;
|
|
8350
|
+
} finally {
|
|
8351
|
+
requestSignal.removeEventListener("abort", cancelBody);
|
|
8352
|
+
reader.releaseLock();
|
|
8353
|
+
}
|
|
8354
|
+
}, signal);
|
|
8355
|
+
}
|
|
8356
|
+
#throwIfAborted(signal) {
|
|
8357
|
+
if (this.options.signal?.aborted || signal?.aborted) {
|
|
8358
|
+
throw new BlobRequestAbortedError("cancelled");
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
async #request(request, signal) {
|
|
8362
|
+
this.#throwIfAborted(signal);
|
|
8363
|
+
const controller = new AbortController();
|
|
8364
|
+
let abortReason = "cancelled";
|
|
8365
|
+
const abort = (reason) => {
|
|
8366
|
+
if (controller.signal.aborted) return;
|
|
8367
|
+
abortReason = reason;
|
|
8368
|
+
controller.abort();
|
|
8369
|
+
};
|
|
8370
|
+
const inheritedSignals = [this.options.signal, signal].filter((value) => value !== void 0);
|
|
8371
|
+
const abortForCancellation = () => abort("cancelled");
|
|
8372
|
+
for (const inheritedSignal of inheritedSignals) {
|
|
8373
|
+
inheritedSignal.addEventListener("abort", abortForCancellation, { once: true });
|
|
8374
|
+
}
|
|
8375
|
+
const deadline = setTimeout(() => abort("deadline"), this.requestDeadlineMs);
|
|
8376
|
+
deadline.unref?.();
|
|
8377
|
+
let rejectAbort = () => {
|
|
8378
|
+
};
|
|
8379
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
8380
|
+
rejectAbort = reject;
|
|
8381
|
+
});
|
|
8382
|
+
const rejectOnAbort = () => rejectAbort(new BlobRequestAbortedError(abortReason));
|
|
8383
|
+
controller.signal.addEventListener("abort", rejectOnAbort, { once: true });
|
|
8384
|
+
try {
|
|
8385
|
+
return await Promise.race([request(controller.signal), aborted]);
|
|
8386
|
+
} catch (error) {
|
|
8387
|
+
if (error instanceof BlobRequestAbortedError) throw error;
|
|
8388
|
+
if (controller.signal.aborted) throw new BlobRequestAbortedError(abortReason);
|
|
8389
|
+
throw error;
|
|
8390
|
+
} finally {
|
|
8391
|
+
clearTimeout(deadline);
|
|
8392
|
+
controller.signal.removeEventListener("abort", rejectOnAbort);
|
|
8393
|
+
for (const inheritedSignal of inheritedSignals) {
|
|
8394
|
+
inheritedSignal.removeEventListener("abort", abortForCancellation);
|
|
8395
|
+
}
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
7864
8398
|
};
|
|
7865
8399
|
var DEFAULT_TIMEOUT_MS2 = 2e3;
|
|
7866
8400
|
var DARWIN_UUID_RE = /"IOPlatformUUID"\s*=\s*"([^"]+)"/u;
|
|
@@ -8107,6 +8641,7 @@ var AnotherControlServerRunningError = class extends Error {
|
|
|
8107
8641
|
}
|
|
8108
8642
|
};
|
|
8109
8643
|
var MAX_HALF_OPEN_CONNECTIONS = 8;
|
|
8644
|
+
var MAX_OUTBOUND_QUEUE_BYTES = 1024 * 1024;
|
|
8110
8645
|
function errorMessage5(err) {
|
|
8111
8646
|
return err instanceof Error ? err.message : String(err);
|
|
8112
8647
|
}
|
|
@@ -8183,11 +8718,12 @@ async function bindControlEndpoint(server, endpoint) {
|
|
|
8183
8718
|
}
|
|
8184
8719
|
function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshakeSettled) {
|
|
8185
8720
|
const reader = new NdjsonLineReader();
|
|
8186
|
-
const
|
|
8721
|
+
const activeRequests = /* @__PURE__ */ new Map();
|
|
8187
8722
|
let phase = "client-hello";
|
|
8188
8723
|
let serverNonce = "";
|
|
8189
8724
|
let destroyed = false;
|
|
8190
8725
|
let handshakeSettled = false;
|
|
8726
|
+
let disposeOutboundWriter;
|
|
8191
8727
|
function settleHandshake() {
|
|
8192
8728
|
if (handshakeSettled) return;
|
|
8193
8729
|
handshakeSettled = true;
|
|
@@ -8197,8 +8733,83 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
|
|
|
8197
8733
|
if (phase !== "ready") socket.destroy();
|
|
8198
8734
|
}, handshakeTimeoutMs);
|
|
8199
8735
|
handshakeTimer.unref?.();
|
|
8736
|
+
function terminateConnection() {
|
|
8737
|
+
if (destroyed) return;
|
|
8738
|
+
destroyed = true;
|
|
8739
|
+
clearTimeout(handshakeTimer);
|
|
8740
|
+
settleHandshake();
|
|
8741
|
+
disposeOutboundWriter?.();
|
|
8742
|
+
const requests = [...activeRequests.values()];
|
|
8743
|
+
activeRequests.clear();
|
|
8744
|
+
for (const request of requests) {
|
|
8745
|
+
if (request.kind === "stream" && !request.controller.signal.aborted) request.controller.abort();
|
|
8746
|
+
}
|
|
8747
|
+
if (!socket.destroyed) socket.destroy();
|
|
8748
|
+
}
|
|
8749
|
+
const queuedFrames = [];
|
|
8750
|
+
let queuedBytes = 0;
|
|
8751
|
+
let blocked = false;
|
|
8752
|
+
let terminal = false;
|
|
8753
|
+
let drainListening = false;
|
|
8754
|
+
function disposeQueuedFrames() {
|
|
8755
|
+
terminal = true;
|
|
8756
|
+
blocked = false;
|
|
8757
|
+
queuedFrames.length = 0;
|
|
8758
|
+
queuedBytes = 0;
|
|
8759
|
+
if (drainListening) {
|
|
8760
|
+
socket.removeListener("drain", onDrain);
|
|
8761
|
+
drainListening = false;
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
function waitForDrain() {
|
|
8765
|
+
if (drainListening || terminal) return;
|
|
8766
|
+
drainListening = true;
|
|
8767
|
+
socket.once("drain", onDrain);
|
|
8768
|
+
}
|
|
8769
|
+
function writeEncodedFrame(encoded) {
|
|
8770
|
+
if (terminal) return;
|
|
8771
|
+
if (!socket.writable) {
|
|
8772
|
+
terminateConnection();
|
|
8773
|
+
return;
|
|
8774
|
+
}
|
|
8775
|
+
try {
|
|
8776
|
+
if (!socket.write(encoded)) {
|
|
8777
|
+
blocked = true;
|
|
8778
|
+
waitForDrain();
|
|
8779
|
+
}
|
|
8780
|
+
} catch {
|
|
8781
|
+
terminateConnection();
|
|
8782
|
+
}
|
|
8783
|
+
}
|
|
8784
|
+
function onDrain() {
|
|
8785
|
+
drainListening = false;
|
|
8786
|
+
if (terminal) return;
|
|
8787
|
+
blocked = false;
|
|
8788
|
+
while (!blocked && queuedFrames.length > 0) {
|
|
8789
|
+
const frame = queuedFrames.shift();
|
|
8790
|
+
queuedBytes -= frame.bytes;
|
|
8791
|
+
writeEncodedFrame(frame.encoded);
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
disposeOutboundWriter = disposeQueuedFrames;
|
|
8200
8795
|
function sendFrame(frame) {
|
|
8201
|
-
if (
|
|
8796
|
+
if (destroyed || terminal) return;
|
|
8797
|
+
const encoded = encodeFrame(frame);
|
|
8798
|
+
const bytes = Buffer.byteLength(encoded);
|
|
8799
|
+
if (bytes > MAX_OUTBOUND_QUEUE_BYTES) {
|
|
8800
|
+
terminateConnection();
|
|
8801
|
+
return;
|
|
8802
|
+
}
|
|
8803
|
+
if (blocked) {
|
|
8804
|
+
if (queuedBytes + bytes > MAX_OUTBOUND_QUEUE_BYTES) {
|
|
8805
|
+
terminateConnection();
|
|
8806
|
+
return;
|
|
8807
|
+
}
|
|
8808
|
+
queuedFrames.push({ encoded, bytes });
|
|
8809
|
+
queuedBytes += bytes;
|
|
8810
|
+
return;
|
|
8811
|
+
}
|
|
8812
|
+
writeEncodedFrame(encoded);
|
|
8202
8813
|
}
|
|
8203
8814
|
function handleClientHello(parsed) {
|
|
8204
8815
|
const hello = parseClientHello(parsed);
|
|
@@ -8221,21 +8832,51 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
|
|
|
8221
8832
|
settleHandshake();
|
|
8222
8833
|
sendFrame({ v: 1, ready: true });
|
|
8223
8834
|
}
|
|
8835
|
+
function rejectDuplicateRequest(id) {
|
|
8836
|
+
sendFrame({
|
|
8837
|
+
v: 1,
|
|
8838
|
+
id,
|
|
8839
|
+
ok: false,
|
|
8840
|
+
error: { code: "duplicate_request_id", message: `request id "${id}" is already active` }
|
|
8841
|
+
});
|
|
8842
|
+
}
|
|
8843
|
+
function registerRequest(id, record3) {
|
|
8844
|
+
if (activeRequests.has(id)) {
|
|
8845
|
+
rejectDuplicateRequest(id);
|
|
8846
|
+
return false;
|
|
8847
|
+
}
|
|
8848
|
+
activeRequests.set(id, record3);
|
|
8849
|
+
return true;
|
|
8850
|
+
}
|
|
8851
|
+
function releaseRequest(id, record3) {
|
|
8852
|
+
if (activeRequests.get(id) !== record3) return false;
|
|
8853
|
+
activeRequests.delete(id);
|
|
8854
|
+
return true;
|
|
8855
|
+
}
|
|
8224
8856
|
function dispatch(id, method, params) {
|
|
8225
8857
|
const unary = methods.unary[method];
|
|
8226
8858
|
if (unary) {
|
|
8227
|
-
|
|
8859
|
+
const record3 = { kind: "unary" };
|
|
8860
|
+
if (!registerRequest(id, record3)) return;
|
|
8861
|
+
Promise.resolve().then(() => unary(params)).then((result) => {
|
|
8862
|
+
if (!releaseRequest(id, record3)) return;
|
|
8863
|
+
sendFrame({ v: 1, id, ok: true, result });
|
|
8864
|
+
}).catch((err) => {
|
|
8865
|
+
if (!releaseRequest(id, record3)) return;
|
|
8866
|
+
sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
|
|
8867
|
+
});
|
|
8228
8868
|
return;
|
|
8229
8869
|
}
|
|
8230
8870
|
const stream = methods.stream[method];
|
|
8231
8871
|
if (stream) {
|
|
8232
8872
|
const controller = new AbortController();
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8873
|
+
const record3 = { kind: "stream", controller };
|
|
8874
|
+
if (!registerRequest(id, record3)) return;
|
|
8875
|
+
Promise.resolve().then(() => stream(params, { emit: (event) => sendFrame({ v: 1, id, event }), signal: controller.signal })).then(() => {
|
|
8876
|
+
if (!releaseRequest(id, record3)) return;
|
|
8236
8877
|
if (!controller.signal.aborted) sendFrame({ v: 1, id, ok: true, done: true });
|
|
8237
8878
|
}).catch((err) => {
|
|
8238
|
-
|
|
8879
|
+
if (!releaseRequest(id, record3)) return;
|
|
8239
8880
|
sendFrame({ v: 1, id, ok: false, error: toControlErrorShape(err) });
|
|
8240
8881
|
});
|
|
8241
8882
|
return;
|
|
@@ -8277,13 +8918,10 @@ function handleConnection(socket, token, methods, handshakeTimeoutMs, onHandshak
|
|
|
8277
8918
|
for (const line of lines) handleLine(line);
|
|
8278
8919
|
});
|
|
8279
8920
|
socket.on("error", () => {
|
|
8921
|
+
terminateConnection();
|
|
8280
8922
|
});
|
|
8281
8923
|
socket.on("close", () => {
|
|
8282
|
-
|
|
8283
|
-
clearTimeout(handshakeTimer);
|
|
8284
|
-
settleHandshake();
|
|
8285
|
-
for (const controller of activeStreams.values()) controller.abort();
|
|
8286
|
-
activeStreams.clear();
|
|
8924
|
+
terminateConnection();
|
|
8287
8925
|
});
|
|
8288
8926
|
}
|
|
8289
8927
|
async function startControlServer(opts) {
|
|
@@ -9201,6 +9839,20 @@ function createFleetJitter(productId, deviceId) {
|
|
|
9201
9839
|
delay: (domain, sequence, baseMs) => deterministicJitterMs({ seed, domain, sequence, baseMs })
|
|
9202
9840
|
};
|
|
9203
9841
|
}
|
|
9842
|
+
|
|
9843
|
+
// src/daemon/replay-cursor.ts
|
|
9844
|
+
var ReplayCursorTooOldError = class extends Error {
|
|
9845
|
+
constructor(recoverableFrom) {
|
|
9846
|
+
super(
|
|
9847
|
+
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}`
|
|
9848
|
+
);
|
|
9849
|
+
this.recoverableFrom = recoverableFrom;
|
|
9850
|
+
this.name = "ReplayCursorTooOldError";
|
|
9851
|
+
}
|
|
9852
|
+
recoverableFrom;
|
|
9853
|
+
};
|
|
9854
|
+
|
|
9855
|
+
// src/daemon/long-poll-transport.ts
|
|
9204
9856
|
var LongPollRouteError = class extends Error {
|
|
9205
9857
|
constructor(endpoint, status, cause) {
|
|
9206
9858
|
super(
|
|
@@ -9216,7 +9868,7 @@ var LongPollRouteError = class extends Error {
|
|
|
9216
9868
|
};
|
|
9217
9869
|
var MAX_TRACKED_VALIDATION_FAILURE_WARNINGS = 1e3;
|
|
9218
9870
|
var MAX_TRACKED_ROUTE_FAILURE_WARNINGS = 1e3;
|
|
9219
|
-
function parseLooseEventsPollResponse(raw) {
|
|
9871
|
+
function parseLooseEventsPollResponse(raw, requestedCursor) {
|
|
9220
9872
|
if (typeof raw !== "object" || raw === null) {
|
|
9221
9873
|
throw new Error("events poll response is not an object");
|
|
9222
9874
|
}
|
|
@@ -9224,19 +9876,48 @@ function parseLooseEventsPollResponse(raw) {
|
|
|
9224
9876
|
if (!Array.isArray(events)) {
|
|
9225
9877
|
throw new Error("events poll response.events is not an array");
|
|
9226
9878
|
}
|
|
9227
|
-
if (
|
|
9228
|
-
throw new Error("events poll response.cursor is not
|
|
9879
|
+
if (!isSafeNonnegativeInteger(cursor)) {
|
|
9880
|
+
throw new Error("events poll response.cursor is not a safe nonnegative integer");
|
|
9881
|
+
}
|
|
9882
|
+
if (cursor < requestedCursor) {
|
|
9883
|
+
throw new Error("events poll response.cursor regressed below the requested cursor");
|
|
9229
9884
|
}
|
|
9230
9885
|
if (capabilities !== void 0 && (!Array.isArray(capabilities) || capabilities.some((flag) => typeof flag !== "string"))) {
|
|
9231
9886
|
throw new Error("events poll response.capabilities is not an array of strings");
|
|
9232
9887
|
}
|
|
9233
9888
|
return { events, cursor, capabilities: capabilities ?? [] };
|
|
9234
9889
|
}
|
|
9890
|
+
function isSafeNonnegativeInteger(value) {
|
|
9891
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
9892
|
+
}
|
|
9893
|
+
function validateTrustedEventsPage(events, requestedCursor, pageCursor) {
|
|
9894
|
+
let previousTaskSeq;
|
|
9895
|
+
for (const raw of events) {
|
|
9896
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
9897
|
+
const { type, seq } = raw;
|
|
9898
|
+
if (typeof type !== "string" || !type.startsWith("task.")) continue;
|
|
9899
|
+
if (!isSafeNonnegativeInteger(seq)) {
|
|
9900
|
+
throw new Error("events poll task seq is not a safe nonnegative integer");
|
|
9901
|
+
}
|
|
9902
|
+
if (seq <= requestedCursor || seq > pageCursor) {
|
|
9903
|
+
throw new Error("events poll task seq lies outside the trusted page cursor range");
|
|
9904
|
+
}
|
|
9905
|
+
if (previousTaskSeq !== void 0 && seq <= previousTaskSeq) {
|
|
9906
|
+
throw new Error("events poll task seq is not strictly page ordered");
|
|
9907
|
+
}
|
|
9908
|
+
const knownTaskType = MESSAGE_TYPES.includes(type);
|
|
9909
|
+
const predecessor = previousTaskSeq ?? requestedCursor;
|
|
9910
|
+
if (!knownTaskType && seq !== predecessor + 1) {
|
|
9911
|
+
throw new Error("events poll unknown task seq would cross an untrusted gap");
|
|
9912
|
+
}
|
|
9913
|
+
previousTaskSeq = seq;
|
|
9914
|
+
}
|
|
9915
|
+
}
|
|
9235
9916
|
function extractSkippableSeq(raw) {
|
|
9236
9917
|
if (typeof raw !== "object" || raw === null) return void 0;
|
|
9237
9918
|
const { type, seq } = raw;
|
|
9238
9919
|
if (typeof type !== "string" || !type.startsWith("task.")) return void 0;
|
|
9239
|
-
return
|
|
9920
|
+
return isSafeNonnegativeInteger(seq) ? seq : void 0;
|
|
9240
9921
|
}
|
|
9241
9922
|
var LongPollClient = class {
|
|
9242
9923
|
constructor(opts) {
|
|
@@ -9247,6 +9928,8 @@ var LongPollClient = class {
|
|
|
9247
9928
|
}
|
|
9248
9929
|
opts;
|
|
9249
9930
|
running = false;
|
|
9931
|
+
/** Owns exactly one active loop generation, including its held GET and retry delays. */
|
|
9932
|
+
loopAbortController;
|
|
9250
9933
|
/**
|
|
9251
9934
|
* Finding R1: seqs this loop has already `console.warn`'d about for a
|
|
9252
9935
|
* validation-failed (recognized-type, invalid-payload) entry — a poison
|
|
@@ -9306,16 +9989,22 @@ var LongPollClient = class {
|
|
|
9306
9989
|
noteRevoked(err) {
|
|
9307
9990
|
if (!(err instanceof DeviceRevokedError)) return false;
|
|
9308
9991
|
this.running = false;
|
|
9992
|
+
this.loopAbortController?.abort();
|
|
9309
9993
|
this.opts.onRevoked?.();
|
|
9310
9994
|
return true;
|
|
9311
9995
|
}
|
|
9312
9996
|
start() {
|
|
9313
9997
|
if (this.running) return;
|
|
9314
9998
|
this.running = true;
|
|
9315
|
-
|
|
9999
|
+
const controller = new AbortController();
|
|
10000
|
+
this.loopAbortController = controller;
|
|
10001
|
+
void this.loop(controller.signal).finally(() => {
|
|
10002
|
+
if (this.loopAbortController === controller) this.loopAbortController = void 0;
|
|
10003
|
+
});
|
|
9316
10004
|
}
|
|
9317
10005
|
stop() {
|
|
9318
10006
|
this.running = false;
|
|
10007
|
+
this.loopAbortController?.abort();
|
|
9319
10008
|
}
|
|
9320
10009
|
/**
|
|
9321
10010
|
* POST one batch of envelopes to `/byok/messages` (finding F6/protocol
|
|
@@ -9324,14 +10013,14 @@ var LongPollClient = class {
|
|
|
9324
10013
|
* (`ConnectionHub.handleInbound`), so a resend of the SAME batch (same
|
|
9325
10014
|
* envelope `id`s — the caller must never rebuild them) is deduped
|
|
9326
10015
|
* server-side into a safe no-op rather than reprocessed (§9). Returns
|
|
9327
|
-
*
|
|
10016
|
+
* validated frozen-v1 counts only after a readable response body.
|
|
9328
10017
|
*/
|
|
9329
10018
|
async postBatch(envelopes) {
|
|
9330
10019
|
try {
|
|
9331
10020
|
await this.opts.auth.getValidAccessToken();
|
|
9332
10021
|
} catch (err) {
|
|
9333
10022
|
this.noteRevoked(err);
|
|
9334
|
-
return
|
|
10023
|
+
return void 0;
|
|
9335
10024
|
}
|
|
9336
10025
|
let res;
|
|
9337
10026
|
try {
|
|
@@ -9347,23 +10036,26 @@ var LongPollClient = class {
|
|
|
9347
10036
|
);
|
|
9348
10037
|
} catch (err) {
|
|
9349
10038
|
if (!this.noteRevoked(err)) this.warnRouteFailure(this.messagesEndpoint, void 0, err);
|
|
9350
|
-
return
|
|
10039
|
+
return void 0;
|
|
9351
10040
|
}
|
|
9352
10041
|
if (!res.ok) {
|
|
9353
10042
|
this.warnRouteFailure(this.messagesEndpoint, res.status, void 0);
|
|
9354
|
-
return
|
|
10043
|
+
return void 0;
|
|
9355
10044
|
}
|
|
9356
10045
|
try {
|
|
9357
|
-
MessagesSendResponseSchema.parse(await res.json());
|
|
10046
|
+
const response = MessagesSendResponseSchema.parse(await res.json());
|
|
10047
|
+
if (response.accepted + (response.rejected ?? 0) !== envelopes.length) {
|
|
10048
|
+
throw new Error("messages response counts do not match the posted batch length");
|
|
10049
|
+
}
|
|
10050
|
+
return response;
|
|
9358
10051
|
} catch (err) {
|
|
9359
10052
|
this.warnRouteFailure(this.messagesEndpoint, res.status, err);
|
|
9360
|
-
return
|
|
10053
|
+
return void 0;
|
|
9361
10054
|
}
|
|
9362
|
-
return true;
|
|
9363
10055
|
}
|
|
9364
|
-
async loop() {
|
|
10056
|
+
async loop(signal) {
|
|
9365
10057
|
let retryAttempt = 0;
|
|
9366
|
-
while (this.running) {
|
|
10058
|
+
while (this.running && !signal.aborted) {
|
|
9367
10059
|
try {
|
|
9368
10060
|
await this.opts.auth.getValidAccessToken();
|
|
9369
10061
|
const base = toHttpBase(this.opts.serverUrl);
|
|
@@ -9372,30 +10064,40 @@ var LongPollClient = class {
|
|
|
9372
10064
|
if (cursor !== void 0) url.searchParams.set("cursor", String(cursor));
|
|
9373
10065
|
let res;
|
|
9374
10066
|
try {
|
|
9375
|
-
res = await authedFetch(url, { method: "GET" }, this.opts.auth);
|
|
10067
|
+
res = await authedFetch(url, { method: "GET", signal }, this.opts.auth);
|
|
9376
10068
|
} catch (err) {
|
|
10069
|
+
if (signal.aborted) return;
|
|
9377
10070
|
if (!(err instanceof DeviceRevokedError)) {
|
|
9378
10071
|
this.warnRouteFailure(this.eventsEndpoint, void 0, err);
|
|
9379
10072
|
}
|
|
9380
10073
|
throw err;
|
|
9381
10074
|
}
|
|
9382
10075
|
if (!res.ok) {
|
|
10076
|
+
const replayCursorTooOld = await parseReplayCursorTooOld(res);
|
|
10077
|
+
if (replayCursorTooOld) {
|
|
10078
|
+
this.running = false;
|
|
10079
|
+
this.opts.onReplayCursorTooOld?.(replayCursorTooOld);
|
|
10080
|
+
return;
|
|
10081
|
+
}
|
|
9383
10082
|
this.warnRouteFailure(this.eventsEndpoint, res.status, void 0);
|
|
9384
|
-
this.opts.
|
|
10083
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
10084
|
+
this.opts.onPollFailure?.();
|
|
9385
10085
|
this.opts.onOperationalOutcome?.("failure");
|
|
9386
10086
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9387
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10087
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9388
10088
|
continue;
|
|
9389
10089
|
}
|
|
9390
10090
|
let parsed;
|
|
9391
10091
|
try {
|
|
9392
|
-
parsed = parseLooseEventsPollResponse(await res.json());
|
|
10092
|
+
parsed = parseLooseEventsPollResponse(await res.json(), cursor ?? 0);
|
|
10093
|
+
validateTrustedEventsPage(parsed.events, cursor ?? 0, parsed.cursor);
|
|
9393
10094
|
} catch (err) {
|
|
9394
10095
|
this.warnRouteFailure(this.eventsEndpoint, res.status, err);
|
|
9395
10096
|
throw err;
|
|
9396
10097
|
}
|
|
9397
10098
|
this.opts.onServerCapabilities?.(parsed.capabilities);
|
|
9398
10099
|
let hadValidationFailureThisBatch = false;
|
|
10100
|
+
let acceptedAnyEntry = false;
|
|
9399
10101
|
for (const raw of parsed.events) {
|
|
9400
10102
|
let envelope;
|
|
9401
10103
|
try {
|
|
@@ -9403,7 +10105,10 @@ var LongPollClient = class {
|
|
|
9403
10105
|
} catch (err) {
|
|
9404
10106
|
if (err instanceof UnknownMessageTypeError) {
|
|
9405
10107
|
const skippableSeq = extractSkippableSeq(raw);
|
|
9406
|
-
if (skippableSeq !== void 0)
|
|
10108
|
+
if (skippableSeq !== void 0) {
|
|
10109
|
+
this.opts.onSkippedSeq?.(skippableSeq);
|
|
10110
|
+
acceptedAnyEntry = true;
|
|
10111
|
+
}
|
|
9407
10112
|
} else {
|
|
9408
10113
|
const failedSeq = extractSkippableSeq(raw);
|
|
9409
10114
|
if (failedSeq !== void 0) {
|
|
@@ -9423,45 +10128,65 @@ var LongPollClient = class {
|
|
|
9423
10128
|
}
|
|
9424
10129
|
continue;
|
|
9425
10130
|
}
|
|
9426
|
-
this.opts.onEnvelope(envelope);
|
|
10131
|
+
if (this.opts.onEnvelope(envelope) !== false) acceptedAnyEntry = true;
|
|
9427
10132
|
}
|
|
9428
10133
|
if (parsed.events.length === 0) {
|
|
9429
10134
|
retryAttempt = 0;
|
|
9430
10135
|
this.opts.onOperationalOutcome?.("success");
|
|
9431
|
-
await sleep(this.opts.idleDelayMs ?? 250);
|
|
9432
|
-
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch) {
|
|
10136
|
+
await sleep(this.opts.idleDelayMs ?? 250, signal);
|
|
10137
|
+
} else if (this.opts.isStalled?.() || hadValidationFailureThisBatch || !acceptedAnyEntry && this.opts.getCursor() === cursor) {
|
|
9433
10138
|
this.opts.onOperationalOutcome?.("failure");
|
|
9434
10139
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9435
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10140
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9436
10141
|
} else {
|
|
9437
10142
|
retryAttempt = 0;
|
|
9438
10143
|
this.opts.onOperationalOutcome?.("success");
|
|
9439
10144
|
}
|
|
9440
10145
|
} catch (err) {
|
|
9441
|
-
this.opts.
|
|
10146
|
+
this.opts.onServerCapabilitiesInvalidated?.();
|
|
9442
10147
|
if (this.noteRevoked(err)) return;
|
|
9443
|
-
if (!this.running) return;
|
|
10148
|
+
if (!this.running || signal.aborted) return;
|
|
10149
|
+
this.opts.onPollFailure?.();
|
|
9444
10150
|
this.opts.onOperationalOutcome?.("failure");
|
|
9445
10151
|
const baseMs = this.opts.retryDelayMs ?? 2e3;
|
|
9446
|
-
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs);
|
|
10152
|
+
await sleep(this.opts.retryDelayForAttempt?.(retryAttempt++, baseMs) ?? baseMs, signal);
|
|
9447
10153
|
}
|
|
9448
10154
|
}
|
|
9449
10155
|
}
|
|
9450
10156
|
};
|
|
9451
|
-
function
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
this.endpoint = endpoint;
|
|
9459
|
-
this.name = "WsUnexpectedStatusError";
|
|
10157
|
+
async function parseReplayCursorTooOld(res) {
|
|
10158
|
+
if (res.status !== 409) return void 0;
|
|
10159
|
+
let body;
|
|
10160
|
+
try {
|
|
10161
|
+
body = await res.json();
|
|
10162
|
+
} catch {
|
|
10163
|
+
return void 0;
|
|
9460
10164
|
}
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
10165
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
10166
|
+
const { error, recoverableFrom } = body;
|
|
10167
|
+
if (error !== "cursor_too_old" || typeof recoverableFrom !== "number" || !Number.isSafeInteger(recoverableFrom) || recoverableFrom < 0) {
|
|
10168
|
+
return void 0;
|
|
10169
|
+
}
|
|
10170
|
+
return new ReplayCursorTooOldError(recoverableFrom);
|
|
10171
|
+
}
|
|
10172
|
+
function sleep(ms, signal) {
|
|
10173
|
+
if (signal.aborted) return Promise.resolve();
|
|
10174
|
+
return new Promise((resolve) => {
|
|
10175
|
+
const finish = () => {
|
|
10176
|
+
clearTimeout(timer);
|
|
10177
|
+
signal.removeEventListener("abort", finish);
|
|
10178
|
+
resolve();
|
|
10179
|
+
};
|
|
10180
|
+
const timer = setTimeout(finish, ms);
|
|
10181
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
10182
|
+
});
|
|
10183
|
+
}
|
|
10184
|
+
|
|
10185
|
+
// src/daemon/connection-manager.ts
|
|
10186
|
+
function isCursorEnvelopeType(type) {
|
|
10187
|
+
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
10188
|
+
}
|
|
10189
|
+
function createConnectionHelloEnvelope(opts, getCursor) {
|
|
9465
10190
|
const configuredToolsets = opts.getConfiguredToolsets?.();
|
|
9466
10191
|
return createEnvelope("conn.hello", {
|
|
9467
10192
|
protocolVersions: [PROTOCOL_VERSION],
|
|
@@ -9471,222 +10196,34 @@ function createConnectionHelloEnvelope(opts) {
|
|
|
9471
10196
|
clientVersion: opts.clientVersion,
|
|
9472
10197
|
runtimes: opts.runtimes,
|
|
9473
10198
|
configuredToolsets: configuredToolsets === void 0 ? void 0 : [...configuredToolsets],
|
|
9474
|
-
cursor:
|
|
10199
|
+
cursor: getCursor()
|
|
9475
10200
|
});
|
|
9476
10201
|
}
|
|
9477
|
-
var
|
|
9478
|
-
constructor(opts) {
|
|
9479
|
-
this.opts = opts;
|
|
9480
|
-
}
|
|
9481
|
-
opts;
|
|
9482
|
-
socket;
|
|
9483
|
-
closedByUser = false;
|
|
9484
|
-
autoReconnect = true;
|
|
9485
|
-
acked = false;
|
|
9486
|
-
everAckedThisAttempt = false;
|
|
9487
|
-
reconnectAttempt = 0;
|
|
9488
|
-
reconnectTimer;
|
|
9489
|
-
livenessTimer;
|
|
9490
|
-
lastActivity = 0;
|
|
9491
|
-
lastUnexpectedStatus;
|
|
9492
|
-
ackWaiters = [];
|
|
9493
|
-
connect(opts = {}) {
|
|
9494
|
-
this.autoReconnect = opts.auto ?? true;
|
|
9495
|
-
this.closedByUser = false;
|
|
9496
|
-
void this.openSocket();
|
|
9497
|
-
}
|
|
9498
|
-
/** 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). */
|
|
9499
|
-
stopAutoReconnect() {
|
|
9500
|
-
this.autoReconnect = false;
|
|
9501
|
-
if (this.reconnectTimer) {
|
|
9502
|
-
clearTimeout(this.reconnectTimer);
|
|
9503
|
-
this.reconnectTimer = void 0;
|
|
9504
|
-
}
|
|
9505
|
-
}
|
|
9506
|
-
/** Resume normal auto-reconnect-on-close behavior (does not itself trigger a connect — only affects future closes). */
|
|
9507
|
-
resumeAutoReconnect() {
|
|
9508
|
-
this.autoReconnect = true;
|
|
9509
|
-
}
|
|
9510
|
-
get isOpen() {
|
|
9511
|
-
return this.acked;
|
|
9512
|
-
}
|
|
9513
|
-
/**
|
|
9514
|
-
* Attempt to send ONE envelope right now; returns whether it actually went
|
|
9515
|
-
* out. `false` means the socket isn't currently open+acked — the caller
|
|
9516
|
-
* (`ConnectionManager.drainOutbox`, Design B/finding N4) owns re-queueing
|
|
9517
|
-
* and retrying later (e.g. on the next `onAcked`), since this transport no
|
|
9518
|
-
* longer buffers anything itself.
|
|
9519
|
-
*/
|
|
9520
|
-
sendNow(envelope) {
|
|
9521
|
-
if (this.socket && this.socket.readyState === WebSocket.OPEN && this.acked) {
|
|
9522
|
-
this.socket.send(encodeEnvelope(envelope));
|
|
9523
|
-
return true;
|
|
9524
|
-
}
|
|
9525
|
-
return false;
|
|
9526
|
-
}
|
|
9527
|
-
waitForAck(timeoutMs = 1e4) {
|
|
9528
|
-
if (this.acked) return Promise.resolve();
|
|
9529
|
-
return new Promise((resolve, reject) => {
|
|
9530
|
-
const timer = setTimeout(() => reject(new Error("Timed out waiting for conn.ack")), timeoutMs);
|
|
9531
|
-
this.ackWaiters.push({
|
|
9532
|
-
resolve: () => {
|
|
9533
|
-
clearTimeout(timer);
|
|
9534
|
-
resolve();
|
|
9535
|
-
},
|
|
9536
|
-
reject: (err) => {
|
|
9537
|
-
clearTimeout(timer);
|
|
9538
|
-
reject(err);
|
|
9539
|
-
}
|
|
9540
|
-
});
|
|
9541
|
-
});
|
|
9542
|
-
}
|
|
9543
|
-
close() {
|
|
9544
|
-
this.closedByUser = true;
|
|
9545
|
-
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
9546
|
-
this.socket?.close();
|
|
9547
|
-
}
|
|
9548
|
-
async openSocket() {
|
|
9549
|
-
const url = toWsUrl(this.opts.serverUrl);
|
|
9550
|
-
const endpoint = describeEndpoint("ws", url);
|
|
9551
|
-
this.acked = false;
|
|
9552
|
-
this.everAckedThisAttempt = false;
|
|
9553
|
-
this.opts.onStateChange?.("connecting");
|
|
9554
|
-
let token;
|
|
9555
|
-
try {
|
|
9556
|
-
token = await this.opts.getToken();
|
|
9557
|
-
} catch (err) {
|
|
9558
|
-
this.opts.onStateChange?.("closed");
|
|
9559
|
-
this.opts.onConnectOutcome?.(false, err, endpoint);
|
|
9560
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
9561
|
-
return;
|
|
9562
|
-
}
|
|
9563
|
-
const socket = new WebSocket(url, {
|
|
9564
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
9565
|
-
});
|
|
9566
|
-
this.socket = socket;
|
|
9567
|
-
this.lastActivity = Date.now();
|
|
9568
|
-
this.startLivenessCheck(socket);
|
|
9569
|
-
socket.on("open", () => {
|
|
9570
|
-
const hello = createConnectionHelloEnvelope(this.opts);
|
|
9571
|
-
socket.send(encodeEnvelope(hello));
|
|
9572
|
-
});
|
|
9573
|
-
socket.on("ping", () => {
|
|
9574
|
-
this.lastActivity = Date.now();
|
|
9575
|
-
});
|
|
9576
|
-
socket.on("message", (data, isBinary) => {
|
|
9577
|
-
this.lastActivity = Date.now();
|
|
9578
|
-
const bytes = toBytes(data);
|
|
9579
|
-
let envelope;
|
|
9580
|
-
try {
|
|
9581
|
-
envelope = decodeEnvelope(bytes);
|
|
9582
|
-
} catch {
|
|
9583
|
-
return;
|
|
9584
|
-
}
|
|
9585
|
-
if (envelope.type === "conn.ack") {
|
|
9586
|
-
this.reconnectAttempt = 0;
|
|
9587
|
-
this.acked = true;
|
|
9588
|
-
this.everAckedThisAttempt = true;
|
|
9589
|
-
this.opts.onStateChange?.("open");
|
|
9590
|
-
for (const waiter of this.ackWaiters.splice(0)) waiter.resolve();
|
|
9591
|
-
this.opts.onAcked?.(envelope.payload.capabilities);
|
|
9592
|
-
}
|
|
9593
|
-
this.opts.onEnvelope(envelope);
|
|
9594
|
-
});
|
|
9595
|
-
socket.on("close", () => {
|
|
9596
|
-
this.socket = void 0;
|
|
9597
|
-
this.stopLivenessCheck();
|
|
9598
|
-
this.opts.onStateChange?.("closed");
|
|
9599
|
-
const acked = this.everAckedThisAttempt;
|
|
9600
|
-
const status = this.lastUnexpectedStatus;
|
|
9601
|
-
this.lastUnexpectedStatus = void 0;
|
|
9602
|
-
this.opts.onConnectOutcome?.(
|
|
9603
|
-
acked,
|
|
9604
|
-
status !== void 0 ? new WsUnexpectedStatusError(status, endpoint) : void 0,
|
|
9605
|
-
endpoint
|
|
9606
|
-
);
|
|
9607
|
-
if (!this.closedByUser && this.autoReconnect) this.scheduleReconnect();
|
|
9608
|
-
});
|
|
9609
|
-
socket.on("error", () => {
|
|
9610
|
-
});
|
|
9611
|
-
socket.on("unexpected-response", (_req, res) => {
|
|
9612
|
-
this.lastUnexpectedStatus = res.statusCode;
|
|
9613
|
-
res.resume();
|
|
9614
|
-
socket.terminate();
|
|
9615
|
-
});
|
|
9616
|
-
}
|
|
9617
|
-
scheduleReconnect() {
|
|
9618
|
-
const { baseMs = 1e3, maxMs = 3e4, factor = 2 } = this.opts.backoff ?? {};
|
|
9619
|
-
const delay3 = Math.min(maxMs, baseMs * factor ** this.reconnectAttempt);
|
|
9620
|
-
const jitter = this.opts.reconnectDelayMs?.(this.reconnectAttempt, delay3) ?? delay3;
|
|
9621
|
-
this.reconnectAttempt += 1;
|
|
9622
|
-
this.reconnectTimer = setTimeout(() => void this.openSocket(), jitter);
|
|
9623
|
-
}
|
|
9624
|
-
startLivenessCheck(socket) {
|
|
9625
|
-
const { timeoutMs = 75e3, checkIntervalMs = Math.max(1e3, Math.floor(timeoutMs / 3)) } = this.opts.liveness ?? {};
|
|
9626
|
-
this.livenessTimer = setInterval(() => {
|
|
9627
|
-
if (Date.now() - this.lastActivity > timeoutMs) {
|
|
9628
|
-
socket.terminate();
|
|
9629
|
-
}
|
|
9630
|
-
}, checkIntervalMs);
|
|
9631
|
-
this.livenessTimer.unref?.();
|
|
9632
|
-
}
|
|
9633
|
-
stopLivenessCheck() {
|
|
9634
|
-
if (this.livenessTimer) {
|
|
9635
|
-
clearInterval(this.livenessTimer);
|
|
9636
|
-
this.livenessTimer = void 0;
|
|
9637
|
-
}
|
|
9638
|
-
}
|
|
9639
|
-
};
|
|
9640
|
-
function toBytes(data, _isBinary) {
|
|
9641
|
-
if (Buffer.isBuffer(data)) return data;
|
|
9642
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
9643
|
-
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
9644
|
-
return Buffer.from(String(data), "utf8");
|
|
9645
|
-
}
|
|
9646
|
-
|
|
9647
|
-
// src/daemon/connection-manager.ts
|
|
9648
|
-
function isCursorEnvelopeType(type) {
|
|
9649
|
-
return type.startsWith("task.") || type === "agent.egress.ack" || type === "agent.content.read" || type === "agent.home.projection";
|
|
9650
|
-
}
|
|
10202
|
+
var MAX_REJECTED_OUTBOX_ENTRIES = 1e3;
|
|
9651
10203
|
var ConnectionManager = class {
|
|
9652
10204
|
constructor(opts) {
|
|
9653
10205
|
this.opts = opts;
|
|
9654
10206
|
this.fleetJitter = opts.fleetJitter ?? createFleetJitter(opts.productId, opts.deviceId);
|
|
9655
|
-
this.ws = new WsTransport({
|
|
9656
|
-
serverUrl: opts.serverUrl,
|
|
9657
|
-
getToken: () => opts.auth.getValidAccessToken(),
|
|
9658
|
-
deviceId: opts.deviceId,
|
|
9659
|
-
productId: opts.productId,
|
|
9660
|
-
capabilities: opts.capabilities,
|
|
9661
|
-
clientVersion: opts.clientVersion,
|
|
9662
|
-
runtimes: opts.runtimes,
|
|
9663
|
-
getConfiguredToolsets: opts.getConfiguredToolsets,
|
|
9664
|
-
getCursor: () => this.cursor,
|
|
9665
|
-
onEnvelope: (envelope) => this.deliver(envelope),
|
|
9666
|
-
onStateChange: (state) => {
|
|
9667
|
-
if (!this.stopped && !this.revoked) this.opts.onStateChange?.(state);
|
|
9668
|
-
},
|
|
9669
|
-
onAcked: (capabilities) => this.onAcked(capabilities),
|
|
9670
|
-
onConnectOutcome: (acked, err) => this.onWsOutcome(acked, err),
|
|
9671
|
-
backoff: opts.backoff,
|
|
9672
|
-
liveness: opts.liveness,
|
|
9673
|
-
reconnectDelayMs: (attempt, baseMs) => this.fleetJitter.delay("reconnect", attempt, baseMs)
|
|
9674
|
-
});
|
|
9675
10207
|
this.longPoll = new LongPollClient({
|
|
9676
10208
|
serverUrl: opts.serverUrl,
|
|
9677
10209
|
auth: opts.auth,
|
|
9678
|
-
//
|
|
9679
|
-
//
|
|
9680
|
-
//
|
|
9681
|
-
//
|
|
9682
|
-
//
|
|
9683
|
-
|
|
9684
|
-
getCursor: () => this.dedupWatermark(),
|
|
10210
|
+
// The long-poll query cursor is also the kernel's irreversible ack.
|
|
10211
|
+
// Only report the successfully processed cursor here. `deliveredSeq`
|
|
10212
|
+
// remains a local dedup watermark; using it on the wire would ack an
|
|
10213
|
+
// in-flight envelope before its handler settles and make a later
|
|
10214
|
+
// failure impossible to redeliver.
|
|
10215
|
+
getCursor: () => this.cursor,
|
|
9685
10216
|
onEnvelope: (envelope) => this.deliver(envelope),
|
|
9686
10217
|
onServerCapabilities: (capabilities) => {
|
|
9687
|
-
|
|
10218
|
+
this.serverCapabilities = capabilities;
|
|
10219
|
+
this.noteConnected();
|
|
9688
10220
|
},
|
|
10221
|
+
onServerCapabilitiesInvalidated: () => {
|
|
10222
|
+
this.serverCapabilities = [];
|
|
10223
|
+
},
|
|
10224
|
+
onPollFailure: () => this.noteDisconnected(),
|
|
9689
10225
|
onRevoked: () => this.enterRevoked(),
|
|
10226
|
+
onReplayCursorTooOld: (error) => this.enterReplayCursorTooOld(error),
|
|
9690
10227
|
// M4 Phase 4 (version-negotiation drill fix): a batch entry
|
|
9691
10228
|
// LongPollClient couldn't parse into a known Envelope at all (an
|
|
9692
10229
|
// unrecognized message type) still needs its cursor/watermark
|
|
@@ -9712,13 +10249,10 @@ var ConnectionManager = class {
|
|
|
9712
10249
|
}
|
|
9713
10250
|
opts;
|
|
9714
10251
|
fleetJitter;
|
|
9715
|
-
ws;
|
|
9716
10252
|
longPoll;
|
|
9717
|
-
mode = "ws";
|
|
9718
|
-
consecutiveFailures = 0;
|
|
9719
|
-
wsRetryTimer;
|
|
9720
|
-
wsProbeSequence = 0;
|
|
9721
10253
|
uploadRetryAttempt = 0;
|
|
10254
|
+
started = false;
|
|
10255
|
+
connected = false;
|
|
9722
10256
|
cursor;
|
|
9723
10257
|
/**
|
|
9724
10258
|
* Finding F3 (at-most-once redelivery): the lowest `task.*` envelope `seq`
|
|
@@ -9740,10 +10274,9 @@ var ConnectionManager = class {
|
|
|
9740
10274
|
* (see `advanceCursor`) — that semantics is unchanged. `deliveredSeq`
|
|
9741
10275
|
* advances eagerly, the instant a `task.*` envelope is admitted past
|
|
9742
10276
|
* dedup (see `deliver`/`noteDelivered`), independent of whether its
|
|
9743
|
-
* handler has even started, let alone succeeded. It exists so a
|
|
9744
|
-
*
|
|
9745
|
-
*
|
|
9746
|
-
* `handleOffer` is NOT idempotent and must never be re-pulled while a
|
|
10277
|
+
* handler has even started, let alone succeeded. It exists so a repeated
|
|
10278
|
+
* read at the durable cursor does not re-dispatch an envelope already in
|
|
10279
|
+
* flight — `handleOffer` must not start a second adapter session while a
|
|
9747
10280
|
* first attempt is still running. On WS this same field is written the
|
|
9748
10281
|
* same way, but since a live WS connection only ever pushes a given `seq`
|
|
9749
10282
|
* once, it never has an observable effect there beyond mirroring
|
|
@@ -9754,18 +10287,18 @@ var ConnectionManager = class {
|
|
|
9754
10287
|
/** Finding F3: serializes `onEnvelope` calls into a per-connection FIFO — one envelope's handler always fully settles before the next one starts. */
|
|
9755
10288
|
processingChain = Promise.resolve();
|
|
9756
10289
|
/**
|
|
9757
|
-
* Design B (finding N4): the ONE outbound queue
|
|
9758
|
-
*
|
|
10290
|
+
* Design B (finding N4): the ONE outbound queue holds `Envelope` OBJECTS,
|
|
10291
|
+
* never re-encoded/rebuilt strings, so a
|
|
9759
10292
|
* resend after a failed send attempt is byte-identical to the original
|
|
9760
10293
|
* (same `id`), which is what lets the server's per-(deviceId,id) dedup
|
|
9761
10294
|
* (Wave 1) recognize it as a safe no-op retry rather than a second
|
|
9762
|
-
* application (protocol §9).
|
|
9763
|
-
* touches this queue — see `drainOutbox` — so nothing queued while one
|
|
9764
|
-
* transport was active is ever stranded when the other takes over.
|
|
10295
|
+
* application (protocol §9).
|
|
9765
10296
|
*/
|
|
9766
10297
|
outbox = [];
|
|
10298
|
+
/** Terminally rejected outbound envelopes, retained as a bounded observable quarantine. */
|
|
10299
|
+
rejectedOutboundEnvelopes = [];
|
|
9767
10300
|
/**
|
|
9768
|
-
* Finding F5(b): how many envelopes `drainOutbox`
|
|
10301
|
+
* Finding F5(b): how many envelopes `drainOutbox` has
|
|
9769
10302
|
* currently spliced OUT of `this.outbox` for an in-flight (not yet
|
|
9770
10303
|
* confirmed delivered) `postBatch` call — 0 the rest of the time. See
|
|
9771
10304
|
* `outboxLength`'s own doc comment for why this needs to be tracked
|
|
@@ -9775,6 +10308,7 @@ var ConnectionManager = class {
|
|
|
9775
10308
|
draining = false;
|
|
9776
10309
|
stopped = false;
|
|
9777
10310
|
revoked = false;
|
|
10311
|
+
terminalError;
|
|
9778
10312
|
settledWaiters = [];
|
|
9779
10313
|
pendingCursorSave = Promise.resolve();
|
|
9780
10314
|
/**
|
|
@@ -9817,38 +10351,22 @@ var ConnectionManager = class {
|
|
|
9817
10351
|
*/
|
|
9818
10352
|
cancelPendingDrainRetry;
|
|
9819
10353
|
/**
|
|
9820
|
-
* The capabilities the
|
|
9821
|
-
* `string[]` for forward compatibility.
|
|
9822
|
-
* long-poll
|
|
9823
|
-
*
|
|
9824
|
-
*
|
|
9825
|
-
*
|
|
9826
|
-
* not per-daemon-lifetime. Cleared to `[]` the instant the acked WS
|
|
9827
|
-
* connection ends for ANY reason — an ordinary disconnect (`onWsOutcome`'s
|
|
9828
|
-
* `acked` branch), `stop()`, or a transport switch to long-poll
|
|
9829
|
-
* (`enterLongPoll`) — and only repopulated by a fresh advertisement from
|
|
9830
|
-
* the transport that is still current.
|
|
9831
|
-
* The previous version of this doc comment claimed long-poll mode simply
|
|
9832
|
-
* "stays at whatever the last real WS `conn.ack` said" — that was the bug:
|
|
9833
|
-
* a daemon that once learned e.g. `approval_resolved` from an earlier WS
|
|
9834
|
-
* session kept believing it applied to whatever it's connected to NOW,
|
|
9835
|
-
* even after a disconnect/degrade where nothing has actually confirmed
|
|
9836
|
-
* that's still true (a reconnect could land on a DIFFERENT server behind a
|
|
9837
|
-
* load balancer). Concretely, `TaskRunner.sendApprovalResolved` gates
|
|
9838
|
-
* `task.approval_resolved` on this list — sending it to a server that
|
|
9839
|
-
* doesn't actually understand it over the long-poll path would get a
|
|
9840
|
-
* batch-level 400 from `MessagesSendRequestSchema` (protocol §8.2), which
|
|
9841
|
-
* `drainOutbox`'s retry-the-same-batch-forever loop then head-of-line
|
|
9842
|
-
* blocks EVERY envelope queued behind it on, permanently. Clearing this
|
|
9843
|
-
* eagerly means that gate reliably fails closed (falls back to the
|
|
9844
|
-
* pre-existing implicit-resume inference, unconditionally — see
|
|
9845
|
-
* `sendApprovalResolved`'s own doc comment) the moment the connection that
|
|
9846
|
-
* advertised the capability is no longer the one actually in use.
|
|
10354
|
+
* The capabilities the current server response advertised — untyped
|
|
10355
|
+
* `string[]` for forward compatibility. An advertisement is scoped to the
|
|
10356
|
+
* current long-poll response stream and is cleared after an HTTP failure,
|
|
10357
|
+
* terminal shutdown, or revocation. This keeps capability-gated outbound
|
|
10358
|
+
* messages fail-closed until the current server has explicitly advertised
|
|
10359
|
+
* support.
|
|
9847
10360
|
*/
|
|
9848
10361
|
serverCapabilities = [];
|
|
9849
10362
|
async start() {
|
|
10363
|
+
if (this.terminalError) throw this.terminalError;
|
|
9850
10364
|
this.cursor = await this.opts.cursorStore.load(this.opts.serverUrl, this.opts.deviceId);
|
|
9851
|
-
this.
|
|
10365
|
+
this.started = true;
|
|
10366
|
+
this.opts.onStateChange?.("connecting");
|
|
10367
|
+
this.longPoll.start();
|
|
10368
|
+
this.outbox.unshift(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
10369
|
+
void this.drainOutbox();
|
|
9852
10370
|
}
|
|
9853
10371
|
/**
|
|
9854
10372
|
* Design B (finding N4): push onto the single shared outbox and try to
|
|
@@ -9859,17 +10377,13 @@ var ConnectionManager = class {
|
|
|
9859
10377
|
this.outbox.push(envelope);
|
|
9860
10378
|
void this.drainOutbox();
|
|
9861
10379
|
}
|
|
10380
|
+
/** Publish a fresh local configuration snapshot while this daemon is running. */
|
|
10381
|
+
refreshHello() {
|
|
10382
|
+
if (!this.started || this.stopped || this.revoked || this.terminalError) return;
|
|
10383
|
+
this.send(createConnectionHelloEnvelope(this.opts, () => this.cursor));
|
|
10384
|
+
}
|
|
9862
10385
|
/**
|
|
9863
|
-
*
|
|
9864
|
-
* transport is currently active, re-checking `this.mode` fresh on every
|
|
9865
|
-
* iteration so a transport switch mid-drain is picked up immediately
|
|
9866
|
-
* rather than fighting a stale decision made before the switch.
|
|
9867
|
-
*
|
|
9868
|
-
* WS: a synchronous, one-at-a-time `sendNow` per envelope while open+
|
|
9869
|
-
* acked; stops (without dropping anything — the remainder stays queued)
|
|
9870
|
-
* the moment it isn't, and is re-invoked once `onAcked` fires.
|
|
9871
|
-
*
|
|
9872
|
-
* Long-poll: POSTs the outbox in chunks of at most
|
|
10386
|
+
* POSTs the outbox through long-poll in chunks of at most
|
|
9873
10387
|
* `MAX_MESSAGES_PER_BATCH` (finding P1) — the server hard-caps a single
|
|
9874
10388
|
* `/byok/messages` batch there (`MessagesSendRequestSchema`, protocol
|
|
9875
10389
|
* §8.2) and 400s the WHOLE request if it's exceeded, which — before this
|
|
@@ -9881,43 +10395,48 @@ var ConnectionManager = class {
|
|
|
9881
10395
|
* failure that SAME chunk is unshifted back (order-preserving, same
|
|
9882
10396
|
* Envelope objects/ids — never rebuilt, so a retry is exactly the resend
|
|
9883
10397
|
* Wave 1's server-side dedup expects) and retried after a short backoff,
|
|
9884
|
-
* re-reading `this.mode` each time so a WS recovery that happens
|
|
9885
|
-
* mid-retry is honored on the very next loop iteration instead of only
|
|
9886
|
-
* after this attempt's backoff chain gives up.
|
|
9887
|
-
*
|
|
9888
10398
|
* Re-entrancy is guarded by `draining`: a call arriving while a drain is
|
|
9889
10399
|
* already in progress just returns — the in-progress loop's own
|
|
9890
|
-
* `while (this.outbox.length > 0)` check
|
|
9891
|
-
* pushed (or left over after a mode switch) on its very next iteration.
|
|
10400
|
+
* `while (this.outbox.length > 0)` check picks up anything newly pushed.
|
|
9892
10401
|
*/
|
|
9893
10402
|
async drainOutbox() {
|
|
9894
|
-
if (this.draining) return;
|
|
10403
|
+
if (!this.started || this.draining) return;
|
|
9895
10404
|
this.draining = true;
|
|
10405
|
+
const isolateRejectedBatch = async (batch) => {
|
|
10406
|
+
const pending = [[...batch]];
|
|
10407
|
+
while (pending.length > 0) {
|
|
10408
|
+
if (this.stopped || this.revoked) return pending.flat();
|
|
10409
|
+
const segment = pending.shift();
|
|
10410
|
+
const result = await this.longPoll.postBatch(segment);
|
|
10411
|
+
if (result === void 0) return [...segment, ...pending.flat()];
|
|
10412
|
+
if ((result.rejected ?? 0) === 0) continue;
|
|
10413
|
+
if (segment.length === 1) {
|
|
10414
|
+
this.quarantineRejectedOutbound(segment[0], "inbound_rejected");
|
|
10415
|
+
continue;
|
|
10416
|
+
}
|
|
10417
|
+
const midpoint = Math.floor(segment.length / 2);
|
|
10418
|
+
pending.unshift(segment.slice(0, midpoint), segment.slice(midpoint));
|
|
10419
|
+
}
|
|
10420
|
+
return void 0;
|
|
10421
|
+
};
|
|
9896
10422
|
try {
|
|
9897
10423
|
while (this.outbox.length > 0) {
|
|
9898
10424
|
if (this.stopped || this.revoked) return;
|
|
9899
|
-
if (this.mode === "ws") {
|
|
9900
|
-
if (!this.ws.isOpen) return;
|
|
9901
|
-
const envelope = this.outbox[0];
|
|
9902
|
-
if (!this.ws.sendNow(envelope)) return;
|
|
9903
|
-
this.outbox.shift();
|
|
9904
|
-
continue;
|
|
9905
|
-
}
|
|
9906
10425
|
const batch = this.outbox.splice(0, MAX_MESSAGES_PER_BATCH);
|
|
9907
10426
|
this.inFlightBatchSize = batch.length;
|
|
9908
|
-
let
|
|
10427
|
+
let retrySegments;
|
|
9909
10428
|
try {
|
|
9910
|
-
|
|
10429
|
+
retrySegments = await isolateRejectedBatch(batch);
|
|
9911
10430
|
} finally {
|
|
9912
10431
|
this.inFlightBatchSize = 0;
|
|
9913
10432
|
}
|
|
9914
|
-
if (
|
|
10433
|
+
if (retrySegments === void 0) {
|
|
9915
10434
|
this.uploadRetryAttempt = 0;
|
|
9916
10435
|
this.opts.onOperationalOutcome?.("success", "upload");
|
|
9917
10436
|
continue;
|
|
9918
10437
|
}
|
|
9919
10438
|
this.opts.onOperationalOutcome?.("failure", "upload");
|
|
9920
|
-
this.outbox.unshift(...
|
|
10439
|
+
this.outbox.unshift(...retrySegments);
|
|
9921
10440
|
if (this.stopped || this.revoked) return;
|
|
9922
10441
|
const baseMs = this.opts.longPollRetryDelayMs ?? 2e3;
|
|
9923
10442
|
await this.drainRetryDelay(this.fleetJitter.delay("upload", this.uploadRetryAttempt++, baseMs));
|
|
@@ -9933,8 +10452,7 @@ var ConnectionManager = class {
|
|
|
9933
10452
|
* in-flight wait immediately instead of leaving `drainOutbox` parked here
|
|
9934
10453
|
* for up to the rest of the delay before it next checks `this.revoked` —
|
|
9935
10454
|
* and (b) unref'd, so the timer never keeps the Node process alive by
|
|
9936
|
-
* itself while nothing else (
|
|
9937
|
-
* legitimately is.
|
|
10455
|
+
* itself while nothing else (such as the live long-poll GET) legitimately is.
|
|
9938
10456
|
*/
|
|
9939
10457
|
drainRetryDelay(ms) {
|
|
9940
10458
|
return new Promise((resolve) => {
|
|
@@ -9950,45 +10468,41 @@ var ConnectionManager = class {
|
|
|
9950
10468
|
};
|
|
9951
10469
|
});
|
|
9952
10470
|
}
|
|
9953
|
-
isTransportDegraded() {
|
|
9954
|
-
return this.mode === "long-poll";
|
|
9955
|
-
}
|
|
9956
10471
|
/**
|
|
9957
|
-
* The capabilities the
|
|
9958
|
-
*
|
|
9959
|
-
* on long-poll. Empty before either transport has supplied its current
|
|
9960
|
-
* advertisement, and cleared across disconnect/switch boundaries.
|
|
10472
|
+
* The capabilities the latest successful `GET /byok/events` response
|
|
10473
|
+
* advertised. Empty before a successful response and after a failed one.
|
|
9961
10474
|
*/
|
|
9962
10475
|
getServerCapabilities() {
|
|
9963
10476
|
return this.serverCapabilities;
|
|
9964
10477
|
}
|
|
10478
|
+
getTerminalError() {
|
|
10479
|
+
return this.terminalError;
|
|
10480
|
+
}
|
|
9965
10481
|
isConnected() {
|
|
9966
|
-
return this.
|
|
10482
|
+
return this.connected;
|
|
9967
10483
|
}
|
|
9968
10484
|
isRevoked() {
|
|
9969
10485
|
return this.revoked;
|
|
9970
10486
|
}
|
|
9971
10487
|
/**
|
|
9972
|
-
* Resolves
|
|
9973
|
-
* connection
|
|
9974
|
-
* lets `daemon.start()` return promptly even when WS is unavailable from
|
|
9975
|
-
* the very first attempt, rather than hanging until a WS `conn.ack` that
|
|
9976
|
-
* may never come.
|
|
10488
|
+
* Resolves after the first successful long-poll response establishes the
|
|
10489
|
+
* authenticated connection.
|
|
9977
10490
|
*
|
|
9978
10491
|
* Rejects with {@link DeviceRevokedError} — instead of hanging until
|
|
9979
10492
|
* `timeoutMs` — if the device turns out to be revoked while settling (or
|
|
9980
10493
|
* already was): a cold `daemon.start()` against an already-revoked device
|
|
9981
10494
|
* must fail fast, not surface a generic timeout (protocol §6.3).
|
|
9982
10495
|
*/
|
|
9983
|
-
|
|
9984
|
-
if (this.
|
|
10496
|
+
waitForConnection(timeoutMs = 1e4) {
|
|
10497
|
+
if (this.connected) return Promise.resolve();
|
|
10498
|
+
if (this.terminalError) return Promise.reject(this.terminalError);
|
|
9985
10499
|
if (this.revoked) return Promise.reject(new DeviceRevokedError());
|
|
9986
10500
|
return new Promise((resolve, reject) => {
|
|
9987
10501
|
let settle = () => {
|
|
9988
10502
|
};
|
|
9989
10503
|
const timer = setTimeout(() => {
|
|
9990
10504
|
this.settledWaiters = this.settledWaiters.filter((w) => w !== settle);
|
|
9991
|
-
reject(new Error("Timed out waiting for the connection to settle
|
|
10505
|
+
reject(new Error("Timed out waiting for the long-poll connection to settle"));
|
|
9992
10506
|
}, timeoutMs);
|
|
9993
10507
|
settle = (err) => {
|
|
9994
10508
|
clearTimeout(timer);
|
|
@@ -9999,7 +10513,7 @@ var ConnectionManager = class {
|
|
|
9999
10513
|
});
|
|
10000
10514
|
}
|
|
10001
10515
|
/**
|
|
10002
|
-
* Stops
|
|
10516
|
+
* Stops the long-poll transport and waits for every in-flight envelope handler
|
|
10003
10517
|
* (the F3 FIFO chain) and the most recent cursor write to actually land on
|
|
10004
10518
|
* disk — otherwise a `stop()` racing a just-processed envelope's
|
|
10005
10519
|
* persistence could lose that cursor advance, or leave a handler running
|
|
@@ -10008,14 +10522,14 @@ var ConnectionManager = class {
|
|
|
10008
10522
|
* Finding F5(b) (cross-model adversarial review): `drainTimeoutMs`, when
|
|
10009
10523
|
* passed, bounds how long this waits for the shared outbox (`this.outbox`
|
|
10010
10524
|
* — Design B) to actually finish draining BEFORE flipping `this.stopped`
|
|
10011
|
-
* and
|
|
10525
|
+
* and stopping the transport. Before this fix, `stop()` set `stopped`
|
|
10012
10526
|
* synchronously and never waited for `drainOutbox` at all: an envelope
|
|
10013
10527
|
* `send()` had just pushed moments earlier (e.g. `TaskRunner.shutdownTask`'s
|
|
10014
10528
|
* own `task.fail`, sent right before `create-daemon.ts`'s
|
|
10015
10529
|
* `performControlShutdown` calls this) could still be sitting UNSENT in
|
|
10016
10530
|
* `this.outbox` — mid long-poll retry backoff, or simply not yet picked up
|
|
10017
10531
|
* by the fire-and-forget `drainOutbox()` `send()` kicked off — and this
|
|
10018
|
-
* method would happily proceed to `stopped = true`
|
|
10532
|
+
* method would happily proceed to `stopped = true` regardless,
|
|
10019
10533
|
* after which NOTHING ever drains it again: silently lost, even though
|
|
10020
10534
|
* `TaskRunner` believed it had been sent. `drainTimeoutMs` omitted (the
|
|
10021
10535
|
* default) preserves the EXACT prior behavior for every other existing
|
|
@@ -10032,9 +10546,9 @@ var ConnectionManager = class {
|
|
|
10032
10546
|
}
|
|
10033
10547
|
this.stopped = true;
|
|
10034
10548
|
this.serverCapabilities = [];
|
|
10035
|
-
if (this.wsRetryTimer) clearInterval(this.wsRetryTimer);
|
|
10036
10549
|
this.longPoll.stop();
|
|
10037
|
-
this.
|
|
10550
|
+
this.connected = false;
|
|
10551
|
+
this.opts.onStateChange?.("closed");
|
|
10038
10552
|
await this.processingChain;
|
|
10039
10553
|
await this.pendingCursorSave;
|
|
10040
10554
|
}
|
|
@@ -10059,6 +10573,10 @@ var ConnectionManager = class {
|
|
|
10059
10573
|
outboxLength() {
|
|
10060
10574
|
return this.outbox.length + this.inFlightBatchSize;
|
|
10061
10575
|
}
|
|
10576
|
+
/** A bounded terminal quarantine for operator inspection; these entries are never retried. */
|
|
10577
|
+
rejectedOutbox() {
|
|
10578
|
+
return this.rejectedOutboundEnvelopes;
|
|
10579
|
+
}
|
|
10062
10580
|
/**
|
|
10063
10581
|
* Finding F5(b): polls {@link outboxLength} (not `this.outbox.length`
|
|
10064
10582
|
* alone — see that method's own doc comment for why a spliced-out,
|
|
@@ -10066,15 +10584,14 @@ var ConnectionManager = class {
|
|
|
10066
10584
|
* a single `drainOutbox()` promise directly — a drain in progress can
|
|
10067
10585
|
* itself loop through multiple retry/backoff cycles (`drainRetryDelay`)
|
|
10068
10586
|
* while the server is unreachable, and a fresh, INDEPENDENT
|
|
10069
|
-
* `drainOutbox()` call can also be triggered concurrently (`send()
|
|
10070
|
-
*
|
|
10587
|
+
* `drainOutbox()` call can also be triggered concurrently (`send()`) —
|
|
10588
|
+
* polling the one thing
|
|
10071
10589
|
* that actually matters (is anything still undelivered) can never go
|
|
10072
10590
|
* stale the way capturing one specific in-flight promise reference
|
|
10073
10591
|
* could. Kicks off one more `drainOutbox()` attempt itself first
|
|
10074
10592
|
* (harmless no-op if one is already running — see its own re-entrancy
|
|
10075
|
-
* guard) in case nothing is currently actively retrying
|
|
10076
|
-
*
|
|
10077
|
-
* isn't just passively hoping something else happens to be making
|
|
10593
|
+
* guard) in case nothing is currently actively retrying, so this bounded
|
|
10594
|
+
* wait isn't just passively hoping something else happens to be making
|
|
10078
10595
|
* progress.
|
|
10079
10596
|
*/
|
|
10080
10597
|
waitForOutboxDrained(timeoutMs) {
|
|
@@ -10117,19 +10634,21 @@ var ConnectionManager = class {
|
|
|
10117
10634
|
deliver(envelope) {
|
|
10118
10635
|
const tracked = isCursorEnvelopeType(envelope.type) && typeof envelope.seq === "number";
|
|
10119
10636
|
const watermark = this.dedupWatermark();
|
|
10120
|
-
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return;
|
|
10637
|
+
if (tracked && watermark !== void 0 && envelope.seq <= watermark) return false;
|
|
10121
10638
|
if (tracked) {
|
|
10122
10639
|
const seq = envelope.seq;
|
|
10123
|
-
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return;
|
|
10640
|
+
if (this.inFlightSeqs.has(seq) || this.processedSeqs.has(seq)) return false;
|
|
10124
10641
|
this.inFlightSeqs.add(seq);
|
|
10125
10642
|
this.noteDelivered(seq);
|
|
10126
10643
|
}
|
|
10127
10644
|
this.processingChain = this.processingChain.then(() => this.process(envelope, tracked));
|
|
10645
|
+
return true;
|
|
10128
10646
|
}
|
|
10129
10647
|
/**
|
|
10130
|
-
*
|
|
10131
|
-
* against
|
|
10132
|
-
*
|
|
10648
|
+
* The local watermark `deliver()` dedupes inbound `task.*` envelopes
|
|
10649
|
+
* against. It is deliberately NOT the long-poll query cursor: that query
|
|
10650
|
+
* is the kernel acknowledgement and uses only the successfully processed
|
|
10651
|
+
* `cursor` (see the constructor). Normally this local watermark is
|
|
10133
10652
|
* `deliveredSeq` — which is always >= `cursor` (every envelope that
|
|
10134
10653
|
* reaches `advanceCursor` already passed through `noteDelivered` first,
|
|
10135
10654
|
* see `deliver`) — so this is the literal `max(cursor, deliveredSeq)` the
|
|
@@ -10145,11 +10664,9 @@ var ConnectionManager = class {
|
|
|
10145
10664
|
* whose outcome wasn't known yet. No separate "reset deliveredSeq on
|
|
10146
10665
|
* reconnect" step is needed for this to be correct — collapsing to
|
|
10147
10666
|
* `cursor` exactly while stalled already produces the right answer on
|
|
10148
|
-
* every
|
|
10149
|
-
*
|
|
10150
|
-
*
|
|
10151
|
-
* re-pulling/re-dispatching something already in flight across a
|
|
10152
|
-
* reconnect that happens to land while a handler is still running.
|
|
10667
|
+
* every long-poll retry path. NOT resetting it unconditionally on every
|
|
10668
|
+
* retry lets `deliveredSeq` keep doing its job of not re-dispatching
|
|
10669
|
+
* something already in flight while a handler is still running.
|
|
10153
10670
|
*/
|
|
10154
10671
|
dedupWatermark() {
|
|
10155
10672
|
return this.stalledAtSeq !== void 0 ? this.cursor : this.deliveredSeq ?? this.cursor;
|
|
@@ -10167,11 +10684,13 @@ var ConnectionManager = class {
|
|
|
10167
10684
|
}
|
|
10168
10685
|
await this.opts.onEnvelope(envelope);
|
|
10169
10686
|
if (!tracked) return;
|
|
10170
|
-
this.
|
|
10171
|
-
|
|
10687
|
+
if (this.stalledAtSeq !== void 0 && envelope.seq !== this.stalledAtSeq) {
|
|
10688
|
+
this.processedSeqs.add(seq);
|
|
10689
|
+
return;
|
|
10690
|
+
}
|
|
10172
10691
|
this.stalledAtSeq = void 0;
|
|
10692
|
+
await this.advanceCursor(envelope.seq);
|
|
10173
10693
|
this.processedSeqs.clear();
|
|
10174
|
-
this.advanceCursor(envelope.seq);
|
|
10175
10694
|
} catch (err) {
|
|
10176
10695
|
if (tracked && this.stalledAtSeq === void 0) this.stalledAtSeq = envelope.seq;
|
|
10177
10696
|
console.error(
|
|
@@ -10185,9 +10704,8 @@ var ConnectionManager = class {
|
|
|
10185
10704
|
/**
|
|
10186
10705
|
* M4 Phase 4 (version-negotiation drill fix): `LongPollClient` calls this
|
|
10187
10706
|
* for a batch entry it could not parse into a known `Envelope` at all (an
|
|
10188
|
-
* unrecognized message type
|
|
10189
|
-
*
|
|
10190
|
-
* `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
10707
|
+
* unrecognized message type (see `long-poll-transport.ts`'s own doc
|
|
10708
|
+
* comment on `parseLooseEventsPollResponse`) but which still carried a numeric,
|
|
10191
10709
|
* task-class envelope-level `seq` (the caller only invokes this for a
|
|
10192
10710
|
* `task.`-prefixed type — see `long-poll-transport.ts`'s own
|
|
10193
10711
|
* `extractSkippableSeq`; `conn.*`-shaped or type-less entries never reach
|
|
@@ -10227,7 +10745,7 @@ var ConnectionManager = class {
|
|
|
10227
10745
|
* `noteDelivered` (the eager, in-memory watermark) stays UNCHAINED —
|
|
10228
10746
|
* called immediately, unconditionally, regardless of `stalledAtSeq` —
|
|
10229
10747
|
* matching `deliver()`'s own eager, unconditional call for a real
|
|
10230
|
-
* envelope: its only job is "don't re-
|
|
10748
|
+
* envelope: its only job is "don't re-dispatch something already handed off,"
|
|
10231
10749
|
* independent of outcome, and that property does not depend on FIFO
|
|
10232
10750
|
* ordering the way the DURABLE cursor does.
|
|
10233
10751
|
*
|
|
@@ -10247,9 +10765,18 @@ var ConnectionManager = class {
|
|
|
10247
10765
|
*/
|
|
10248
10766
|
noteSkippedSeq(seq) {
|
|
10249
10767
|
this.noteDelivered(seq);
|
|
10250
|
-
this.processingChain = this.processingChain.then(() => {
|
|
10768
|
+
this.processingChain = this.processingChain.then(async () => {
|
|
10251
10769
|
if (this.stalledAtSeq === void 0 || seq === this.stalledAtSeq) {
|
|
10252
|
-
|
|
10770
|
+
try {
|
|
10771
|
+
await this.advanceCursor(seq);
|
|
10772
|
+
if (this.stalledAtSeq === seq) this.stalledAtSeq = void 0;
|
|
10773
|
+
} catch (err) {
|
|
10774
|
+
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10775
|
+
console.error(
|
|
10776
|
+
`[byok/client] unknown task seq=${seq} could not durably persist its cursor; cursor left unadvanced for redelivery:`,
|
|
10777
|
+
err
|
|
10778
|
+
);
|
|
10779
|
+
}
|
|
10253
10780
|
}
|
|
10254
10781
|
});
|
|
10255
10782
|
}
|
|
@@ -10295,94 +10822,52 @@ var ConnectionManager = class {
|
|
|
10295
10822
|
if (this.stalledAtSeq === void 0) this.stalledAtSeq = seq;
|
|
10296
10823
|
});
|
|
10297
10824
|
}
|
|
10298
|
-
advanceCursor(seq) {
|
|
10825
|
+
async advanceCursor(seq) {
|
|
10299
10826
|
if (this.cursor !== void 0 && seq <= this.cursor) return;
|
|
10827
|
+
const save = this.pendingCursorSave.catch(() => void 0).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq));
|
|
10828
|
+
this.pendingCursorSave = save;
|
|
10829
|
+
await save;
|
|
10300
10830
|
this.cursor = seq;
|
|
10301
|
-
this.pendingCursorSave = this.pendingCursorSave.catch(() => {
|
|
10302
|
-
}).then(() => this.opts.cursorStore.save(this.opts.serverUrl, this.opts.deviceId, seq)).catch(() => {
|
|
10303
|
-
});
|
|
10304
|
-
}
|
|
10305
|
-
/**
|
|
10306
|
-
* Fires the moment a connection attempt reaches `conn.ack` — independent
|
|
10307
|
-
* of whether/when it later closes. This is the ONLY place that can
|
|
10308
|
-
* reliably detect "WS is back up" while long-polling: a healthy
|
|
10309
|
-
* connection stays open indefinitely, so it never reaches `onWsOutcome`
|
|
10310
|
-
* (which is close-only) at all.
|
|
10311
|
-
*/
|
|
10312
|
-
onAcked(capabilities) {
|
|
10313
|
-
this.serverCapabilities = capabilities;
|
|
10314
|
-
this.consecutiveFailures = 0;
|
|
10315
|
-
this.notifySettled();
|
|
10316
|
-
this.opts.onOperationalOutcome?.("success", "reconnect");
|
|
10317
|
-
if (this.mode === "long-poll") this.exitLongPoll();
|
|
10318
|
-
void this.drainOutbox();
|
|
10319
10831
|
}
|
|
10320
|
-
|
|
10321
|
-
if (this.
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
});
|
|
10327
|
-
}
|
|
10328
|
-
if (acked) return;
|
|
10329
|
-
this.opts.onOperationalOutcome?.("failure", "reconnect");
|
|
10330
|
-
this.consecutiveFailures += 1;
|
|
10331
|
-
if (this.mode === "ws" && this.consecutiveFailures >= (this.opts.wsFailureThreshold ?? 3)) {
|
|
10332
|
-
this.enterLongPoll();
|
|
10333
|
-
}
|
|
10832
|
+
quarantineRejectedOutbound(envelope, reason) {
|
|
10833
|
+
if (this.rejectedOutboundEnvelopes.length === MAX_REJECTED_OUTBOX_ENTRIES) this.rejectedOutboundEnvelopes.shift();
|
|
10834
|
+
this.rejectedOutboundEnvelopes.push({ envelope, reason });
|
|
10835
|
+
console.error(
|
|
10836
|
+
`[byok/client] outbound envelope ${envelope.id} was permanently rejected (${reason}) and moved to the terminal quarantine.`
|
|
10837
|
+
);
|
|
10334
10838
|
}
|
|
10335
10839
|
notifySettled(err) {
|
|
10336
10840
|
for (const waiter of this.settledWaiters.splice(0)) waiter(err);
|
|
10337
10841
|
}
|
|
10338
|
-
|
|
10339
|
-
this.
|
|
10340
|
-
this.
|
|
10341
|
-
this.
|
|
10342
|
-
this.opts.onStateChange?.("degraded");
|
|
10343
|
-
this.longPoll.start();
|
|
10344
|
-
this.outbox.unshift(createConnectionHelloEnvelope({
|
|
10345
|
-
deviceId: this.opts.deviceId,
|
|
10346
|
-
productId: this.opts.productId,
|
|
10347
|
-
capabilities: this.opts.capabilities,
|
|
10348
|
-
clientVersion: this.opts.clientVersion,
|
|
10349
|
-
runtimes: this.opts.runtimes,
|
|
10350
|
-
getConfiguredToolsets: this.opts.getConfiguredToolsets,
|
|
10351
|
-
getCursor: () => this.cursor
|
|
10352
|
-
}));
|
|
10842
|
+
noteConnected() {
|
|
10843
|
+
if (this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10844
|
+
this.connected = true;
|
|
10845
|
+
this.opts.onStateChange?.("open");
|
|
10353
10846
|
this.notifySettled();
|
|
10354
|
-
void this.drainOutbox();
|
|
10355
|
-
this.scheduleWsProbe();
|
|
10356
10847
|
}
|
|
10357
|
-
|
|
10358
|
-
if (this.
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
}
|
|
10362
|
-
this.longPoll.stop();
|
|
10363
|
-
this.mode = "ws";
|
|
10364
|
-
this.consecutiveFailures = 0;
|
|
10365
|
-
this.ws.resumeAutoReconnect();
|
|
10366
|
-
void this.drainOutbox();
|
|
10848
|
+
noteDisconnected() {
|
|
10849
|
+
if (!this.connected || this.stopped || this.revoked || this.terminalError) return;
|
|
10850
|
+
this.connected = false;
|
|
10851
|
+
this.opts.onStateChange?.("connecting");
|
|
10367
10852
|
}
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
this.
|
|
10853
|
+
enterReplayCursorTooOld(error) {
|
|
10854
|
+
if (this.terminalError) return;
|
|
10855
|
+
this.terminalError = error;
|
|
10856
|
+
this.stopped = true;
|
|
10857
|
+
this.serverCapabilities = [];
|
|
10858
|
+
this.longPoll.stop();
|
|
10859
|
+
this.connected = false;
|
|
10860
|
+
this.cancelPendingDrainRetry?.();
|
|
10861
|
+
this.notifySettled(error);
|
|
10862
|
+
this.opts.onStateChange?.("closed");
|
|
10863
|
+
this.opts.onTerminalError?.(error);
|
|
10379
10864
|
}
|
|
10380
10865
|
enterRevoked() {
|
|
10381
10866
|
if (this.revoked) return;
|
|
10382
10867
|
this.revoked = true;
|
|
10383
|
-
|
|
10868
|
+
this.serverCapabilities = [];
|
|
10384
10869
|
this.longPoll.stop();
|
|
10385
|
-
this.
|
|
10870
|
+
this.connected = false;
|
|
10386
10871
|
this.cancelPendingDrainRetry?.();
|
|
10387
10872
|
this.opts.onStateChange?.("revoked");
|
|
10388
10873
|
this.notifySettled(new DeviceRevokedError());
|
|
@@ -13083,21 +13568,28 @@ async function recordAuditWarning(context, kind, values) {
|
|
|
13083
13568
|
}
|
|
13084
13569
|
}
|
|
13085
13570
|
var agentMemoryHomeQueues = /* @__PURE__ */ new Map();
|
|
13086
|
-
|
|
13087
|
-
|
|
13571
|
+
var agentMemoryProjectionTransactionQueues = /* @__PURE__ */ new Map();
|
|
13572
|
+
async function exclusiveAgentMemoryHomeQueue(queues2, home, fn) {
|
|
13573
|
+
const previous = queues2.get(home) ?? Promise.resolve();
|
|
13088
13574
|
let release;
|
|
13089
13575
|
const next = new Promise((resolve) => {
|
|
13090
13576
|
release = resolve;
|
|
13091
13577
|
});
|
|
13092
|
-
|
|
13578
|
+
queues2.set(home, next);
|
|
13093
13579
|
await previous;
|
|
13094
13580
|
try {
|
|
13095
13581
|
return await fn();
|
|
13096
13582
|
} finally {
|
|
13097
13583
|
release();
|
|
13098
|
-
if (
|
|
13584
|
+
if (queues2.get(home) === next) queues2.delete(home);
|
|
13099
13585
|
}
|
|
13100
13586
|
}
|
|
13587
|
+
async function exclusiveAgentMemoryHome(home, fn) {
|
|
13588
|
+
return exclusiveAgentMemoryHomeQueue(agentMemoryHomeQueues, home, fn);
|
|
13589
|
+
}
|
|
13590
|
+
async function exclusiveAgentMemoryProjectionTransaction(home, fn) {
|
|
13591
|
+
return exclusiveAgentMemoryHomeQueue(agentMemoryProjectionTransactionQueues, home, fn);
|
|
13592
|
+
}
|
|
13101
13593
|
var AgentMemoryService = class {
|
|
13102
13594
|
constructor(input) {
|
|
13103
13595
|
this.input = input;
|
|
@@ -13401,13 +13893,16 @@ var AgentMemoryRedactedOutbox = class _AgentMemoryRedactedOutbox {
|
|
|
13401
13893
|
async function snapshotAndProjectAgentMemory(input, projection) {
|
|
13402
13894
|
if (projection?.capability !== AGENT_MEMORY_PROJECTION_CAPABILITY || !projection.grant || !projection.redactor || !projection.port) return;
|
|
13403
13895
|
const context = taskContext(input);
|
|
13404
|
-
const
|
|
13405
|
-
|
|
13406
|
-
|
|
13407
|
-
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
13896
|
+
const { grant, redactor, port } = projection;
|
|
13897
|
+
await exclusiveAgentMemoryProjectionTransaction(context.canonicalHome, async () => {
|
|
13898
|
+
const outbox = await AgentMemoryRedactedOutbox.open(context, grant);
|
|
13899
|
+
const initialReplay = await outbox.replay(port);
|
|
13900
|
+
if (initialReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(initialReplay);
|
|
13901
|
+
const snapshot = await captureAgentMemorySnapshot(context);
|
|
13902
|
+
await outbox.append(redactedBytes(snapshot, await redactor.redact(snapshot)));
|
|
13903
|
+
const trailingReplay = await outbox.replay(port);
|
|
13904
|
+
if (trailingReplay.status === "pending") throw new AgentMemoryProjectionReplayPendingError(trailingReplay);
|
|
13905
|
+
});
|
|
13411
13906
|
}
|
|
13412
13907
|
var AGENT_MEMORY_FILESYSTEM_HELPER_PROTOCOL = 2;
|
|
13413
13908
|
var AGENT_MEMORY_FILESYSTEM_HELPER_VERSION = "2";
|
|
@@ -13706,6 +14201,7 @@ function resultDocumentRejectionDetail(check) {
|
|
|
13706
14201
|
}
|
|
13707
14202
|
}
|
|
13708
14203
|
var DEFAULT_MAX_TASK_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
14204
|
+
var DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME = 1;
|
|
13709
14205
|
function isKnownRuntimeId(id) {
|
|
13710
14206
|
return RuntimeIdSchema.safeParse(id).success;
|
|
13711
14207
|
}
|
|
@@ -13815,6 +14311,7 @@ var TaskRunner = class {
|
|
|
13815
14311
|
deps;
|
|
13816
14312
|
tasks = /* @__PURE__ */ new Map();
|
|
13817
14313
|
pendingMessageTasks = /* @__PURE__ */ new Map();
|
|
14314
|
+
messageOutboxesByHome = /* @__PURE__ */ new Map();
|
|
13818
14315
|
messageContextByToken = /* @__PURE__ */ new Map();
|
|
13819
14316
|
messageContextByTask = /* @__PURE__ */ new Map();
|
|
13820
14317
|
memoryContextByToken = /* @__PURE__ */ new Map();
|
|
@@ -13875,6 +14372,8 @@ var TaskRunner = class {
|
|
|
13875
14372
|
* costs nothing.
|
|
13876
14373
|
*/
|
|
13877
14374
|
inFlightOffers = /* @__PURE__ */ new Set();
|
|
14375
|
+
/** Blob I/O before an offer becomes an active task still belongs to that offer's cancellation authority. */
|
|
14376
|
+
inFlightBlobAborts = /* @__PURE__ */ new Map();
|
|
13878
14377
|
/**
|
|
13879
14378
|
* Finding P2 (Fix 2c): taskIds that have reached a terminal outcome
|
|
13880
14379
|
* (Complete/Failed/Cancelled) this session — populated in `finish()`.
|
|
@@ -13949,6 +14448,10 @@ var TaskRunner = class {
|
|
|
13949
14448
|
get maxTaskOutputBytes() {
|
|
13950
14449
|
return this.deps.maxTaskOutputBytes ?? DEFAULT_MAX_TASK_OUTPUT_BYTES;
|
|
13951
14450
|
}
|
|
14451
|
+
/** WP0: effective per-canonical-Agent-home Attempt cap — see {@link DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME}. */
|
|
14452
|
+
get maxConcurrentMutableSessionsPerAgentHome() {
|
|
14453
|
+
return this.deps.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
14454
|
+
}
|
|
13952
14455
|
/**
|
|
13953
14456
|
* M4 Phase 4 (part B.3, observability): per-active-task queue watermarks
|
|
13954
14457
|
* for the control socket's `status` result — see
|
|
@@ -14001,6 +14504,7 @@ var TaskRunner = class {
|
|
|
14001
14504
|
async recoverAgentMessageOutboxes(agentsRoot) {
|
|
14002
14505
|
if (this.deps.tenantId === void 0) throw new Error("Agent message recovery requires authenticated tenant enrollment");
|
|
14003
14506
|
for (const outbox of await AgentMessageOutbox.recover(agentsRoot, this.deps.tenantId)) {
|
|
14507
|
+
this.messageOutboxesByHome.set(outbox.homeDir, Promise.resolve(outbox));
|
|
14004
14508
|
for (const record3 of outbox.retryableRecords()) {
|
|
14005
14509
|
if (record3.sessionRef === void 0) continue;
|
|
14006
14510
|
const existing = this.recoveredMessageOutboxes.get(record3.taskId);
|
|
@@ -14009,6 +14513,16 @@ var TaskRunner = class {
|
|
|
14009
14513
|
}
|
|
14010
14514
|
}
|
|
14011
14515
|
}
|
|
14516
|
+
agentMessageOutbox(homeDir) {
|
|
14517
|
+
const existing = this.messageOutboxesByHome.get(homeDir);
|
|
14518
|
+
if (existing !== void 0) return existing;
|
|
14519
|
+
const opened = AgentMessageOutbox.open(homeDir);
|
|
14520
|
+
this.messageOutboxesByHome.set(homeDir, opened);
|
|
14521
|
+
void opened.catch(() => {
|
|
14522
|
+
if (this.messageOutboxesByHome.get(homeDir) === opened) this.messageOutboxesByHome.delete(homeDir);
|
|
14523
|
+
});
|
|
14524
|
+
return opened;
|
|
14525
|
+
}
|
|
14012
14526
|
/** Retry stable recovered records after a transport handshake/re-handshake. */
|
|
14013
14527
|
retryRecoveredAgentMessages() {
|
|
14014
14528
|
for (const [taskId, outbox] of this.recoveredMessageOutboxes) {
|
|
@@ -14164,6 +14678,7 @@ var TaskRunner = class {
|
|
|
14164
14678
|
if (active.finalizationStarted) return this.finish(active.taskId);
|
|
14165
14679
|
if (!this.reserveSemanticTerminal(active)) return active.semanticTerminalSettled ?? false;
|
|
14166
14680
|
active.beingTornDown = true;
|
|
14681
|
+
active.blobAbort.abort();
|
|
14167
14682
|
await this.observeGit(active, "salvage");
|
|
14168
14683
|
const timeoutMs = this.deps.shutdownInterruptTimeoutMs ?? DEFAULT_SHUTDOWN_INTERRUPT_TIMEOUT_MS;
|
|
14169
14684
|
await raceSettleFirst(() => active.session.interrupt(), timeoutMs);
|
|
@@ -14310,6 +14825,8 @@ var TaskRunner = class {
|
|
|
14310
14825
|
return;
|
|
14311
14826
|
}
|
|
14312
14827
|
this.inFlightOffers.add(taskId);
|
|
14828
|
+
const blobAbort = new AbortController();
|
|
14829
|
+
this.inFlightBlobAborts.set(taskId, blobAbort);
|
|
14313
14830
|
let agentBinding;
|
|
14314
14831
|
let agentLeaseTransferred = false;
|
|
14315
14832
|
try {
|
|
@@ -14324,6 +14841,24 @@ var TaskRunner = class {
|
|
|
14324
14841
|
decline("strict Agent-only daemon refuses legacy task offers", false);
|
|
14325
14842
|
return;
|
|
14326
14843
|
}
|
|
14844
|
+
if (agentRef !== void 0) {
|
|
14845
|
+
const limit = this.maxConcurrentMutableSessionsPerAgentHome;
|
|
14846
|
+
let canonicalHome;
|
|
14847
|
+
try {
|
|
14848
|
+
canonicalHome = await this.deps.agentHome.layout.canonicalHomePath(agentRef);
|
|
14849
|
+
} catch (error) {
|
|
14850
|
+
decline(
|
|
14851
|
+
`Agent home admission failed: ${errorMessage6(error)}`,
|
|
14852
|
+
!(error instanceof AgentHomeResolutionError)
|
|
14853
|
+
);
|
|
14854
|
+
return;
|
|
14855
|
+
}
|
|
14856
|
+
const active2 = this.deps.agentHome.executionLeaseManager.activeAttemptCount(canonicalHome);
|
|
14857
|
+
if (active2 >= limit) {
|
|
14858
|
+
decline(`agent home busy: ${active2} active attempt(s)`, true);
|
|
14859
|
+
return;
|
|
14860
|
+
}
|
|
14861
|
+
}
|
|
14327
14862
|
const guarded = this.deps.admissionGuard?.({ taskId, payload });
|
|
14328
14863
|
if (guarded !== void 0 && !guarded.admit) {
|
|
14329
14864
|
decline(guarded.reason, guarded.retryable);
|
|
@@ -14468,7 +15003,7 @@ var TaskRunner = class {
|
|
|
14468
15003
|
}
|
|
14469
15004
|
if (agentRef !== void 0) {
|
|
14470
15005
|
try {
|
|
14471
|
-
agentBinding = await this.deps.agentHome.
|
|
15006
|
+
agentBinding = await this.deps.agentHome.acquireExecution(agentRef, { taskId, sessionRef });
|
|
14472
15007
|
} catch (error) {
|
|
14473
15008
|
decline(
|
|
14474
15009
|
`Agent home admission failed: ${errorMessage6(error)}`,
|
|
@@ -14503,7 +15038,7 @@ var TaskRunner = class {
|
|
|
14503
15038
|
}
|
|
14504
15039
|
}
|
|
14505
15040
|
try {
|
|
14506
|
-
await this.deps.agentHome.
|
|
15041
|
+
await this.deps.agentHome.initializeExecution(agentBinding);
|
|
14507
15042
|
} catch (error) {
|
|
14508
15043
|
await agentBinding.lease.release().catch(() => {
|
|
14509
15044
|
});
|
|
@@ -14513,7 +15048,7 @@ var TaskRunner = class {
|
|
|
14513
15048
|
}
|
|
14514
15049
|
if (messageRequirement !== void 0) {
|
|
14515
15050
|
try {
|
|
14516
|
-
const outbox = await
|
|
15051
|
+
const outbox = await this.agentMessageOutbox(agentBinding.resolution.canonicalHome);
|
|
14517
15052
|
this.pendingMessageTasks.set(taskId, {
|
|
14518
15053
|
taskId,
|
|
14519
15054
|
agentRef: agentBinding.resolution.agentRef,
|
|
@@ -14632,7 +15167,7 @@ var TaskRunner = class {
|
|
|
14632
15167
|
);
|
|
14633
15168
|
let resolvedInstruction;
|
|
14634
15169
|
try {
|
|
14635
|
-
resolvedInstruction = await this.resolveInstruction(payload.instruction);
|
|
15170
|
+
resolvedInstruction = await this.resolveInstruction(payload.instruction, blobAbort.signal);
|
|
14636
15171
|
if (plainWorkspaceNeedsResolve) workspaceDir = await this.resolveWorkspaceDir(taskId, known?.workspaceDir);
|
|
14637
15172
|
} catch (err) {
|
|
14638
15173
|
gitLease?.release();
|
|
@@ -14745,9 +15280,29 @@ var TaskRunner = class {
|
|
|
14745
15280
|
}
|
|
14746
15281
|
return;
|
|
14747
15282
|
}
|
|
15283
|
+
if (agentBinding !== void 0) {
|
|
15284
|
+
try {
|
|
15285
|
+
await agentBinding.lease.bindSession(session.sessionRef);
|
|
15286
|
+
} catch (error) {
|
|
15287
|
+
await session.close().catch(() => {
|
|
15288
|
+
});
|
|
15289
|
+
await this.failClaimedAgent(
|
|
15290
|
+
taskId,
|
|
15291
|
+
`Agent session execution lease could not bind the runtime session: ${errorMessage6(error)}`,
|
|
15292
|
+
false,
|
|
15293
|
+
{
|
|
15294
|
+
binding: agentBinding,
|
|
15295
|
+
runtimeId: pick.descriptor.id,
|
|
15296
|
+
sessionRef: session.sessionRef
|
|
15297
|
+
}
|
|
15298
|
+
);
|
|
15299
|
+
return;
|
|
15300
|
+
}
|
|
15301
|
+
}
|
|
14748
15302
|
let active;
|
|
14749
15303
|
active = {
|
|
14750
15304
|
taskId,
|
|
15305
|
+
blobAbort,
|
|
14751
15306
|
egressEnabled: "egressPolicy" in payload,
|
|
14752
15307
|
adapter: pick.adapter,
|
|
14753
15308
|
session,
|
|
@@ -14788,15 +15343,16 @@ var TaskRunner = class {
|
|
|
14788
15343
|
...terminalProjection === void 0 ? {} : { terminalProjection }
|
|
14789
15344
|
};
|
|
14790
15345
|
if (agentBinding !== void 0) {
|
|
15346
|
+
const binding = agentBinding;
|
|
14791
15347
|
try {
|
|
14792
|
-
await this.deps.agentSessionHandoffs.record({
|
|
14793
|
-
agentRef:
|
|
15348
|
+
await this.deps.agentHome.mutateExecution(binding, () => this.deps.agentSessionHandoffs.record({
|
|
15349
|
+
agentRef: binding.resolution.agentRef,
|
|
14794
15350
|
taskId,
|
|
14795
15351
|
sessionRef: session.sessionRef,
|
|
14796
15352
|
runtimeId: pick.descriptor.id,
|
|
14797
15353
|
cwd: workspaceDir,
|
|
14798
|
-
leaseId:
|
|
14799
|
-
});
|
|
15354
|
+
leaseId: binding.lease.leaseId
|
|
15355
|
+
}));
|
|
14800
15356
|
} catch (error) {
|
|
14801
15357
|
await session.close().catch(() => {
|
|
14802
15358
|
});
|
|
@@ -14838,6 +15394,7 @@ var TaskRunner = class {
|
|
|
14838
15394
|
}
|
|
14839
15395
|
this.deps.send(createEnvelope("task.started", {}, { taskId }));
|
|
14840
15396
|
agentLeaseTransferred = agentBinding !== void 0;
|
|
15397
|
+
this.inFlightBlobAborts.delete(taskId);
|
|
14841
15398
|
this.tasks.set(taskId, active);
|
|
14842
15399
|
this.pendingMessageTasks.delete(taskId);
|
|
14843
15400
|
if (active.messageOutbox !== void 0 && activatedMessageRecord !== void 0) {
|
|
@@ -14869,6 +15426,7 @@ var TaskRunner = class {
|
|
|
14869
15426
|
await agentBinding.lease.release().catch(() => {
|
|
14870
15427
|
});
|
|
14871
15428
|
}
|
|
15429
|
+
this.inFlightBlobAborts.delete(taskId);
|
|
14872
15430
|
this.inFlightOffers.delete(taskId);
|
|
14873
15431
|
}
|
|
14874
15432
|
}
|
|
@@ -15020,9 +15578,9 @@ var TaskRunner = class {
|
|
|
15020
15578
|
void this.closeAgentMemoryFilesystem(taskId);
|
|
15021
15579
|
}
|
|
15022
15580
|
/** Protocol §7: an instruction too large to inline arrives as a `blobRef` — resolve it via the blob client rather than failing closed. */
|
|
15023
|
-
async resolveInstruction(instruction) {
|
|
15581
|
+
async resolveInstruction(instruction, signal) {
|
|
15024
15582
|
if (typeof instruction === "string") return instruction;
|
|
15025
|
-
return this.deps.blobClient.resolveInstruction(instruction.blobRef);
|
|
15583
|
+
return this.deps.blobClient.resolveInstruction(instruction.blobRef, { signal });
|
|
15026
15584
|
}
|
|
15027
15585
|
/** Resolve every requested logical id locally and reject missing/colliding server authority before claim. */
|
|
15028
15586
|
resolveMcpServers(requiredToolsets) {
|
|
@@ -15254,7 +15812,9 @@ var TaskRunner = class {
|
|
|
15254
15812
|
}
|
|
15255
15813
|
}
|
|
15256
15814
|
try {
|
|
15257
|
-
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType
|
|
15815
|
+
const blobRef = await this.deps.blobClient.uploadArtifact(bytes, contentType, {
|
|
15816
|
+
signal: active.blobAbort.signal
|
|
15817
|
+
});
|
|
15258
15818
|
this.deps.send(createEnvelope("task.artifact", { name, contentType, blobRef }, { taskId: active.taskId }));
|
|
15259
15819
|
} catch (err) {
|
|
15260
15820
|
this.reportArtifactError(active, name, `failed to upload artifact "${name}": ${errorMessage6(err)}`);
|
|
@@ -15269,6 +15829,7 @@ var TaskRunner = class {
|
|
|
15269
15829
|
const active = this.tasks.get(taskId);
|
|
15270
15830
|
if (!active) {
|
|
15271
15831
|
this.setPendingCancelled(taskId, reason);
|
|
15832
|
+
this.inFlightBlobAborts.get(taskId)?.abort();
|
|
15272
15833
|
return;
|
|
15273
15834
|
}
|
|
15274
15835
|
if (active.finalizationStarted) {
|
|
@@ -15279,6 +15840,7 @@ var TaskRunner = class {
|
|
|
15279
15840
|
await active.semanticTerminalSettled;
|
|
15280
15841
|
return;
|
|
15281
15842
|
}
|
|
15843
|
+
active.blobAbort.abort();
|
|
15282
15844
|
try {
|
|
15283
15845
|
await active.session.interrupt();
|
|
15284
15846
|
} catch {
|
|
@@ -15778,7 +16340,7 @@ var TaskRunner = class {
|
|
|
15778
16340
|
async failClaimedAgent(taskId, reason, retryable, context) {
|
|
15779
16341
|
const agentRef = context.binding.resolution.agentRef;
|
|
15780
16342
|
const result = await this.retryAgentTerminalEvidence(
|
|
15781
|
-
() => this.deps.agentSessionHandoffs.recordTaskTerminal({
|
|
16343
|
+
() => this.deps.agentHome.mutateExecution(context.binding, () => this.deps.agentSessionHandoffs.recordTaskTerminal({
|
|
15782
16344
|
agentRef,
|
|
15783
16345
|
taskId,
|
|
15784
16346
|
runtimeId: context.runtimeId,
|
|
@@ -15786,7 +16348,7 @@ var TaskRunner = class {
|
|
|
15786
16348
|
leaseId: context.binding.lease.leaseId,
|
|
15787
16349
|
...context.sessionRef === void 0 ? {} : { sessionRef: context.sessionRef },
|
|
15788
16350
|
terminalReason: reason
|
|
15789
|
-
})
|
|
16351
|
+
}))
|
|
15790
16352
|
);
|
|
15791
16353
|
if (!result.ok) {
|
|
15792
16354
|
this.reportAgentTerminalEvidenceFailure({
|
|
@@ -16775,6 +17337,8 @@ var AgentEgressController = class {
|
|
|
16775
17337
|
options;
|
|
16776
17338
|
latest = new AgentLatestValueState();
|
|
16777
17339
|
spools = /* @__PURE__ */ new Map();
|
|
17340
|
+
spoolOpens = /* @__PURE__ */ new Map();
|
|
17341
|
+
reliableAppendTail = Promise.resolve();
|
|
16778
17342
|
latestStatus = emptyLane();
|
|
16779
17343
|
reliableStatus = emptyLane();
|
|
16780
17344
|
drops = [];
|
|
@@ -16829,7 +17393,8 @@ var AgentEgressController = class {
|
|
|
16829
17393
|
return latest === void 0 ? [] : Object.freeze([latest]);
|
|
16830
17394
|
}
|
|
16831
17395
|
async appendReliable(input) {
|
|
16832
|
-
|
|
17396
|
+
const tenantId = this.options.tenantId;
|
|
17397
|
+
if (!this.active || tenantId === void 0) {
|
|
16833
17398
|
this.noteDrop("reliable", "policy_denied", input.agentRef);
|
|
16834
17399
|
return { ok: false, reason: "policy_denied" };
|
|
16835
17400
|
}
|
|
@@ -16844,17 +17409,21 @@ var AgentEgressController = class {
|
|
|
16844
17409
|
return { ok: false, reason: "sanitizer_rejected" };
|
|
16845
17410
|
}
|
|
16846
17411
|
try {
|
|
16847
|
-
const
|
|
16848
|
-
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
16853
|
-
|
|
16854
|
-
|
|
16855
|
-
|
|
16856
|
-
|
|
16857
|
-
|
|
17412
|
+
const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
|
|
17413
|
+
return await this.withAppendTail(async () => {
|
|
17414
|
+
const spool = await spoolOpen;
|
|
17415
|
+
this.bindSpool(input.agentRef, spool, spoolOpen);
|
|
17416
|
+
const record3 = await spool.append({
|
|
17417
|
+
agentRef: input.agentRef,
|
|
17418
|
+
tenantId,
|
|
17419
|
+
policyRevision: this.options.policy.policyRevision,
|
|
17420
|
+
payload,
|
|
17421
|
+
sessionRef: input.sessionRef,
|
|
17422
|
+
...input.taskId === void 0 ? {} : { taskId: input.taskId },
|
|
17423
|
+
...input.eventId === void 0 ? {} : { eventId: input.eventId }
|
|
17424
|
+
}, this.options.policy, this.tenantPendingBytes());
|
|
17425
|
+
return { ok: true, record: record3 };
|
|
17426
|
+
});
|
|
16858
17427
|
} catch (error) {
|
|
16859
17428
|
const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
|
|
16860
17429
|
this.noteDrop("reliable", reason, input.agentRef);
|
|
@@ -16867,21 +17436,26 @@ var AgentEgressController = class {
|
|
|
16867
17436
|
* with `wireType: agent.content.receipt` before any transport attempt.
|
|
16868
17437
|
*/
|
|
16869
17438
|
async appendContentReceipt(input) {
|
|
16870
|
-
|
|
17439
|
+
const tenantId = this.options.tenantId;
|
|
17440
|
+
if (!this.active || tenantId === void 0) {
|
|
16871
17441
|
this.noteDrop("reliable", "policy_denied", input.agentRef);
|
|
16872
17442
|
return { ok: false, reason: "policy_denied" };
|
|
16873
17443
|
}
|
|
16874
17444
|
try {
|
|
16875
|
-
const
|
|
16876
|
-
|
|
16877
|
-
|
|
16878
|
-
|
|
16879
|
-
|
|
16880
|
-
|
|
16881
|
-
|
|
16882
|
-
|
|
16883
|
-
|
|
16884
|
-
|
|
17445
|
+
const spoolOpen = this.spoolFor(input.homeDir, input.agentRef);
|
|
17446
|
+
return await this.withAppendTail(async () => {
|
|
17447
|
+
const spool = await spoolOpen;
|
|
17448
|
+
this.bindSpool(input.agentRef, spool, spoolOpen);
|
|
17449
|
+
const record3 = await spool.appendContentReceipt({
|
|
17450
|
+
agentRef: input.agentRef,
|
|
17451
|
+
tenantId,
|
|
17452
|
+
policyRevision: this.options.policy.policyRevision,
|
|
17453
|
+
sessionRef: input.payload.sessionRef,
|
|
17454
|
+
payload: input.payload,
|
|
17455
|
+
...input.taskId === void 0 ? {} : { taskId: input.taskId }
|
|
17456
|
+
}, this.options.policy, this.tenantPendingBytes());
|
|
17457
|
+
return { ok: true, record: record3 };
|
|
17458
|
+
});
|
|
16885
17459
|
} catch (error) {
|
|
16886
17460
|
const reason = error instanceof AgentReliableQuotaError ? error.reason : "backpressure";
|
|
16887
17461
|
this.noteDrop("reliable", reason, input.agentRef);
|
|
@@ -16956,20 +17530,59 @@ var AgentEgressController = class {
|
|
|
16956
17530
|
}
|
|
16957
17531
|
return this.reliableRecords();
|
|
16958
17532
|
}
|
|
16959
|
-
|
|
17533
|
+
spoolFor(homeDir, agentRef) {
|
|
16960
17534
|
const key = agentKey(agentRef);
|
|
16961
17535
|
const existing = this.spools.get(key);
|
|
16962
|
-
if (existing)
|
|
16963
|
-
|
|
16964
|
-
|
|
16965
|
-
|
|
17536
|
+
if (existing) {
|
|
17537
|
+
if (existing.homeDir !== homeDir) throw new Error("Agent reliable spool is already bound to a different home");
|
|
17538
|
+
return Promise.resolve(existing);
|
|
17539
|
+
}
|
|
17540
|
+
const inFlight = this.spoolOpens.get(key);
|
|
17541
|
+
if (inFlight !== void 0) {
|
|
17542
|
+
if (inFlight.homeDir !== homeDir) throw new Error("Agent reliable spool opening is already bound to a different home");
|
|
17543
|
+
return inFlight.promise;
|
|
17544
|
+
}
|
|
17545
|
+
const opened = (async () => {
|
|
17546
|
+
const spool = await AgentReliableSpool.open(homeDir);
|
|
17547
|
+
if (spool.records().some((record3) => !sameAgent(record3.agentRef, agentRef))) {
|
|
17548
|
+
throw new Error("Agent-local reliable spool has a different AgentRef");
|
|
17549
|
+
}
|
|
17550
|
+
return spool;
|
|
17551
|
+
})();
|
|
17552
|
+
const slot = { homeDir, promise: opened };
|
|
17553
|
+
this.spoolOpens.set(key, slot);
|
|
17554
|
+
void opened.catch(() => {
|
|
17555
|
+
if (this.spoolOpens.get(key) === slot) this.spoolOpens.delete(key);
|
|
17556
|
+
});
|
|
17557
|
+
return opened;
|
|
17558
|
+
}
|
|
17559
|
+
bindSpool(agentRef, spool, spoolOpen) {
|
|
17560
|
+
const key = agentKey(agentRef);
|
|
17561
|
+
const existing = this.spools.get(key);
|
|
17562
|
+
try {
|
|
17563
|
+
if (existing && existing !== spool) throw new Error("multiple Agent egress spools claim the same AgentRef");
|
|
17564
|
+
this.spools.set(key, spool);
|
|
17565
|
+
} finally {
|
|
17566
|
+
const slot = this.spoolOpens.get(key);
|
|
17567
|
+
if (slot?.promise === spoolOpen) this.spoolOpens.delete(key);
|
|
16966
17568
|
}
|
|
16967
|
-
this.spools.set(key, spool);
|
|
16968
|
-
return spool;
|
|
16969
17569
|
}
|
|
16970
17570
|
tenantPendingBytes() {
|
|
16971
17571
|
return this.reliableRecords().reduce((total, record3) => total + record3.byteCount, 0);
|
|
16972
17572
|
}
|
|
17573
|
+
async withAppendTail(operation) {
|
|
17574
|
+
const previous = this.reliableAppendTail;
|
|
17575
|
+
let release;
|
|
17576
|
+
this.reliableAppendTail = new Promise((resolve) => {
|
|
17577
|
+
release = resolve;
|
|
17578
|
+
});
|
|
17579
|
+
try {
|
|
17580
|
+
await previous;
|
|
17581
|
+
return await operation();
|
|
17582
|
+
} finally {
|
|
17583
|
+
release();
|
|
17584
|
+
}
|
|
17585
|
+
}
|
|
16973
17586
|
noteDrop(lane, reason, agentRef, countDrop = true) {
|
|
16974
17587
|
const laneStatus = lane === "latest-value" ? this.latestStatus : this.reliableStatus;
|
|
16975
17588
|
if (countDrop) laneStatus.dropped += 1;
|
|
@@ -17957,7 +18570,7 @@ async function detectRuntimes(adapters) {
|
|
|
17957
18570
|
}
|
|
17958
18571
|
return runtimes;
|
|
17959
18572
|
}
|
|
17960
|
-
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies) {
|
|
18573
|
+
function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentOnly = false, agentHomeProjectionConfigured = false, agentEgressConfigured = false, contentReadPolicies, providerProfileBindingConfigured = false) {
|
|
17961
18574
|
const flags = [];
|
|
17962
18575
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.steer)) flags.push("steer");
|
|
17963
18576
|
flags.push("blob-upload");
|
|
@@ -17969,6 +18582,9 @@ function computeCapabilities(adapters, agentHomeConfigured = false, strictAgentO
|
|
|
17969
18582
|
if (selectionAdapters.length > 0 && selectionAdapters.every((adapter) => adapter.descriptor.supportsDispatchSelection === true)) {
|
|
17970
18583
|
flags.push("dispatch-selection");
|
|
17971
18584
|
}
|
|
18585
|
+
if (providerProfileBindingConfigured && adapters.some((adapter) => adapter.descriptor.id === "pi" && adapter.descriptor.supportsDispatchSelection)) {
|
|
18586
|
+
flags.push(PROVIDER_PROFILE_BINDING_CAPABILITY);
|
|
18587
|
+
}
|
|
17972
18588
|
if (adapters.some((adapter) => adapter.descriptor.capabilities.mcpToolsets === true)) {
|
|
17973
18589
|
flags.push("toolset-selection");
|
|
17974
18590
|
}
|
|
@@ -18131,6 +18747,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18131
18747
|
if (config.strictAgentOnly === true && config.agentHome === void 0) {
|
|
18132
18748
|
throw new Error("DaemonConfig.strictAgentOnly requires DaemonConfig.agentHome");
|
|
18133
18749
|
}
|
|
18750
|
+
if (config.maxConcurrentMutableSessionsPerAgentHome !== void 0 && !(Number.isSafeInteger(config.maxConcurrentMutableSessionsPerAgentHome) && config.maxConcurrentMutableSessionsPerAgentHome > 0)) {
|
|
18751
|
+
throw new Error(
|
|
18752
|
+
`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".`
|
|
18753
|
+
);
|
|
18754
|
+
}
|
|
18755
|
+
const agentHomeAttemptLimit = config.maxConcurrentMutableSessionsPerAgentHome ?? DEFAULT_MAX_CONCURRENT_MUTABLE_SESSIONS_PER_AGENT_HOME;
|
|
18134
18756
|
const egressPolicy = resolveAgentEgressPolicy(config.agentEgress?.policy);
|
|
18135
18757
|
const egressBatcherOptions = egressPolicy.activity.mode === "contentful-trajectory" ? {
|
|
18136
18758
|
...config.progressBatch,
|
|
@@ -18241,6 +18863,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18241
18863
|
const observer = new DaemonObserver();
|
|
18242
18864
|
const approvalRegistry = new ApprovalRegistry();
|
|
18243
18865
|
let connection;
|
|
18866
|
+
let blobLifecycleAbort;
|
|
18244
18867
|
let connectionState = "closed";
|
|
18245
18868
|
let daemonStarted = false;
|
|
18246
18869
|
let tenantRebinding = false;
|
|
@@ -18259,6 +18882,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18259
18882
|
function buildAuthManager() {
|
|
18260
18883
|
return new AuthManager({
|
|
18261
18884
|
serverUrl: config.serverUrl,
|
|
18885
|
+
authRequestDeadlineMs: config.authRequestDeadlineMs,
|
|
18262
18886
|
store,
|
|
18263
18887
|
deviceName: config.deviceName,
|
|
18264
18888
|
machineId: config.machineId ?? (() => resolveMachineId({ productId: config.productId })),
|
|
@@ -18276,7 +18900,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18276
18900
|
assertServerUrlAllowed(config.serverUrl, { dangerouslyAllowInsecureRemote: true });
|
|
18277
18901
|
const reason = err instanceof Error ? err.message : String(err);
|
|
18278
18902
|
console.warn(
|
|
18279
|
-
`[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})`
|
|
18903
|
+
`[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})`
|
|
18280
18904
|
);
|
|
18281
18905
|
}
|
|
18282
18906
|
}
|
|
@@ -18436,9 +19060,10 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18436
19060
|
await gitWorkspaceStore?.initialize();
|
|
18437
19061
|
await gitWorkspaceStore?.reconcile();
|
|
18438
19062
|
}
|
|
19063
|
+
blobLifecycleAbort = new AbortController();
|
|
18439
19064
|
const [runtimes, blobClient] = await Promise.all([
|
|
18440
19065
|
detectRuntimes(adapters),
|
|
18441
|
-
Promise.resolve(new BlobClient(config.serverUrl, auth))
|
|
19066
|
+
Promise.resolve(new BlobClient(config.serverUrl, auth, { signal: blobLifecycleAbort.signal }))
|
|
18442
19067
|
]);
|
|
18443
19068
|
observer.noteRuntimesDetected(runtimes);
|
|
18444
19069
|
detectedRuntimeFacts = runtimes;
|
|
@@ -18448,7 +19073,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18448
19073
|
config.strictAgentOnly === true,
|
|
18449
19074
|
agentHomeManager?.supportsTaskFreeProjection() === true,
|
|
18450
19075
|
config.agentEgress !== void 0,
|
|
18451
|
-
agentContentReadPolicies
|
|
19076
|
+
agentContentReadPolicies,
|
|
19077
|
+
config.piByokLauncher !== void 0
|
|
18452
19078
|
);
|
|
18453
19079
|
const agentHomeProjectionCompletion = agentHomeManager?.supportsTaskFreeProjection() === true ? new AgentHomeProjectionCompletionClient({
|
|
18454
19080
|
serverUrl: config.serverUrl,
|
|
@@ -18507,6 +19133,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18507
19133
|
workspaceRoot: config.workspaceRoot,
|
|
18508
19134
|
...agentHomeManager === void 0 ? {} : { agentHome: agentHomeManager },
|
|
18509
19135
|
...config.strictAgentOnly === true ? { strictAgentOnly: true } : {},
|
|
19136
|
+
// WP0: already validated up front — see `DaemonConfig.maxConcurrentMutableSessionsPerAgentHome`.
|
|
19137
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
18510
19138
|
...agentSessionHandoffs === void 0 ? {} : { agentSessionHandoffs },
|
|
18511
19139
|
deviceId: record3.deviceId,
|
|
18512
19140
|
// M5: see `DaemonConfig.runtimeEnvironment`'s own doc comment above.
|
|
@@ -18791,16 +19419,12 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18791
19419
|
return runner?.handleEnvelope(envelope) ?? Promise.resolve();
|
|
18792
19420
|
},
|
|
18793
19421
|
onStateChange: (state) => {
|
|
18794
|
-
const wasSettled = connectionState === "open"
|
|
19422
|
+
const wasSettled = connectionState === "open";
|
|
18795
19423
|
connectionState = state;
|
|
18796
19424
|
observer.noteConnectionState(state);
|
|
18797
|
-
if (!wasSettled &&
|
|
18798
|
-
if (!wasSettled &&
|
|
19425
|
+
if (!wasSettled && state === "open") runner?.retryRecoveredAgentMessages();
|
|
19426
|
+
if (!wasSettled && state === "open") runPresenceDiscovery();
|
|
18799
19427
|
},
|
|
18800
|
-
backoff: overrides.backoff,
|
|
18801
|
-
liveness: overrides.liveness,
|
|
18802
|
-
wsFailureThreshold: overrides.longPoll?.wsFailureThreshold,
|
|
18803
|
-
wsRetryIntervalMs: overrides.longPoll?.wsRetryIntervalMs,
|
|
18804
19428
|
longPollRetryDelayMs: overrides.longPoll?.retryDelayMs,
|
|
18805
19429
|
longPollIdleDelayMs: overrides.longPoll?.idleDelayMs,
|
|
18806
19430
|
fleetJitter,
|
|
@@ -18812,7 +19436,7 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18812
19436
|
}
|
|
18813
19437
|
});
|
|
18814
19438
|
await connection.start();
|
|
18815
|
-
await connection.
|
|
19439
|
+
await connection.waitForConnection();
|
|
18816
19440
|
runner.retryRecoveredAgentMessages();
|
|
18817
19441
|
for (const record4 of agentEgress.retryableReliableRecords(connection.getServerCapabilities())) {
|
|
18818
19442
|
dispatchReliableRecord(record4);
|
|
@@ -18867,6 +19491,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
18867
19491
|
}
|
|
18868
19492
|
async function runShutdownSequence(reason, opts = {}) {
|
|
18869
19493
|
shuttingDown = true;
|
|
19494
|
+
blobLifecycleAbort?.abort();
|
|
19495
|
+
blobLifecycleAbort = void 0;
|
|
18870
19496
|
const errors = [];
|
|
18871
19497
|
let mutationBarrierComplete = hostedStorageInitializationBarrierComplete;
|
|
18872
19498
|
if (!hostedStorageInitializationBarrierComplete) {
|
|
@@ -19069,7 +19695,9 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19069
19695
|
// fine.
|
|
19070
19696
|
...storageStatus === void 0 ? {} : { storage: storageStatus },
|
|
19071
19697
|
operationalHealth: operationalHealth.snapshot(),
|
|
19072
|
-
toolsets: toolsetRegistry.status()
|
|
19698
|
+
toolsets: toolsetRegistry.status(),
|
|
19699
|
+
// WP0: same counts `Daemon.status()` reports, from the same reader.
|
|
19700
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
19073
19701
|
};
|
|
19074
19702
|
}
|
|
19075
19703
|
async function performControlShutdown(reason) {
|
|
@@ -19304,9 +19932,8 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19304
19932
|
const minted = mintDeviceAssertion({
|
|
19305
19933
|
record: record3,
|
|
19306
19934
|
// `toHttpBase` is the one place a configured serverUrl is normalized
|
|
19307
|
-
// (
|
|
19308
|
-
//
|
|
19309
|
-
// spelling of the same deployment produce the same issuer.
|
|
19935
|
+
// (path stripped), so the issuer ignores an operator-provided path
|
|
19936
|
+
// or query.
|
|
19310
19937
|
issuer: new URL(toHttpBase(config.serverUrl)).origin,
|
|
19311
19938
|
productId: config.productId,
|
|
19312
19939
|
audience: parsed.audience,
|
|
@@ -19421,23 +20048,33 @@ function buildDaemonWithAdapters(config, adapters, overrides = {}, assertionProb
|
|
|
19421
20048
|
await binding.lease.release();
|
|
19422
20049
|
}
|
|
19423
20050
|
}
|
|
20051
|
+
function agentHomeExecutionStatus() {
|
|
20052
|
+
const summary = agentHomeManager?.executionLeaseManager.activeAttemptSummary();
|
|
20053
|
+
return {
|
|
20054
|
+
maxConcurrentMutableSessionsPerAgentHome: agentHomeAttemptLimit,
|
|
20055
|
+
activeHomes: summary?.homes ?? 0,
|
|
20056
|
+
activeAttempts: summary?.attempts ?? 0
|
|
20057
|
+
};
|
|
20058
|
+
}
|
|
19424
20059
|
function status() {
|
|
19425
20060
|
return {
|
|
19426
20061
|
localAgentRelease,
|
|
19427
20062
|
paired: auth.deviceId !== void 0,
|
|
19428
20063
|
connected: connectionState === "open",
|
|
19429
|
-
degraded: connection?.isTransportDegraded() ?? false,
|
|
19430
20064
|
revoked: connection?.isRevoked() ?? auth.isRevoked(),
|
|
19431
20065
|
deviceId: auth.deviceId,
|
|
19432
20066
|
activeTaskCount: runner?.activeTaskCount ?? 0,
|
|
19433
20067
|
branding: config.branding,
|
|
19434
20068
|
operationalHealth: operationalHealth.snapshot(),
|
|
19435
20069
|
toolsets: toolsetRegistry.status(),
|
|
19436
|
-
egress: agentEgress.status()
|
|
20070
|
+
egress: agentEgress.status(),
|
|
20071
|
+
agentHomeExecution: agentHomeExecutionStatus()
|
|
19437
20072
|
};
|
|
19438
20073
|
}
|
|
19439
20074
|
function reloadMcpToolsets(mcpToolsets, expectedRevision) {
|
|
19440
|
-
|
|
20075
|
+
const receipt = toolsetRegistry.reload(mcpToolsets, expectedRevision);
|
|
20076
|
+
if (daemonStarted) connection?.refreshHello();
|
|
20077
|
+
return receipt;
|
|
19441
20078
|
}
|
|
19442
20079
|
function reportMcpToolsetObservation(toolsetId, expectedDefinitionRevision, observation) {
|
|
19443
20080
|
toolsetRegistry.report(toolsetId, expectedDefinitionRevision, observation);
|
|
@@ -20687,6 +21324,6 @@ function createServiceLifecycle(def, opts = {}) {
|
|
|
20687
21324
|
}
|
|
20688
21325
|
}
|
|
20689
21326
|
|
|
20690
|
-
export { AGENT_CONTENT_READ_CAPABILITIES, AGENT_CONTENT_READ_CAPABILITY_ARTIFACT, AGENT_CONTENT_READ_CAPABILITY_TRANSCRIPT, AGENT_CONTENT_READ_CAPABILITY_WORKSPACE, AGENT_HOME_PROJECTION_STATE_FILE, AGENT_MEMORY_AUDIT_FILENAME, AGENT_MEMORY_OUTBOX_FILENAME, AgentHomeBusyError, AgentHomeCollisionError, AgentHomeError, AgentHomeLayout, AgentHomeLeaseCorruptError, AgentHomeLeaseManager, AgentHomeManager, AgentHomeResolutionError, AgentMemoryError, AgentMemoryRevisionConflictError, AgentRefValidationError, AgentSessionHandoffCorruptError, AgentSessionHandoffMismatchError, AgentSessionHandoffStore, AgentSessionHandoffStoreError, BYOK_SDK_HELPER_SUBCOMMAND, BlobClient, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStateRelocationBusyError, LocalStateRelocationError, LocalStateRelocationIntegrityError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, LocalTeamWorkspace, LocalTeamWorkspace as LocalTeamWorkspaceService, McpToolsetDefinitionRevisionConflictError, McpToolsetRevisionConflictError, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON, RuntimeDisposalFailure, RuntimeExecutionFailure, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, TeamTmuxViewError, TeamWorkspaceConflictError, TeamWorkspaceCorruptError, TeamWorkspaceError, TeamWorkspaceLeaseError, TeamWorkspaceNotFoundError, TeamWorkspaceQuotaError, TeamWorkspaceReceiptError, TeamWorkspaceValidationError, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createAgentHomeProjection, createAgentHomeProjectionConsumer, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, decodeTeamMemberContext, encodeTeamMemberContext, ensureSecureDir, freezeRuntimeAdapterDescriptor, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isAgentMemorySecureFilesystemAvailable, isGitWorkspaceConfig, isRuntimeDisposalFailure, isRuntimeExecutionFailure, journalHash, listInstalledSkillPacks, localStateRelocation, nodeAgentProgram, openTeamTmuxView, prependGitWorkspaceGuidance, projectRuntimeBoundaryFailure, projectRuntimeExecutionFailure, projectSkillPack, readDeviceEnrollmentStatus, requestDeviceAssertion, resolveLocalAgentReleaseIdentity, resolveLocalStoragePolicy, resolveSdkReservedHelperBin, runSdkReservedHelperCommand, sanitizeServiceName, sealRuntimeOperationManifest, skillPacksRoot, stableAgentHomeOwnerId, validateAgentRef };
|
|
21327
|
+
export { AGENT_CONTENT_READ_CAPABILITIES, AGENT_CONTENT_READ_CAPABILITY_ARTIFACT, AGENT_CONTENT_READ_CAPABILITY_TRANSCRIPT, AGENT_CONTENT_READ_CAPABILITY_WORKSPACE, AGENT_HOME_PROJECTION_STATE_FILE, AGENT_MEMORY_AUDIT_FILENAME, AGENT_MEMORY_OUTBOX_FILENAME, AgentHomeBusyError, AgentHomeCollisionError, AgentHomeError, AgentHomeLayout, AgentHomeLeaseCorruptError, AgentHomeLeaseManager, AgentHomeManager, AgentHomeResolutionError, AgentMemoryError, AgentMemoryRevisionConflictError, AgentRefValidationError, AgentSessionHandoffCorruptError, AgentSessionHandoffMismatchError, AgentSessionHandoffStore, AgentSessionHandoffStoreError, BYOK_SDK_HELPER_SUBCOMMAND, BlobClient, BlobRequestAbortedError, ClaudeAdapter, CodexAdapter, DEFAULT_ACK_CRITICAL_RESERVE_BYTES, DEFAULT_CLEANUP_BATCH_LIMIT, DEFAULT_HARD_BUDGET_RATIO, DEFAULT_INCREMENTAL_VACUUM_PAGES, DEFAULT_LOG_ROTATION, DEFAULT_NORMAL_COMPACTION_INTERVAL_MS, DEFAULT_PRESSURE_COMPACTION_INTERVAL_MS, DEFAULT_RETENTION_MS, DEFAULT_SOFT_BUDGET_RATIO, DaemonObserver, GitWorkspaceError, GitWorkspaceManager, GitWorkspaceStore, JOURNAL_DB_FILENAME, JOURNAL_QUARANTINE_DIRNAME, JOURNAL_TASK_REF_PREFIX, JournalClosedError, JournalCorruptError, JournalRecordTooLargeError, JournalUnavailableError, JournalUnknownTaskError, LocalStateRelocationBusyError, LocalStateRelocationError, LocalStateRelocationIntegrityError, LocalStorageEmergencyError, LocalStoragePolicyError, LocalStoragePressureEngine, LocalTeamWorkspace, LocalTeamWorkspace as LocalTeamWorkspaceService, McpToolsetDefinitionRevisionConflictError, McpToolsetRevisionConflictError, PI_PACKAGE_NAME, PiAdapter, PolicyUnsupportedError, RUNTIME_ADAPTER_CONTRACT_VIOLATION_REASON, ReplayCursorTooOldError, RuntimeDisposalFailure, RuntimeExecutionFailure, SKILL_PACKS_CAPABILITY, SKILL_PACKS_DIRNAME, SKILL_PACK_AUDIT_FILENAME, SKILL_PACK_INSTALL_ERROR_CODES, SKILL_PACK_LOCK_FILENAME, SKILL_PACK_LOCK_SCHEMA, SKILL_PACK_RESPONSE_MAX_BYTES, SecureDirHardeningError, SkillPackInstallError, SqliteLocalTaskJournal, SteerUnsupportedError, TeamTmuxViewError, TeamWorkspaceConflictError, TeamWorkspaceCorruptError, TeamWorkspaceError, TeamWorkspaceLeaseError, TeamWorkspaceNotFoundError, TeamWorkspaceQuotaError, TeamWorkspaceReceiptError, TeamWorkspaceValidationError, TruthMemoryClient, TruthMemoryClientError, UnsupportedServicePlatformError, buildIcaclsArgs, cleanupEligibleAt, cleanupOrderFor, computePressureState, createAgentHomeProjection, createAgentHomeProjectionConsumer, createDaemon, createDaemonWithAdapters, createFilesystemCleanupExecutor, createServiceLifecycle, createStatfsFreeBytesProvider, decodeTeamMemberContext, encodeTeamMemberContext, ensureSecureDir, freezeRuntimeAdapterDescriptor, generateLaunchdPlist, generateSystemdUnit, generateWinswXml, installSkillPacks, isAgentMemorySecureFilesystemAvailable, isGitWorkspaceConfig, isRuntimeDisposalFailure, isRuntimeExecutionFailure, journalHash, listInstalledSkillPacks, localStateRelocation, nodeAgentProgram, openTeamTmuxView, prependGitWorkspaceGuidance, projectRuntimeBoundaryFailure, projectRuntimeExecutionFailure, projectSkillPack, readDeviceEnrollmentStatus, requestDeviceAssertion, resolveLocalAgentReleaseIdentity, resolveLocalStoragePolicy, resolveSdkReservedHelperBin, runSdkReservedHelperCommand, sanitizeServiceName, sealRuntimeOperationManifest, skillPacksRoot, stableAgentHomeOwnerId, validateAgentRef };
|
|
20691
21328
|
//# sourceMappingURL=index.js.map
|
|
20692
21329
|
//# sourceMappingURL=index.js.map
|