@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.
- package/README.md +84 -26
- package/dist/browser/client.d.ts +2 -0
- package/dist/browser/client.js +1 -0
- package/dist/browser/index.d.ts +520 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/message-decoders-BB8n-H2X.d.ts +252 -0
- package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +1 -0
- package/dist/browser/message-decoders-CDmDoZPR.js +2 -0
- package/dist/browser/message-decoders-CDmDoZPR.js.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +766 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +765 -27
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/node.cjs +1 -0
- package/dist/node/node.d.cts +109 -0
- package/dist/node/node.d.cts.map +1 -0
- package/dist/node/node.d.ts +109 -0
- package/dist/node/node.d.ts.map +1 -0
- package/dist/node/node.js +2 -0
- package/dist/node/node.js.map +1 -0
- package/package.json +68 -47
- package/src/__tests__/access-token-verifier.test.ts +672 -0
- package/src/__tests__/admission.test.ts +130 -0
- package/src/__tests__/channel-frames.test.ts +214 -0
- package/src/__tests__/driver-attribution-capability.test.ts +83 -0
- package/src/__tests__/handoff-chunking.test.ts +215 -0
- package/src/__tests__/handoff-manifest.test.ts +52 -0
- package/src/__tests__/message-decoders.test.ts +249 -0
- package/src/__tests__/outbound-delivery.test.ts +558 -0
- package/src/__tests__/peer-message-ledger.test.ts +200 -0
- package/src/__tests__/personal-usage-report.test.ts +193 -0
- package/src/__tests__/resume-buffer.test.ts +93 -0
- package/src/__tests__/session-event-delivery.test.ts +101 -0
- package/src/__tests__/session-message-handler-e5.test.ts +99 -0
- package/src/__tests__/session-message-handler.test.ts +590 -0
- package/src/__tests__/session-resume-bridge.test.ts +406 -0
- package/src/__tests__/submit-prompt-shape.test.ts +78 -0
- package/src/__tests__/usage-report-carrier.test.ts +98 -0
- package/src/__tests__/ws-broadcast-events.test.ts +84 -0
- package/src/background-messages.ts +169 -0
- package/src/channel-frames.ts +182 -0
- package/src/client.ts +9 -0
- package/src/handoff-chunking.ts +235 -0
- package/src/index.ts +112 -3
- package/src/message-decoders.ts +229 -0
- package/src/message-parser.ts +33 -0
- package/src/node/access-token-verifier.ts +363 -0
- package/src/node/admission.ts +153 -0
- package/src/node/handoff-manifest.ts +77 -0
- package/src/node/index.ts +10 -0
- package/src/outbound-delivery.ts +231 -0
- package/src/peer-message-ledger.ts +193 -0
- package/src/protocol-session.ts +28 -0
- package/src/resume-buffer.ts +123 -0
- package/src/session-events.ts +198 -0
- package/src/session-message-handler.ts +282 -0
- package/src/session-query-messages.ts +42 -0
- package/src/session-resume-bridge.ts +252 -0
- package/src/usage-messages.ts +195 -0
- package/src/wire-messages.ts +165 -0
- package/dist/node/headless/index.cjs +0 -1
- package/dist/node/headless/index.d.ts +0 -2
- package/dist/node/headless/index.js +0 -1
- package/dist/node/headless-DG_MEbf0.js +0 -19
- package/dist/node/headless-DG_MEbf0.js.map +0 -1
- package/dist/node/headless-xq77JBq0.cjs +0 -18
- package/dist/node/index-CrJ2Lhn3.d.ts +0 -96
- package/dist/node/index-CrJ2Lhn3.d.ts.map +0 -1
- package/dist/node/index-JIvzrtaA.d.ts +0 -52
- package/dist/node/index-JIvzrtaA.d.ts.map +0 -1
- package/dist/node/index-JTD_xtEQ.d.ts +0 -96
- package/dist/node/index-JTD_xtEQ.d.ts.map +0 -1
- package/dist/node/index-y_HBOfzO.d.ts +0 -52
- package/dist/node/index-y_HBOfzO.d.ts.map +0 -1
- package/dist/node/programmatic/index.cjs +0 -1
- package/dist/node/programmatic/index.d.ts +0 -2
- package/dist/node/programmatic/index.js +0 -1
- package/dist/node/programmatic-6wLf68qJ.cjs +0 -1
- package/dist/node/programmatic-70dxIXtS.js +0 -2
- package/dist/node/programmatic-70dxIXtS.js.map +0 -1
- package/dist/node/testing/index.cjs +0 -1
- package/dist/node/testing/index.d.ts +0 -2
- package/dist/node/testing/index.js +0 -1
- package/src/__tests__/programmatic/programmatic-ask-user.test.ts +0 -32
- package/src/__tests__/programmatic/programmatic-driver.test.ts +0 -107
- package/src/__tests__/programmatic/programmatic-flows.test.ts +0 -140
- package/src/__tests__/transport-registry.stop-all.test.ts +0 -53
- package/src/headless/HeadlessInteractionChannel.ts +0 -129
- package/src/headless/__tests__/headless-channel-options.test.ts +0 -106
- package/src/headless/__tests__/headless-goal-runner.test.ts +0 -108
- package/src/headless/__tests__/headless-provider-failure.integration.test.ts +0 -143
- package/src/headless/__tests__/headless-runner-initialization.test.ts +0 -45
- package/src/headless/__tests__/headless-runner.test.ts +0 -508
- package/src/headless/__tests__/headless-skill-activation.integration.test.ts +0 -430
- package/src/headless/__tests__/headless-transport.test.ts +0 -267
- package/src/headless/cli-input.ts +0 -50
- package/src/headless/headless-runner.ts +0 -230
- package/src/headless/headless-stream-json.ts +0 -148
- package/src/headless/headless-transport.ts +0 -43
- package/src/headless/index.ts +0 -8
- package/src/headless/print-terminal.ts +0 -57
- package/src/programmatic/ProgrammaticInteractionChannel.ts +0 -82
- package/src/programmatic/createProgrammaticAgent.ts +0 -71
- package/src/programmatic/index.ts +0 -5
- package/src/testing/index.ts +0 -10
- package/src/transport-registry.ts +0 -103
package/src/index.ts
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe entry point for `@robota-sdk/agent-transport`.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this file exists
|
|
5
|
+
*
|
|
6
|
+
* The package's `./node` entry owns `admission.ts` and `handoff-manifest.ts`, the only modules
|
|
7
|
+
* that import `node:crypto`. Keeping those modules out of this root prevents a browser consumer
|
|
8
|
+
* from pulling a Node builtin into its bundle.
|
|
9
|
+
*
|
|
10
|
+
* ## What is in, and why
|
|
11
|
+
*
|
|
12
|
+
* Everything in `src/index.ts` EXCEPT the two modules that touch `node:crypto`. Read off the actual
|
|
13
|
+
* import graph, not off intent:
|
|
14
|
+
*
|
|
15
|
+
* node:crypto reachers `admission.ts`, `handoff-manifest.ts` — and nothing inside the package
|
|
16
|
+
* imports either of them, so removing both from this barrel severs the
|
|
17
|
+
* builtin from this entry's graph entirely.
|
|
18
|
+
* everything else `session-message-handler` → {`background-messages`, `message-decoders`,
|
|
19
|
+
* `session-events`}, `session-resume-bridge` → {`outbound-delivery`,
|
|
20
|
+
* `resume-buffer`, `session-message-handler`, `session-events`}, `channel-frames`,
|
|
21
|
+
* `peer-message-ledger`, `handoff-chunking`,
|
|
22
|
+
* `protocol-session`, `wire-messages` — no builtin on any path.
|
|
23
|
+
*
|
|
24
|
+
* Handoff authority, offer refusal, and inventory decisions live in
|
|
25
|
+
* `agent-interface-session-mobility`, outside this wire package.
|
|
26
|
+
*
|
|
27
|
+
* The handoff MANIFEST types are omitted along with their functions. Keeping the `export type` lines
|
|
28
|
+
* would be free at runtime, but it would put `node/handoff-manifest.ts` back on this entry's import graph
|
|
29
|
+
* — and the `browser-package-node-subpath` scan that guards this file walks EVERY import edge rather
|
|
30
|
+
* than trying to tell a type edge from a value one. A check that has to be clever about which edges
|
|
31
|
+
* count is a check that can be fooled; a browser consumer that needs those types is asking for the
|
|
32
|
+
* Node surface and should say so by importing `./node`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export { createSessionMessageHandler } from './session-message-handler.js';
|
|
36
|
+
export type { ISessionMessageHandlerOptions } from './session-message-handler.js';
|
|
37
|
+
// ARCH-030: the connection-scoped outbound delivery boundary every carrier builds and passes down.
|
|
38
|
+
export {
|
|
39
|
+
createOutboundDelivery,
|
|
40
|
+
createPendingStallClock,
|
|
41
|
+
isOverPendingBudget,
|
|
42
|
+
DEFAULT_MAX_PENDING_BYTES,
|
|
43
|
+
DEFAULT_MAX_PENDING_MS,
|
|
44
|
+
} from './outbound-delivery.js';
|
|
45
|
+
export type {
|
|
46
|
+
IPendingStallClock,
|
|
47
|
+
TDeliveryErrorHandler,
|
|
48
|
+
TOutboundDeliver,
|
|
49
|
+
} from './outbound-delivery.js';
|
|
50
|
+
export { PROTOCOL_SESSION_EVENT_CLASSIFICATION } from './session-events.js';
|
|
51
|
+
export type { TProtocolSessionEventClassification } from './session-events.js';
|
|
52
|
+
export type { IProtocolSession } from './protocol-session.js';
|
|
53
|
+
export type { TClientMessage, TServerMessage, TSeqServerMessage } from './wire-messages.js';
|
|
54
|
+
// Issue #2045: the owner-side runtime decoders; carriers implement `raw → decodeFrame → typed`.
|
|
55
|
+
export {
|
|
56
|
+
MAX_INBOUND_FRAME_BYTES,
|
|
57
|
+
decodeClientMessage,
|
|
58
|
+
decodeFrame,
|
|
59
|
+
decodeServerMessage,
|
|
60
|
+
} from './message-decoders.js';
|
|
61
|
+
export type { TMessageDecodeResult } from './message-decoders.js';
|
|
62
|
+
export { ResumeBuffer } from './resume-buffer.js';
|
|
63
|
+
export type { IResumeBufferOptions, IBufferedFrame, TResumeTail } from './resume-buffer.js';
|
|
64
|
+
// TRANS-001 — payload-agnostic channel frame codec (transport-neutral, body-opaque).
|
|
65
|
+
export {
|
|
66
|
+
CHANNEL_FRAME_MAGIC,
|
|
67
|
+
CHANNEL_FRAME_VERSION,
|
|
68
|
+
decodeChannelFrame,
|
|
69
|
+
encodeBinaryFrame,
|
|
70
|
+
encodeChannelEventFrame,
|
|
71
|
+
isChannelFrame,
|
|
72
|
+
} from './channel-frames.js';
|
|
73
|
+
export { SessionResumeBridge } from './session-resume-bridge.js';
|
|
74
|
+
export type {
|
|
75
|
+
ISessionResumeBridgeOptions,
|
|
76
|
+
TResumeSink,
|
|
77
|
+
IAttachOptions,
|
|
78
|
+
} from './session-resume-bridge.js';
|
|
79
|
+
|
|
80
|
+
// SEC-008 admission is NOT here: `src/node/admission.ts` mints and compares transport tokens with
|
|
81
|
+
// `node:crypto`. A browser never mints one — it presents one it was handed — so the exclusion costs
|
|
82
|
+
// the browser surface nothing it could have used.
|
|
83
|
+
|
|
84
|
+
// PEER-001 (#1809): the receiver's record of what it has already taken responsibility for. Lives
|
|
85
|
+
// here rather than in a carrier because duplicate, retry and gap are questions about what was SEEN
|
|
86
|
+
// BEFORE, and no socket or frame codec has the memory to answer them.
|
|
87
|
+
export {
|
|
88
|
+
acknowledgePeerMessage,
|
|
89
|
+
admitPeerMessage,
|
|
90
|
+
createPeerMessageLedger,
|
|
91
|
+
forgetPeerOrigin,
|
|
92
|
+
} from './peer-message-ledger.js';
|
|
93
|
+
export type {
|
|
94
|
+
IPeerMessageLedger,
|
|
95
|
+
IPeerMessageRejection,
|
|
96
|
+
IPeerMessageVerdict,
|
|
97
|
+
} from './peer-message-ledger.js';
|
|
98
|
+
|
|
99
|
+
// The handoff integrity helpers (`sealHandoffRecord`, `verifyHandoffPayload`) are NOT
|
|
100
|
+
// here: it hashes with `node:crypto`. Chunking is, because it never hashes anything.
|
|
101
|
+
export {
|
|
102
|
+
chunkCountFor,
|
|
103
|
+
chunkHandoffPayload,
|
|
104
|
+
DEFAULT_MAX_CHUNK_BYTES,
|
|
105
|
+
HandoffChunkAssembler,
|
|
106
|
+
} from './handoff-chunking.js';
|
|
107
|
+
export type {
|
|
108
|
+
IChunkResult,
|
|
109
|
+
IHandoffChunk,
|
|
110
|
+
TChunkOutcome,
|
|
111
|
+
TChunkRejection,
|
|
112
|
+
} from './handoff-chunking.js';
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-side runtime decoders for the wire message unions (issue #2045 / TRANS-005 adjacent).
|
|
3
|
+
*
|
|
4
|
+
* `wire-messages.ts` owns the TypeScript unions; this file owns turning `JSON.parse` output into one
|
|
5
|
+
* of them. Before it, the server did `JSON.parse(data) as TClientMessage` and each client did the
|
|
6
|
+
* same for `TServerMessage`, so valid JSON with an invalid shape crossed the boundary wearing a
|
|
7
|
+
* protocol type. Carriers now implement `raw string → owner decoder → typed message` and answer a
|
|
8
|
+
* refusal with the protocol's own `protocol_error` (server side) or client error (client side).
|
|
9
|
+
*
|
|
10
|
+
* `decodeClientMessage` is TOTAL: every variant and every nested field it carries is validated.
|
|
11
|
+
* `decodeServerMessage` validates every variant's discriminator and declared top-level fields; a
|
|
12
|
+
* payload OWNED BY ANOTHER PACKAGE (`IToolState`, `IExecutionResult`, background events, …) is
|
|
13
|
+
* checked as a non-array record (or array of records) here — its own total decoding belongs to its
|
|
14
|
+
* owner and is tracked there, not duplicated in a transport package.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { TBackgroundControlAction, TClientMessage, TServerMessage } from './wire-messages.js';
|
|
18
|
+
|
|
19
|
+
export type TMessageDecodeResult<TMessage> =
|
|
20
|
+
| { readonly ok: true; readonly message: TMessage }
|
|
21
|
+
| { readonly ok: false; readonly reason: string };
|
|
22
|
+
|
|
23
|
+
/** Serialized frames larger than this are refused before any parse (a bounded inbound budget). */
|
|
24
|
+
export const MAX_INBOUND_FRAME_BYTES = 4 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
type TRecord = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
function isRecord(value: unknown): value is TRecord {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
const isString = (v: unknown): v is string => typeof v === 'string';
|
|
32
|
+
const isNonEmptyString = (v: unknown): v is string => typeof v === 'string' && v.length > 0;
|
|
33
|
+
const isBoolean = (v: unknown): v is boolean => typeof v === 'boolean';
|
|
34
|
+
const isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
|
|
35
|
+
const isOptional =
|
|
36
|
+
(check: (v: unknown) => boolean) =>
|
|
37
|
+
(v: unknown): boolean =>
|
|
38
|
+
v === undefined || check(v);
|
|
39
|
+
const isRecordArray = (v: unknown): boolean => Array.isArray(v) && v.every(isRecord);
|
|
40
|
+
const isNullableRecord = (v: unknown): boolean => v === null || isRecord(v);
|
|
41
|
+
const oneOf =
|
|
42
|
+
(members: readonly string[]) =>
|
|
43
|
+
(v: unknown): boolean =>
|
|
44
|
+
typeof v === 'string' && members.includes(v);
|
|
45
|
+
|
|
46
|
+
type TFieldCheck = (value: unknown) => boolean;
|
|
47
|
+
type TVariantShape = Readonly<Record<string, TFieldCheck>>;
|
|
48
|
+
|
|
49
|
+
const BACKGROUND_TASK_KINDS = ['agent', 'process', 'scheduled'] as const;
|
|
50
|
+
const BACKGROUND_TASK_MODES = ['foreground', 'background'] as const;
|
|
51
|
+
const BACKGROUND_TASK_STATUSES = [
|
|
52
|
+
'queued',
|
|
53
|
+
'running',
|
|
54
|
+
'waiting_permission',
|
|
55
|
+
'sleeping',
|
|
56
|
+
'paused',
|
|
57
|
+
'completed',
|
|
58
|
+
'failed',
|
|
59
|
+
'cancelled',
|
|
60
|
+
] as const;
|
|
61
|
+
const BACKGROUND_CONTROL_ACTIONS: readonly TBackgroundControlAction[] = ['cancel', 'close', 'send'];
|
|
62
|
+
|
|
63
|
+
const isBackgroundTaskListFilter: TFieldCheck = (v) =>
|
|
64
|
+
isRecord(v) &&
|
|
65
|
+
isOptional(oneOf(BACKGROUND_TASK_KINDS))(v['kind']) &&
|
|
66
|
+
isOptional(oneOf(BACKGROUND_TASK_STATUSES))(v['status']) &&
|
|
67
|
+
isOptional(oneOf(BACKGROUND_TASK_MODES))(v['mode']) &&
|
|
68
|
+
isOptional(isBoolean)(v['includeClosed']);
|
|
69
|
+
const isBackgroundTaskInput: TFieldCheck = (v) =>
|
|
70
|
+
isRecord(v) && isOptional(isString)(v['prompt']) && isOptional(isString)(v['stdin']);
|
|
71
|
+
const isBackgroundTaskLogCursor: TFieldCheck = (v) => isRecord(v) && isFiniteNumber(v['offset']);
|
|
72
|
+
const isPermissionResultValue: TFieldCheck = (v) =>
|
|
73
|
+
isBoolean(v) || v === 'allow-session' || v === 'allow-project';
|
|
74
|
+
const isActionResponse: TFieldCheck = (v) =>
|
|
75
|
+
isRecord(v) &&
|
|
76
|
+
((v['type'] === 'answer' &&
|
|
77
|
+
Array.isArray(v['values']) &&
|
|
78
|
+
v['values'].every(isString) &&
|
|
79
|
+
isOptional(isString)(v['text'])) ||
|
|
80
|
+
(v['type'] === 'cancelled' && v['values'] === undefined && v['text'] === undefined));
|
|
81
|
+
|
|
82
|
+
/** One entry per `TClientMessage` variant — a variant added to the union without one fails the test. */
|
|
83
|
+
export const CLIENT_MESSAGE_SHAPES: Readonly<Record<TClientMessage['type'], TVariantShape>> = {
|
|
84
|
+
submit: { prompt: isNonEmptyString },
|
|
85
|
+
command: { name: isNonEmptyString, args: isOptional(isString) },
|
|
86
|
+
abort: {},
|
|
87
|
+
'cancel-queue': {},
|
|
88
|
+
'get-messages': {},
|
|
89
|
+
'get-context': {},
|
|
90
|
+
'get-usage-report': {},
|
|
91
|
+
'get-personal-usage-report': {
|
|
92
|
+
requestId: isNonEmptyString,
|
|
93
|
+
period: oneOf(['7d', '30d']),
|
|
94
|
+
timezone: isNonEmptyString,
|
|
95
|
+
},
|
|
96
|
+
'get-stored-session-usage-report': {
|
|
97
|
+
requestId: isNonEmptyString,
|
|
98
|
+
sessionId: isNonEmptyString,
|
|
99
|
+
},
|
|
100
|
+
'get-executing': {},
|
|
101
|
+
'get-pending': {},
|
|
102
|
+
'get-execution-workspace': {},
|
|
103
|
+
'get-background-tasks': { filter: isOptional(isBackgroundTaskListFilter) },
|
|
104
|
+
'get-background-task': { taskId: isNonEmptyString },
|
|
105
|
+
'get-background-job-groups': {},
|
|
106
|
+
'get-background-job-group': { groupId: isNonEmptyString },
|
|
107
|
+
'wait-background-job-group': { groupId: isNonEmptyString },
|
|
108
|
+
'cancel-background-task': { taskId: isNonEmptyString, reason: isOptional(isString) },
|
|
109
|
+
'close-background-task': { taskId: isNonEmptyString },
|
|
110
|
+
'send-background-task': { taskId: isNonEmptyString, input: isBackgroundTaskInput },
|
|
111
|
+
'read-background-task-log': {
|
|
112
|
+
taskId: isNonEmptyString,
|
|
113
|
+
cursor: isOptional(isBackgroundTaskLogCursor),
|
|
114
|
+
},
|
|
115
|
+
'permission-response': { id: isNonEmptyString, result: isPermissionResultValue },
|
|
116
|
+
'ask-response': { id: isNonEmptyString, response: isActionResponse },
|
|
117
|
+
resume: { lastSeq: isFiniteNumber },
|
|
118
|
+
ack: { seq: isFiniteNumber },
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const authored: TVariantShape = { driverId: isOptional(isString) };
|
|
122
|
+
|
|
123
|
+
/** One entry per `TServerMessage` variant. Foreign-owned payloads are checked as records (see header). */
|
|
124
|
+
export const SERVER_MESSAGE_SHAPES: Readonly<Record<TServerMessage['type'], TVariantShape>> = {
|
|
125
|
+
text_delta: { delta: isString, ...authored },
|
|
126
|
+
user_message: { content: isString, ...authored },
|
|
127
|
+
tool_start: { state: isRecord, ...authored },
|
|
128
|
+
tool_end: { state: isRecord, ...authored },
|
|
129
|
+
thinking: { isThinking: isBoolean, ...authored },
|
|
130
|
+
complete: { result: isRecord, ...authored },
|
|
131
|
+
interrupted: { result: isRecord, ...authored },
|
|
132
|
+
error: { message: isString, ...authored },
|
|
133
|
+
command_result: { name: isString, message: isString, success: isBoolean },
|
|
134
|
+
messages: { messages: isRecordArray },
|
|
135
|
+
context: { state: isRecord },
|
|
136
|
+
usage_report: { report: isRecord },
|
|
137
|
+
personal_usage_report: { requestId: isNonEmptyString, report: isRecord },
|
|
138
|
+
personal_usage_report_error: {
|
|
139
|
+
requestId: isNonEmptyString,
|
|
140
|
+
code: oneOf(['not_available', 'report_failed']),
|
|
141
|
+
message: isString,
|
|
142
|
+
},
|
|
143
|
+
stored_session_usage_report: {
|
|
144
|
+
requestId: isNonEmptyString,
|
|
145
|
+
sessionId: isNonEmptyString,
|
|
146
|
+
report: isRecord,
|
|
147
|
+
},
|
|
148
|
+
stored_session_usage_report_error: {
|
|
149
|
+
requestId: isNonEmptyString,
|
|
150
|
+
sessionId: isNonEmptyString,
|
|
151
|
+
code: oneOf(['not_available', 'report_failed']),
|
|
152
|
+
message: isString,
|
|
153
|
+
},
|
|
154
|
+
executing: { executing: isBoolean },
|
|
155
|
+
pending: { pending: (v) => v === null || isString(v) },
|
|
156
|
+
execution_workspace_event: { snapshot: isRecord },
|
|
157
|
+
background_task_event: { event: isRecord },
|
|
158
|
+
background_job_group_event: { event: isRecord },
|
|
159
|
+
plan_event: { event: isRecord },
|
|
160
|
+
context_file_refreshed: { event: isRecord },
|
|
161
|
+
branch_event: { event: isRecord },
|
|
162
|
+
background_tasks: { tasks: isRecordArray },
|
|
163
|
+
background_task: { taskId: isString, task: isNullableRecord },
|
|
164
|
+
background_job_groups: { groups: isRecordArray },
|
|
165
|
+
background_job_group: { groupId: isString, group: isNullableRecord },
|
|
166
|
+
background_task_log: { taskId: isString, page: isRecord },
|
|
167
|
+
permission_request: { event: isRecord },
|
|
168
|
+
ask_request: { event: isRecord },
|
|
169
|
+
prompt_resolved: { event: isRecord },
|
|
170
|
+
ui_intent: { event: isRecord },
|
|
171
|
+
session_renamed: { event: isRecord },
|
|
172
|
+
history_cleared: {},
|
|
173
|
+
background_task_control_result: {
|
|
174
|
+
action: oneOf(BACKGROUND_CONTROL_ACTIONS),
|
|
175
|
+
taskId: isString,
|
|
176
|
+
success: isBoolean,
|
|
177
|
+
message: isOptional(isString),
|
|
178
|
+
},
|
|
179
|
+
protocol_error: { message: isString },
|
|
180
|
+
resume_gap: {},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function decodeVariant<TMessage extends { type: string }>(
|
|
184
|
+
value: unknown,
|
|
185
|
+
shapes: Readonly<Record<string, TVariantShape>>,
|
|
186
|
+
reasonFor: (field: string) => string,
|
|
187
|
+
): TMessageDecodeResult<TMessage> {
|
|
188
|
+
if (!isRecord(value)) return { ok: false, reason: 'message must be a JSON object' };
|
|
189
|
+
const type = value['type'];
|
|
190
|
+
if (!isString(type)) return { ok: false, reason: 'message type must be a string' };
|
|
191
|
+
const shape = Object.prototype.hasOwnProperty.call(shapes, type) ? shapes[type] : undefined;
|
|
192
|
+
if (shape === undefined) return { ok: false, reason: `Unknown message type: ${type}` };
|
|
193
|
+
for (const [field, check] of Object.entries(shape)) {
|
|
194
|
+
if (!check(value[field])) return { ok: false, reason: reasonFor(field) };
|
|
195
|
+
}
|
|
196
|
+
return { ok: true, message: value as unknown as TMessage };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Total decode of a parsed client frame. */
|
|
200
|
+
export function decodeClientMessage(value: unknown): TMessageDecodeResult<TClientMessage> {
|
|
201
|
+
return decodeVariant<TClientMessage>(value, CLIENT_MESSAGE_SHAPES, (field) =>
|
|
202
|
+
field === 'prompt' ? 'prompt must be a non-empty string' : `${field} has an invalid shape`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Variant-level decode of a parsed server frame (foreign-owned payloads as records, see header). */
|
|
207
|
+
export function decodeServerMessage(value: unknown): TMessageDecodeResult<TServerMessage> {
|
|
208
|
+
return decodeVariant<TServerMessage>(
|
|
209
|
+
value,
|
|
210
|
+
SERVER_MESSAGE_SHAPES,
|
|
211
|
+
(field) => `${field} has an invalid shape`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** `raw string → JSON → owner decoder`, the one path a carrier implements. */
|
|
216
|
+
export function decodeFrame<TMessage>(
|
|
217
|
+
data: string,
|
|
218
|
+
decode: (value: unknown) => TMessageDecodeResult<TMessage>,
|
|
219
|
+
): TMessageDecodeResult<TMessage> {
|
|
220
|
+
if (data.length > MAX_INBOUND_FRAME_BYTES) return { ok: false, reason: 'Frame too large' };
|
|
221
|
+
let parsed: unknown;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(data);
|
|
224
|
+
// allow-fallback: the parse failure is returned as a typed refusal the carrier answers on the wire
|
|
225
|
+
} catch {
|
|
226
|
+
return { ok: false, reason: 'Invalid JSON' };
|
|
227
|
+
}
|
|
228
|
+
return decode(parsed);
|
|
229
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { decodeClientMessage, decodeFrame } from './message-decoders.js';
|
|
2
|
+
import { malformedUsageRequest } from './usage-messages.js';
|
|
3
|
+
|
|
4
|
+
import type { TOutboundDeliver } from './outbound-delivery.js';
|
|
5
|
+
import type { TClientMessage } from './wire-messages.js';
|
|
6
|
+
|
|
7
|
+
/** Decode a client frame and preserve validated usage-request correlation on malformed payloads. */
|
|
8
|
+
export function parseClientMessage(data: string, deliver: TOutboundDeliver): TClientMessage | null {
|
|
9
|
+
const decoded = decodeFrame(data, decodeClientMessage);
|
|
10
|
+
if (decoded.ok) return decoded.message;
|
|
11
|
+
const malformedUsage = malformedUsageRequest(data);
|
|
12
|
+
if (malformedUsage?.type === 'personal') {
|
|
13
|
+
deliver({
|
|
14
|
+
type: 'personal_usage_report_error',
|
|
15
|
+
requestId: malformedUsage.requestId,
|
|
16
|
+
code: 'report_failed',
|
|
17
|
+
message: 'Invalid personal usage report request.',
|
|
18
|
+
});
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
if (malformedUsage?.type === 'stored-session') {
|
|
22
|
+
deliver({
|
|
23
|
+
type: 'stored_session_usage_report_error',
|
|
24
|
+
requestId: malformedUsage.requestId,
|
|
25
|
+
sessionId: malformedUsage.sessionId,
|
|
26
|
+
code: 'report_failed',
|
|
27
|
+
message: 'Invalid stored-session usage report request.',
|
|
28
|
+
});
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
deliver({ type: 'protocol_error', message: decoded.reason });
|
|
32
|
+
return null;
|
|
33
|
+
}
|