@byok-sdk/server 0.5.0 → 0.6.1
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/dist/hub.d.ts +37 -18
- package/dist/index.d.ts +8 -2
- package/dist/index.js +271 -20
- package/dist/index.js.map +1 -1
- package/dist/task-store.d.ts +2 -1
- package/dist/types.d.ts +26 -3
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { mkdir, writeFile, readFile } from 'fs/promises';
|
|
|
5
5
|
import { mkdtempSync, mkdirSync, existsSync, chmodSync } from 'fs';
|
|
6
6
|
import { tmpdir } from 'os';
|
|
7
7
|
import path, { dirname } from 'path';
|
|
8
|
-
import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, DispatchSelectionSchema, RequiredToolsetsSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, BYOK_CHALLENGE_PATH, ChallengeRequestSchema, BYOK_TOKEN_PATH, TokenRequestSchema, BYOK_BLOBS_PATH, CreateBlobRequestSchema, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendRequestSchema, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
8
|
+
import { CAPABILITY_FLAGS, byokBlobContentPath, canTransition, PROTOCOL_VERSION, encodeEnvelope, DAEMON_TO_SERVER_TYPES, AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY, AgentRefSchema, AgentEgressPolicySchema, DispatchSelectionSchema, RequiredToolsetsSchema, AgentContentReadPayloadSchema, createEnvelope, TASK_STATES, BYOK_PAIR_PATH, PairRequestSchema, BYOK_CHALLENGE_PATH, ChallengeRequestSchema, BYOK_TOKEN_PATH, TokenRequestSchema, BYOK_BLOBS_PATH, CreateBlobRequestSchema, BYOK_BLOB_FINALIZE_ROUTE, BYOK_BLOB_URL_ROUTE, BYOK_BLOB_CONTENT_ROUTE, BYOK_EVENTS_PATH, BYOK_MESSAGES_PATH, MessagesSendRequestSchema, AGENT_CONTENT_ARTIFACT_READ_CAPABILITY, AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY, AGENT_CONTENT_WORKSPACE_READ_CAPABILITY, decodeEnvelope, BYOK_WS_PATH } from '@byok-sdk/protocol';
|
|
9
9
|
import { Hono } from 'hono';
|
|
10
10
|
import { WebSocketServer } from 'ws';
|
|
11
11
|
import { createRequire } from 'module';
|
|
@@ -500,7 +500,7 @@ function buildHonoApp(deps) {
|
|
|
500
500
|
let accepted = 0;
|
|
501
501
|
let rejected = 0;
|
|
502
502
|
for (const envelope of parsed.data.messages) {
|
|
503
|
-
const result = deps.hub.handleInbound(principal.deviceId, envelope);
|
|
503
|
+
const result = deps.hub.handleInbound(principal.deviceId, envelope, principal.productId);
|
|
504
504
|
if (result === "rate_limited") {
|
|
505
505
|
return c.json({ error: "rate limit exceeded" }, 429);
|
|
506
506
|
}
|
|
@@ -711,6 +711,25 @@ function isTerminal(state) {
|
|
|
711
711
|
function isClaimedState(state) {
|
|
712
712
|
return state === "Claimed" || state === "Running" || state === "AwaitApproval";
|
|
713
713
|
}
|
|
714
|
+
function sameAgentRef(expected, actual) {
|
|
715
|
+
return actual?.agentId === expected.agentId && actual.profileRevision === expected.profileRevision;
|
|
716
|
+
}
|
|
717
|
+
function sameAgentEgressPayload(expected, actual) {
|
|
718
|
+
return sameAgentRef(expected.agentRef, actual.agentRef) && expected.sessionRef === actual.sessionRef && expected.policyRevision === actual.policyRevision && expected.eventId === actual.eventId && expected.cursor === actual.cursor && expected.contentHash === actual.contentHash && expected.byteCount === actual.byteCount && JSON.stringify(expected.payload) === JSON.stringify(actual.payload);
|
|
719
|
+
}
|
|
720
|
+
function matchesContentReadReceipt(request, receipt) {
|
|
721
|
+
return request.requestId === receipt.requestId && request.surface === receipt.surface && request.actor.kind === receipt.actor.kind && request.actor.id === receipt.actor.id && sameAgentRef(request.agentRef, receipt.agentRef) && request.sessionRef === receipt.sessionRef && request.runtime === receipt.runtime && request.cwd === receipt.cwd && request.policyRevision === receipt.policyRevision && request.target === receipt.target && request.mimeType === receipt.mimeType && request.decodeAs === receipt.decodeAs;
|
|
722
|
+
}
|
|
723
|
+
function contentReadCapability(surface) {
|
|
724
|
+
switch (surface) {
|
|
725
|
+
case "workspace":
|
|
726
|
+
return AGENT_CONTENT_WORKSPACE_READ_CAPABILITY;
|
|
727
|
+
case "transcript":
|
|
728
|
+
return AGENT_CONTENT_TRANSCRIPT_READ_CAPABILITY;
|
|
729
|
+
case "artifact":
|
|
730
|
+
return AGENT_CONTENT_ARTIFACT_READ_CAPABILITY;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
714
733
|
var UnknownTaskError = class extends Error {
|
|
715
734
|
constructor(taskId) {
|
|
716
735
|
super(`unknown taskId: ${taskId}`);
|
|
@@ -788,6 +807,12 @@ var ConnectionHub = class {
|
|
|
788
807
|
longPollWaiters = /* @__PURE__ */ new Map();
|
|
789
808
|
runtimes = /* @__PURE__ */ new Map();
|
|
790
809
|
serverEvents = new AsyncEventQueue();
|
|
810
|
+
/** First-write-wins reliable facts; the reference composition's bounded in-memory readback. */
|
|
811
|
+
agentEgressReceipts = /* @__PURE__ */ new Map();
|
|
812
|
+
/** Accepted requests are the authority that later receipts/transfers must echo exactly. */
|
|
813
|
+
agentContentReadRequests = /* @__PURE__ */ new Map();
|
|
814
|
+
/** Content-free explicit-read audit facts keyed by exact authenticated device/request identity. */
|
|
815
|
+
agentContentReceipts = /* @__PURE__ */ new Map();
|
|
791
816
|
/**
|
|
792
817
|
* Per-task last-inbound-activity timestamp (epoch ms) — the task-lease
|
|
793
818
|
* reaper's condition (c), see the "task-lease reaper" section below. Reset
|
|
@@ -844,12 +869,13 @@ var ConnectionHub = class {
|
|
|
844
869
|
* connection this hub never learns capabilities for simply reads back
|
|
845
870
|
* `undefined` from {@link getDeviceCapabilities}.
|
|
846
871
|
*/
|
|
847
|
-
registerConnection(deviceId, ws, runtimes, capabilities, configuredToolsets) {
|
|
872
|
+
registerConnection(deviceId, ws, runtimes, capabilities, configuredToolsets, clientVersion) {
|
|
848
873
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
849
874
|
this.connections.set(deviceId, {
|
|
850
875
|
ws,
|
|
851
876
|
connected: true,
|
|
852
877
|
lastSeen: at,
|
|
878
|
+
clientVersion,
|
|
853
879
|
runtimes,
|
|
854
880
|
capabilities,
|
|
855
881
|
configuredToolsets
|
|
@@ -993,9 +1019,10 @@ var ConnectionHub = class {
|
|
|
993
1019
|
* taskStore lookup or dedup bookkeeping. See {@link handleRateLimited}
|
|
994
1020
|
* for what happens on exceed (never a silent drop).
|
|
995
1021
|
* 1. **type-allow (P2)** — only {@link DAEMON_TO_SERVER_TYPES} may pass; a
|
|
996
|
-
* server -> daemon type
|
|
997
|
-
* `conn.hello`
|
|
998
|
-
*
|
|
1022
|
+
* server -> daemon type arriving inbound is rejected before it's
|
|
1023
|
+
* dispatched or counted accepted. `conn.hello` is the one non-task
|
|
1024
|
+
* exception, and is accepted only from the bearer-authenticated
|
|
1025
|
+
* long-poll route with an exact device/product/protocol match.
|
|
999
1026
|
* 2. **ownership (N2)** — an envelope for a task already owned by a
|
|
1000
1027
|
* *different* device is dropped (logged), never force-failed:
|
|
1001
1028
|
* force-failing on an authz mismatch would let an attacker who merely
|
|
@@ -1015,16 +1042,34 @@ var ConnectionHub = class {
|
|
|
1015
1042
|
* wire-level success even though no handler ran a second time; only
|
|
1016
1043
|
* `rejected`/`rate_limited` (gate steps 0-2) are excluded from that count.
|
|
1017
1044
|
*/
|
|
1018
|
-
handleInbound(deviceId, envelope) {
|
|
1045
|
+
handleInbound(deviceId, envelope, authenticatedProductId) {
|
|
1019
1046
|
this.envelopesInCount++;
|
|
1020
1047
|
if (!this.rateLimiter.consume(deviceId)) {
|
|
1021
1048
|
this.handleRateLimited(deviceId);
|
|
1022
1049
|
return "rate_limited";
|
|
1023
1050
|
}
|
|
1024
1051
|
this.rateLimitEventEmittedFor.delete(deviceId);
|
|
1052
|
+
if (envelope.type === "conn.hello") {
|
|
1053
|
+
const payload = envelope.payload;
|
|
1054
|
+
if (authenticatedProductId === void 0 || payload.deviceId !== deviceId || payload.productId !== authenticatedProductId || !payload.protocolVersions.includes(PROTOCOL_VERSION)) {
|
|
1055
|
+
return "rejected";
|
|
1056
|
+
}
|
|
1057
|
+
if (this.checkAndRecordDuplicate(deviceId, envelope.id)) {
|
|
1058
|
+
this.dedupDropCount++;
|
|
1059
|
+
return "duplicate";
|
|
1060
|
+
}
|
|
1061
|
+
this.registerLongPollHello(deviceId, payload);
|
|
1062
|
+
return "accepted";
|
|
1063
|
+
}
|
|
1025
1064
|
if (!DAEMON_TO_SERVER_TYPES.includes(envelope.type)) {
|
|
1026
1065
|
return "rejected";
|
|
1027
1066
|
}
|
|
1067
|
+
if (envelope.type === "agent.egress.reliable") {
|
|
1068
|
+
return this.handleAgentEgressReliable(deviceId, envelope.payload);
|
|
1069
|
+
}
|
|
1070
|
+
if (envelope.type === "agent.content.receipt") {
|
|
1071
|
+
return this.handleAgentContentReceipt(deviceId, envelope.payload);
|
|
1072
|
+
}
|
|
1028
1073
|
const taskId = envelope.task_id;
|
|
1029
1074
|
if (taskId === void 0) return "rejected";
|
|
1030
1075
|
const record = this.taskStore.get(taskId);
|
|
@@ -1039,6 +1084,100 @@ var ConnectionHub = class {
|
|
|
1039
1084
|
this.dispatchToHandler(deviceId, taskId, envelope);
|
|
1040
1085
|
return "accepted";
|
|
1041
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Store before acking. Replays must agree on every identity/cursor/hash
|
|
1089
|
+
* field and receive the original receipt id; a same event id with changed
|
|
1090
|
+
* facts is rejected rather than treated as an update.
|
|
1091
|
+
*/
|
|
1092
|
+
handleAgentEgressReliable(deviceId, payload) {
|
|
1093
|
+
if (!this.hasDeviceCapabilities(deviceId, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
1094
|
+
return "rejected";
|
|
1095
|
+
}
|
|
1096
|
+
const key = this.agentEgressReceiptKey(deviceId, payload.eventId);
|
|
1097
|
+
const existing = this.agentEgressReceipts.get(key);
|
|
1098
|
+
if (existing !== void 0) {
|
|
1099
|
+
if (!sameAgentEgressPayload(existing.payload, payload)) return "rejected";
|
|
1100
|
+
this.sendAgentEgressAck(deviceId, existing);
|
|
1101
|
+
this.dedupDropCount++;
|
|
1102
|
+
return "duplicate";
|
|
1103
|
+
}
|
|
1104
|
+
const receipt = {
|
|
1105
|
+
deviceId,
|
|
1106
|
+
payload,
|
|
1107
|
+
receiptId: crypto.randomUUID(),
|
|
1108
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1109
|
+
};
|
|
1110
|
+
this.agentEgressReceipts.set(key, receipt);
|
|
1111
|
+
this.sendAgentEgressAck(deviceId, receipt);
|
|
1112
|
+
return "accepted";
|
|
1113
|
+
}
|
|
1114
|
+
handleAgentContentReceipt(deviceId, payload) {
|
|
1115
|
+
const capability = contentReadCapability(payload.surface);
|
|
1116
|
+
if (!this.hasDeviceCapabilities(deviceId, [capability, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) return "rejected";
|
|
1117
|
+
if (payload.eventId !== payload.requestId) return "rejected";
|
|
1118
|
+
const key = `${deviceId}\0${payload.requestId}`;
|
|
1119
|
+
const request = this.agentContentReadRequests.get(key);
|
|
1120
|
+
if (request === void 0 || !matchesContentReadReceipt(request, payload)) return "rejected";
|
|
1121
|
+
const existing = this.agentContentReceipts.get(key);
|
|
1122
|
+
if (existing !== void 0) {
|
|
1123
|
+
if (JSON.stringify(existing.payload) !== JSON.stringify(payload)) return "rejected";
|
|
1124
|
+
this.sendAgentContentReceiptAck(deviceId, existing);
|
|
1125
|
+
this.dedupDropCount++;
|
|
1126
|
+
return "duplicate";
|
|
1127
|
+
}
|
|
1128
|
+
const receipt = {
|
|
1129
|
+
deviceId,
|
|
1130
|
+
payload,
|
|
1131
|
+
receiptId: payload.requestId,
|
|
1132
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1133
|
+
};
|
|
1134
|
+
this.agentContentReceipts.set(key, receipt);
|
|
1135
|
+
this.sendAgentContentReceiptAck(deviceId, receipt);
|
|
1136
|
+
return "accepted";
|
|
1137
|
+
}
|
|
1138
|
+
sendAgentEgressAck(deviceId, receipt) {
|
|
1139
|
+
this.sendToDevice(
|
|
1140
|
+
deviceId,
|
|
1141
|
+
"agent.egress.ack",
|
|
1142
|
+
{
|
|
1143
|
+
agentRef: receipt.payload.agentRef,
|
|
1144
|
+
sessionRef: receipt.payload.sessionRef,
|
|
1145
|
+
policyRevision: receipt.payload.policyRevision,
|
|
1146
|
+
eventId: receipt.payload.eventId,
|
|
1147
|
+
cursor: receipt.payload.cursor,
|
|
1148
|
+
receiptId: receipt.receiptId
|
|
1149
|
+
},
|
|
1150
|
+
{}
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
sendAgentContentReceiptAck(deviceId, receipt) {
|
|
1154
|
+
this.sendToDevice(
|
|
1155
|
+
deviceId,
|
|
1156
|
+
"agent.egress.ack",
|
|
1157
|
+
{
|
|
1158
|
+
agentRef: receipt.payload.agentRef,
|
|
1159
|
+
sessionRef: receipt.payload.sessionRef,
|
|
1160
|
+
policyRevision: receipt.payload.policyRevision,
|
|
1161
|
+
eventId: receipt.payload.eventId,
|
|
1162
|
+
cursor: receipt.payload.cursor,
|
|
1163
|
+
receiptId: receipt.receiptId
|
|
1164
|
+
},
|
|
1165
|
+
{}
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
agentEgressReceiptKey(deviceId, eventId) {
|
|
1169
|
+
return `${deviceId}\0${eventId}`;
|
|
1170
|
+
}
|
|
1171
|
+
/** Record the authenticated long-poll equivalent of the WS opening frame. */
|
|
1172
|
+
registerLongPollHello(deviceId, payload) {
|
|
1173
|
+
this.takeOverAsLongPoll(deviceId);
|
|
1174
|
+
const connection = this.connections.get(deviceId);
|
|
1175
|
+
if (!connection) return;
|
|
1176
|
+
connection.runtimes = payload.runtimes;
|
|
1177
|
+
connection.capabilities = payload.capabilities;
|
|
1178
|
+
connection.configuredToolsets = payload.configuredToolsets;
|
|
1179
|
+
connection.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
1180
|
+
}
|
|
1042
1181
|
/**
|
|
1043
1182
|
* M4 Phase 4 (part A): `deviceId` just exceeded its inbound-envelope rate
|
|
1044
1183
|
* limit. Never a silent drop: counts the occurrence
|
|
@@ -1140,7 +1279,7 @@ var ConnectionHub = class {
|
|
|
1140
1279
|
this.onStarted(envelope.task_id, envelope.payload);
|
|
1141
1280
|
return;
|
|
1142
1281
|
case "task.decline":
|
|
1143
|
-
this.onDecline(envelope.task_id, envelope.payload);
|
|
1282
|
+
this.onDecline(deviceId, envelope.task_id, envelope.payload);
|
|
1144
1283
|
return;
|
|
1145
1284
|
case "task.progress":
|
|
1146
1285
|
this.onProgress(envelope.task_id, envelope.payload);
|
|
@@ -1206,6 +1345,14 @@ var ConnectionHub = class {
|
|
|
1206
1345
|
onClaim(deviceId, taskId, payload) {
|
|
1207
1346
|
const record = this.taskStore.get(taskId);
|
|
1208
1347
|
if (!record) return;
|
|
1348
|
+
if (record.deviceId !== void 0 && record.deviceId !== deviceId) {
|
|
1349
|
+
console.warn(`[byok/server] dropping task.claim for ${taskId}: offered to a different device`);
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (record.agentRef && !sameAgentRef(record.agentRef, payload.agentRef)) {
|
|
1353
|
+
this.forceFailOrDrop(taskId, "task.claim AgentRef does not exactly match the offered AgentRef");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1209
1356
|
if (record.state === "Claimed" || record.state === "Running") return;
|
|
1210
1357
|
this.applyOrFail(taskId, "Claimed", {
|
|
1211
1358
|
deviceId,
|
|
@@ -1230,10 +1377,18 @@ var ConnectionHub = class {
|
|
|
1230
1377
|
* ever legal from `Offered`; anything else is stale. Ownership is already
|
|
1231
1378
|
* enforced by {@link handleInbound} (N2) before this runs.
|
|
1232
1379
|
*/
|
|
1233
|
-
onDecline(taskId, payload) {
|
|
1380
|
+
onDecline(deviceId, taskId, payload) {
|
|
1234
1381
|
const record = this.taskStore.get(taskId);
|
|
1235
1382
|
if (!record) return;
|
|
1236
1383
|
if (record.state !== "Offered") return;
|
|
1384
|
+
if (record.deviceId !== void 0 && record.deviceId !== deviceId) {
|
|
1385
|
+
console.warn(`[byok/server] dropping task.decline for ${taskId}: offered to a different device`);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (record.agentRef && !sameAgentRef(record.agentRef, payload.agentRef)) {
|
|
1389
|
+
this.forceFailOrDrop(taskId, "task.decline AgentRef does not exactly match the offered AgentRef");
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1237
1392
|
this.applyOrFail(taskId, "Failed", {
|
|
1238
1393
|
result: { state: "Failed", reason: payload.reason, retryable: payload.retryable }
|
|
1239
1394
|
});
|
|
@@ -1283,6 +1438,10 @@ var ConnectionHub = class {
|
|
|
1283
1438
|
const record = this.taskStore.get(taskId);
|
|
1284
1439
|
if (!record) return;
|
|
1285
1440
|
if (isTerminal(record.state)) return;
|
|
1441
|
+
if (record.agentRef && !sameAgentRef(record.agentRef, payload.agentRef)) {
|
|
1442
|
+
this.forceFailOrDrop(taskId, "task.complete AgentRef does not exactly match the offered AgentRef");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1286
1445
|
this.resumeIfImplicitlyApproved(record);
|
|
1287
1446
|
const result = {
|
|
1288
1447
|
state: "Complete",
|
|
@@ -1306,6 +1465,10 @@ var ConnectionHub = class {
|
|
|
1306
1465
|
const record = this.taskStore.get(taskId);
|
|
1307
1466
|
if (!record) return;
|
|
1308
1467
|
if (isTerminal(record.state)) return;
|
|
1468
|
+
if (record.agentRef && !sameAgentRef(record.agentRef, payload.agentRef)) {
|
|
1469
|
+
this.forceFailOrDrop(taskId, "task.fail AgentRef does not exactly match the offered AgentRef");
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1309
1472
|
const result = { state: "Failed", reason: payload.reason, retryable: payload.retryable };
|
|
1310
1473
|
this.applyOrFail(taskId, "Failed", { result });
|
|
1311
1474
|
}
|
|
@@ -1324,6 +1487,10 @@ var ConnectionHub = class {
|
|
|
1324
1487
|
if (!record) return;
|
|
1325
1488
|
if (record.state === "Cancelled") return;
|
|
1326
1489
|
if (isTerminal(record.state)) return;
|
|
1490
|
+
if (record.agentRef && !sameAgentRef(record.agentRef, payload.agentRef)) {
|
|
1491
|
+
this.forceFailOrDrop(taskId, "task.cancelled AgentRef does not exactly match the offered AgentRef");
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1327
1494
|
this.applyOrFail(taskId, "Cancelled", { result: { state: "Cancelled", reason: payload.reason } });
|
|
1328
1495
|
}
|
|
1329
1496
|
/**
|
|
@@ -1682,14 +1849,35 @@ var ConnectionHub = class {
|
|
|
1682
1849
|
// dispatch() and the TaskHandle it returns
|
|
1683
1850
|
// ---------------------------------------------------------------------
|
|
1684
1851
|
async dispatch(input) {
|
|
1852
|
+
const agentRef = input.agentRef === void 0 ? void 0 : AgentRefSchema.parse(input.agentRef);
|
|
1853
|
+
const egressPolicy = input.egressPolicy === void 0 ? void 0 : AgentEgressPolicySchema.parse(input.egressPolicy);
|
|
1685
1854
|
const dispatchSelection = input.dispatchSelection === void 0 ? void 0 : DispatchSelectionSchema.parse(input.dispatchSelection);
|
|
1686
1855
|
const requiredToolsets = input.requiredToolsets === void 0 ? void 0 : RequiredToolsetsSchema.parse(input.requiredToolsets);
|
|
1687
1856
|
const deviceId = input.deviceId ?? this.pickFirstConnectedDevice(requiredToolsets);
|
|
1857
|
+
if (agentRef !== void 0 && input.deviceId === void 0) {
|
|
1858
|
+
throw new Error("Agent-bound dispatch requires an explicit deviceId for capability admission");
|
|
1859
|
+
}
|
|
1860
|
+
if (egressPolicy !== void 0 && agentRef === void 0) {
|
|
1861
|
+
throw new Error("Agent egress policy requires an explicit AgentRef; legacy task dispatch cannot consume it");
|
|
1862
|
+
}
|
|
1863
|
+
if (egressPolicy !== void 0 && input.sessionRef === void 0) {
|
|
1864
|
+
throw new Error("Agent egress policy requires an exact sessionRef");
|
|
1865
|
+
}
|
|
1688
1866
|
if (!deviceId || !this.connections.get(deviceId)?.connected) {
|
|
1689
1867
|
throw new Error(
|
|
1690
1868
|
deviceId ? `device ${deviceId} is not connected` : "no connected device to dispatch to (M0 does not queue tasks until a device connects)"
|
|
1691
1869
|
);
|
|
1692
1870
|
}
|
|
1871
|
+
if (agentRef !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("agent-home-contract") ?? false)) {
|
|
1872
|
+
throw new Error(
|
|
1873
|
+
`device ${deviceId} did not advertise agent-home-contract capability; refusing Agent-bound dispatch`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
if (egressPolicy !== void 0 && !this.hasDeviceCapabilities(deviceId, [AGENT_EGRESS_POLICY_CAPABILITY, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
1877
|
+
throw new Error(
|
|
1878
|
+
`device ${deviceId} did not advertise Agent egress policy and reliable acknowledgement capabilities; refusing before enqueue`
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1693
1881
|
if (dispatchSelection !== void 0 && !(this.getDeviceCapabilities(deviceId)?.includes("dispatch-selection") ?? false)) {
|
|
1694
1882
|
throw new Error(
|
|
1695
1883
|
`device ${deviceId} did not advertise dispatch-selection capability; refusing authoritative provider/model dispatch`
|
|
@@ -1730,7 +1918,8 @@ var ConnectionHub = class {
|
|
|
1730
1918
|
policy,
|
|
1731
1919
|
requiredToolsets,
|
|
1732
1920
|
deviceId,
|
|
1733
|
-
sessionRef: input.sessionRef
|
|
1921
|
+
sessionRef: input.sessionRef,
|
|
1922
|
+
agentRef
|
|
1734
1923
|
});
|
|
1735
1924
|
const queue = new AsyncEventQueue();
|
|
1736
1925
|
let resolveResult;
|
|
@@ -1747,7 +1936,27 @@ var ConnectionHub = class {
|
|
|
1747
1936
|
dispatchSelection,
|
|
1748
1937
|
sessionRef: input.sessionRef
|
|
1749
1938
|
};
|
|
1750
|
-
if (
|
|
1939
|
+
if (agentRef !== void 0 && egressPolicy !== void 0) {
|
|
1940
|
+
this.sendToDevice(
|
|
1941
|
+
deviceId,
|
|
1942
|
+
"task.offer_for_agent_with_egress",
|
|
1943
|
+
{
|
|
1944
|
+
...commonOffer,
|
|
1945
|
+
sessionRef: input.sessionRef,
|
|
1946
|
+
agentRef,
|
|
1947
|
+
egressPolicy,
|
|
1948
|
+
...requiredToolsets === void 0 ? {} : { requiredToolsets }
|
|
1949
|
+
},
|
|
1950
|
+
{ taskId, sessionRef: input.sessionRef }
|
|
1951
|
+
);
|
|
1952
|
+
} else if (agentRef !== void 0) {
|
|
1953
|
+
this.sendToDevice(
|
|
1954
|
+
deviceId,
|
|
1955
|
+
"task.offer_for_agent",
|
|
1956
|
+
{ ...commonOffer, agentRef, ...requiredToolsets === void 0 ? {} : { requiredToolsets } },
|
|
1957
|
+
{ taskId, sessionRef: input.sessionRef }
|
|
1958
|
+
);
|
|
1959
|
+
} else if (requiredToolsets === void 0) {
|
|
1751
1960
|
this.sendToDevice(deviceId, "task.offer", commonOffer, { taskId, sessionRef: input.sessionRef });
|
|
1752
1961
|
} else {
|
|
1753
1962
|
this.sendToDevice(
|
|
@@ -1759,6 +1968,27 @@ var ConnectionHub = class {
|
|
|
1759
1968
|
}
|
|
1760
1969
|
return this.buildTaskHandle(taskId);
|
|
1761
1970
|
}
|
|
1971
|
+
/** Capability-gated control-plane read request; no request enters the outbox on omission. */
|
|
1972
|
+
async requestAgentContentRead(input) {
|
|
1973
|
+
const payload = AgentContentReadPayloadSchema.parse(input.payload);
|
|
1974
|
+
const deviceId = input.deviceId;
|
|
1975
|
+
if (!this.connections.get(deviceId)?.connected) {
|
|
1976
|
+
throw new Error(`device ${deviceId} is not connected`);
|
|
1977
|
+
}
|
|
1978
|
+
const capability = contentReadCapability(payload.surface);
|
|
1979
|
+
if (!this.hasDeviceCapabilities(deviceId, [capability, AGENT_EGRESS_RELIABLE_ACK_CAPABILITY])) {
|
|
1980
|
+
throw new Error(
|
|
1981
|
+
`device ${deviceId} did not advertise ${capability} and reliable acknowledgement support; refusing Agent content read before enqueue`
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
const key = `${deviceId}\0${payload.requestId}`;
|
|
1985
|
+
const existing = this.agentContentReadRequests.get(key);
|
|
1986
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(payload)) {
|
|
1987
|
+
throw new Error(`Agent content request ${payload.requestId} already exists with a different immutable body`);
|
|
1988
|
+
}
|
|
1989
|
+
this.agentContentReadRequests.set(key, payload);
|
|
1990
|
+
this.sendToDevice(deviceId, "agent.content.read", payload, {});
|
|
1991
|
+
}
|
|
1762
1992
|
buildTaskHandle(taskId) {
|
|
1763
1993
|
const hub = this;
|
|
1764
1994
|
return {
|
|
@@ -1887,8 +2117,7 @@ var ConnectionHub = class {
|
|
|
1887
2117
|
* not {@link getDeviceCapabilities}, not `ConnectionState.runtimes`, and
|
|
1888
2118
|
* with no fallback to either when the snapshot is absent. See
|
|
1889
2119
|
* {@link SteerRejectedError} for why a connection-sourced input is wrong
|
|
1890
|
-
*
|
|
1891
|
-
* reach (absent entirely on long-poll-only daemons).
|
|
2120
|
+
* in scope (it describes a daemon build, not this task's runtime).
|
|
1892
2121
|
*/
|
|
1893
2122
|
async steerTask(taskId, text) {
|
|
1894
2123
|
const record = this.taskStore.get(taskId);
|
|
@@ -1941,7 +2170,8 @@ var ConnectionHub = class {
|
|
|
1941
2170
|
const envelope = createEnvelope(type, payload, combinedOpts);
|
|
1942
2171
|
const taskId = opts.taskId;
|
|
1943
2172
|
const redeliverThroughTerminal = type === "task.cancel" || type === "task.reject";
|
|
1944
|
-
|
|
2173
|
+
const redeliverWithoutTask = type === "agent.egress.ack" || type === "agent.content.read";
|
|
2174
|
+
outbox.ring.push({ seq, taskId, envelope, redeliverThroughTerminal, redeliverWithoutTask });
|
|
1945
2175
|
if (outbox.ring.length > OUTBOX_RING_CAPACITY) outbox.ring.shift();
|
|
1946
2176
|
this.deliverToDevice(deviceId, envelope);
|
|
1947
2177
|
return envelope;
|
|
@@ -1966,7 +2196,7 @@ var ConnectionHub = class {
|
|
|
1966
2196
|
const outbox = this.outboxes.get(deviceId);
|
|
1967
2197
|
if (!outbox) return [];
|
|
1968
2198
|
return outbox.ring.filter(
|
|
1969
|
-
(entry) => entry.seq > cursor && entry.taskId
|
|
2199
|
+
(entry) => entry.seq > cursor && (entry.taskId === void 0 ? entry.redeliverWithoutTask === true : !this.isTaskTerminal(entry.taskId) || entry.redeliverThroughTerminal)
|
|
1970
2200
|
).map((entry) => entry.envelope);
|
|
1971
2201
|
}
|
|
1972
2202
|
isTaskTerminal(taskId) {
|
|
@@ -1997,6 +2227,7 @@ var ConnectionHub = class {
|
|
|
1997
2227
|
deviceName,
|
|
1998
2228
|
connected: conn?.connected ?? false,
|
|
1999
2229
|
lastSeen: conn?.lastSeen,
|
|
2230
|
+
...conn?.clientVersion === void 0 ? {} : { clientVersion: conn.clientVersion },
|
|
2000
2231
|
runtimes: conn?.runtimes,
|
|
2001
2232
|
configuredToolsets: conn?.configuredToolsets ? [...conn.configuredToolsets] : void 0
|
|
2002
2233
|
};
|
|
@@ -2018,6 +2249,13 @@ var ConnectionHub = class {
|
|
|
2018
2249
|
getDeviceCapabilities(deviceId) {
|
|
2019
2250
|
return this.connections.get(deviceId)?.capabilities;
|
|
2020
2251
|
}
|
|
2252
|
+
hasDeviceCapabilities(deviceId, required) {
|
|
2253
|
+
const advertised = this.getDeviceCapabilities(deviceId);
|
|
2254
|
+
return advertised !== void 0 && required.every((capability) => advertised.includes(capability));
|
|
2255
|
+
}
|
|
2256
|
+
getAgentEgressReceipt(deviceId, eventId) {
|
|
2257
|
+
return this.agentEgressReceipts.get(this.agentEgressReceiptKey(deviceId, eventId));
|
|
2258
|
+
}
|
|
2021
2259
|
getTask(taskId) {
|
|
2022
2260
|
return this.taskStore.get(taskId);
|
|
2023
2261
|
}
|
|
@@ -2081,6 +2319,7 @@ var InMemoryTaskStore = class {
|
|
|
2081
2319
|
requiredToolsets: input.requiredToolsets,
|
|
2082
2320
|
deviceId: input.deviceId,
|
|
2083
2321
|
sessionRef: input.sessionRef,
|
|
2322
|
+
agentRef: input.agentRef,
|
|
2084
2323
|
createdAt: now,
|
|
2085
2324
|
updatedAt: now
|
|
2086
2325
|
};
|
|
@@ -2251,7 +2490,8 @@ function handleConnection(ws, principal, deps) {
|
|
|
2251
2490
|
ws,
|
|
2252
2491
|
payload.runtimes,
|
|
2253
2492
|
payload.capabilities,
|
|
2254
|
-
payload.configuredToolsets
|
|
2493
|
+
payload.configuredToolsets,
|
|
2494
|
+
payload.clientVersion
|
|
2255
2495
|
);
|
|
2256
2496
|
deps.hub.sendConnAck(deviceId, SUPPORTED_CAPABILITIES);
|
|
2257
2497
|
if (payload.cursor !== void 0) {
|
|
@@ -2365,6 +2605,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
2365
2605
|
policy_json TEXT NOT NULL,
|
|
2366
2606
|
device_id TEXT,
|
|
2367
2607
|
session_ref TEXT,
|
|
2608
|
+
agent_ref_json TEXT,
|
|
2368
2609
|
created_at TEXT NOT NULL,
|
|
2369
2610
|
updated_at TEXT NOT NULL,
|
|
2370
2611
|
result_json TEXT,
|
|
@@ -2385,7 +2626,8 @@ var ADDITIVE_COLUMNS = [
|
|
|
2385
2626
|
{
|
|
2386
2627
|
name: "claimed_runtime_capabilities_json",
|
|
2387
2628
|
ddl: "ALTER TABLE tasks ADD COLUMN claimed_runtime_capabilities_json TEXT"
|
|
2388
|
-
}
|
|
2629
|
+
},
|
|
2630
|
+
{ name: "agent_ref_json", ddl: "ALTER TABLE tasks ADD COLUMN agent_ref_json TEXT" }
|
|
2389
2631
|
];
|
|
2390
2632
|
function currentTaskColumns(db) {
|
|
2391
2633
|
return new Set(db.prepare("PRAGMA table_info(tasks)").all().map((c) => c.name));
|
|
@@ -2411,6 +2653,7 @@ function rowToRecord(row) {
|
|
|
2411
2653
|
const resultJson = row.result_json;
|
|
2412
2654
|
const requiredToolsetsJson = row.required_toolsets_json;
|
|
2413
2655
|
const claimedRuntimeCapabilitiesJson = row.claimed_runtime_capabilities_json;
|
|
2656
|
+
const agentRefJson = row.agent_ref_json;
|
|
2414
2657
|
return {
|
|
2415
2658
|
taskId: row.task_id,
|
|
2416
2659
|
state: row.state,
|
|
@@ -2420,6 +2663,7 @@ function rowToRecord(row) {
|
|
|
2420
2663
|
policy: JSON.parse(row.policy_json),
|
|
2421
2664
|
deviceId: row.device_id ?? void 0,
|
|
2422
2665
|
sessionRef: row.session_ref ?? void 0,
|
|
2666
|
+
agentRef: agentRefJson ? JSON.parse(agentRefJson) : void 0,
|
|
2423
2667
|
createdAt: row.created_at,
|
|
2424
2668
|
updatedAt: row.updated_at,
|
|
2425
2669
|
result: resultJson ? JSON.parse(resultJson) : void 0,
|
|
@@ -2444,13 +2688,13 @@ var SqliteTaskStore = class {
|
|
|
2444
2688
|
secureSqliteFilePermissions(opts.path);
|
|
2445
2689
|
this.insertStmt = this.db.prepare(
|
|
2446
2690
|
`INSERT INTO tasks
|
|
2447
|
-
(task_id, state, instruction, runtime, required_toolsets_json, policy_json, device_id, session_ref, created_at, updated_at, result_json)
|
|
2448
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2691
|
+
(task_id, state, instruction, runtime, required_toolsets_json, policy_json, device_id, session_ref, agent_ref_json, created_at, updated_at, result_json)
|
|
2692
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2449
2693
|
);
|
|
2450
2694
|
this.updateStmt = this.db.prepare(
|
|
2451
2695
|
`UPDATE tasks SET
|
|
2452
2696
|
state = ?, instruction = ?, runtime = ?, required_toolsets_json = ?, policy_json = ?, device_id = ?,
|
|
2453
|
-
session_ref = ?, created_at = ?, updated_at = ?, result_json = ?, pending_approval_id = ?,
|
|
2697
|
+
session_ref = ?, agent_ref_json = ?, created_at = ?, updated_at = ?, result_json = ?, pending_approval_id = ?,
|
|
2454
2698
|
claimed_runtime = ?, claimed_runtime_capabilities_json = ?
|
|
2455
2699
|
WHERE task_id = ? AND state = ?`
|
|
2456
2700
|
);
|
|
@@ -2478,6 +2722,7 @@ var SqliteTaskStore = class {
|
|
|
2478
2722
|
policy: input.policy,
|
|
2479
2723
|
deviceId: input.deviceId,
|
|
2480
2724
|
sessionRef: input.sessionRef,
|
|
2725
|
+
agentRef: input.agentRef,
|
|
2481
2726
|
createdAt: now,
|
|
2482
2727
|
updatedAt: now
|
|
2483
2728
|
};
|
|
@@ -2490,6 +2735,7 @@ var SqliteTaskStore = class {
|
|
|
2490
2735
|
JSON.stringify(record.policy),
|
|
2491
2736
|
record.deviceId ?? null,
|
|
2492
2737
|
record.sessionRef ?? null,
|
|
2738
|
+
record.agentRef ? JSON.stringify(record.agentRef) : null,
|
|
2493
2739
|
record.createdAt,
|
|
2494
2740
|
record.updatedAt,
|
|
2495
2741
|
record.result ? JSON.stringify(record.result) : null
|
|
@@ -2548,6 +2794,7 @@ var SqliteTaskStore = class {
|
|
|
2548
2794
|
JSON.stringify(updated.policy),
|
|
2549
2795
|
updated.deviceId ?? null,
|
|
2550
2796
|
updated.sessionRef ?? null,
|
|
2797
|
+
updated.agentRef ? JSON.stringify(updated.agentRef) : null,
|
|
2551
2798
|
updated.createdAt,
|
|
2552
2799
|
updated.updatedAt,
|
|
2553
2800
|
updated.result ? JSON.stringify(updated.result) : null,
|
|
@@ -2780,10 +3027,14 @@ function createByokServer(opts) {
|
|
|
2780
3027
|
createPairingCode: (claims) => pairing.createPairingCode(claims)
|
|
2781
3028
|
},
|
|
2782
3029
|
dispatch: (input) => hub.dispatch(input),
|
|
3030
|
+
requestAgentContentRead: (input) => hub.requestAgentContentRead(input),
|
|
2783
3031
|
tasks: {
|
|
2784
3032
|
get: (taskId) => hub.getTask(taskId),
|
|
2785
3033
|
list: () => hub.listTasks()
|
|
2786
3034
|
},
|
|
3035
|
+
egress: {
|
|
3036
|
+
get: (deviceId, eventId) => hub.getAgentEgressReceipt(deviceId, eventId)
|
|
3037
|
+
},
|
|
2787
3038
|
machines: {
|
|
2788
3039
|
list: () => hub.listMachines()
|
|
2789
3040
|
},
|