@platless/axom-cli 0.1.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.
- package/README.md +64 -0
- package/dist/args.js +29 -0
- package/dist/cli.js +76 -0
- package/dist/commands/auth.js +52 -0
- package/dist/commands/execute.js +40 -0
- package/dist/commands/guide.js +38 -0
- package/dist/commands/record.js +99 -0
- package/dist/commands/skills.js +69 -0
- package/dist/commands/struct.js +50 -0
- package/dist/commands/update.js +88 -0
- package/dist/commands/whoami.js +8 -0
- package/dist/commands/workstation.js +171 -0
- package/dist/config.js +131 -0
- package/dist/credentialBroker.js +340 -0
- package/dist/credentials.js +211 -0
- package/dist/install/agentsDir.js +35 -0
- package/dist/install/claude/commands.js +89 -0
- package/dist/install/claude/materialize.js +42 -0
- package/dist/install/installer.js +45 -0
- package/dist/install/targets/claude.js +154 -0
- package/dist/mcp.js +152 -0
- package/dist/output.js +37 -0
- package/dist/workstation/acp/acpAgents.js +1287 -0
- package/dist/workstation/acp/acpClient.js +691 -0
- package/dist/workstation/acp/acpSession.js +1047 -0
- package/dist/workstation/acp/adoption.js +396 -0
- package/dist/workstation/acp/auth.js +80 -0
- package/dist/workstation/acp/authUrlSniffer.js +52 -0
- package/dist/workstation/acp/binaryInstall.js +411 -0
- package/dist/workstation/acp/configOptions.js +257 -0
- package/dist/workstation/acp/contract.js +494 -0
- package/dist/workstation/acp/draftInterceptor.js +154 -0
- package/dist/workstation/acp/draftSubagents.js +38 -0
- package/dist/workstation/acp/installErrors.js +89 -0
- package/dist/workstation/acp/installProbe.js +363 -0
- package/dist/workstation/acp/permission.js +327 -0
- package/dist/workstation/acp/registry/catalog.js +29 -0
- package/dist/workstation/acp/registry/launch.js +128 -0
- package/dist/workstation/acp/registry/registryClient.js +160 -0
- package/dist/workstation/acp/registry/registryTypes.js +8 -0
- package/dist/workstation/acp/sessionUpdateMapper.js +602 -0
- package/dist/workstation/acp/test/fakeAgent.js +435 -0
- package/dist/workstation/acp/test/sessionUpdateFixtures.js +165 -0
- package/dist/workstation/acp/timedCommand.js +47 -0
- package/dist/workstation/acp/uvxInstall.js +248 -0
- package/dist/workstation/acp/validate.js +134 -0
- package/dist/workstation/agentSessions.js +1628 -0
- package/dist/workstation/channels/agent.js +96 -0
- package/dist/workstation/channels/computer.js +91 -0
- package/dist/workstation/channels/management.js +231 -0
- package/dist/workstation/channels/url.js +42 -0
- package/dist/workstation/config.js +47 -0
- package/dist/workstation/connection/encryptedChannel.js +261 -0
- package/dist/workstation/connection/retryPolicy.js +14 -0
- package/dist/workstation/connection/supervisor.js +353 -0
- package/dist/workstation/crypto.js +63 -0
- package/dist/workstation/daemon.js +225 -0
- package/dist/workstation/daemonctl.js +95 -0
- package/dist/workstation/exec.js +112 -0
- package/dist/workstation/journal.js +257 -0
- package/dist/workstation/names.js +17 -0
- package/dist/workstation/oauth/browser.js +24 -0
- package/dist/workstation/oauth/deviceAuthorization.js +198 -0
- package/dist/workstation/osinfo.js +4 -0
- package/dist/workstation/protocol/agent.js +606 -0
- package/dist/workstation/protocol/common.js +136 -0
- package/dist/workstation/protocol/computer.js +31 -0
- package/dist/workstation/protocol/management.js +116 -0
- package/dist/workstation/register.js +42 -0
- package/dist/workstation/service/installer.js +362 -0
- package/dist/workstation/service/launchd.js +46 -0
- package/dist/workstation/service/loginShell.js +36 -0
- package/dist/workstation/service/systemd.js +48 -0
- package/dist/workstation/service/types.js +73 -0
- package/dist/workstation/supervision.js +52 -0
- package/dist/workstation/systemMetrics.js +271 -0
- package/dist/workstation/update/artifact.js +61 -0
- package/dist/workstation/update/coordinator.js +240 -0
- package/dist/workstation/update/npmInstaller.js +91 -0
- package/dist/workstation/update/redact.js +36 -0
- package/dist/workstation/update/runUpdate.js +215 -0
- package/dist/workstation/update/stateStore.js +87 -0
- package/dist/workstation/update/taskRegistry.js +142 -0
- package/dist/workstation/update/types.js +3 -0
- package/dist/workstation/update/watchdog.js +311 -0
- package/dist/workstation/version.js +12 -0
- package/package.json +39 -0
- package/skills/axom/SKILL.md +83 -0
- package/skills/axom-definition/SKILL.md +74 -0
- package/skills/axom-platform/SKILL.md +78 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableAuthMethodInfoArray, isNullableString, isNumber, isObject, isString, isStringArray, isStringRecord, } from './common.js';
|
|
2
|
+
/** Current version and feature floors for the independently negotiated agent protocol. */
|
|
3
|
+
export const AGENT_WIRE_VERSION = 14;
|
|
4
|
+
export const AGENT_WIRE_MIN_VERSION = 1;
|
|
5
|
+
export const AGENT_WIRE_SESSION_LOST_VERSION = 2;
|
|
6
|
+
export const AGENT_WIRE_ADOPTION_VERSION = 3;
|
|
7
|
+
/** On-demand verification and authentication (`agent_verify` / `agent_authenticate`). */
|
|
8
|
+
export const AGENT_WIRE_AUTH_VERSION = 4;
|
|
9
|
+
/**
|
|
10
|
+
* Adapter installation (`agent_install` / `agent_installed`), and with it the roster's
|
|
11
|
+
* `cliInstalled` / `adapterInstalled`. A v4 peer answers neither, which is why the server checks
|
|
12
|
+
* this floor before sending rather than waiting out a request nothing will ever reply to.
|
|
13
|
+
*/
|
|
14
|
+
export const AGENT_WIRE_INSTALL_VERSION = 5;
|
|
15
|
+
/**
|
|
16
|
+
* Steering (`agent_steer` / `agent_steered`). Checked before sending rather than waited out: a v5
|
|
17
|
+
* peer has no case for `agent_steer`, drops it, and the server's request dies at its own timeout
|
|
18
|
+
* with nothing said about why.
|
|
19
|
+
*/
|
|
20
|
+
export const AGENT_WIRE_STEER_VERSION = 6;
|
|
21
|
+
/** Reconnect-time journal inventory (`agent_resume_offer`). */
|
|
22
|
+
export const AGENT_WIRE_RESUME_OFFER_VERSION = 7;
|
|
23
|
+
/**
|
|
24
|
+
* The same inventory carried on every `heartbeat` rather than once per connection
|
|
25
|
+
* (`heartbeat.sessions`).
|
|
26
|
+
*
|
|
27
|
+
* **Not a compatibility gate.** The server's `ChannelProtocolJson` sets `ignoreUnknownKeys`, so a v7
|
|
28
|
+
* server drops the field and acks the heartbeat exactly as it did before. The floor exists only to
|
|
29
|
+
* decide whether the bytes are worth sending: an inventory the peer will discard is pure overhead on
|
|
30
|
+
* a frame that goes out every ten seconds for the life of the daemon.
|
|
31
|
+
*/
|
|
32
|
+
export const AGENT_WIRE_HEARTBEAT_INVENTORY_VERSION = 8;
|
|
33
|
+
/**
|
|
34
|
+
* Session config options: `agent_opened`/`agent_verified` carry the agent's `configOptions`
|
|
35
|
+
* snapshot and `agent_set_config_option` / `agent_config_option_set` round-trip
|
|
36
|
+
* `session/set_config_option` (`types.gen.d.ts:5102`). Checked before sending, like steering: a
|
|
37
|
+
* v8 peer has no branch for the request and would let it die at the server's timeout.
|
|
38
|
+
*/
|
|
39
|
+
export const AGENT_WIRE_CONFIG_OPTIONS_VERSION = 9;
|
|
40
|
+
/**
|
|
41
|
+
* Orphan journals in `agent_resume_offer`: this client also lists refs whose journal is still on
|
|
42
|
+
* disk but whose agent is gone -- a daemon restart or a machine reboot -- as `agentAlive: false`.
|
|
43
|
+
*
|
|
44
|
+
* **A real compatibility gate, unlike AGENT_WIRE_HEARTBEAT_INVENTORY_VERSION.** That one only
|
|
45
|
+
* decided whether bytes were worth sending, because a peer that does not know the field ignores it.
|
|
46
|
+
* This one widens the meaning of an existing list whose *absence* is load-bearing: the server ends a
|
|
47
|
+
* live turn precisely because its ref is missing from the offer. A v9 server handed an orphan entry
|
|
48
|
+
* would stop ending that turn and, knowing nothing about reopening it, leave it RUNNING forever --
|
|
49
|
+
* strictly worse than today's honest failure. So orphans are withheld below this version.
|
|
50
|
+
*/
|
|
51
|
+
export const AGENT_WIRE_ORPHAN_OFFER_VERSION = 10;
|
|
52
|
+
/**
|
|
53
|
+
* The login frames — `agent_auth_url` / `agent_auth_url_done` — and the `pending` acknowledgement on
|
|
54
|
+
* `agent_authenticated` that goes with them.
|
|
55
|
+
*
|
|
56
|
+
* **The hardest gate of the ladder so far, because getting it wrong does not cost one message.** A
|
|
57
|
+
* v10 server has no `SerialName` for `agent_auth_url`: `AgentCodec.decode` throws,
|
|
58
|
+
* `classifyUndecodableAgentFrame` files it as `Unattributable` (it has no `sessionRef` to attribute
|
|
59
|
+
* it to), and every one of those counts toward `MAX_CONSECUTIVE_UNDECODABLE_AGENT_FRAMES = 8` —
|
|
60
|
+
* which closes the agent channel **and every session multiplexed on it**. So the floor is checked
|
|
61
|
+
* before `emit`, never after, and below it `agent_authenticate` keeps its old synchronous shape:
|
|
62
|
+
* one reply, sent when the login is over, which is what a v10 server knows how to wait for.
|
|
63
|
+
*/
|
|
64
|
+
export const AGENT_WIRE_AUTH_URL_VERSION = 11;
|
|
65
|
+
/**
|
|
66
|
+
* What the **session** path says about a refusal the agent itself produced: `authMethods` — what
|
|
67
|
+
* this process observed on that agent's `initialize` (`AcpAgents.observedAuthMethods`) — and
|
|
68
|
+
* `errorCode`, the JSON-RPC code of that refusal, on both `agent_opened` and `agent_turn_done`.
|
|
69
|
+
*
|
|
70
|
+
* A login card needs two halves — that the agent refused *for want of credentials*, and how it says
|
|
71
|
+
* to sign in. The probe reply (`agent_verified`) has carried both since v4. These two frames — the
|
|
72
|
+
* only ones by which a failed reopen and a failed prompt reach the server — carried neither, and
|
|
73
|
+
* neither could be reconstructed there: `errorMessage(err)` had already flattened ACP's
|
|
74
|
+
* `auth_required` (JSON-RPC -32000) to free text by the time a frame was built, and the
|
|
75
|
+
* `agent_hello` roster is a handshake snapshot that is empty for exactly the agent nobody has
|
|
76
|
+
* connected to yet.
|
|
77
|
+
*
|
|
78
|
+
* **Both keys ship under this one version because neither is worth a bump of its own** — not because
|
|
79
|
+
* they always arrive together. They do not: `refusalFields` spreads each on its own condition, so an
|
|
80
|
+
* agent that refused outside JSON-RPC sends the table with no code. The server requires the code
|
|
81
|
+
* regardless, and for a reason that has nothing to do with versioning: `auth_required` is a code, so
|
|
82
|
+
* a refusal carrying none is not one. ("Methods, therefore a login" was the old rule that made every
|
|
83
|
+
* refused prompt look like an expired token.)
|
|
84
|
+
*
|
|
85
|
+
* **The gentlest floor on the ladder, and deliberately not the shape of {@link
|
|
86
|
+
* AGENT_WIRE_AUTH_URL_VERSION}.** That one added whole frames, and a server without the
|
|
87
|
+
* `@SerialName` throws inside `AgentCodec.decode` — eight of those close the channel. This adds an
|
|
88
|
+
* optional key to two frames the server already decodes with `ignoreUnknownKeys`, so an older
|
|
89
|
+
* server reads them exactly as before. What is gated is therefore only whether the bytes are worth
|
|
90
|
+
* sending, like {@link AGENT_WIRE_HEARTBEAT_INVENTORY_VERSION}, and the check belongs on this side
|
|
91
|
+
* because both frames travel workstation -> server.
|
|
92
|
+
*/
|
|
93
|
+
export const AGENT_WIRE_SESSION_AUTH_METHODS_VERSION = 12;
|
|
94
|
+
/**
|
|
95
|
+
* `agent_opened.reloaded`: whether **this open** actually performed `session/load` or
|
|
96
|
+
* `session/new`, as opposed to reusing a live session or piggybacking on another open's real
|
|
97
|
+
* result. The server needs this to settle background jobs left over from a previous agent
|
|
98
|
+
* process — those jobs live only in that process's memory, so a genuine reload is the one event
|
|
99
|
+
* that proves them gone.
|
|
100
|
+
*
|
|
101
|
+
* **Same shape as {@link AGENT_WIRE_SESSION_AUTH_METHODS_VERSION}, and for the same reason that
|
|
102
|
+
* one declares itself un-enforceable by anything mechanical**: this is an optional key on a frame
|
|
103
|
+
* the server already decodes with `ignoreUnknownKeys`, so a peer below this floor does not fail to
|
|
104
|
+
* decode — it just never sees the key. The floor exists to record the compatibility fact, not to
|
|
105
|
+
* gate a decode failure: a v12 (or older) peer never sends `reloaded` at all, and **its absence
|
|
106
|
+
* there must never be read as "this open did not reload"** — that peer cannot tell you either way.
|
|
107
|
+
* A server that settled a background job on that silence would be acting on a fact nobody
|
|
108
|
+
* observed. Below this floor the only correct read of "no `reloaded` key" is "unknown, do not
|
|
109
|
+
* settle"; at or above it, absence reverts to its ordinary meaning — `false`, like
|
|
110
|
+
* `supportsSteering`.
|
|
111
|
+
*/
|
|
112
|
+
export const AGENT_WIRE_RELOADED_VERSION = 13;
|
|
113
|
+
/**
|
|
114
|
+
* `agent_steer` carries the **whole message** — `images` beside its `text` — and the delivery
|
|
115
|
+
* timing it is to be injected with (`priority`).
|
|
116
|
+
*
|
|
117
|
+
* **The criterion for "does an additive key need a floor?" is not "is it new" — it is: a key needs
|
|
118
|
+
* no floor only when a capability the peer must already advertise implies it can decode that key.**
|
|
119
|
+
* `ignoreUnknownKeys` means the question is never "will it throw"; it is "can the sender know the
|
|
120
|
+
* key will be understood without asking the version".
|
|
121
|
+
*
|
|
122
|
+
* `agent_prompt.images` passes that test and so has no floor: `supportsImageInput` shipped with it,
|
|
123
|
+
* so a peer advertising the capability necessarily understands the key. `agent_opened.reloaded` and
|
|
124
|
+
* the session-path `authMethods` pass a weaker version of it — they carry an *observation* a reader
|
|
125
|
+
* can simply do without — which is why theirs are "worth the bytes" floors rather than gates.
|
|
126
|
+
*
|
|
127
|
+
* These two fail on both counts. `supportsImageInput` predates them, so a peer can truthfully
|
|
128
|
+
* advertise it and still not know the keys exist; and `agent_steer` is an **RPC** — an `id`, a
|
|
129
|
+
* paired `agent_steered`, a server that acts on the answer — so the key is an instruction, not an
|
|
130
|
+
* observation. A peer below this floor decodes the frame fine, drops both keys, steers with the
|
|
131
|
+
* text alone and answers `injected` — so the reader's attached image never leaves their machine and
|
|
132
|
+
* every record on both sides says the message was delivered. Nothing observes the loss.
|
|
133
|
+
*
|
|
134
|
+
* So the server refuses to send a steer carrying either key below this floor rather than letting it
|
|
135
|
+
* degrade, and the engine's capability read reports no accepted timings at all below it, which
|
|
136
|
+
* routes the message to the queue — where it is delivered whole. A text-only steer with no priority
|
|
137
|
+
* is byte-for-byte the frame it always was and still goes out at {@link AGENT_WIRE_STEER_VERSION}.
|
|
138
|
+
*/
|
|
139
|
+
export const AGENT_WIRE_STEER_BLOCKS_VERSION = 14;
|
|
140
|
+
/**
|
|
141
|
+
* Every agent frame's shape, as data rather than as a `switch`.
|
|
142
|
+
*
|
|
143
|
+
* Declarative on purpose: the required-field half of this table is the *only* statement anywhere of
|
|
144
|
+
* which fields the Kotlin twin will refuse to decode without, and the contract suite
|
|
145
|
+
* (`acp/contract.ts`'s `assertRequiredFieldsEnforced`) reads it directly to prove each one is really
|
|
146
|
+
* enforced. Left as inline `hasRequired({...})` calls inside a `switch`, that list is unreadable from
|
|
147
|
+
* a test, so the only way to check it would be a hand-maintained copy in the test file — which is
|
|
148
|
+
* precisely how `agent_authenticate.methodId` came to have no guard at all while a report still
|
|
149
|
+
* "decoded fine" and `authenticate(undefined)` ran all the way to the agent.
|
|
150
|
+
*
|
|
151
|
+
* `DISCRIMINATORS` is derived from the keys below for the same reason: a frame type added to the
|
|
152
|
+
* union and to this table can no longer be forgotten in a second, parallel list.
|
|
153
|
+
*/
|
|
154
|
+
export const AGENT_FRAME_SHAPES = {
|
|
155
|
+
agent_hello: { required: { ticket: isString }, optional: { bridges: isBridgeInfoArray } },
|
|
156
|
+
agent_ready: { required: { generation: isString, agentWire: isNumber } },
|
|
157
|
+
agent_resume_offer: {
|
|
158
|
+
required: {
|
|
159
|
+
sessions: (sessions) => Array.isArray(sessions) && sessions.every(isAgentResumeSession),
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
heartbeat: {
|
|
163
|
+
required: { seq: isNumber, sentAtEpochMs: isNumber },
|
|
164
|
+
optional: { sessions: (sessions) => Array.isArray(sessions) && sessions.every(isAgentResumeSession) },
|
|
165
|
+
},
|
|
166
|
+
heartbeat_ack: { required: { seq: isNumber } },
|
|
167
|
+
agent_open: {
|
|
168
|
+
required: {
|
|
169
|
+
id: isString, sessionRef: isString, agentKey: isString, bridge: isString, cwd: isString, shell: isString,
|
|
170
|
+
},
|
|
171
|
+
optional: {
|
|
172
|
+
env: isStringRecord, pathPrepend: isStringArray, agentEnv: isStringRecord, command: isNullableString,
|
|
173
|
+
args: isStringArray, nativeSessionId: isNullableString,
|
|
174
|
+
resumeFromSeq: isNumber, backfill: isBoolean,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
agent_opened: {
|
|
178
|
+
required: { id: isString, ok: isBoolean },
|
|
179
|
+
// `supportsSteering` is `true` and nothing else, for `BridgeInfo.supportsSteering`'s reason: a
|
|
180
|
+
// `false` would be a second spelling of what its absence already says.
|
|
181
|
+
optional: {
|
|
182
|
+
nativeSessionId: isNullableString, error: isNullableString, supportsSteering: isVerifiedTrue,
|
|
183
|
+
// Plain strings, not a closed vocabulary: the sender already dropped everything it did not
|
|
184
|
+
// recognise, and validating the values again here would make a future timing fail the frame
|
|
185
|
+
// rather than be ignored — the opposite of what ruling N4 asks for.
|
|
186
|
+
steeringTimings: isStringArray,
|
|
187
|
+
// `reloaded` is `true`-or-absent for the same reason as `supportsSteering`: a `false` would
|
|
188
|
+
// be a second spelling of what its absence already says.
|
|
189
|
+
reloaded: isVerifiedTrue,
|
|
190
|
+
agentInfo: isAgentInfo, supportsImageInput: isVerifiedTrue, configOptions: isNullableSessionConfigOptionArray,
|
|
191
|
+
// Same guard as `agent_verified.authMethods`, and the same absent-vs-`[]` reading: a
|
|
192
|
+
// half-decoded method table is worse than none, because the reader would offer a login the
|
|
193
|
+
// agent never named.
|
|
194
|
+
authMethods: isNullableAuthMethodInfoArray,
|
|
195
|
+
errorCode: isNullableJsonRpcErrorCode,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray } },
|
|
199
|
+
agent_answer: { required: { sessionRef: isString, requestId: isString, outcome: isString } },
|
|
200
|
+
agent_interrupt: { required: { sessionRef: isString } },
|
|
201
|
+
agent_close: { required: { sessionRef: isString } },
|
|
202
|
+
agent_event: { required: { sessionRef: isString, seq: isNumber, eventName: isString, payload: isRecord } },
|
|
203
|
+
agent_turn_done: {
|
|
204
|
+
required: { sessionRef: isString, seq: isNumber, status: isString },
|
|
205
|
+
optional: {
|
|
206
|
+
stopReason: isString, nativeSessionId: isNullableString, error: isNullableString,
|
|
207
|
+
authMethods: isNullableAuthMethodInfoArray,
|
|
208
|
+
errorCode: isNullableJsonRpcErrorCode,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
agent_event_ack: { required: { sessionRef: isString, throughSeq: isNumber } },
|
|
212
|
+
agent_session_lost: { required: { sessionRef: isString, action: isString } },
|
|
213
|
+
// The session manager deliberately accepts malformed native-query payloads (including null)
|
|
214
|
+
// to return its protocol-level `ok: false` response. The transport only requires that the
|
|
215
|
+
// field is present; semantic validation remains at the request handler boundary.
|
|
216
|
+
agent_list_native: { required: { id: isString, bridge: isString, query: () => true } },
|
|
217
|
+
agent_native_sessions: {
|
|
218
|
+
required: { id: isString, ok: isBoolean },
|
|
219
|
+
optional: {
|
|
220
|
+
sessions: (sessions) => Array.isArray(sessions) && sessions.every(isNativeSession), error: isNullableString,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
// Same lenience as `agent_list_native` above, for the same reason: the session manager wants to
|
|
224
|
+
// answer a malformed query with its own `ok: false` rather than have the transport drop it.
|
|
225
|
+
agent_verify: {
|
|
226
|
+
required: { id: isString, bridge: isString, query: () => true },
|
|
227
|
+
// 与 `query` 那条**刻意的宽松**相反,这里是严格的:一份畸形的 query 还有个能回
|
|
228
|
+
// `ok: false` 的接收方(会话管理器),而一个畸形的 timeoutMs 谁也接不住——`"30s"` 或 `NaN`
|
|
229
|
+
// 一路传到 setTimeout 就是"立刻超时"或"永不超时",两种都不是任何人的意思。缺席与畸形在
|
|
230
|
+
// 这里必须分得开:缺席 = 没人指定预算(用本地默认),畸形 = 对方说错了话,整帧拒掉。
|
|
231
|
+
optional: { timeoutMs: isNullableProbeTimeoutMs },
|
|
232
|
+
},
|
|
233
|
+
agent_authenticate: {
|
|
234
|
+
required: { id: isString, bridge: isString, methodId: isString, query: () => true },
|
|
235
|
+
},
|
|
236
|
+
agent_verified: {
|
|
237
|
+
required: { id: isString, ok: isBoolean },
|
|
238
|
+
optional: {
|
|
239
|
+
authMethods: isNullableAuthMethodInfoArray,
|
|
240
|
+
// Not `isBoolean`: `false` is not a value this frame has. The decoder refusing it is what
|
|
241
|
+
// stops "we could not verify" from ever reaching a UI as "you are signed out".
|
|
242
|
+
authenticated: isVerifiedTrue,
|
|
243
|
+
sessionError: isNullableString,
|
|
244
|
+
error: isNullableString,
|
|
245
|
+
availableCommands: isNullableAvailableCommandArray,
|
|
246
|
+
configOptions: isNullableSessionConfigOptionArray,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
agent_authenticated: {
|
|
250
|
+
required: { id: isString, ok: isBoolean },
|
|
251
|
+
// `pending` is true-or-absent, the same rule as `agent_verified.authenticated` and for the same
|
|
252
|
+
// reason: absence already means "this is the final word", so a `false` on the wire would be a
|
|
253
|
+
// second spelling of it that a reader could disagree with.
|
|
254
|
+
optional: { pending: isVerifiedTrue, error: isNullableString },
|
|
255
|
+
},
|
|
256
|
+
agent_auth_url: { required: { id: isString, elicitationId: isString, url: isString } },
|
|
257
|
+
agent_auth_url_done: { required: { id: isString, elicitationId: isString } },
|
|
258
|
+
agent_install: { required: { id: isString, bridge: isString } },
|
|
259
|
+
agent_installed: {
|
|
260
|
+
required: { id: isString, ok: isBoolean },
|
|
261
|
+
optional: {
|
|
262
|
+
// Not `isBoolean`: like `agent_verified.authenticated`, this field has no `false` — an
|
|
263
|
+
// install that did not put the adapter on the machine says so by leaving the key out.
|
|
264
|
+
adapterInstalled: isVerifiedTrue,
|
|
265
|
+
alreadyInstalled: isBoolean,
|
|
266
|
+
unsupported: isBoolean,
|
|
267
|
+
error: isNullableString,
|
|
268
|
+
pinnedVersionUnavailable: isPinnedVersionUnavailable,
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
agent_steer: {
|
|
272
|
+
required: { id: isString, sessionRef: isString, text: isString },
|
|
273
|
+
optional: { images: isImageBlockArray, priority: isString },
|
|
274
|
+
},
|
|
275
|
+
agent_steered: {
|
|
276
|
+
required: { id: isString, ok: isBoolean },
|
|
277
|
+
optional: { outcome: isSteerOutcome, error: isNullableString },
|
|
278
|
+
},
|
|
279
|
+
agent_backfill_events: {
|
|
280
|
+
required: { id: isString, events: (events) => Array.isArray(events) && events.every(isEvent) },
|
|
281
|
+
},
|
|
282
|
+
agent_backfill_done: {
|
|
283
|
+
required: { id: isString, ok: isBoolean, count: isNumber },
|
|
284
|
+
optional: { error: isNullableString },
|
|
285
|
+
},
|
|
286
|
+
agent_set_config_option: {
|
|
287
|
+
required: { id: isString, sessionRef: isString, configId: isString, value: isConfigOptionValue },
|
|
288
|
+
},
|
|
289
|
+
agent_config_option_set: {
|
|
290
|
+
required: { id: isString, ok: isBoolean },
|
|
291
|
+
optional: { configOptions: isNullableSessionConfigOptionArray, error: isNullableString },
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
const DISCRIMINATORS = new Set(Object.keys(AGENT_FRAME_SHAPES));
|
|
295
|
+
function isRecord(value) { return isObject(value); }
|
|
296
|
+
function isAgentResumeSession(value) {
|
|
297
|
+
return isObject(value)
|
|
298
|
+
&& hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean });
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Accepts only the two outcomes this client can produce. `startedNewTurn` is refused on purpose: it
|
|
302
|
+
* is the extension's third outcome, and receiving one means a peer invented a disposition for a
|
|
303
|
+
* message the server has already put on its timeline — see `acpSession.ts`'s `mapSteerOutcome`.
|
|
304
|
+
*/
|
|
305
|
+
function isSteerOutcome(value) { return value === 'injected' || value === 'promptRequired'; }
|
|
306
|
+
/** Accepts `true` and nothing else — the whole point of `authenticated` having no `false`. */
|
|
307
|
+
function isVerifiedTrue(value) { return value === true; }
|
|
308
|
+
/**
|
|
309
|
+
* 一个能当毫秒预算用的数,或者「没人指定」。
|
|
310
|
+
*
|
|
311
|
+
* `null` 与缺席都读作后者(服务端的 `AgentVerify.timeoutMs` 是 `Long?`,虽然它配了
|
|
312
|
+
* `EncodeDefault.NEVER` 因而不会真把 null 写上线,但守卫不该依赖发送方的编码设置)。**拒绝**
|
|
313
|
+
* `NaN` / `Infinity` / 负数 / 0 / 非数:它们全都会变成一个没有人想要的 setTimeout —— 立刻超时
|
|
314
|
+
* 或永不超时,而不是"这次多等一会儿"。上界不在这里判,在 `clampProbeTimeout` 里夹紧:一个过大
|
|
315
|
+
* 但成形的数是**可以执行**的请求(夹到上限即可),而一个畸形的数不是。
|
|
316
|
+
*/
|
|
317
|
+
function isNullableProbeTimeoutMs(value) {
|
|
318
|
+
if (value === undefined || value === null)
|
|
319
|
+
return true;
|
|
320
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* A JSON-RPC error code, or "this failure had none".
|
|
324
|
+
*
|
|
325
|
+
* `null` and absent both read as the latter — a refusal that was not a JSON-RPC one, a workstation
|
|
326
|
+
* failure the agent never answered, or a peer that predates the field. Anything else must be an
|
|
327
|
+
* integer inside the Kotlin twin's `Int`: a fractional or out-of-range number does not merely lose
|
|
328
|
+
* precision on the way over, it fails to decode and takes the **whole frame** with it, which is
|
|
329
|
+
* exactly how `stat().mtimeMs` once cost an entire agent channel. This guard is the second of the
|
|
330
|
+
* two that stop it — `jsonRpcErrorCode` already refuses to produce one on the way out — and it is
|
|
331
|
+
* the one that also covers a peer this daemon did not write.
|
|
332
|
+
*/
|
|
333
|
+
function isNullableJsonRpcErrorCode(value) {
|
|
334
|
+
if (value === undefined || value === null)
|
|
335
|
+
return true;
|
|
336
|
+
return typeof value === 'number' && Number.isInteger(value)
|
|
337
|
+
&& value >= -2_147_483_648 && value <= 2_147_483_647;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* 一整份命令表,或者「没人看过」。
|
|
341
|
+
*
|
|
342
|
+
* 缺席与空表在这里必须分得开:缺席读作「问不到」,`[]` 读作「这个 agent 声明了零条命令」。
|
|
343
|
+
* 所以这个守卫接受 `undefined`(键不在)与 `[]`(键在、表空),但**拒绝**任何一条不成形的条目
|
|
344
|
+
* ——半解码的表比没有表更糟,它会让菜单列出点下去什么也不会发生的项。
|
|
345
|
+
*/
|
|
346
|
+
function isNullableAvailableCommandArray(value) {
|
|
347
|
+
if (value === undefined || value === null)
|
|
348
|
+
return true;
|
|
349
|
+
if (!Array.isArray(value))
|
|
350
|
+
return false;
|
|
351
|
+
return value.every((entry) => typeof entry === 'object' && entry !== null &&
|
|
352
|
+
typeof entry.name === 'string' &&
|
|
353
|
+
typeof entry.description === 'string' &&
|
|
354
|
+
(entry.inputHint === undefined ||
|
|
355
|
+
entry.inputHint === null ||
|
|
356
|
+
typeof entry.inputHint === 'string'));
|
|
357
|
+
}
|
|
358
|
+
/** Guards {@link SelectOptionWire}: `value`/`name` required strings, `description` optional-and-nullable. */
|
|
359
|
+
function isSelectOption(value) {
|
|
360
|
+
return isObject(value) && typeof value.value === 'string'
|
|
361
|
+
&& typeof value.name === 'string'
|
|
362
|
+
&& hasOptional(value, { description: isNullableString });
|
|
363
|
+
}
|
|
364
|
+
/** Guards {@link SelectGroupWire}: a named cluster whose `options` are all {@link SelectOptionWire}. */
|
|
365
|
+
function isSelectGroup(value) {
|
|
366
|
+
return isObject(value) && typeof value.group === 'string'
|
|
367
|
+
&& typeof value.name === 'string'
|
|
368
|
+
&& Array.isArray(value.options) && value.options.every(isSelectOption);
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Guards one {@link SessionConfigOptionWire}. `currentValue`'s required type follows the
|
|
372
|
+
* discriminator — a `select` option's value is the id of one of its own `options`, a `boolean`
|
|
373
|
+
* option's is the toggle state — so a mismatch here (a `select` with a boolean `currentValue`, or
|
|
374
|
+
* vice versa) is refused rather than forwarded as an option no UI can render.
|
|
375
|
+
*/
|
|
376
|
+
function isSessionConfigOption(value) {
|
|
377
|
+
if (!isObject(value))
|
|
378
|
+
return false;
|
|
379
|
+
const o = value;
|
|
380
|
+
if (typeof o.id !== 'string' || typeof o.name !== 'string')
|
|
381
|
+
return false;
|
|
382
|
+
if (!hasOptional(value, { description: isNullableString, category: isNullableString }))
|
|
383
|
+
return false;
|
|
384
|
+
if (o.type === 'boolean')
|
|
385
|
+
return typeof o.currentValue === 'boolean';
|
|
386
|
+
if (o.type === 'select') {
|
|
387
|
+
if (typeof o.currentValue !== 'string' || !Array.isArray(o.options))
|
|
388
|
+
return false;
|
|
389
|
+
return o.options.every(isSelectOption) || o.options.every(isSelectGroup);
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 一整份配置选项快照,或者「没人看过」。同 {@link isNullableAvailableCommandArray} 的缺席/空表
|
|
395
|
+
* 约定:缺席读作「问不到」,`[]` 读作「这个 agent 声明了零个配置项」,任何一条不成形的条目都拒绝
|
|
396
|
+
* ——半解码的表比没有表更糟。
|
|
397
|
+
*/
|
|
398
|
+
export function isNullableSessionConfigOptionArray(value) {
|
|
399
|
+
if (value === undefined || value === null)
|
|
400
|
+
return true;
|
|
401
|
+
return Array.isArray(value) && value.every(isSessionConfigOption);
|
|
402
|
+
}
|
|
403
|
+
/** Guards {@link ConfigOptionValueWire}: `agent_set_config_option.value` is a select id or a boolean toggle. */
|
|
404
|
+
function isConfigOptionValue(value) { return typeof value === 'string' || typeof value === 'boolean'; }
|
|
405
|
+
/**
|
|
406
|
+
* Guards {@link AgentInfoWire}: `name` required, `title`/`version` optional-and-nullable.
|
|
407
|
+
*
|
|
408
|
+
* This is deliberately laxer than ACP's own `Implementation` (`types.gen.d.ts:2306-2321`), which
|
|
409
|
+
* declares `version` required and non-nullable (`title` is the one that is optional-and-nullable
|
|
410
|
+
* there). ACP requires it; this guard accepts its absence anyway, because the consequence of
|
|
411
|
+
* refusing it is disproportionate: `agent_opened` is a single reply, not a per-field message, so a
|
|
412
|
+
* decode failure here does not drop just `agentInfo` — `outboundAgentFrameRejection`'s round-trip
|
|
413
|
+
* check fails the whole frame, `agentSessions.ts` sends `{ok:false}` instead, and the session fails
|
|
414
|
+
* to open over a display-only version string. Accepting a spec-incomplete `agentInfo` costs nothing
|
|
415
|
+
* a consumer cares about; rejecting one costs the session.
|
|
416
|
+
*/
|
|
417
|
+
function isAgentInfo(value) {
|
|
418
|
+
return isObject(value)
|
|
419
|
+
&& hasRequired(value, { name: isString })
|
|
420
|
+
&& hasOptional(value, { title: isNullableString, version: isNullableString });
|
|
421
|
+
}
|
|
422
|
+
/** Guards `agent_installed.pinnedVersionUnavailable` — see that field's own doc comment. */
|
|
423
|
+
function isPinnedVersionUnavailable(value) {
|
|
424
|
+
return isObject(value) && hasRequired(value, { pinned: isString, installedInstead: isString });
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Guards {@link ImageBlockWire}. Strict on every field ACP declares required, and on the
|
|
428
|
+
* discriminator: a block missing `mimeType` reaches the agent as an undecodable blob, and one whose
|
|
429
|
+
* `type` is anything but `image` would be some other content variant smuggled through a field named
|
|
430
|
+
* for images. Both are refused here rather than half-forwarded -- the same rule the Kotlin side's
|
|
431
|
+
* `AcpContentBlock.fromWire` follows ("a missing required field yields null, never a partial
|
|
432
|
+
* block"), because half an image is worse than no image: the agent answers about something it could
|
|
433
|
+
* not read, and nothing on the timeline says why.
|
|
434
|
+
*
|
|
435
|
+
* `uri` is ACP's own optional-and-nullable field (`types.gen.d.ts:330-332`).
|
|
436
|
+
*/
|
|
437
|
+
function isImageBlock(value) {
|
|
438
|
+
return isObject(value)
|
|
439
|
+
&& value.type === 'image'
|
|
440
|
+
&& hasRequired(value, { data: isString, mimeType: isString })
|
|
441
|
+
&& hasOptional(value, { uri: isNullableString });
|
|
442
|
+
}
|
|
443
|
+
function isImageBlockArray(value) {
|
|
444
|
+
return Array.isArray(value) && value.every(isImageBlock);
|
|
445
|
+
}
|
|
446
|
+
function isNativeSession(value) {
|
|
447
|
+
return isObject(value) && hasRequired(value, {
|
|
448
|
+
nativeSessionId: isString, title: isString, updatedAtEpochMs: isNumber, cwd: isString,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function isEvent(value) {
|
|
452
|
+
return isObject(value) && hasRequired(value, { eventName: isString, payload: isRecord });
|
|
453
|
+
}
|
|
454
|
+
function isAgentFrame(value) {
|
|
455
|
+
// Cast, not a narrowed lookup: `value.t` is unvetted wire input, so it may not be one of
|
|
456
|
+
// `AgentFrame['t']`'s literals at all — the `!shape` check right below is what actually handles
|
|
457
|
+
// that, exactly as it did when this table was indexed by plain `string`.
|
|
458
|
+
const shape = AGENT_FRAME_SHAPES[String(value.t)];
|
|
459
|
+
if (!shape)
|
|
460
|
+
return false;
|
|
461
|
+
return hasRequired(value, shape.required) && hasOptional(value, shape.optional ?? {});
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Fields the Kotlin twin declares as `Long` or `Int`, per frame type.
|
|
465
|
+
*
|
|
466
|
+
* TypeScript has one number type and the shape guards above accept any finite one, so a fractional
|
|
467
|
+
* value passes every check on this side and then fails to decode on the other — the field does not
|
|
468
|
+
* lose precision, the whole frame becomes unreadable. `stat().mtimeMs` is fractional on macOS and
|
|
469
|
+
* reached the wire twice, from two different bridges, before this table existed.
|
|
470
|
+
*/
|
|
471
|
+
const INTEGER_FIELDS = {
|
|
472
|
+
agent_ready: ['agentWire'],
|
|
473
|
+
heartbeat: ['seq', 'sentAtEpochMs'],
|
|
474
|
+
heartbeat_ack: ['seq'],
|
|
475
|
+
agent_open: ['resumeFromSeq'],
|
|
476
|
+
agent_opened: ['errorCode'],
|
|
477
|
+
agent_event: ['seq'],
|
|
478
|
+
agent_turn_done: ['seq', 'errorCode'],
|
|
479
|
+
agent_event_ack: ['throughSeq'],
|
|
480
|
+
agent_backfill_done: ['count'],
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Why an `authenticated` field must not go out, or null when it may.
|
|
484
|
+
*
|
|
485
|
+
* ACP v1 offers no way to ask an agent whether it is authenticated, and no way to be told that it is
|
|
486
|
+
* not: the only positive evidence is a session the agent accepted, and everything else — a
|
|
487
|
+
* subprocess that would not start, an agent we have never connected to, a `session/new` that failed
|
|
488
|
+
* for some unrelated reason — is *unknown*. Publishing unknown as `authenticated: false` would put a
|
|
489
|
+
* "sign in again" prompt in front of a user whose credentials are perfectly good, and they would
|
|
490
|
+
* have no way to tell the fabricated verdict from a real one.
|
|
491
|
+
*
|
|
492
|
+
* So the field is `true`-or-absent, and this is where that is enforced rather than merely intended.
|
|
493
|
+
* {@link isVerifiedTrue} already makes the decoder refuse a `false`, but a decode failure names only
|
|
494
|
+
* the frame; this names the field, which is what a caller needs to find the line that produced it.
|
|
495
|
+
*/
|
|
496
|
+
function authenticatedViolation(frame) {
|
|
497
|
+
if (frame.t !== 'agent_verified')
|
|
498
|
+
return null;
|
|
499
|
+
const value = frame.authenticated;
|
|
500
|
+
if (value === undefined || value === true)
|
|
501
|
+
return null;
|
|
502
|
+
return `${frame.t}.authenticated may only be true or absent (absent means unknown, never ` +
|
|
503
|
+
`"not authenticated"), got ${String(value)}`;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* {@link authenticatedViolation}'s twin for `agent_installed.adapterInstalled`, and it exists for
|
|
507
|
+
* the same reason rather than out of symmetry: the settings page turns that field into the word
|
|
508
|
+
* "Configured" in front of a reader, so a `false` written where a key should simply be missing
|
|
509
|
+
* would be a claim about their machine that nothing on this side established.
|
|
510
|
+
*/
|
|
511
|
+
function adapterInstalledViolation(frame) {
|
|
512
|
+
if (frame.t !== 'agent_installed')
|
|
513
|
+
return null;
|
|
514
|
+
const value = frame.adapterInstalled;
|
|
515
|
+
if (value === undefined || value === true)
|
|
516
|
+
return null;
|
|
517
|
+
return `${frame.t}.adapterInstalled may only be true or absent (absent means the adapter did not ` +
|
|
518
|
+
`get onto the machine), got ${String(value)}`;
|
|
519
|
+
}
|
|
520
|
+
function integerViolation(frame) {
|
|
521
|
+
for (const field of INTEGER_FIELDS[frame.t] ?? []) {
|
|
522
|
+
const value = frame[field];
|
|
523
|
+
if (value !== undefined && !Number.isInteger(value)) {
|
|
524
|
+
return `${frame.t}.${field} must be a whole number, got ${String(value)}`;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// The one integer that does not sit at the top level, and the one that actually broke.
|
|
528
|
+
if (frame.t === 'agent_native_sessions' && Array.isArray(frame.sessions)) {
|
|
529
|
+
for (const [index, session] of frame.sessions.entries()) {
|
|
530
|
+
const value = session?.updatedAtEpochMs;
|
|
531
|
+
if (!Number.isInteger(value)) {
|
|
532
|
+
return `agent_native_sessions.sessions[${index}].updatedAtEpochMs must be a whole number, got ${String(value)}`;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// `journalMaxSeq` is a Kotlin `Long` on both carriers of the inventory, so both need the rule: a
|
|
537
|
+
// fractional value does not lose precision on the far side, it makes the whole frame undecodable
|
|
538
|
+
// -- and on the heartbeat that costs liveness, not just an event.
|
|
539
|
+
if (frame.t === 'agent_resume_offer' || frame.t === 'heartbeat') {
|
|
540
|
+
for (const [index, session] of (frame.sessions ?? []).entries()) {
|
|
541
|
+
if (!Number.isInteger(session.journalMaxSeq)) {
|
|
542
|
+
return `${frame.t}.sessions[${index}].journalMaxSeq must be a whole number, got ${String(session.journalMaxSeq)}`;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Why this frame must not be put on the wire, or null when it may go.
|
|
550
|
+
*
|
|
551
|
+
* Checked by round-tripping through our own decoder rather than against a second schema, so the
|
|
552
|
+
* outbound contract cannot drift from the inbound one, plus the integer rule the decoder cannot
|
|
553
|
+
* express. What the peer will read is exactly what gets checked: `JSON.stringify` drops `undefined`
|
|
554
|
+
* and applies `toJSON`, so the parsed form is the only faithful subject.
|
|
555
|
+
*
|
|
556
|
+
* Total — every failure comes back as a string, including a frame that cannot be serialized at all.
|
|
557
|
+
*/
|
|
558
|
+
export function outboundAgentFrameRejection(frame) {
|
|
559
|
+
// Integers first, because that check names the offending field while the decoder can only name
|
|
560
|
+
// the frame. It is also the only one that sees `NaN`: `JSON.stringify` writes NaN as `null`, so
|
|
561
|
+
// by the time the round trip runs, a timestamp that was never a number looks like a missing one.
|
|
562
|
+
const violation = integerViolation(frame) ?? authenticatedViolation(frame) ?? adapterInstalledViolation(frame);
|
|
563
|
+
if (violation)
|
|
564
|
+
return violation;
|
|
565
|
+
try {
|
|
566
|
+
decodeAgentFrame(encodeAgentFrame(frame));
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
return error instanceof Error ? error.message : String(error);
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* A well-formed failure to send in place of a frame that answered a server request, or null when
|
|
575
|
+
* the frame answered nothing and can only be dropped.
|
|
576
|
+
*
|
|
577
|
+
* Without this the requester waits out its timeout and reports the workstation as unresponsive,
|
|
578
|
+
* which sends whoever is debugging after the network instead of after the bridge that produced the
|
|
579
|
+
* bad value. `agent_backfill_events` is not itself an answer, but losing a batch makes the backfill
|
|
580
|
+
* incomplete, so it fails as its terminator would.
|
|
581
|
+
*/
|
|
582
|
+
export function outboundFailureReply(frame, reason) {
|
|
583
|
+
const id = frame.id;
|
|
584
|
+
// The id is what routes the failure back. If that is the malformed part there is no one to tell.
|
|
585
|
+
if (typeof id !== 'string' || id.length === 0)
|
|
586
|
+
return null;
|
|
587
|
+
const error = `the workstation client produced a malformed ${frame.t}: ${reason}`;
|
|
588
|
+
switch (frame.t) {
|
|
589
|
+
case 'agent_opened': return { t: 'agent_opened', id, ok: false, error };
|
|
590
|
+
case 'agent_native_sessions': return { t: 'agent_native_sessions', id, ok: false, error };
|
|
591
|
+
case 'agent_verified': return { t: 'agent_verified', id, ok: false, error };
|
|
592
|
+
case 'agent_authenticated': return { t: 'agent_authenticated', id, ok: false, error };
|
|
593
|
+
case 'agent_installed': return { t: 'agent_installed', id, ok: false, error };
|
|
594
|
+
case 'agent_steered': return { t: 'agent_steered', id, ok: false, error };
|
|
595
|
+
case 'agent_config_option_set': return { t: 'agent_config_option_set', id, ok: false, error };
|
|
596
|
+
case 'agent_backfill_done':
|
|
597
|
+
case 'agent_backfill_events': return { t: 'agent_backfill_done', id, ok: false, count: 0, error };
|
|
598
|
+
default: return null;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
export function encodeAgentFrame(frame) {
|
|
602
|
+
return JSON.stringify(frame);
|
|
603
|
+
}
|
|
604
|
+
export function decodeAgentFrame(text) {
|
|
605
|
+
return decodeChannelFrame(text, DISCRIMINATORS, isAgentFrame);
|
|
606
|
+
}
|