@stelis/agent-q-core 0.0.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.
Files changed (47) hide show
  1. package/README.md +144 -0
  2. package/dist/adapter-internal.d.ts +5 -0
  3. package/dist/adapter-internal.js +6 -0
  4. package/dist/adapter-internal.js.map +1 -0
  5. package/dist/config.d.ts +74 -0
  6. package/dist/config.js +489 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/core.d.ts +320 -0
  9. package/dist/core.js +840 -0
  10. package/dist/core.js.map +1 -0
  11. package/dist/device.d.ts +55 -0
  12. package/dist/device.js +23 -0
  13. package/dist/device.js.map +1 -0
  14. package/dist/errors.d.ts +6 -0
  15. package/dist/errors.js +20 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/host-output-schema.d.ts +2437 -0
  18. package/dist/host-output-schema.js +655 -0
  19. package/dist/host-output-schema.js.map +1 -0
  20. package/dist/protocol-error.d.ts +4 -0
  21. package/dist/protocol-error.js +9 -0
  22. package/dist/protocol-error.js.map +1 -0
  23. package/dist/protocol-management-primitives.d.ts +27 -0
  24. package/dist/protocol-management-primitives.js +51 -0
  25. package/dist/protocol-management-primitives.js.map +1 -0
  26. package/dist/protocol-primitives.d.ts +53 -0
  27. package/dist/protocol-primitives.js +331 -0
  28. package/dist/protocol-primitives.js.map +1 -0
  29. package/dist/protocol.d.ts +207 -0
  30. package/dist/protocol.js +897 -0
  31. package/dist/protocol.js.map +1 -0
  32. package/dist/provider-protocol.d.ts +262 -0
  33. package/dist/provider-protocol.js +637 -0
  34. package/dist/provider-protocol.js.map +1 -0
  35. package/dist/public-error.d.ts +9 -0
  36. package/dist/public-error.js +79 -0
  37. package/dist/public-error.js.map +1 -0
  38. package/dist/safe-text.d.ts +31 -0
  39. package/dist/safe-text.js +143 -0
  40. package/dist/safe-text.js.map +1 -0
  41. package/dist/transport-invariants.d.ts +18 -0
  42. package/dist/transport-invariants.js +24 -0
  43. package/dist/transport-invariants.js.map +1 -0
  44. package/dist/usb.d.ts +76 -0
  45. package/dist/usb.js +454 -0
  46. package/dist/usb.js.map +1 -0
  47. package/package.json +58 -0
