@robota-sdk/agent-transport 3.0.0-beta.100

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 (93) hide show
  1. package/CHANGELOG.md +1137 -0
  2. package/LICENSE +661 -0
  3. package/README.md +121 -0
  4. package/dist/browser/client.d.ts +2 -0
  5. package/dist/browser/client.js +1 -0
  6. package/dist/browser/index.d.ts +624 -0
  7. package/dist/browser/index.d.ts.map +1 -0
  8. package/dist/browser/index.js +2 -0
  9. package/dist/browser/index.js.map +1 -0
  10. package/dist/browser/observer-messages-4fRQZU2D.d.ts +499 -0
  11. package/dist/browser/observer-messages-4fRQZU2D.d.ts.map +1 -0
  12. package/dist/browser/observer-messages-O8jTPEQO.js +2 -0
  13. package/dist/browser/observer-messages-O8jTPEQO.js.map +1 -0
  14. package/dist/node/index.cjs +1 -0
  15. package/dist/node/index.d.cts +1115 -0
  16. package/dist/node/index.d.cts.map +1 -0
  17. package/dist/node/index.d.ts +1115 -0
  18. package/dist/node/index.d.ts.map +1 -0
  19. package/dist/node/index.js +2 -0
  20. package/dist/node/index.js.map +1 -0
  21. package/dist/node/node.cjs +1 -0
  22. package/dist/node/node.d.cts +317 -0
  23. package/dist/node/node.d.cts.map +1 -0
  24. package/dist/node/node.d.ts +317 -0
  25. package/dist/node/node.d.ts.map +1 -0
  26. package/dist/node/node.js +2 -0
  27. package/dist/node/node.js.map +1 -0
  28. package/package.json +122 -0
  29. package/src/__tests__/__fixtures__/korean-text.json +6 -0
  30. package/src/__tests__/access-token-verifier.test.ts +672 -0
  31. package/src/__tests__/admission.test.ts +130 -0
  32. package/src/__tests__/app-tool-messages.test.ts +463 -0
  33. package/src/__tests__/bearer-resource-gate.test.ts +120 -0
  34. package/src/__tests__/channel-frames.test.ts +215 -0
  35. package/src/__tests__/driver-attribution-capability.test.ts +83 -0
  36. package/src/__tests__/file-transfer.test.ts +281 -0
  37. package/src/__tests__/handoff-chunking.test.ts +261 -0
  38. package/src/__tests__/handoff-manifest.test.ts +52 -0
  39. package/src/__tests__/history-attached-budget.test.ts +216 -0
  40. package/src/__tests__/message-decoders.test.ts +870 -0
  41. package/src/__tests__/outbound-delivery.test.ts +558 -0
  42. package/src/__tests__/peer-message-ledger.test.ts +200 -0
  43. package/src/__tests__/permission-summary.test.ts +18 -0
  44. package/src/__tests__/personal-usage-report.test.ts +193 -0
  45. package/src/__tests__/project-read-messages.test.ts +118 -0
  46. package/src/__tests__/resume-buffer.test.ts +93 -0
  47. package/src/__tests__/session-detail-loop-status.test.ts +169 -0
  48. package/src/__tests__/session-directory-messages.test.ts +322 -0
  49. package/src/__tests__/session-error-classification.test.ts +113 -0
  50. package/src/__tests__/session-event-delivery.test.ts +101 -0
  51. package/src/__tests__/session-history-wire.test.ts +364 -0
  52. package/src/__tests__/session-message-handler-e5.test.ts +99 -0
  53. package/src/__tests__/session-message-handler-observe.test.ts +223 -0
  54. package/src/__tests__/session-message-handler.test.ts +726 -0
  55. package/src/__tests__/session-resume-bridge.test.ts +472 -0
  56. package/src/__tests__/settings-messages.test.ts +193 -0
  57. package/src/__tests__/stdio-carrier.test.ts +85 -0
  58. package/src/__tests__/submit-prompt-shape.test.ts +78 -0
  59. package/src/__tests__/usage-report-carrier.test.ts +98 -0
  60. package/src/__tests__/ws-broadcast-events.test.ts +94 -0
  61. package/src/app-tool-messages.ts +140 -0
  62. package/src/background-messages.ts +169 -0
  63. package/src/channel-frames.ts +182 -0
  64. package/src/client.ts +17 -0
  65. package/src/handoff-chunking.ts +275 -0
  66. package/src/index.ts +135 -0
  67. package/src/loop-control-messages.ts +34 -0
  68. package/src/message-decoders.ts +485 -0
  69. package/src/message-parser.ts +33 -0
  70. package/src/node/access-token-verifier.ts +363 -0
  71. package/src/node/admission.ts +153 -0
  72. package/src/node/bearer-resource-gate.ts +331 -0
  73. package/src/node/file-transfer.ts +530 -0
  74. package/src/node/handoff-manifest.ts +77 -0
  75. package/src/node/index.ts +45 -0
  76. package/src/node/stdio-carrier.ts +59 -0
  77. package/src/observer-messages.ts +47 -0
  78. package/src/open-prompts.ts +76 -0
  79. package/src/outbound-delivery.ts +240 -0
  80. package/src/peer-message-ledger.ts +193 -0
  81. package/src/permission-summary.ts +18 -0
  82. package/src/project-read-messages.ts +74 -0
  83. package/src/protocol-session.ts +56 -0
  84. package/src/resume-buffer.ts +123 -0
  85. package/src/session-directory-messages.ts +162 -0
  86. package/src/session-error-classification.ts +91 -0
  87. package/src/session-events.ts +290 -0
  88. package/src/session-message-handler.ts +413 -0
  89. package/src/session-query-messages.ts +160 -0
  90. package/src/session-resume-bridge.ts +275 -0
  91. package/src/settings-messages.ts +103 -0
  92. package/src/usage-messages.ts +195 -0
  93. package/src/wire-messages.ts +436 -0
