@robota-sdk/agent-transport 3.0.0-beta.78 → 3.0.0-beta.81

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 (111) hide show
  1. package/README.md +84 -26
  2. package/dist/browser/client.d.ts +2 -0
  3. package/dist/browser/client.js +1 -0
  4. package/dist/browser/index.d.ts +520 -0
  5. package/dist/browser/index.d.ts.map +1 -0
  6. package/dist/browser/index.js +2 -0
  7. package/dist/browser/index.js.map +1 -0
  8. package/dist/browser/message-decoders-BB8n-H2X.d.ts +252 -0
  9. package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +1 -0
  10. package/dist/browser/message-decoders-CDmDoZPR.js +2 -0
  11. package/dist/browser/message-decoders-CDmDoZPR.js.map +1 -0
  12. package/dist/node/index.cjs +1 -1
  13. package/dist/node/index.d.cts +766 -0
  14. package/dist/node/index.d.cts.map +1 -0
  15. package/dist/node/index.d.ts +765 -27
  16. package/dist/node/index.d.ts.map +1 -1
  17. package/dist/node/index.js +1 -1
  18. package/dist/node/index.js.map +1 -1
  19. package/dist/node/node.cjs +1 -0
  20. package/dist/node/node.d.cts +109 -0
  21. package/dist/node/node.d.cts.map +1 -0
  22. package/dist/node/node.d.ts +109 -0
  23. package/dist/node/node.d.ts.map +1 -0
  24. package/dist/node/node.js +2 -0
  25. package/dist/node/node.js.map +1 -0
  26. package/package.json +68 -47
  27. package/src/__tests__/access-token-verifier.test.ts +672 -0
  28. package/src/__tests__/admission.test.ts +130 -0
  29. package/src/__tests__/channel-frames.test.ts +214 -0
  30. package/src/__tests__/driver-attribution-capability.test.ts +83 -0
  31. package/src/__tests__/handoff-chunking.test.ts +215 -0
  32. package/src/__tests__/handoff-manifest.test.ts +52 -0
  33. package/src/__tests__/message-decoders.test.ts +249 -0
  34. package/src/__tests__/outbound-delivery.test.ts +558 -0
  35. package/src/__tests__/peer-message-ledger.test.ts +200 -0
  36. package/src/__tests__/personal-usage-report.test.ts +193 -0
  37. package/src/__tests__/resume-buffer.test.ts +93 -0
  38. package/src/__tests__/session-event-delivery.test.ts +101 -0
  39. package/src/__tests__/session-message-handler-e5.test.ts +99 -0
  40. package/src/__tests__/session-message-handler.test.ts +590 -0
  41. package/src/__tests__/session-resume-bridge.test.ts +406 -0
  42. package/src/__tests__/submit-prompt-shape.test.ts +78 -0
  43. package/src/__tests__/usage-report-carrier.test.ts +98 -0
  44. package/src/__tests__/ws-broadcast-events.test.ts +84 -0
  45. package/src/background-messages.ts +169 -0
  46. package/src/channel-frames.ts +182 -0
  47. package/src/client.ts +9 -0
  48. package/src/handoff-chunking.ts +235 -0
  49. package/src/index.ts +112 -3
  50. package/src/message-decoders.ts +229 -0
  51. package/src/message-parser.ts +33 -0
  52. package/src/node/access-token-verifier.ts +363 -0
  53. package/src/node/admission.ts +153 -0
  54. package/src/node/handoff-manifest.ts +77 -0
  55. package/src/node/index.ts +10 -0
  56. package/src/outbound-delivery.ts +231 -0
  57. package/src/peer-message-ledger.ts +193 -0
  58. package/src/protocol-session.ts +28 -0
  59. package/src/resume-buffer.ts +123 -0
  60. package/src/session-events.ts +198 -0
  61. package/src/session-message-handler.ts +282 -0
  62. package/src/session-query-messages.ts +42 -0
  63. package/src/session-resume-bridge.ts +252 -0
  64. package/src/usage-messages.ts +195 -0
  65. package/src/wire-messages.ts +165 -0
  66. package/dist/node/headless/index.cjs +0 -1
  67. package/dist/node/headless/index.d.ts +0 -2
  68. package/dist/node/headless/index.js +0 -1
  69. package/dist/node/headless-DG_MEbf0.js +0 -19
  70. package/dist/node/headless-DG_MEbf0.js.map +0 -1
  71. package/dist/node/headless-xq77JBq0.cjs +0 -18
  72. package/dist/node/index-CrJ2Lhn3.d.ts +0 -96
  73. package/dist/node/index-CrJ2Lhn3.d.ts.map +0 -1
  74. package/dist/node/index-JIvzrtaA.d.ts +0 -52
  75. package/dist/node/index-JIvzrtaA.d.ts.map +0 -1
  76. package/dist/node/index-JTD_xtEQ.d.ts +0 -96
  77. package/dist/node/index-JTD_xtEQ.d.ts.map +0 -1
  78. package/dist/node/index-y_HBOfzO.d.ts +0 -52
  79. package/dist/node/index-y_HBOfzO.d.ts.map +0 -1
  80. package/dist/node/programmatic/index.cjs +0 -1
  81. package/dist/node/programmatic/index.d.ts +0 -2
  82. package/dist/node/programmatic/index.js +0 -1
  83. package/dist/node/programmatic-6wLf68qJ.cjs +0 -1
  84. package/dist/node/programmatic-70dxIXtS.js +0 -2
  85. package/dist/node/programmatic-70dxIXtS.js.map +0 -1
  86. package/dist/node/testing/index.cjs +0 -1
  87. package/dist/node/testing/index.d.ts +0 -2
  88. package/dist/node/testing/index.js +0 -1
  89. package/src/__tests__/programmatic/programmatic-ask-user.test.ts +0 -32
  90. package/src/__tests__/programmatic/programmatic-driver.test.ts +0 -107
  91. package/src/__tests__/programmatic/programmatic-flows.test.ts +0 -140
  92. package/src/__tests__/transport-registry.stop-all.test.ts +0 -53
  93. package/src/headless/HeadlessInteractionChannel.ts +0 -129
  94. package/src/headless/__tests__/headless-channel-options.test.ts +0 -106
  95. package/src/headless/__tests__/headless-goal-runner.test.ts +0 -108
  96. package/src/headless/__tests__/headless-provider-failure.integration.test.ts +0 -143
  97. package/src/headless/__tests__/headless-runner-initialization.test.ts +0 -45
  98. package/src/headless/__tests__/headless-runner.test.ts +0 -508
  99. package/src/headless/__tests__/headless-skill-activation.integration.test.ts +0 -430
  100. package/src/headless/__tests__/headless-transport.test.ts +0 -267
  101. package/src/headless/cli-input.ts +0 -50
  102. package/src/headless/headless-runner.ts +0 -230
  103. package/src/headless/headless-stream-json.ts +0 -148
  104. package/src/headless/headless-transport.ts +0 -43
  105. package/src/headless/index.ts +0 -8
  106. package/src/headless/print-terminal.ts +0 -57
  107. package/src/programmatic/ProgrammaticInteractionChannel.ts +0 -82
  108. package/src/programmatic/createProgrammaticAgent.ts +0 -71
  109. package/src/programmatic/index.ts +0 -5
  110. package/src/testing/index.ts +0 -10
  111. package/src/transport-registry.ts +0 -103
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { sealHandoffRecord, verifyHandoffPayload } from '../node/handoff-manifest.js';
4
+
5
+ import type { IInteractiveSessionRecord } from '@robota-sdk/agent-interface-session';
6
+
7
+ function aRecord(over: Partial<IInteractiveSessionRecord> = {}): IInteractiveSessionRecord {
8
+ return {
9
+ id: 'session_1',
10
+ cwd: '/home/alice/project',
11
+ createdAt: '2026-08-17T00:00:00.000Z',
12
+ updatedAt: '2026-08-17T01:00:00.000Z',
13
+ messages: [],
14
+ ...over,
15
+ };
16
+ }
17
+
18
+ describe('HANDOFF-001 TC-06 — a payload that did not arrive whole is refused', () => {
19
+ it('accepts the exact bytes that were sealed', () => {
20
+ const { serialized, integrity } = sealHandoffRecord(aRecord());
21
+
22
+ expect(verifyHandoffPayload(serialized, integrity).intact).toBe(true);
23
+ });
24
+
25
+ it('reports a truncated payload as truncated, not as corruption', () => {
26
+ // Checked BEFORE the digest. Hashing a short buffer to discover it was short wastes the work
27
+ // and reports a dropped connection as tampering.
28
+ const { serialized, integrity } = sealHandoffRecord(aRecord());
29
+
30
+ const verdict = verifyHandoffPayload(serialized.slice(0, -20), integrity);
31
+
32
+ expect(verdict.failure).toBe('truncated');
33
+ expect(verdict.expectedBytes).toBe(integrity.byteLength);
34
+ expect(verdict.actualBytes).toBeLessThan(integrity.byteLength);
35
+ });
36
+
37
+ it('reports a same-length substitution as a digest mismatch', () => {
38
+ // The case length alone cannot catch, which is why the digest is there at all.
39
+ const { serialized, integrity } = sealHandoffRecord(aRecord());
40
+ const tampered = serialized.replace('/home/alice/project', '/home/mallor/projec');
41
+
42
+ expect(tampered).toHaveLength(serialized.length);
43
+ expect(verifyHandoffPayload(tampered, integrity).failure).toBe('digest-mismatch');
44
+ });
45
+
46
+ it('two different records do not seal to the same digest', () => {
47
+ const a = sealHandoffRecord(aRecord());
48
+ const b = sealHandoffRecord(aRecord({ cwd: '/elsewhere' }));
49
+
50
+ expect(a.integrity.digest).not.toBe(b.integrity.digest);
51
+ });
52
+ });
@@ -0,0 +1,249 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ CLIENT_MESSAGE_SHAPES,
5
+ MAX_INBOUND_FRAME_BYTES,
6
+ SERVER_MESSAGE_SHAPES,
7
+ decodeClientMessage,
8
+ decodeFrame,
9
+ decodeServerMessage,
10
+ } from '../message-decoders.js';
11
+
12
+ import type { TClientMessage, TServerMessage } from '../wire-messages.js';
13
+
14
+ /**
15
+ * Issue #2045 — the owner-side runtime decoders. One well-formed sample per union variant round-trips
16
+ * through JSON; a shared malformed corpus (null, arrays, missing fields, wrong primitives, malformed
17
+ * nested values, oversized frames) is refused consistently. Both carriers call these same functions.
18
+ */
19
+ const CLIENT_SAMPLES: Readonly<Record<TClientMessage['type'], TClientMessage>> = {
20
+ submit: { type: 'submit', prompt: 'hi' },
21
+ command: { type: 'command', name: 'help', args: 'x' },
22
+ abort: { type: 'abort' },
23
+ 'cancel-queue': { type: 'cancel-queue' },
24
+ 'get-messages': { type: 'get-messages' },
25
+ 'get-context': { type: 'get-context' },
26
+ 'get-usage-report': { type: 'get-usage-report' },
27
+ 'get-personal-usage-report': {
28
+ type: 'get-personal-usage-report',
29
+ requestId: 'request-1',
30
+ period: '7d',
31
+ timezone: 'UTC',
32
+ },
33
+ 'get-stored-session-usage-report': {
34
+ type: 'get-stored-session-usage-report',
35
+ requestId: 'request-2',
36
+ sessionId: 'session-1',
37
+ },
38
+ 'get-executing': { type: 'get-executing' },
39
+ 'get-pending': { type: 'get-pending' },
40
+ 'get-execution-workspace': { type: 'get-execution-workspace' },
41
+ 'get-background-tasks': {
42
+ type: 'get-background-tasks',
43
+ filter: { kind: 'agent', includeClosed: true },
44
+ },
45
+ 'get-background-task': { type: 'get-background-task', taskId: 't' },
46
+ 'get-background-job-groups': { type: 'get-background-job-groups' },
47
+ 'get-background-job-group': { type: 'get-background-job-group', groupId: 'g' },
48
+ 'wait-background-job-group': { type: 'wait-background-job-group', groupId: 'g' },
49
+ 'cancel-background-task': { type: 'cancel-background-task', taskId: 't', reason: 'r' },
50
+ 'close-background-task': { type: 'close-background-task', taskId: 't' },
51
+ 'send-background-task': { type: 'send-background-task', taskId: 't', input: { prompt: 'p' } },
52
+ 'read-background-task-log': {
53
+ type: 'read-background-task-log',
54
+ taskId: 't',
55
+ cursor: { offset: 3 },
56
+ },
57
+ 'permission-response': { type: 'permission-response', id: 'p1', result: 'allow-session' },
58
+ 'ask-response': { type: 'ask-response', id: 'a1', response: { type: 'answer', values: ['y'] } },
59
+ resume: { type: 'resume', lastSeq: 4 },
60
+ ack: { type: 'ack', seq: 9 },
61
+ };
62
+
63
+ const SERVER_SAMPLES: Readonly<Record<TServerMessage['type'], TServerMessage>> = {
64
+ text_delta: { type: 'text_delta', delta: 'd', driverId: 'drv' },
65
+ user_message: { type: 'user_message', content: 'c' },
66
+ tool_start: { type: 'tool_start', state: {} as never },
67
+ tool_end: { type: 'tool_end', state: {} as never },
68
+ thinking: { type: 'thinking', isThinking: true },
69
+ complete: { type: 'complete', result: {} as never },
70
+ interrupted: { type: 'interrupted', result: {} as never },
71
+ error: { type: 'error', message: 'm' },
72
+ command_result: { type: 'command_result', name: 'n', message: 'm', success: true },
73
+ messages: { type: 'messages', messages: [] },
74
+ context: { type: 'context', state: {} as never },
75
+ usage_report: { type: 'usage_report', report: {} as never },
76
+ personal_usage_report: {
77
+ type: 'personal_usage_report',
78
+ requestId: 'request-1',
79
+ report: {} as never,
80
+ },
81
+ personal_usage_report_error: {
82
+ type: 'personal_usage_report_error',
83
+ requestId: 'request-1',
84
+ code: 'not_available',
85
+ message: 'not available',
86
+ },
87
+ stored_session_usage_report: {
88
+ type: 'stored_session_usage_report',
89
+ requestId: 'request-2',
90
+ sessionId: 'session-1',
91
+ report: {} as never,
92
+ },
93
+ stored_session_usage_report_error: {
94
+ type: 'stored_session_usage_report_error',
95
+ requestId: 'request-2',
96
+ sessionId: 'session-1',
97
+ code: 'report_failed',
98
+ message: 'not found',
99
+ },
100
+ executing: { type: 'executing', executing: false },
101
+ pending: { type: 'pending', pending: null },
102
+ execution_workspace_event: { type: 'execution_workspace_event', snapshot: {} as never },
103
+ background_task_event: { type: 'background_task_event', event: {} as never },
104
+ background_job_group_event: { type: 'background_job_group_event', event: {} as never },
105
+ plan_event: { type: 'plan_event', event: {} as never },
106
+ context_file_refreshed: { type: 'context_file_refreshed', event: {} as never },
107
+ branch_event: { type: 'branch_event', event: {} as never },
108
+ background_tasks: { type: 'background_tasks', tasks: [] },
109
+ background_task: { type: 'background_task', taskId: 't', task: null },
110
+ background_job_groups: { type: 'background_job_groups', groups: [] },
111
+ background_job_group: { type: 'background_job_group', groupId: 'g', group: null },
112
+ background_task_log: { type: 'background_task_log', taskId: 't', page: {} as never },
113
+ permission_request: { type: 'permission_request', event: {} as never },
114
+ ask_request: { type: 'ask_request', event: {} as never },
115
+ prompt_resolved: { type: 'prompt_resolved', event: {} as never },
116
+ ui_intent: { type: 'ui_intent', event: {} as never },
117
+ session_renamed: { type: 'session_renamed', event: {} as never },
118
+ history_cleared: { type: 'history_cleared' },
119
+ background_task_control_result: {
120
+ type: 'background_task_control_result',
121
+ action: 'cancel',
122
+ taskId: 't',
123
+ success: true,
124
+ },
125
+ protocol_error: { type: 'protocol_error', message: 'm' },
126
+ resume_gap: { type: 'resume_gap' },
127
+ };
128
+
129
+ const MALFORMED_CLIENT: ReadonlyArray<[string, unknown]> = [
130
+ ['null', null],
131
+ ['an array', [{ type: 'abort' }]],
132
+ ['a string', 'abort'],
133
+ ['no type', { prompt: 'x' }],
134
+ ['a numeric type', { type: 1 }],
135
+ ['an unknown type', { type: 'nope' }],
136
+ ['a prototype-only type', { type: 'toString' }],
137
+ ['submit without prompt', { type: 'submit' }],
138
+ ['submit with an empty prompt', { type: 'submit', prompt: '' }],
139
+ ['submit with an array prompt', { type: 'submit', prompt: ['x'] }],
140
+ ['command with numeric args', { type: 'command', name: 'n', args: 1 }],
141
+ ['get-background-tasks with an array filter', { type: 'get-background-tasks', filter: [] }],
142
+ [
143
+ 'get-background-tasks with an unknown kind',
144
+ { type: 'get-background-tasks', filter: { kind: 'x' } },
145
+ ],
146
+ [
147
+ 'send-background-task with a string input',
148
+ { type: 'send-background-task', taskId: 't', input: 'p' },
149
+ ],
150
+ [
151
+ 'send-background-task with a numeric prompt',
152
+ { type: 'send-background-task', taskId: 't', input: { prompt: 1 } },
153
+ ],
154
+ [
155
+ 'read-background-task-log with a string offset',
156
+ { type: 'read-background-task-log', taskId: 't', cursor: { offset: '3' } },
157
+ ],
158
+ [
159
+ 'permission-response with a string result',
160
+ { type: 'permission-response', id: 'p', result: 'yes' },
161
+ ],
162
+ [
163
+ 'ask-response with values not strings',
164
+ { type: 'ask-response', id: 'a', response: { type: 'answer', values: [1] } },
165
+ ],
166
+ [
167
+ 'ask-response cancelled carrying values',
168
+ { type: 'ask-response', id: 'a', response: { type: 'cancelled', values: [] } },
169
+ ],
170
+ ['resume with a string lastSeq', { type: 'resume', lastSeq: '4' }],
171
+ ['ack with NaN', { type: 'ack', seq: Number.NaN }],
172
+ ['cancel-background-task with an empty taskId', { type: 'cancel-background-task', taskId: '' }],
173
+ ];
174
+
175
+ const MALFORMED_SERVER: ReadonlyArray<[string, unknown]> = [
176
+ ['null', null],
177
+ ['an array', [{ type: 'resume_gap' }]],
178
+ ['an unknown type', { type: 'nope' }],
179
+ ['text_delta without delta', { type: 'text_delta' }],
180
+ ['text_delta with a numeric driverId', { type: 'text_delta', delta: 'd', driverId: 1 }],
181
+ ['tool_start with an array state', { type: 'tool_start', state: [] }],
182
+ ['thinking with a string flag', { type: 'thinking', isThinking: 'yes' }],
183
+ ['messages with a non-array', { type: 'messages', messages: {} }],
184
+ ['messages with a primitive entry', { type: 'messages', messages: ['x'] }],
185
+ ['pending with a number', { type: 'pending', pending: 1 }],
186
+ ['background_task with an array task', { type: 'background_task', taskId: 't', task: [] }],
187
+ [
188
+ 'control result with an unknown action',
189
+ { type: 'background_task_control_result', action: 'nuke', taskId: 't', success: true },
190
+ ],
191
+ ['command_result without success', { type: 'command_result', name: 'n', message: 'm' }],
192
+ ];
193
+
194
+ describe('decodeClientMessage (issue #2045)', () => {
195
+ it('has a shape for every union variant and every sample decodes through JSON', () => {
196
+ for (const [type, sample] of Object.entries(CLIENT_SAMPLES)) {
197
+ expect(CLIENT_MESSAGE_SHAPES).toHaveProperty(type);
198
+ expect(decodeClientMessage(JSON.parse(JSON.stringify(sample)))).toEqual({
199
+ ok: true,
200
+ message: sample,
201
+ });
202
+ }
203
+ });
204
+
205
+ it.each(MALFORMED_CLIENT)('refuses %s', (_label, value) => {
206
+ expect(decodeClientMessage(value).ok).toBe(false);
207
+ });
208
+
209
+ it('keeps the protocol wording for a bad submit prompt', () => {
210
+ expect(decodeClientMessage({ type: 'submit', prompt: 1 })).toEqual({
211
+ ok: false,
212
+ reason: 'prompt must be a non-empty string',
213
+ });
214
+ });
215
+ });
216
+
217
+ describe('decodeServerMessage (issue #2045)', () => {
218
+ it('has a shape for every union variant and every sample decodes through JSON', () => {
219
+ for (const [type, sample] of Object.entries(SERVER_SAMPLES)) {
220
+ expect(SERVER_MESSAGE_SHAPES).toHaveProperty(type);
221
+ expect(decodeServerMessage(JSON.parse(JSON.stringify(sample)))).toEqual({
222
+ ok: true,
223
+ message: sample,
224
+ });
225
+ }
226
+ });
227
+
228
+ it.each(MALFORMED_SERVER)('refuses %s', (_label, value) => {
229
+ expect(decodeServerMessage(value).ok).toBe(false);
230
+ });
231
+ });
232
+
233
+ describe('decodeFrame — the one carrier path', () => {
234
+ it('refuses invalid JSON with the protocol wording and an oversized frame before parsing', () => {
235
+ expect(decodeFrame('not json', decodeClientMessage)).toEqual({
236
+ ok: false,
237
+ reason: 'Invalid JSON',
238
+ });
239
+ const huge = `{"type":"submit","prompt":"${'x'.repeat(MAX_INBOUND_FRAME_BYTES)}"}`;
240
+ expect(decodeFrame(huge, decodeClientMessage)).toEqual({
241
+ ok: false,
242
+ reason: 'Frame too large',
243
+ });
244
+ expect(decodeFrame('{"type":"abort"}', decodeClientMessage)).toEqual({
245
+ ok: true,
246
+ message: { type: 'abort' },
247
+ });
248
+ });
249
+ });