package/dist/core.js ADDED
@@ -0,0 +1,840 @@
1
+ import { ConfigError, ConfigStore, RESERVED_PURPOSES, isValidPurpose, } from "./config.js";
2
+ import { AgentQError, toAgentQError } from "./errors.js";
3
+ import { isClientName, isSafeDeviceId, sanitizePortHint } from "./safe-text.js";
4
+ import { normalizeErrorCode, toPublicError } from "./public-error.js";
5
+ import { identifySignRoute, validateSignPersonalMessageParams, validateSignTransactionParams, } from "./provider-protocol.js";
6
+ import { INTERNAL_CONNECT_DEADLINE_MS, INTERNAL_DISCONNECT_DEADLINE_MS, INTERNAL_POLICY_UPDATE_DEADLINE_MS, INTERNAL_SIGN_PERSONAL_MESSAGE_DEADLINE_MS, INTERNAL_SIGN_TRANSACTION_DEADLINE_MS, } from "./transport-invariants.js";
7
+ import { SerialPortUsbDriver } from "./usb.js";
8
+ export { SerialPortUsbDriver } from "./usb.js";
9
+ import { createIdentificationCode, ProtocolError, validateApprovalHistoryInput, validatePolicyProposeInput, validatePolicyProposeRequestInput, } from "./protocol.js";
10
+ import { INTERNAL_USB_DEADLINE_MS, deadlineEnforcingDriver, mapErrorToUnavailableReason, scanUsbDeviceStatuses, scanUsbDevices, } from "./usb.js";
11
+ // Why a disconnect ended (or did not establish) Agent-Q's local session view.
12
+ // Single-sourced so the MCP output schema enumerates exactly these values and a
13
+ // different output adapter cannot invent its own.
14
+ export const DISCONNECT_REASONS = [
15
+ "firmware_confirmed",
16
+ "invalid_session",
17
+ "transport_unavailable",
18
+ "timeout",
19
+ "not_connected",
20
+ ];
21
+ export const DISCONNECT_ENDED_REASONS = [
22
+ "firmware_confirmed",
23
+ "invalid_session",
24
+ "transport_unavailable",
25
+ "timeout",
26
+ ];
27
+ export const GET_ACCOUNTS_SESSION_ENDED_REASONS = [
28
+ "invalid_session",
29
+ "transport_unavailable",
30
+ "timeout",
31
+ ];
32
+ export const GET_CAPABILITIES_SESSION_ENDED_REASONS = GET_ACCOUNTS_SESSION_ENDED_REASONS;
33
+ export const POLICY_GET_SESSION_ENDED_REASONS = GET_ACCOUNTS_SESSION_ENDED_REASONS;
34
+ export const GET_APPROVAL_HISTORY_SESSION_ENDED_REASONS = GET_ACCOUNTS_SESSION_ENDED_REASONS;
35
+ export const POLICY_PROPOSE_SESSION_ENDED_REASONS = GET_ACCOUNTS_SESSION_ENDED_REASONS;
36
+ export const SIGN_TRANSACTION_SESSION_ENDED_REASONS = GET_ACCOUNTS_SESSION_ENDED_REASONS;
37
+ export const SIGN_PERSONAL_MESSAGE_SESSION_ENDED_REASONS = GET_ACCOUNTS_SESSION_ENDED_REASONS;
38
+ export const DEFAULT_CLIENT_NAME = "Agent-Q";
39
+ export class AgentQCore {
40
+ configStore;
41
+ clock;
42
+ runtimeSessions = new Map();
43
+ usbDriver;
44
+ constructor(configStore, usbDriver, clock = () => new Date()) {
45
+ this.configStore = configStore;
46
+ this.clock = clock;
47
+ // Wrap the driver once so every deadline-bearing transport call is bounded by
48
+ // its internal deadline at a single boundary, regardless of whether the
49
+ // injected driver honors it.
50
+ this.usbDriver = deadlineEnforcingDriver(usbDriver);
51
+ }
52
+ async scanDevices(input = {}) {
53
+ rejectUnsupportedInputFields(input, NO_INPUT_KEYS, "scanDevices");
54
+ const scanResult = await scanUsbDeviceStatuses(this.usbDriver, INTERNAL_USB_DEADLINE_MS);
55
+ const devices = [];
56
+ for (const liveDevice of scanResult.devices) {
57
+ await this.configStore.rememberUsbStatus(liveDevice.protocolResponse, liveDevice.portPath, {
58
+ setActive: false,
59
+ });
60
+ devices.push(toLiveStatus(liveDevice));
61
+ }
62
+ this.clearRuntimeSessionsAbsentFromLiveUsbScan(new Set(scanResult.devices.map((liveDevice) => liveDevice.protocolResponse.device.deviceId)));
63
+ const config = await this.configStore.load();
64
+ return {
65
+ source: "live",
66
+ devices,
67
+ failures: scanResult.failures.map(toScanFailure),
68
+ activeDeviceId: config.activeDeviceId,
69
+ };
70
+ }
71
+ async identifyDevices(input = {}) {
72
+ rejectUnsupportedInputFields(input, NO_INPUT_KEYS, "identifyDevices");
73
+ const deadlineMs = INTERNAL_USB_DEADLINE_MS;
74
+ // deadlineMs is the total internal transport budget for the whole call
75
+ // (discovery plus every identify handshake), not a per-device limit.
76
+ const deadline = this.clock().getTime() + deadlineMs;
77
+ const liveDevices = await scanUsbDevices(this.usbDriver, deadlineMs);
78
+ const devices = [];
79
+ const usedCodes = new Set();
80
+ for (const liveDevice of liveDevices) {
81
+ const remainingMs = deadline - this.clock().getTime();
82
+ if (remainingMs <= 0) {
83
+ // Transport budget exhausted before this device was reached. The nested
84
+ // error is already public (canonical code + message) at the source.
85
+ devices.push({
86
+ source: "error",
87
+ connected: false,
88
+ portPath: sanitizePortHint(liveDevice.portPath),
89
+ deviceId: liveDevice.protocolResponse.device.deviceId,
90
+ status: "error",
91
+ error: toPublicError("timeout", true),
92
+ });
93
+ continue;
94
+ }
95
+ const code = createUniqueIdentificationCode(usedCodes);
96
+ try {
97
+ const response = await this.usbDriver.identifyDevice(liveDevice.portPath, code, remainingMs);
98
+ if (response.device.deviceId !== liveDevice.protocolResponse.device.deviceId) {
99
+ throw new AgentQError("handshake_failed", `Identify response device id did not match status response. Expected ${liveDevice.protocolResponse.device.deviceId}, got ${response.device.deviceId}.`, true);
100
+ }
101
+ if (response.code !== code) {
102
+ throw new AgentQError("handshake_failed", "Identify response code did not match request.", true);
103
+ }
104
+ await this.configStore.rememberUsbStatus({ device: response.device, provisioning: liveDevice.protocolResponse.provisioning }, liveDevice.portPath);
105
+ devices.push({
106
+ source: "live",
107
+ connected: true,
108
+ portPath: sanitizePortHint(liveDevice.portPath),
109
+ status: "displayed",
110
+ code,
111
+ protocolResponse: response,
112
+ });
113
+ }
114
+ catch (error) {
115
+ const agentQError = toAgentQError(error);
116
+ // Canonicalize the nested error at the source so the returned data is
117
+ // public-safe for ANY adapter, not only after MCP re-sanitizes it.
118
+ devices.push({
119
+ source: "error",
120
+ connected: false,
121
+ portPath: sanitizePortHint(liveDevice.portPath),
122
+ deviceId: liveDevice.protocolResponse.device.deviceId,
123
+ status: "error",
124
+ error: toPublicError(agentQError.code, agentQError.retryable),
125
+ });
126
+ }
127
+ }
128
+ const config = await this.configStore.load();
129
+ return {
130
+ source: "live",
131
+ devices,
132
+ activeDeviceId: config.activeDeviceId,
133
+ };
134
+ }
135
+ async selectDevice(input) {
136
+ rejectUnsupportedInputFields(input, DEVICE_SCOPED_INPUT_KEYS, "selectDevice");
137
+ if (!isSafeDeviceId(input.deviceId)) {
138
+ throw new AgentQError("invalid_device_id", "deviceId is not a valid device identifier.", false);
139
+ }
140
+ let record;
141
+ try {
142
+ record = await this.configStore.setActiveDevice(input.deviceId, input.purpose);
143
+ }
144
+ catch (error) {
145
+ throw mapConfigError(error);
146
+ }
147
+ return {
148
+ source: "selected",
149
+ activeDeviceId: record.deviceId,
150
+ purpose: input.purpose ?? null,
151
+ device: record.lastStatus.device,
152
+ };
153
+ }
154
+ async listDevices() {
155
+ const config = await this.configStore.load();
156
+ const listings = await this.configStore.listDevices();
157
+ const devices = listings.map((listing) => ({
158
+ ...listing,
159
+ runtimeSession: toRuntimeSessionView(this.peekRuntimeSession(listing.deviceId)),
160
+ }));
161
+ return {
162
+ source: "list",
163
+ devices,
164
+ activeDeviceId: config.activeDeviceId,
165
+ activeDeviceIdsByPurpose: { ...config.activeDeviceIdsByPurpose },
166
+ };
167
+ }
168
+ async setDeviceMetadata(input) {
169
+ rejectUnsupportedInputFields(input, SET_DEVICE_METADATA_INPUT_KEYS, "setDeviceMetadata");
170
+ if (!isSafeDeviceId(input.deviceId)) {
171
+ throw new AgentQError("invalid_device_id", "deviceId is not a valid device identifier.", false);
172
+ }
173
+ let record;
174
+ try {
175
+ record = await this.configStore.setDeviceMetadata({
176
+ deviceId: input.deviceId,
177
+ label: input.label,
178
+ });
179
+ }
180
+ catch (error) {
181
+ throw mapConfigError(error);
182
+ }
183
+ return {
184
+ source: "metadata",
185
+ deviceId: record.deviceId,
186
+ label: record.label,
187
+ };
188
+ }
189
+ async connectDevice(input = {}) {
190
+ rejectUnsupportedInputFields(input, CONNECT_DEVICE_INPUT_KEYS, "connectDevice");
191
+ const clientName = validateClientName(input.clientName);
192
+ const target = await this.resolveTargetDevice(input);
193
+ const scanDeadlineMs = INTERNAL_USB_DEADLINE_MS;
194
+ let matchingPort;
195
+ try {
196
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
197
+ }
198
+ catch (error) {
199
+ this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
200
+ throw error;
201
+ }
202
+ // Record the live device before sending connect so a rejected or timed-out
203
+ // attempt still refreshes lastSeenAt and the cached status for this device.
204
+ await this.configStore.rememberUsbStatus(matchingPort.protocolResponse, matchingPort.portPath, { observedAt: this.clock() });
205
+ const existingSession = this.peekRuntimeSession(target.deviceId);
206
+ if (existingSession !== null) {
207
+ try {
208
+ await this.usbDriver.getCapabilities(matchingPort.portPath, existingSession.sessionId, scanDeadlineMs);
209
+ return {
210
+ source: "connected",
211
+ deviceId: target.deviceId,
212
+ sessionTtlMs: existingSession.sessionTtlMs,
213
+ connectedAt: existingSession.connectedAt,
214
+ device: matchingPort.protocolResponse.device,
215
+ };
216
+ }
217
+ catch (error) {
218
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
219
+ if (reason !== "invalid_session") {
220
+ throw error;
221
+ }
222
+ }
223
+ }
224
+ let response;
225
+ try {
226
+ response = await this.usbDriver.connectDevice(matchingPort.portPath, clientName, INTERNAL_CONNECT_DEADLINE_MS);
227
+ }
228
+ catch (error) {
229
+ this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
230
+ throw error;
231
+ }
232
+ if (response.status === "rejected") {
233
+ throw new AgentQError(response.error.code, response.error.message, response.error.code === "timeout");
234
+ }
235
+ if (response.device.deviceId !== target.deviceId) {
236
+ throw new AgentQError("handshake_failed", `Connect response device id did not match. Expected ${target.deviceId}, got ${response.device.deviceId}.`, true);
237
+ }
238
+ const session = this.recordSession(target.deviceId, response.sessionId, response.sessionTtlMs);
239
+ return {
240
+ source: "connected",
241
+ deviceId: target.deviceId,
242
+ sessionTtlMs: session.sessionTtlMs,
243
+ connectedAt: session.connectedAt,
244
+ device: response.device,
245
+ };
246
+ }
247
+ async disconnectDevice(input = {}) {
248
+ const target = await this.resolveTargetDevice(input);
249
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
250
+ const session = this.peekRuntimeSession(target.deviceId);
251
+ if (session === null) {
252
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
253
+ }
254
+ rejectUnsupportedInputFields(input, DEVICE_SCOPED_INPUT_KEYS, "disconnectDevice");
255
+ let matchingPort;
256
+ try {
257
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
258
+ }
259
+ catch (error) {
260
+ // The device could not be located. clearRuntimeSessionMirrorIfEnded owns the policy
261
+ // for which disconnect failures end Agent-Q's local session view; clearing
262
+ // it here prevents reusing a session Agent-Q can no longer confirm.
263
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
264
+ if (reason !== null) {
265
+ return { source: "disconnected", deviceId: target.deviceId, reason };
266
+ }
267
+ throw error;
268
+ }
269
+ try {
270
+ await this.usbDriver.disconnectDevice(matchingPort.portPath, session.sessionId, scanDeadlineMs);
271
+ }
272
+ catch (error) {
273
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
274
+ if (reason !== null) {
275
+ return { source: "disconnected", deviceId: target.deviceId, reason };
276
+ }
277
+ throw error;
278
+ }
279
+ this.clearRuntimeSessionMirror(target.deviceId);
280
+ return { source: "disconnected", deviceId: target.deviceId, reason: "firmware_confirmed" };
281
+ }
282
+ async getCapabilities(input = {}) {
283
+ const target = await this.resolveTargetDevice(input);
284
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
285
+ const session = this.peekRuntimeSession(target.deviceId);
286
+ if (session === null) {
287
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
288
+ }
289
+ rejectUnsupportedInputFields(input, DEVICE_SCOPED_INPUT_KEYS, "getCapabilities");
290
+ let matchingPort;
291
+ try {
292
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
293
+ }
294
+ catch (error) {
295
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
296
+ if (reason !== null) {
297
+ return { source: "session_ended", deviceId: target.deviceId, reason };
298
+ }
299
+ throw error;
300
+ }
301
+ try {
302
+ const response = await this.usbDriver.getCapabilities(matchingPort.portPath, session.sessionId, scanDeadlineMs);
303
+ return {
304
+ source: "live",
305
+ deviceId: target.deviceId,
306
+ capabilities: response.chains,
307
+ ...(response.signing === undefined ? {} : { signing: response.signing }),
308
+ };
309
+ }
310
+ catch (error) {
311
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
312
+ if (reason !== null) {
313
+ return { source: "session_ended", deviceId: target.deviceId, reason };
314
+ }
315
+ throw error;
316
+ }
317
+ }
318
+ async getAccounts(input = {}) {
319
+ const target = await this.resolveTargetDevice(input);
320
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
321
+ const session = this.peekRuntimeSession(target.deviceId);
322
+ if (session === null) {
323
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
324
+ }
325
+ rejectUnsupportedInputFields(input, DEVICE_SCOPED_INPUT_KEYS, "getAccounts");
326
+ let matchingPort;
327
+ try {
328
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
329
+ }
330
+ catch (error) {
331
+ // A transport/session failure while locating the device may end Agent-Q's
332
+ // local session view; clearRuntimeSessionMirrorIfEnded owns that policy. get_accounts
333
+ // is read-only, so a recognized clearing reason is reported as session_ended
334
+ // (the firmware session is presumed gone) rather than throwing.
335
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
336
+ if (reason !== null) {
337
+ return { source: "session_ended", deviceId: target.deviceId, reason };
338
+ }
339
+ throw error;
340
+ }
341
+ try {
342
+ const response = await this.usbDriver.getAccounts(matchingPort.portPath, session.sessionId, scanDeadlineMs);
343
+ // Read-only: the session is retained on success.
344
+ return { source: "live", deviceId: target.deviceId, accounts: response.accounts };
345
+ }
346
+ catch (error) {
347
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
348
+ if (reason !== null) {
349
+ return { source: "session_ended", deviceId: target.deviceId, reason };
350
+ }
351
+ throw error;
352
+ }
353
+ }
354
+ async policyGet(input = {}) {
355
+ const target = await this.resolveTargetDevice(input);
356
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
357
+ const session = this.peekRuntimeSession(target.deviceId);
358
+ if (session === null) {
359
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
360
+ }
361
+ rejectUnsupportedInputFields(input, DEVICE_SCOPED_INPUT_KEYS, "policyGet");
362
+ let matchingPort;
363
+ try {
364
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
365
+ }
366
+ catch (error) {
367
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
368
+ if (reason !== null) {
369
+ return { source: "session_ended", deviceId: target.deviceId, reason };
370
+ }
371
+ throw error;
372
+ }
373
+ try {
374
+ const response = await this.usbDriver.policyGet(matchingPort.portPath, session.sessionId, scanDeadlineMs);
375
+ return { source: "live", deviceId: target.deviceId, policy: response.policy };
376
+ }
377
+ catch (error) {
378
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
379
+ if (reason !== null) {
380
+ return { source: "session_ended", deviceId: target.deviceId, reason };
381
+ }
382
+ throw error;
383
+ }
384
+ }
385
+ async getApprovalHistory(input = {}) {
386
+ const target = await this.resolveTargetDevice(input);
387
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
388
+ const session = this.peekRuntimeSession(target.deviceId);
389
+ if (session === null) {
390
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
391
+ }
392
+ rejectUnsupportedInputFields(input, GET_APPROVAL_HISTORY_INPUT_KEYS, "getApprovalHistory");
393
+ const params = validateApprovalHistoryInput({
394
+ limit: input.limit,
395
+ beforeSeq: input.beforeSeq,
396
+ });
397
+ let matchingPort;
398
+ try {
399
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
400
+ }
401
+ catch (error) {
402
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
403
+ if (reason !== null) {
404
+ return { source: "session_ended", deviceId: target.deviceId, reason };
405
+ }
406
+ throw error;
407
+ }
408
+ try {
409
+ const response = await this.usbDriver.getApprovalHistory(matchingPort.portPath, session.sessionId, params, scanDeadlineMs);
410
+ return {
411
+ source: "live",
412
+ deviceId: target.deviceId,
413
+ records: response.records,
414
+ hasMore: response.hasMore,
415
+ };
416
+ }
417
+ catch (error) {
418
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
419
+ if (reason !== null) {
420
+ return { source: "session_ended", deviceId: target.deviceId, reason };
421
+ }
422
+ throw error;
423
+ }
424
+ }
425
+ async policyPropose(input) {
426
+ const target = await this.resolveTargetDevice(input);
427
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
428
+ const policyUpdateDeadlineMs = INTERNAL_POLICY_UPDATE_DEADLINE_MS;
429
+ const session = this.peekRuntimeSession(target.deviceId);
430
+ if (session === null) {
431
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
432
+ }
433
+ rejectUnsupportedInputFields(input, POLICY_PROPOSE_INPUT_KEYS, "policyPropose");
434
+ validatePolicyProposeInput(input.policy);
435
+ validatePolicyProposeRequestInput(session.sessionId, input.policy);
436
+ let matchingPort;
437
+ try {
438
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
439
+ }
440
+ catch (error) {
441
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
442
+ if (reason !== null) {
443
+ return { source: "session_ended", deviceId: target.deviceId, reason };
444
+ }
445
+ throw error;
446
+ }
447
+ try {
448
+ const response = await this.usbDriver.policyPropose(matchingPort.portPath, session.sessionId, input.policy, policyUpdateDeadlineMs);
449
+ if (response.status === "consistency_error") {
450
+ this.clearRuntimeSessionMirror(target.deviceId);
451
+ }
452
+ return {
453
+ source: "live",
454
+ deviceId: target.deviceId,
455
+ status: response.status,
456
+ reasonCode: response.reasonCode,
457
+ ...(response.policy === undefined ? {} : { policy: response.policy }),
458
+ };
459
+ }
460
+ catch (error) {
461
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
462
+ if (reason !== null) {
463
+ return { source: "session_ended", deviceId: target.deviceId, reason };
464
+ }
465
+ throw error;
466
+ }
467
+ }
468
+ async signTransaction(input) {
469
+ const route = identifySignHostRoute("sign_transaction", input.chain, input.method);
470
+ const target = await this.resolveTargetDevice(input);
471
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
472
+ const deadlineMs = INTERNAL_SIGN_TRANSACTION_DEADLINE_MS;
473
+ const session = this.peekRuntimeSession(target.deviceId);
474
+ if (session === null) {
475
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
476
+ }
477
+ rejectUnsupportedInputFields(input, SIGN_TRANSACTION_INPUT_KEYS, "signTransaction");
478
+ const params = validateSignHostInput({
479
+ requestType: "sign_transaction",
480
+ network: input.network,
481
+ txBytes: input.txBytes,
482
+ });
483
+ let matchingPort;
484
+ try {
485
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
486
+ }
487
+ catch (error) {
488
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
489
+ if (reason !== null) {
490
+ return { source: "session_ended", deviceId: target.deviceId, reason };
491
+ }
492
+ throw error;
493
+ }
494
+ try {
495
+ const response = await this.usbDriver.signTransaction(matchingPort.portPath, session.sessionId, route, params, deadlineMs);
496
+ return toLiveSignResult(target.deviceId, response);
497
+ }
498
+ catch (error) {
499
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
500
+ if (reason !== null) {
501
+ return { source: "session_ended", deviceId: target.deviceId, reason };
502
+ }
503
+ throw error;
504
+ }
505
+ }
506
+ async signPersonalMessage(input) {
507
+ const route = identifySignHostRoute("sign_personal_message", input.chain, input.method);
508
+ const target = await this.resolveTargetDevice(input);
509
+ const scanDeadlineMs = INTERNAL_DISCONNECT_DEADLINE_MS;
510
+ const deadlineMs = INTERNAL_SIGN_PERSONAL_MESSAGE_DEADLINE_MS;
511
+ const session = this.peekRuntimeSession(target.deviceId);
512
+ if (session === null) {
513
+ return { source: "not_connected", deviceId: target.deviceId, reason: "not_connected" };
514
+ }
515
+ rejectUnsupportedInputFields(input, SIGN_PERSONAL_MESSAGE_INPUT_KEYS, "signPersonalMessage");
516
+ const params = validateSignPersonalMessageHostInput({
517
+ requestType: "sign_personal_message",
518
+ network: input.network,
519
+ message: input.message,
520
+ });
521
+ let matchingPort;
522
+ try {
523
+ matchingPort = await this.findLivePortForDevice(target.record, scanDeadlineMs);
524
+ }
525
+ catch (error) {
526
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
527
+ if (reason !== null) {
528
+ return { source: "session_ended", deviceId: target.deviceId, reason };
529
+ }
530
+ throw error;
531
+ }
532
+ try {
533
+ const response = await this.usbDriver.signPersonalMessage(matchingPort.portPath, session.sessionId, route, params, deadlineMs);
534
+ return toLiveSignResult(target.deviceId, response);
535
+ }
536
+ catch (error) {
537
+ const reason = this.clearRuntimeSessionMirrorIfEnded(target.deviceId, error);
538
+ if (reason !== null) {
539
+ return { source: "session_ended", deviceId: target.deviceId, reason };
540
+ }
541
+ throw error;
542
+ }
543
+ }
544
+ peekRuntimeSession(deviceId) {
545
+ const session = this.runtimeSessions.get(deviceId);
546
+ if (session === undefined) {
547
+ return null;
548
+ }
549
+ return session;
550
+ }
551
+ recordSession(deviceId, sessionId, sessionTtlMs) {
552
+ const connectedAt = this.clock();
553
+ const session = {
554
+ deviceId,
555
+ sessionId,
556
+ sessionTtlMs,
557
+ connectedAt: connectedAt.toISOString(),
558
+ };
559
+ this.runtimeSessions.set(deviceId, session);
560
+ return session;
561
+ }
562
+ clearRuntimeSessionsAbsentFromLiveUsbScan(liveDeviceIds) {
563
+ for (const deviceId of this.runtimeSessions.keys()) {
564
+ if (!liveDeviceIds.has(deviceId)) {
565
+ this.clearRuntimeSessionMirror(deviceId);
566
+ }
567
+ }
568
+ }
569
+ clearRuntimeSessionMirror(deviceId) {
570
+ this.runtimeSessions.delete(deviceId);
571
+ }
572
+ clearRuntimeSessionMirrorIfEnded(deviceId, error) {
573
+ const reason = runtimeSessionMirrorEndReason(error);
574
+ if (reason !== null) {
575
+ this.clearRuntimeSessionMirror(deviceId);
576
+ }
577
+ return reason;
578
+ }
579
+ async resolveTargetDevice(input) {
580
+ if (input.purpose !== undefined && RESERVED_PURPOSES.has(input.purpose)) {
581
+ throw new AgentQError("reserved_purpose", `purpose '${input.purpose}' is reserved. Omit purpose to use the default device.`, false);
582
+ }
583
+ if (input.purpose !== undefined && !isValidPurpose(input.purpose)) {
584
+ throw new AgentQError("invalid_purpose", "purpose must be 1-32 characters of [A-Za-z0-9_.-].", false);
585
+ }
586
+ const config = await this.configStore.load();
587
+ let deviceId;
588
+ if (input.deviceId !== undefined && input.deviceId.length > 0) {
589
+ deviceId = input.deviceId;
590
+ }
591
+ else if (input.purpose !== undefined) {
592
+ // Own-property lookup: an inherited name must not resolve to a prototype value.
593
+ deviceId = Object.prototype.hasOwnProperty.call(config.activeDeviceIdsByPurpose, input.purpose)
594
+ ? config.activeDeviceIdsByPurpose[input.purpose]
595
+ : undefined;
596
+ }
597
+ else if (config.activeDeviceId !== null) {
598
+ deviceId = config.activeDeviceId;
599
+ }
600
+ if (deviceId === undefined || deviceId.length === 0) {
601
+ throw new AgentQError("no_active_device", "No active device is configured.", false);
602
+ }
603
+ const record = config.devices.find((candidate) => candidate.deviceId === deviceId);
604
+ if (record === undefined) {
605
+ throw new AgentQError("device_not_found", "Device is not known to Agent-Q.", true);
606
+ }
607
+ return { deviceId, record };
608
+ }
609
+ async findLivePortForDevice(record, deadlineMs) {
610
+ const scanResult = await scanUsbDeviceStatuses(this.usbDriver, deadlineMs);
611
+ const matching = scanResult.devices.find((candidate) => candidate.protocolResponse.device.deviceId === record.deviceId);
612
+ if (matching !== undefined) {
613
+ return matching;
614
+ }
615
+ const knownPortFailure = scanResult.failures.find((failure) => failure.portPath === record.lastPortHint);
616
+ if (knownPortFailure !== undefined) {
617
+ throw new AgentQError(knownPortFailure.unavailableReason, `Device ${record.deviceId} is not reachable on the last known port.`, true);
618
+ }
619
+ const knownPortExists = scanResult.ports.some((port) => port.path === record.lastPortHint);
620
+ if (!knownPortExists) {
621
+ throw new AgentQError("port_not_found", `Device ${record.deviceId} is not connected.`, true);
622
+ }
623
+ throw new AgentQError("handshake_failed", `Device ${record.deviceId} did not respond to a status handshake.`, true);
624
+ }
625
+ async getDeviceStatus(input = {}) {
626
+ rejectUnsupportedInputFields(input, DEVICE_SCOPED_INPUT_KEYS, "getDeviceStatus");
627
+ const deadlineMs = INTERNAL_USB_DEADLINE_MS;
628
+ const { record } = await this.resolveTargetDevice(input);
629
+ try {
630
+ const scanResult = await scanUsbDeviceStatuses(this.usbDriver, deadlineMs);
631
+ const matchingDevice = scanResult.devices.find((candidate) => candidate.protocolResponse.device.deviceId === record.deviceId);
632
+ if (matchingDevice !== undefined) {
633
+ await this.configStore.rememberUsbStatus(matchingDevice.protocolResponse, matchingDevice.portPath);
634
+ return toLiveStatus(matchingDevice);
635
+ }
636
+ const knownPortFailure = scanResult.failures.find((failure) => failure.portPath === record.lastPortHint);
637
+ if (knownPortFailure !== undefined) {
638
+ return toCachedStatus(record, knownPortFailure.unavailableReason, knownPortFailure.firmwareErrorCode);
639
+ }
640
+ const knownPortExists = scanResult.ports.some((port) => port.path === record.lastPortHint);
641
+ if (!knownPortExists) {
642
+ return toCachedStatus(record, "port_not_found");
643
+ }
644
+ return toCachedStatus(record, "handshake_failed");
645
+ }
646
+ catch (scanError) {
647
+ return toCachedStatus(record, mapErrorToUnavailableReason(scanError));
648
+ }
649
+ }
650
+ }
651
+ function toLiveStatus(liveDevice) {
652
+ return {
653
+ source: "live",
654
+ connected: true,
655
+ // portPath is an OS-supplied string; sanitize the copy that reaches MCP
656
+ // output. The raw path is still used for I/O elsewhere via UsbStatusResult.
657
+ portPath: sanitizePortHint(liveDevice.portPath),
658
+ protocolResponse: liveDevice.protocolResponse,
659
+ };
660
+ }
661
+ function toCachedStatus(record, unavailableReason, firmwareErrorCode) {
662
+ return {
663
+ source: "cached",
664
+ connected: false,
665
+ statusObservedAt: record.lastSeenAt,
666
+ unavailableReason,
667
+ // Normalize the Firmware diagnostic code to an allowlisted public code at the
668
+ // source, so the cached result is public-safe for any adapter.
669
+ ...(firmwareErrorCode === undefined ? {} : { firmwareErrorCode: normalizeErrorCode(firmwareErrorCode) }),
670
+ cachedStatus: record.lastStatus,
671
+ };
672
+ }
673
+ function toScanFailure(failure) {
674
+ return {
675
+ source: "error",
676
+ connected: false,
677
+ portPath: sanitizePortHint(failure.portPath),
678
+ unavailableReason: failure.unavailableReason,
679
+ ...(failure.firmwareErrorCode === undefined
680
+ ? {}
681
+ : { firmwareErrorCode: normalizeErrorCode(failure.firmwareErrorCode) }),
682
+ };
683
+ }
684
+ function validateClientName(value) {
685
+ if (value === undefined) {
686
+ return DEFAULT_CLIENT_NAME;
687
+ }
688
+ if (!isClientName(value)) {
689
+ throw new AgentQError("invalid_client_name", "clientName must be 1-64 printable ASCII characters.", false);
690
+ }
691
+ return value;
692
+ }
693
+ function validateSignHostInput(input) {
694
+ try {
695
+ return validateSignTransactionParams({
696
+ network: input.network,
697
+ txBytes: input.txBytes,
698
+ }, input.requestType);
699
+ }
700
+ catch (error) {
701
+ if (error instanceof ProtocolError) {
702
+ throw new AgentQError(error.code, error.message, false);
703
+ }
704
+ throw error;
705
+ }
706
+ }
707
+ function validateSignPersonalMessageHostInput(input) {
708
+ try {
709
+ return validateSignPersonalMessageParams({
710
+ network: input.network,
711
+ message: input.message,
712
+ }, input.requestType);
713
+ }
714
+ catch (error) {
715
+ if (error instanceof ProtocolError) {
716
+ throw new AgentQError(error.code, error.message, false);
717
+ }
718
+ throw error;
719
+ }
720
+ }
721
+ function identifySignHostRoute(operation, chain, method) {
722
+ try {
723
+ return identifySignRoute(operation, chain, method);
724
+ }
725
+ catch (error) {
726
+ if (error instanceof ProtocolError) {
727
+ throw new AgentQError(error.code, error.message, false);
728
+ }
729
+ throw error;
730
+ }
731
+ }
732
+ function toLiveSignResult(deviceId, response) {
733
+ if (response.status === "signed") {
734
+ return {
735
+ source: "live",
736
+ deviceId,
737
+ status: "signed",
738
+ authorization: response.authorization,
739
+ chain: response.chain,
740
+ method: response.method,
741
+ signature: response.signature,
742
+ ...(response.method === "sign_personal_message" ? { messageBytes: response.messageBytes } : {}),
743
+ };
744
+ }
745
+ if (response.status === "policy_rejected") {
746
+ return {
747
+ source: "live",
748
+ deviceId,
749
+ status: "policy_rejected",
750
+ authorization: "policy",
751
+ policyHash: response.policyHash,
752
+ ruleRef: response.ruleRef,
753
+ error: response.error,
754
+ };
755
+ }
756
+ return {
757
+ source: "live",
758
+ deviceId,
759
+ status: response.status,
760
+ authorization: response.authorization,
761
+ error: response.error,
762
+ };
763
+ }
764
+ const NO_INPUT_KEYS = new Set();
765
+ const DEVICE_SCOPED_INPUT_KEYS = new Set(["deviceId", "purpose"]);
766
+ const CONNECT_DEVICE_INPUT_KEYS = new Set(["deviceId", "purpose", "clientName"]);
767
+ const SET_DEVICE_METADATA_INPUT_KEYS = new Set(["deviceId", "label"]);
768
+ const GET_APPROVAL_HISTORY_INPUT_KEYS = new Set(["deviceId", "purpose", "limit", "beforeSeq"]);
769
+ const POLICY_PROPOSE_INPUT_KEYS = new Set(["deviceId", "purpose", "policy"]);
770
+ const SIGN_TRANSACTION_INPUT_KEYS = new Set(["deviceId", "purpose", "chain", "method", "network", "txBytes"]);
771
+ const SIGN_PERSONAL_MESSAGE_INPUT_KEYS = new Set(["deviceId", "purpose", "chain", "method", "network", "message"]);
772
+ function rejectUnsupportedInputFields(input, allowedKeys, inputName) {
773
+ if (typeof input !== "object" || input === null || Array.isArray(input)) {
774
+ return;
775
+ }
776
+ for (const key of Object.keys(input)) {
777
+ if (!allowedKeys.has(key)) {
778
+ throw new AgentQError("invalid_params", `${inputName} input contains unsupported fields.`, false);
779
+ }
780
+ }
781
+ }
782
+ function mapConfigError(error) {
783
+ if (error instanceof ConfigError) {
784
+ return new AgentQError(error.code, error.message, error.code === "device_not_found");
785
+ }
786
+ return toAgentQError(error);
787
+ }
788
+ function toRuntimeSessionView(session) {
789
+ if (session === null) {
790
+ return null;
791
+ }
792
+ return {
793
+ sessionTtlMs: session.sessionTtlMs,
794
+ connectedAt: session.connectedAt,
795
+ };
796
+ }
797
+ // Single owner of the session-clearing transport policy (see specs/PROTOCOL.md):
798
+ // these failures mean Agent-Q can no longer confirm the session, so it clears its
799
+ // local view to avoid reusing a session Firmware may have already dropped. The
800
+ // returned reason explains why; an unrecognized error returns null, so the caller
801
+ // rethrows and keeps the session.
802
+ function runtimeSessionMirrorEndReason(error) {
803
+ if (!(error instanceof AgentQError)) {
804
+ return null;
805
+ }
806
+ switch (error.code) {
807
+ case "invalid_session":
808
+ return "invalid_session";
809
+ case "timeout":
810
+ return "timeout";
811
+ case "port_not_found":
812
+ case "transport_closed":
813
+ case "port_in_use":
814
+ case "port_permission_denied":
815
+ return "transport_unavailable";
816
+ default:
817
+ return null;
818
+ }
819
+ }
820
+ function createUniqueIdentificationCode(usedCodes) {
821
+ for (let attempt = 0; attempt < 20; attempt += 1) {
822
+ const code = createIdentificationCode();
823
+ if (!usedCodes.has(code)) {
824
+ usedCodes.add(code);
825
+ return code;
826
+ }
827
+ }
828
+ for (let value = 0; value <= 9999; value += 1) {
829
+ const code = value.toString().padStart(4, "0");
830
+ if (!usedCodes.has(code)) {
831
+ usedCodes.add(code);
832
+ return code;
833
+ }
834
+ }
835
+ throw new AgentQError("identification_code_exhausted", "Could not create a unique identification code.", true);
836
+ }
837
+ export function createDefaultAgentQCore() {
838
+ return new AgentQCore(new ConfigStore(), new SerialPortUsbDriver());
839
+ }
840
+ //# sourceMappingURL=core.js.map