@@ -0,0 +1,223 @@
1
+ /**
2
+ * The read-only `observe` role: an observer reads the session's own conversation and state, never
3
+ * counts as a surface that can answer a prompt, and cannot submit, answer, or control anything.
4
+ */
5
+
6
+ import { createTestInteractiveSession } from '@robota-sdk/agent-interface-session/testing';
7
+ import { describe, expect, it, vi } from 'vitest';
8
+
9
+ import {
10
+ ATTACHED_SURFACE_MAX_PENDING_BYTES,
11
+ createOutboundDelivery,
12
+ } from '../outbound-delivery.js';
13
+ import { createSessionMessageHandler } from '../session-message-handler.js';
14
+
15
+ import type { TClientMessage, TServerMessage } from '../wire-messages.js';
16
+ import type { IInteractiveSession } from '@robota-sdk/agent-interface-session';
17
+
18
+ type TListener = (...args: unknown[]) => void;
19
+
20
+ function createSession(): IInteractiveSession & {
21
+ emit: (event: string, ...args: unknown[]) => void;
22
+ listenerCount: (event: string) => number;
23
+ // #3282 §4c: `Partial<ISessionProjectRead>` — not part of `IInteractiveSession`, so it is assigned
24
+ // ad hoc (below) rather than passed to `createTestInteractiveSession`'s strict literal.
25
+ readProjectStatus: ReturnType<typeof vi.fn>;
26
+ } {
27
+ const listeners = new Map<string, Set<TListener>>();
28
+ const session = createTestInteractiveSession({
29
+ submit: vi.fn(),
30
+ abort: vi.fn(),
31
+ cancelQueue: vi.fn(),
32
+ executeCommand: vi.fn(),
33
+ resolvePermission: vi.fn(),
34
+ resolveAsk: vi.fn(),
35
+ cancelBackgroundTask: vi.fn(),
36
+ closeBackgroundTask: vi.fn(),
37
+ sendBackgroundTask: vi.fn(),
38
+ invokeRuntimeTool: vi.fn(),
39
+ readToolAppView: vi.fn(),
40
+ getMessages: vi.fn().mockReturnValue([{ role: 'user', content: 'hi' }]),
41
+ isExecuting: vi.fn().mockReturnValue(true),
42
+ on: ((event: string, handler: TListener) => {
43
+ if (!listeners.has(event)) listeners.set(event, new Set());
44
+ listeners.get(event)!.add(handler);
45
+ }) as IInteractiveSession['on'],
46
+ off: ((event: string, handler: TListener) => {
47
+ listeners.get(event)?.delete(handler);
48
+ }) as IInteractiveSession['off'],
49
+ });
50
+ return Object.assign(session, {
51
+ readProjectStatus: vi.fn().mockResolvedValue({ kind: 'not-a-repository' }),
52
+ emit: (event: string, ...args: unknown[]) => {
53
+ listeners.get(event)?.forEach((handler) => handler(...args));
54
+ },
55
+ listenerCount: (event: string) => listeners.get(event)?.size ?? 0,
56
+ });
57
+ }
58
+
59
+ function attach(
60
+ session: IInteractiveSession,
61
+ role?: 'drive' | 'observe',
62
+ ): { sent: TServerMessage[]; send: (message: TClientMessage) => void; cleanup: () => void } {
63
+ const sent: TServerMessage[] = [];
64
+ const { onMessage, cleanup } = createSessionMessageHandler({
65
+ session,
66
+ deliver: createOutboundDelivery((message) => sent.push(message), vi.fn()),
67
+ driverId: role === 'observe' ? 'attach:2' : 'attach:1',
68
+ ...(role === undefined ? {} : { role }),
69
+ });
70
+ return { sent, send: (message) => onMessage(JSON.stringify(message)), cleanup };
71
+ }
72
+
73
+ const MUTATING: readonly TClientMessage[] = [
74
+ { type: 'submit', prompt: 'hello' },
75
+ { type: 'command', name: 'exit' },
76
+ { type: 'abort' },
77
+ { type: 'cancel-queue' },
78
+ { type: 'cancel-background-task', taskId: 't1' },
79
+ { type: 'close-background-task', taskId: 't1' },
80
+ { type: 'send-background-task', taskId: 't1', input: { prompt: 'x' } },
81
+ { type: 'permission-response', id: 'p1', result: true },
82
+ { type: 'ask-response', id: 'a1', response: { type: 'answer', values: ['React'] } } as TClientMessage,
83
+ { type: 'get-personal-usage-report', requestId: 'r1', period: '7d', timezone: 'UTC' },
84
+ { type: 'get-stored-session-usage-report', requestId: 'r2', sessionId: 'other' },
85
+ { type: 'call-app-tool', requestId: 'r3', viewId: 'view-1', name: 'Write', arguments: {} },
86
+ { type: 'read-app-view', requestId: 'r4', viewId: 'view-1' },
87
+ ];
88
+
89
+ describe('observe role', () => {
90
+ it('refuses every mutating or cross-session message with protocol_error and touches nothing', () => {
91
+ const session = createSession();
92
+ const observer = attach(session, 'observe');
93
+ for (const message of MUTATING) {
94
+ observer.sent.length = 0;
95
+ observer.send(message);
96
+ expect(observer.sent).toEqual([
97
+ { type: 'protocol_error', message: `Not permitted for an observer: ${message.type}` },
98
+ ]);
99
+ }
100
+ expect(session.submit).not.toHaveBeenCalled();
101
+ expect(session.executeCommand).not.toHaveBeenCalled();
102
+ expect(session.abort).not.toHaveBeenCalled();
103
+ expect(session.cancelQueue).not.toHaveBeenCalled();
104
+ expect(session.cancelBackgroundTask).not.toHaveBeenCalled();
105
+ expect(session.closeBackgroundTask).not.toHaveBeenCalled();
106
+ expect(session.sendBackgroundTask).not.toHaveBeenCalled();
107
+ expect(session.resolvePermission).not.toHaveBeenCalled();
108
+ expect(session.resolveAsk).not.toHaveBeenCalled();
109
+ expect(session.invokeRuntimeTool).not.toHaveBeenCalled();
110
+ expect(session.readToolAppView).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('keeps the read queries for this session', () => {
114
+ const session = createSession();
115
+ const observer = attach(session, 'observe');
116
+ observer.send({ type: 'get-messages' });
117
+ observer.send({ type: 'get-executing' });
118
+ expect(observer.sent).toEqual([
119
+ // #3288 §2: `display` rides the same frame (the test double's default projects to `[]`).
120
+ { type: 'messages', messages: [{ role: 'user', content: 'hi' }], display: [] },
121
+ { type: 'executing', executing: true },
122
+ ]);
123
+ });
124
+
125
+ it('#3282 §4c: reads the Project panel status — read-only, so an observer may too', async () => {
126
+ const session = createSession();
127
+ const observer = attach(session, 'observe');
128
+ observer.send({ type: 'project-status', requestId: 'ps-1' });
129
+ await Promise.resolve();
130
+ expect(observer.sent).toEqual([
131
+ { type: 'project_status', requestId: 'ps-1', result: { kind: 'not-a-repository' } },
132
+ ]);
133
+ });
134
+
135
+ it('#3282 §4: reads the agent switcher roster, a query, never a write', () => {
136
+ const session = createSession();
137
+ const observer = attach(session, 'observe');
138
+ observer.send({ type: 'get-agent-definitions', requestId: 'req-agents-1' });
139
+ expect(observer.sent).toEqual([
140
+ { type: 'agent_definitions', requestId: 'req-agents-1', agents: [], current: 'general-purpose' },
141
+ ]);
142
+ });
143
+
144
+ it('#3186: reads the session status and the command catalog, which every client shows', () => {
145
+ const session = createSession();
146
+ const observer = attach(session, 'observe');
147
+ observer.send({ type: 'get-status' });
148
+ observer.send({ type: 'get-commands' });
149
+ expect(observer.sent).toEqual([
150
+ { type: 'session_status', status: session.getStatusSnapshot() },
151
+ { type: 'commands', commands: [], skills: [] },
152
+ ]);
153
+ });
154
+
155
+ it('never subscribes to prompt events, so it does not count as a surface that can answer', () => {
156
+ const session = createSession();
157
+ const observer = attach(session, 'observe');
158
+ expect(session.listenerCount('permission_request')).toBe(0);
159
+ expect(session.listenerCount('ask_request')).toBe(0);
160
+ expect(session.listenerCount('prompt_resolved')).toBe(1);
161
+ expect(session.listenerCount('text_delta')).toBe(1);
162
+ session.emit('permission_request', { id: 'p1', toolName: 'Bash', toolArgs: {} });
163
+ session.emit('ask_request', { id: 'a1', request: { id: 'r', title: 'Pick' } });
164
+ session.emit('prompt_resolved', { id: 'p1' });
165
+ expect(observer.sent).toEqual([{ type: 'prompt_resolved', event: { id: 'p1' } }]);
166
+ observer.cleanup();
167
+ expect(session.listenerCount('prompt_resolved')).toBe(0);
168
+ });
169
+
170
+ it('cannot answer a prompt that a drive surface holds', () => {
171
+ const session = createSession();
172
+ const drive = attach(session, 'drive');
173
+ const observer = attach(session, 'observe');
174
+ session.emit('permission_request', { id: 'p1', toolName: 'Bash', toolArgs: {} });
175
+ expect(drive.sent).toContainEqual({
176
+ type: 'permission_request',
177
+ event: { id: 'p1', toolName: 'Bash', toolArgs: {} },
178
+ });
179
+ observer.send({ type: 'permission-response', id: 'p1', result: true });
180
+ expect(session.resolvePermission).not.toHaveBeenCalled();
181
+ drive.send({ type: 'permission-response', id: 'p1', result: false });
182
+ expect(session.resolvePermission).toHaveBeenCalledExactlyOnceWith('p1', false, 'attach:1');
183
+ });
184
+
185
+ it('leaves the drive role unchanged', () => {
186
+ const session = createSession();
187
+ const drive = attach(session);
188
+ expect(session.listenerCount('permission_request')).toBe(1);
189
+ expect(session.listenerCount('ask_request')).toBe(1);
190
+ drive.send({ type: 'abort' });
191
+ drive.send({ type: 'permission-response', id: 'p1', result: true });
192
+ expect(session.abort).toHaveBeenCalledTimes(1);
193
+ expect(session.resolvePermission).toHaveBeenCalledExactlyOnceWith('p1', true, 'attach:1');
194
+ expect(drive.sent).toEqual([]);
195
+ });
196
+
197
+ it('disconnects an observer that stops reading while the session keeps streaming to others', () => {
198
+ const session = createSession();
199
+ const drive = attach(session, 'drive');
200
+ let pending = 0;
201
+ const onDeliveryError = vi.fn();
202
+ const received: TServerMessage[] = [];
203
+ createSessionMessageHandler({
204
+ session,
205
+ role: 'observe',
206
+ deliver: createOutboundDelivery(
207
+ (message) => {
208
+ received.push(message);
209
+ pending += 64 * 1024;
210
+ },
211
+ onDeliveryError,
212
+ () => pending,
213
+ ATTACHED_SURFACE_MAX_PENDING_BYTES,
214
+ ),
215
+ });
216
+ for (let index = 0; index < 64; index++) {
217
+ expect(() => session.emit('text_delta', `delta ${index}`)).not.toThrow();
218
+ }
219
+ expect(onDeliveryError).toHaveBeenCalledTimes(1);
220
+ expect(received.length).toBeLessThan(64);
221
+ expect(drive.sent.filter((message) => message.type === 'text_delta')).toHaveLength(64);
222
+ });
223
+ });