@platless/axom-cli 0.1.0 → 0.3.13
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 +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +110 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +34 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +49 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +81 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +41 -0
- package/dist/axom-patch/cli/qoder-steering.json +52 -0
- package/dist/axom-patch/cli/qoder-steering.md +30 -0
- package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
- package/dist/axom-patch/cli/runtime.json +37 -0
- package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
- package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
- package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
- package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
- package/dist/cli.js +9 -1
- package/dist/commands/auth.js +4 -13
- package/dist/commands/execute.js +4 -4
- package/dist/commands/record.js +6 -6
- package/dist/commands/struct.js +4 -4
- package/dist/commands/workstation.js +2 -2
- package/dist/credentials.js +2 -2
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +32 -14
- package/dist/workstation/acp/acpAgents.js +447 -166
- package/dist/workstation/acp/acpClient.js +232 -18
- package/dist/workstation/acp/acpSession.js +502 -74
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +107 -49
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/contract.js +11 -6
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/draftInterceptor.js +9 -0
- package/dist/workstation/acp/managedPatches.js +228 -0
- package/dist/workstation/acp/patchAssets.js +63 -0
- package/dist/workstation/acp/permission.js +300 -34
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +263 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/sessionScope.js +19 -0
- package/dist/workstation/acp/sessionUpdateMapper.js +39 -21
- package/dist/workstation/acp/terminalLogin.js +197 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentSessions.js +532 -44
- package/dist/workstation/channels/agent.js +7 -1
- package/dist/workstation/channels/computer.js +152 -4
- package/dist/workstation/channels/management.js +3 -3
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +25 -3
- package/dist/workstation/desktop/binding.js +36 -0
- package/dist/workstation/desktop/bridge.js +212 -0
- package/dist/workstation/desktop/flow-control.js +52 -0
- package/dist/workstation/desktop/focused-text.js +130 -0
- package/dist/workstation/desktop/protocol.js +35 -0
- package/dist/workstation/fileOps.js +277 -0
- package/dist/workstation/filePaths.js +174 -0
- package/dist/workstation/fileWorker.js +81 -0
- package/dist/workstation/fileWorkerClient.js +121 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/agent.js +154 -5
- package/dist/workstation/protocol/common.js +6 -0
- package/dist/workstation/protocol/computer.js +92 -3
- package/dist/workstation/qoderAdapter/backgroundProcess.js +106 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +262 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +30 -0
- package/dist/workstation/qoderAdapter/history.js +87 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +93 -0
- package/dist/workstation/qoderAdapter/mapper.js +117 -0
- package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
- package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
- package/dist/workstation/qoderAdapter/permissions.js +61 -0
- package/dist/workstation/qoderAdapter/profile.js +93 -0
- package/dist/workstation/qoderAdapter/sdkDriver.js +49 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +77 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -0
- package/dist/workstation/qoderAdapter/taskEvent.js +26 -0
- package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
- package/dist/workstation/qoderAdapter/types.js +1 -0
- package/dist/workstation/qoderAdapter/usage.js +37 -0
- package/dist/workstation/register.js +4 -0
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +269 -0
- package/dist/workstation/tools/core.js +50 -0
- package/dist/workstation/tools/files.js +525 -0
- package/dist/workstation/tools/keeperProcess.js +114 -0
- package/dist/workstation/tools/terminal.js +214 -0
- package/dist/workstation/tools/validation.js +50 -0
- package/dist/workstation/update/coordinator.js +29 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +24 -0
- package/dist/workstation/update/taskRegistry.js +44 -20
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +14 -4
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
|
@@ -2,6 +2,8 @@ import WebSocket from 'ws';
|
|
|
2
2
|
import { RetryPolicy } from '../connection/retryPolicy.js';
|
|
3
3
|
import { tryOpenAuthorizationUrl } from './browser.js';
|
|
4
4
|
import { loadDeviceId } from '../../config.js';
|
|
5
|
+
import { DEFAULT_CLI_SCOPES, grantedCliScopes } from '../../authScopes.js';
|
|
6
|
+
import { revokeRefreshToken } from '../../credentials.js';
|
|
5
7
|
export const DEVICE_CODE_GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
6
8
|
export class DeviceAuthorizationError extends Error {
|
|
7
9
|
}
|
|
@@ -87,7 +89,7 @@ function waitForNotification(socket, sessionId, notificationTicket, deadline, no
|
|
|
87
89
|
socket.on('error', () => finish('reconnect'));
|
|
88
90
|
});
|
|
89
91
|
}
|
|
90
|
-
async function exchangeDeviceCode(server, clientId, deviceCode, fetch_) {
|
|
92
|
+
async function exchangeDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes) {
|
|
91
93
|
const response = await requestJson(fetch_, new URL('/oauth/token', server), {
|
|
92
94
|
method: 'POST',
|
|
93
95
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
@@ -98,14 +100,25 @@ async function exchangeDeviceCode(server, clientId, deviceCode, fetch_) {
|
|
|
98
100
|
device_id: await loadDeviceId(),
|
|
99
101
|
}),
|
|
100
102
|
}, 200, 'Device token exchange');
|
|
103
|
+
return validateTokenResponse(server, response, requestedScopes, fetch_);
|
|
104
|
+
}
|
|
105
|
+
async function validateTokenResponse(server, response, requestedScopes, fetch_) {
|
|
101
106
|
const accessToken = requireString(response.access_token, 'access_token', 'Device token response');
|
|
102
107
|
const refreshToken = requireString(response.refresh_token, 'refresh_token', 'Device token response');
|
|
103
108
|
if (!Number.isFinite(response.expires_in) || response.expires_in <= 0) {
|
|
104
109
|
throw new DeviceAuthorizationError('Device token response has invalid expires_in');
|
|
105
110
|
}
|
|
106
|
-
|
|
111
|
+
let scopes;
|
|
112
|
+
try {
|
|
113
|
+
scopes = grantedCliScopes(response.scope, requestedScopes);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
await revokeRefreshToken(server, refreshToken, fetch_);
|
|
117
|
+
throw new DeviceAuthorizationError(error instanceof Error ? error.message : 'Device token response has invalid scopes');
|
|
118
|
+
}
|
|
119
|
+
return { accessToken, refreshToken, expiresInSeconds: response.expires_in, scopes };
|
|
107
120
|
}
|
|
108
|
-
async function pollDeviceCode(server, clientId, deviceCode, fetch_) {
|
|
121
|
+
async function pollDeviceCode(server, clientId, deviceCode, fetch_, requestedScopes) {
|
|
109
122
|
const response = await fetch_(new URL('/oauth/token', server), {
|
|
110
123
|
method: 'POST',
|
|
111
124
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
@@ -128,17 +141,13 @@ async function pollDeviceCode(server, clientId, deviceCode, fetch_) {
|
|
|
128
141
|
if (response.status !== 200)
|
|
129
142
|
throw new DeviceAuthorizationError(`Device token polling failed: ${response.status}`);
|
|
130
143
|
const token = await response.json();
|
|
131
|
-
return
|
|
132
|
-
accessToken: requireString(token.access_token, 'access_token', 'Device token response'),
|
|
133
|
-
refreshToken: requireString(token.refresh_token, 'refresh_token', 'Device token response'),
|
|
134
|
-
expiresInSeconds: token.expires_in,
|
|
135
|
-
};
|
|
144
|
+
return validateTokenResponse(server, token, requestedScopes, fetch_);
|
|
136
145
|
}
|
|
137
146
|
/**
|
|
138
147
|
* Uses OAuth Device Authorization for both desktop and headless clients. The notification socket
|
|
139
148
|
* only carries state; the tokens exist only in this call's return value.
|
|
140
149
|
*/
|
|
141
|
-
export async function authorizeDevice(server, clientName, hostname, deps = {}) {
|
|
150
|
+
export async function authorizeDevice(server, clientName, hostname, deps = {}, requestedScopes = DEFAULT_CLI_SCOPES) {
|
|
142
151
|
const baseUrl = normalizedServer(server);
|
|
143
152
|
const fetch_ = deps.fetch ?? globalThis.fetch;
|
|
144
153
|
const print = deps.print ?? console.log;
|
|
@@ -151,7 +160,7 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}) {
|
|
|
151
160
|
const authorization = await requestJson(fetch_, new URL('/oauth/device_authorization', baseUrl), {
|
|
152
161
|
method: 'POST',
|
|
153
162
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
154
|
-
body: new URLSearchParams({ client_id: clientId, hostname }),
|
|
163
|
+
body: new URLSearchParams({ client_id: clientId, hostname, scope: requestedScopes.join(' ') }),
|
|
155
164
|
}, 200, 'Device authorization request');
|
|
156
165
|
const deviceCode = requireString(authorization.device_code, 'device_code', 'Device authorization response');
|
|
157
166
|
const verificationUrl = requireString(authorization.verification_uri_complete, 'verification_uri_complete', 'Device authorization response');
|
|
@@ -178,14 +187,14 @@ export async function authorizeDevice(server, clientName, hostname, deps = {}) {
|
|
|
178
187
|
}
|
|
179
188
|
const status = await waitForNotification(socket, sessionId, notificationTicket, deadline, now, () => retry.recordSuccess());
|
|
180
189
|
if (status === 'approved') {
|
|
181
|
-
const tokens = await exchangeDeviceCode(baseUrl, clientId, deviceCode, fetch_);
|
|
190
|
+
const tokens = await exchangeDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes);
|
|
182
191
|
return { clientId, ...tokens };
|
|
183
192
|
}
|
|
184
193
|
if (status === 'denied')
|
|
185
194
|
throw new DeviceAuthorizationError('Device authorization was denied');
|
|
186
195
|
if (status === 'expired')
|
|
187
196
|
break;
|
|
188
|
-
const polled = await pollDeviceCode(baseUrl, clientId, deviceCode, fetch_);
|
|
197
|
+
const polled = await pollDeviceCode(baseUrl, clientId, deviceCode, fetch_, requestedScopes);
|
|
189
198
|
if (polled)
|
|
190
199
|
return { clientId, ...polled };
|
|
191
200
|
const pollingFloor = Math.max(1, authorization.interval ?? 5) * 1_000;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { isRuntimeDiagnostics } from '../qoderAdapter/profile.js';
|
|
1
2
|
import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableAuthMethodInfoArray, isNullableString, isNumber, isObject, isString, isStringArray, isStringRecord, } from './common.js';
|
|
2
3
|
/** Current version and feature floors for the independently negotiated agent protocol. */
|
|
3
|
-
export const AGENT_WIRE_VERSION =
|
|
4
|
+
export const AGENT_WIRE_VERSION = 24;
|
|
5
|
+
/** `agent_delete_native` / `agent_native_deleted`: purge a session Axom created on an external agent. */
|
|
6
|
+
export const AGENT_WIRE_NATIVE_DELETE_VERSION = 24;
|
|
7
|
+
export const AGENT_WIRE_PROVIDER_TASK_VERSION = 23;
|
|
8
|
+
export const AGENT_WIRE_RUNTIME_PROFILE_VERSION = 22;
|
|
9
|
+
export const AGENT_WIRE_DELEGATION_RENEW_VERSION = 21;
|
|
10
|
+
export const AGENT_WIRE_TURN_IDENTITY_VERSION = 20;
|
|
11
|
+
export const AGENT_WIRE_DELEGATION_MCP_VERSION = 19;
|
|
12
|
+
/** Scoped replay/history is enabled only when both peers preserve native origin. */
|
|
13
|
+
export const AGENT_WIRE_SCOPED_ORIGIN_VERSION = 18;
|
|
14
|
+
/** agent_open.requireSessionLoad: reject unsupported/failed load without session/new fallback. */
|
|
15
|
+
export const AGENT_WIRE_STRICT_LOAD_VERSION = 17;
|
|
4
16
|
export const AGENT_WIRE_MIN_VERSION = 1;
|
|
5
17
|
export const AGENT_WIRE_SESSION_LOST_VERSION = 2;
|
|
6
18
|
export const AGENT_WIRE_ADOPTION_VERSION = 3;
|
|
@@ -137,6 +149,59 @@ export const AGENT_WIRE_RELOADED_VERSION = 13;
|
|
|
137
149
|
* is byte-for-byte the frame it always was and still goes out at {@link AGENT_WIRE_STEER_VERSION}.
|
|
138
150
|
*/
|
|
139
151
|
export const AGENT_WIRE_STEER_BLOCKS_VERSION = 14;
|
|
152
|
+
/**
|
|
153
|
+
* The session activity-state push (`agent_session_state`) and the single-background-task stop
|
|
154
|
+
* request (`agent_async_task_stop`). **Both must be checked before sending — the check just lives
|
|
155
|
+
* in a different process for each, because the two frames do not share a sender.**
|
|
156
|
+
*
|
|
157
|
+
* ## Why both need a floor, not just one
|
|
158
|
+
*
|
|
159
|
+
* A version floor answers one question: *before sending, does the peer understand this frame?*
|
|
160
|
+
* These two frames travel opposite directions, so the check lives on opposite sides:
|
|
161
|
+
*
|
|
162
|
+
* - `agent_async_task_stop` is the **server's** request to this client. A v14 *client* has no case
|
|
163
|
+
* for it in `AGENT_FRAME_SHAPES` — the same shape as {@link AGENT_WIRE_STEER_VERSION} and
|
|
164
|
+
* {@link AGENT_WIRE_INSTALL_VERSION} — so the check lives on the Kotlin server (this constant's
|
|
165
|
+
* twin, `AgentChannelSession`, a later task), before every `agent_async_task_stop` it sends. There
|
|
166
|
+
* is nothing for this file to check on receipt: this daemon only ever handles the frame, never
|
|
167
|
+
* sends it.
|
|
168
|
+
* - `agent_session_state` is the **client's** push to the server, and getting this half wrong is
|
|
169
|
+
* what an earlier revision of this comment did — it argued "an old server just never sees the
|
|
170
|
+
* frame" was enough, which is only half the compatibility matrix and not the dangerous half. The
|
|
171
|
+
* dangerous direction is **a newer client talking to an older server**: this daemon is a
|
|
172
|
+
* separately-installed npm package on the user's machine, so the client upgrading ahead of the
|
|
173
|
+
* server is the ordinary update order here, not an edge case. A v15 client sending
|
|
174
|
+
* `agent_session_state` to a v14 server hits a decoder that throws on the unrecognised
|
|
175
|
+
* discriminator, and because the frame carries a top-level `sessionRef` and no `id`, the server's
|
|
176
|
+
* own recovery (`classifyUndecodableAgentFrame`) charges it to `FailSession` — declaring that
|
|
177
|
+
* session lost and forcing a reopen. This frame exists to be sent over and over, so on an
|
|
178
|
+
* unupgraded server every single push would force a session loss, and a run of them closes the
|
|
179
|
+
* whole agent channel — worse than the `agent_auth_url` incident this file already documents,
|
|
180
|
+
* since that frame carries no `sessionRef` and only ever reaches `Unattributable`.
|
|
181
|
+
*
|
|
182
|
+
* The correct precedent is {@link AGENT_WIRE_AUTH_URL_VERSION}, not
|
|
183
|
+
* {@link AGENT_WIRE_RELOADED_VERSION}: `agent_opened.reloaded` is an **optional key on a frame the
|
|
184
|
+
* peer already decodes**, safely absorbed by `ignoreUnknownKeys`, so nothing on the sending side
|
|
185
|
+
* needs to check anything. `agent_session_state` is a **new discriminator**, and a decoder throws
|
|
186
|
+
* on an unrecognised one before `ignoreUnknownKeys` ever gets a say — the same shape
|
|
187
|
+
* `agent_auth_url` is, and that constant's own floor is checked client-side (`serverAgentWire <
|
|
188
|
+
* AGENT_WIRE_AUTH_URL_VERSION`, see `agentSessions.ts`'s `onAuthenticate`/`present`/`dismiss`) for
|
|
189
|
+
* exactly this reason. `agent_session_state` follows the same rule: the client checks
|
|
190
|
+
* `this.serverAgentWire` before sending and **discards** the push below this floor, rather than
|
|
191
|
+
* erroring — a level nobody downstream can read is no worse than one that was never collected.
|
|
192
|
+
*
|
|
193
|
+
* So both frames are gated, on opposite sides: this file's own send path (a later task) must check
|
|
194
|
+
* `serverAgentWire` before every `agent_session_state`, and the Kotlin twin must check
|
|
195
|
+
* `clientAgentWire` before every `agent_async_task_stop`.
|
|
196
|
+
*/
|
|
197
|
+
export const AGENT_WIRE_SESSION_STATE_VERSION = 15;
|
|
198
|
+
/**
|
|
199
|
+
* Elicitation answers (`agent_elicitation_answer`). Checked before sending, like
|
|
200
|
+
* {@link AGENT_WIRE_INSTALL_VERSION} and {@link AGENT_WIRE_CONFIG_OPTIONS_VERSION}: a peer below
|
|
201
|
+
* this floor has no case for the frame at all and would drop it, leaving the agent process blocked
|
|
202
|
+
* inside `elicitation/create` rather than ever reaching the user's answer.
|
|
203
|
+
*/
|
|
204
|
+
export const AGENT_WIRE_ELICITATION_VERSION = 16;
|
|
140
205
|
/**
|
|
141
206
|
* Every agent frame's shape, as data rather than as a `switch`.
|
|
142
207
|
*
|
|
@@ -171,7 +236,10 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
171
236
|
optional: {
|
|
172
237
|
env: isStringRecord, pathPrepend: isStringArray, agentEnv: isStringRecord, command: isNullableString,
|
|
173
238
|
args: isStringArray, nativeSessionId: isNullableString,
|
|
174
|
-
resumeFromSeq: isNumber, backfill: isBoolean,
|
|
239
|
+
resumeFromSeq: isNumber, backfill: isBoolean, requireSessionLoad: isBoolean,
|
|
240
|
+
delegationMcp: (value) => isObject(value) &&
|
|
241
|
+
isString(value.url) && isStringRecord(value.headers) && isNumber(value.expiresAtEpochMs) &&
|
|
242
|
+
(value.bindingId === undefined || isString(value.bindingId)),
|
|
175
243
|
},
|
|
176
244
|
},
|
|
177
245
|
agent_opened: {
|
|
@@ -180,6 +248,7 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
180
248
|
// `false` would be a second spelling of what its absence already says.
|
|
181
249
|
optional: {
|
|
182
250
|
nativeSessionId: isNullableString, error: isNullableString, supportsSteering: isVerifiedTrue,
|
|
251
|
+
runtimeDiagnostics: isRuntimeDiagnostics,
|
|
183
252
|
// Plain strings, not a closed vocabulary: the sender already dropped everything it did not
|
|
184
253
|
// recognise, and validating the values again here would make a future timing fail the frame
|
|
185
254
|
// rather than be ignored — the opposite of what ruling N4 asks for.
|
|
@@ -187,16 +256,36 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
187
256
|
// `reloaded` is `true`-or-absent for the same reason as `supportsSteering`: a `false` would
|
|
188
257
|
// be a second spelling of what its absence already says.
|
|
189
258
|
reloaded: isVerifiedTrue,
|
|
259
|
+
delegationExpiresAtEpochMs: isNumber,
|
|
260
|
+
delegationRefreshPending: isVerifiedTrue,
|
|
261
|
+
delegationInstanceId: isString, delegationBindingId: isString, delegationRenewalRevision: isNumber,
|
|
190
262
|
agentInfo: isAgentInfo, supportsImageInput: isVerifiedTrue, configOptions: isNullableSessionConfigOptionArray,
|
|
191
263
|
// Same guard as `agent_verified.authMethods`, and the same absent-vs-`[]` reading: a
|
|
192
264
|
// half-decoded method table is worse than none, because the reader would offer a login the
|
|
193
265
|
// agent never named.
|
|
194
266
|
authMethods: isNullableAuthMethodInfoArray,
|
|
195
267
|
errorCode: isNullableJsonRpcErrorCode,
|
|
268
|
+
// `true`-or-absent, the same rule as `supportsSteering` above: absence already means "no
|
|
269
|
+
// collector wired up", so a `false` would be a second spelling of it.
|
|
270
|
+
reportsActivity: isVerifiedTrue,
|
|
271
|
+
supportsAsyncTaskStop: isVerifiedTrue,
|
|
272
|
+
// Vendor-verbatim and uninterpreted here (decision L1) -- any object shape is accepted, the
|
|
273
|
+
// same lenience `agent_event.payload` gets via `isRecord`.
|
|
274
|
+
activityRaw: isRecord,
|
|
196
275
|
},
|
|
197
276
|
},
|
|
198
|
-
agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray } },
|
|
277
|
+
agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray, platformTurnId: isString } },
|
|
278
|
+
agent_delegation_renew: { required: { id: isString, sessionRef: isString, nativeSessionId: isString,
|
|
279
|
+
instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
|
|
280
|
+
agent_delegation_renewed: { required: { id: isString, ok: isBoolean }, optional: { error: isString,
|
|
281
|
+
nativeSessionId: isString, instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
|
|
199
282
|
agent_answer: { required: { sessionRef: isString, requestId: isString, outcome: isString } },
|
|
283
|
+
// `content`'s interior is never validated here -- it is ACP's free-form elicitation payload, and
|
|
284
|
+
// this table is transport, not translator. `isRecord` only rejects a non-object.
|
|
285
|
+
agent_elicitation_answer: {
|
|
286
|
+
required: { sessionRef: isString, requestId: isString, action: isString },
|
|
287
|
+
optional: { content: isRecord },
|
|
288
|
+
},
|
|
200
289
|
agent_interrupt: { required: { sessionRef: isString } },
|
|
201
290
|
agent_close: { required: { sessionRef: isString } },
|
|
202
291
|
agent_event: { required: { sessionRef: isString, seq: isNumber, eventName: isString, payload: isRecord } },
|
|
@@ -206,6 +295,8 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
206
295
|
stopReason: isString, nativeSessionId: isNullableString, error: isNullableString,
|
|
207
296
|
authMethods: isNullableAuthMethodInfoArray,
|
|
208
297
|
errorCode: isNullableJsonRpcErrorCode,
|
|
298
|
+
platformTurnId: isString,
|
|
299
|
+
executionEnded: isVerifiedTrue,
|
|
209
300
|
},
|
|
210
301
|
},
|
|
211
302
|
agent_event_ack: { required: { sessionRef: isString, throughSeq: isNumber } },
|
|
@@ -214,6 +305,12 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
214
305
|
// to return its protocol-level `ok: false` response. The transport only requires that the
|
|
215
306
|
// field is present; semantic validation remains at the request handler boundary.
|
|
216
307
|
agent_list_native: { required: { id: isString, bridge: isString, query: () => true } },
|
|
308
|
+
// `query` is as lenient as `agent_list_native`'s, for the same reason.
|
|
309
|
+
agent_delete_native: { required: { id: isString, bridge: isString, query: () => true, nativeSessionIds: isStringArray } },
|
|
310
|
+
agent_native_deleted: {
|
|
311
|
+
required: { id: isString, ok: isBoolean },
|
|
312
|
+
optional: { results: isNativeDeleteResults, error: isNullableString },
|
|
313
|
+
},
|
|
217
314
|
agent_native_sessions: {
|
|
218
315
|
required: { id: isString, ok: isBoolean },
|
|
219
316
|
optional: {
|
|
@@ -290,9 +387,39 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
290
387
|
required: { id: isString, ok: isBoolean },
|
|
291
388
|
optional: { configOptions: isNullableSessionConfigOptionArray, error: isNullableString },
|
|
292
389
|
},
|
|
390
|
+
agent_session_state: { required: { sessionRef: isString, vendor: isString, raw: isRecord } },
|
|
391
|
+
agent_runtime_profile: { required: { sessionRef: isString, diagnostics: isRuntimeDiagnostics } },
|
|
392
|
+
agent_provider_task_event: { required: { sessionRef: isString, rootSessionId: isTaskId,
|
|
393
|
+
runtimeGeneration: isTaskId, taskId: isTaskId, uuid: isTaskId, state: isProviderTaskState,
|
|
394
|
+
live: isBoolean, stale: isBoolean }, optional: { taskType: isTaskType, background: isBoolean,
|
|
395
|
+
agentId: isTaskId, launchingToolUseId: isTaskId, status: isProviderTerminalState, summary: isTaskSummary } },
|
|
396
|
+
agent_provider_task_stop: { required: { id: isTaskId, sessionRef: isTaskId, rootSessionId: isTaskId,
|
|
397
|
+
runtimeGeneration: isTaskId, taskId: isTaskId, requestId: isTaskId } },
|
|
398
|
+
agent_provider_task_stopped: { required: { id: isTaskId, state: isProviderStopState },
|
|
399
|
+
optional: { error: isTaskSummary } },
|
|
400
|
+
agent_async_task_stop: { required: { sessionRef: isString, asyncTaskId: isString } },
|
|
293
401
|
};
|
|
294
402
|
const DISCRIMINATORS = new Set(Object.keys(AGENT_FRAME_SHAPES));
|
|
295
403
|
function isRecord(value) { return isObject(value); }
|
|
404
|
+
function isTaskId(value) {
|
|
405
|
+
return typeof value === 'string' && value.length > 0 &&
|
|
406
|
+
Buffer.byteLength(value, 'utf8') <= 256 && !/[\x00-\x1f\x7f]/.test(value);
|
|
407
|
+
}
|
|
408
|
+
function isTaskType(value) { return isTaskId(value) && Buffer.byteLength(value, 'utf8') <= 128; }
|
|
409
|
+
function isTaskSummary(value) {
|
|
410
|
+
return typeof value === 'string' &&
|
|
411
|
+
Buffer.byteLength(value, 'utf8') <= 8192;
|
|
412
|
+
}
|
|
413
|
+
function isProviderTaskState(value) {
|
|
414
|
+
return value === 'pending' || value === 'paused' || value === 'running' || value === 'completed' ||
|
|
415
|
+
value === 'stopped' || value === 'failed' || value === 'unknown';
|
|
416
|
+
}
|
|
417
|
+
function isProviderTerminalState(value) {
|
|
418
|
+
return value === 'completed' || value === 'failed' || value === 'stopped';
|
|
419
|
+
}
|
|
420
|
+
function isProviderStopState(value) {
|
|
421
|
+
return value === 'accepted' || value === 'unknown' || value === 'rejected';
|
|
422
|
+
}
|
|
296
423
|
function isAgentResumeSession(value) {
|
|
297
424
|
return isObject(value)
|
|
298
425
|
&& hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean });
|
|
@@ -448,8 +575,17 @@ function isNativeSession(value) {
|
|
|
448
575
|
nativeSessionId: isString, title: isString, updatedAtEpochMs: isNumber, cwd: isString,
|
|
449
576
|
});
|
|
450
577
|
}
|
|
578
|
+
function isNativeDeleteResult(value) {
|
|
579
|
+
return isObject(value)
|
|
580
|
+
&& hasRequired(value, { nativeSessionId: isString, outcome: (o) => o === 'deleted' || o === 'unsupported' || o === 'failed' })
|
|
581
|
+
&& hasOptional(value, { error: isNullableString });
|
|
582
|
+
}
|
|
583
|
+
function isNativeDeleteResults(value) {
|
|
584
|
+
return Array.isArray(value) && value.every(isNativeDeleteResult);
|
|
585
|
+
}
|
|
451
586
|
function isEvent(value) {
|
|
452
|
-
return isObject(value) && hasRequired(value, { eventName: isString, payload: isRecord })
|
|
587
|
+
return isObject(value) && hasRequired(value, { eventName: isString, payload: isRecord })
|
|
588
|
+
&& hasOptional(value, { acpSessionId: isNullableString });
|
|
453
589
|
}
|
|
454
590
|
function isAgentFrame(value) {
|
|
455
591
|
// Cast, not a narrowed lookup: `value.t` is unvetted wire input, so it may not be one of
|
|
@@ -588,11 +724,14 @@ export function outboundFailureReply(frame, reason) {
|
|
|
588
724
|
switch (frame.t) {
|
|
589
725
|
case 'agent_opened': return { t: 'agent_opened', id, ok: false, error };
|
|
590
726
|
case 'agent_native_sessions': return { t: 'agent_native_sessions', id, ok: false, error };
|
|
727
|
+
case 'agent_native_deleted': return { t: 'agent_native_deleted', id, ok: false, error };
|
|
591
728
|
case 'agent_verified': return { t: 'agent_verified', id, ok: false, error };
|
|
592
729
|
case 'agent_authenticated': return { t: 'agent_authenticated', id, ok: false, error };
|
|
593
730
|
case 'agent_installed': return { t: 'agent_installed', id, ok: false, error };
|
|
594
731
|
case 'agent_steered': return { t: 'agent_steered', id, ok: false, error };
|
|
595
732
|
case 'agent_config_option_set': return { t: 'agent_config_option_set', id, ok: false, error };
|
|
733
|
+
case 'agent_delegation_renewed': return { t: 'agent_delegation_renewed', id, ok: false, error };
|
|
734
|
+
case 'agent_provider_task_stopped': return { t: 'agent_provider_task_stopped', id, state: 'unknown', error };
|
|
596
735
|
case 'agent_backfill_done':
|
|
597
736
|
case 'agent_backfill_events': return { t: 'agent_backfill_done', id, ok: false, count: 0, error };
|
|
598
737
|
default: return null;
|
|
@@ -602,5 +741,15 @@ export function encodeAgentFrame(frame) {
|
|
|
602
741
|
return JSON.stringify(frame);
|
|
603
742
|
}
|
|
604
743
|
export function decodeAgentFrame(text) {
|
|
605
|
-
|
|
744
|
+
const frame = decodeChannelFrame(text, DISCRIMINATORS, isAgentFrame);
|
|
745
|
+
if (frame.t === 'agent_provider_task_event') {
|
|
746
|
+
if ((frame.state === 'completed' || frame.state === 'failed' || frame.state === 'stopped') && frame.live)
|
|
747
|
+
throw new TypeError('Invalid provider task terminal live state');
|
|
748
|
+
if (frame.status !== undefined &&
|
|
749
|
+
(frame.status !== frame.state || !['completed', 'failed', 'stopped'].includes(frame.status)))
|
|
750
|
+
throw new TypeError('Invalid provider task terminal status');
|
|
751
|
+
if (frame.summary !== undefined && frame.status === undefined)
|
|
752
|
+
throw new TypeError('Provider task summary requires terminal status');
|
|
753
|
+
}
|
|
754
|
+
return frame;
|
|
606
755
|
}
|
|
@@ -23,6 +23,9 @@ export function isAuthMethodInfo(value) {
|
|
|
23
23
|
&& hasOptional(value, { type: isNullableString, description: isNullableString, args: isNullableStringArray });
|
|
24
24
|
}
|
|
25
25
|
export const isNullableAuthMethodInfoArray = (value) => value === null || (Array.isArray(value) && value.every(isAuthMethodInfo));
|
|
26
|
+
export function isMissingPrerequisite(value) {
|
|
27
|
+
return isObject(value) && hasRequired(value, { label: isString }) && hasOptional(value, { hint: isString });
|
|
28
|
+
}
|
|
26
29
|
export function isBridgeInfo(value) {
|
|
27
30
|
return isObject(value)
|
|
28
31
|
&& hasRequired(value, { type: isString })
|
|
@@ -49,7 +52,10 @@ export function isBridgeInfo(value) {
|
|
|
49
52
|
// string is all a peer sending it without the flag is asked to satisfy.
|
|
50
53
|
platformTriple: isString,
|
|
51
54
|
// `true` and nothing else, like `supportsSteering`: see `BridgeInfo.recommended`.
|
|
55
|
+
curated: isTrueOnly,
|
|
52
56
|
recommended: isTrueOnly,
|
|
57
|
+
detected: isTrueOnly,
|
|
58
|
+
missingPrerequisite: isMissingPrerequisite,
|
|
53
59
|
defaultName: isNullableString,
|
|
54
60
|
defaultLabel: isNullableString,
|
|
55
61
|
// Optional on purpose: see BridgeInfo's doc comment — absent is "unknown", and a guard that
|
|
@@ -1,9 +1,63 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { validDesktopFrame } from '../desktop/protocol.js';
|
|
2
|
+
import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isNumber, isObject, isString, isStringArray, isStringRecord } from './common.js';
|
|
3
|
+
import { TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES } from '../tools/validation.js';
|
|
4
|
+
export const MAX_COMPUTER_FRAME_BYTES = 8 * 1024 * 1024;
|
|
2
5
|
const DISCRIMINATORS = new Set([
|
|
6
|
+
'desktop_capability', 'desktop_open', 'desktop_opened', 'desktop_data', 'desktop_credit', 'desktop_close',
|
|
3
7
|
'computer_hello', 'computer_ready', 'heartbeat', 'heartbeat_ack',
|
|
4
|
-
'exec_request', 'exec_output', 'exec_exit', 'exec_cancel',
|
|
8
|
+
'exec_request', 'exec_output', 'exec_exit', 'exec_cancel', 'tool_request', 'tool_result',
|
|
9
|
+
'file_read_request', 'file_read_result', 'file_write_request', 'file_write_result',
|
|
10
|
+
'file_ls_request', 'file_ls_result', 'file_mkdir_request', 'file_mkdir_result',
|
|
5
11
|
]);
|
|
12
|
+
const fileErrorCodes = new Set([
|
|
13
|
+
'invalid_argument', 'invalid_path', 'unsupported_workstation', 'unsupported_protocol',
|
|
14
|
+
'workstation_offline', 'not_found', 'not_regular_file', 'invalid_encoding', 'line_too_long',
|
|
15
|
+
'too_large', 'conflict', 'permission_denied', 'io_error', 'outcome_unknown',
|
|
16
|
+
]);
|
|
17
|
+
const isNonnegativeInteger = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
18
|
+
const isPositiveInteger = (value) => Number.isSafeInteger(value) && value >= 1;
|
|
19
|
+
const isSha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|
|
20
|
+
const onlyKeys = (value, keys) => Object.keys(value).every(key => keys.includes(key));
|
|
21
|
+
function isFileError(value) {
|
|
22
|
+
return isObject(value) && hasRequired(value, { code: isString, message: isString })
|
|
23
|
+
&& fileErrorCodes.has(value.code) && onlyKeys(value, ['code', 'message']);
|
|
24
|
+
}
|
|
25
|
+
function isReadData(value) {
|
|
26
|
+
return isObject(value) && hasRequired(value, {
|
|
27
|
+
path: isString, content: isString, startLine: isPositiveInteger, endLine: isNonnegativeInteger,
|
|
28
|
+
truncated: isBoolean, sizeBytes: isNonnegativeInteger, sha256: isSha256,
|
|
29
|
+
}) && hasOptional(value, { nextLine: isPositiveInteger })
|
|
30
|
+
&& onlyKeys(value, ['path', 'content', 'startLine', 'endLine', 'nextLine', 'truncated', 'sizeBytes', 'sha256']);
|
|
31
|
+
}
|
|
32
|
+
function isWriteData(value) {
|
|
33
|
+
return isObject(value) && hasRequired(value, { path: isString, sizeBytes: isNonnegativeInteger, sha256: isSha256, created: isBoolean })
|
|
34
|
+
&& onlyKeys(value, ['path', 'sizeBytes', 'sha256', 'created']);
|
|
35
|
+
}
|
|
36
|
+
const dirEntryKinds = new Set(['file', 'directory', 'symlink', 'other']);
|
|
37
|
+
function isLsData(value) {
|
|
38
|
+
return isObject(value) && hasRequired(value, { path: isString, truncated: isBoolean })
|
|
39
|
+
&& hasOptional(value, { nextAfter: isString })
|
|
40
|
+
&& onlyKeys(value, ['path', 'entries', 'truncated', 'nextAfter'])
|
|
41
|
+
&& Array.isArray(value.entries)
|
|
42
|
+
&& value.entries.every((entry) => isObject(entry) && hasRequired(entry, { name: isString, kind: isString })
|
|
43
|
+
&& dirEntryKinds.has(entry.kind)
|
|
44
|
+
&& hasOptional(entry, { sizeBytes: isNonnegativeInteger })
|
|
45
|
+
&& onlyKeys(entry, ['name', 'kind', 'sizeBytes']));
|
|
46
|
+
}
|
|
47
|
+
function isMkdirData(value) {
|
|
48
|
+
return isObject(value) && hasRequired(value, { path: isString, created: isBoolean })
|
|
49
|
+
&& onlyKeys(value, ['path', 'created']);
|
|
50
|
+
}
|
|
51
|
+
function isFileResult(value, validData) {
|
|
52
|
+
if (!hasRequired(value, { id: isString }) || !onlyKeys(value, ['t', 'id', 'data', 'error']))
|
|
53
|
+
return false;
|
|
54
|
+
const hasData = Object.hasOwn(value, 'data');
|
|
55
|
+
const hasError = Object.hasOwn(value, 'error');
|
|
56
|
+
return hasData !== hasError && (hasData ? validData(value.data) : isFileError(value.error));
|
|
57
|
+
}
|
|
6
58
|
function isComputerFrame(value) {
|
|
59
|
+
if (typeof value.t === 'string' && value.t.startsWith('desktop_'))
|
|
60
|
+
return validDesktopFrame(value);
|
|
7
61
|
switch (value.t) {
|
|
8
62
|
case 'computer_hello': return hasRequired(value, { ticket: isString });
|
|
9
63
|
case 'computer_ready': return hasRequired(value, { generation: isString, wireVersion: isNumber });
|
|
@@ -18,14 +72,49 @@ function isComputerFrame(value) {
|
|
|
18
72
|
id: isString, exitCode: isNullableNumber, stdout: isString, stderr: isString,
|
|
19
73
|
timedOut: isBoolean, truncated: isBoolean,
|
|
20
74
|
});
|
|
75
|
+
case 'tool_request': return hasRequired(value, { id: isString, method: isString, params: isObject })
|
|
76
|
+
&& value.id.length > 0 && value.id.length <= 128
|
|
77
|
+
&& value.method.length > 0 && value.method.length <= 64
|
|
78
|
+
&& Buffer.byteLength(JSON.stringify(value)) <= TOOL_REQUEST_BYTES
|
|
79
|
+
&& onlyKeys(value, ['t', 'id', 'method', 'params']);
|
|
80
|
+
case 'tool_result': return hasRequired(value, { id: isString, ok: isBoolean, result: isObject, error: value => value === null || isString(value) })
|
|
81
|
+
&& value.id.length > 0 && value.id.length <= 128
|
|
82
|
+
&& (value.ok ? value.error === null : typeof value.error === 'string' && value.error.length > 0)
|
|
83
|
+
&& Buffer.byteLength(JSON.stringify(value)) <= TOOL_RESULT_BYTES
|
|
84
|
+
&& onlyKeys(value, ['t', 'id', 'ok', 'result', 'error']);
|
|
21
85
|
case 'exec_cancel': return hasRequired(value, { id: isString });
|
|
86
|
+
case 'file_read_request': return hasRequired(value, {
|
|
87
|
+
id: isString, root: isString, path: isString, startLine: isPositiveInteger, maxLines: isPositiveInteger,
|
|
88
|
+
}) && value.maxLines <= 2000 && onlyKeys(value, ['t', 'id', 'root', 'path', 'startLine', 'maxLines']);
|
|
89
|
+
case 'file_write_request': return hasRequired(value, {
|
|
90
|
+
id: isString, root: isString, path: isString, content: isString, mode: isString,
|
|
91
|
+
}) && (value.mode === 'create' || value.mode === 'replace')
|
|
92
|
+
&& hasOptional(value, { expectedSha256: isSha256 })
|
|
93
|
+
&& onlyKeys(value, ['t', 'id', 'root', 'path', 'content', 'mode', 'expectedSha256']);
|
|
94
|
+
case 'file_ls_request': return hasRequired(value, {
|
|
95
|
+
id: isString, root: isString, path: isString, maxEntries: isPositiveInteger,
|
|
96
|
+
}) && value.maxEntries <= 2000
|
|
97
|
+
&& hasOptional(value, { startAfter: isString })
|
|
98
|
+
&& onlyKeys(value, ['t', 'id', 'root', 'path', 'startAfter', 'maxEntries']);
|
|
99
|
+
case 'file_mkdir_request': return hasRequired(value, {
|
|
100
|
+
id: isString, root: isString, path: isString, parents: isBoolean,
|
|
101
|
+
}) && onlyKeys(value, ['t', 'id', 'root', 'path', 'parents']);
|
|
102
|
+
case 'file_read_result': return isFileResult(value, isReadData);
|
|
103
|
+
case 'file_write_result': return isFileResult(value, isWriteData);
|
|
104
|
+
case 'file_ls_result': return isFileResult(value, isLsData);
|
|
105
|
+
case 'file_mkdir_result': return isFileResult(value, isMkdirData);
|
|
22
106
|
default: return false;
|
|
23
107
|
}
|
|
24
108
|
}
|
|
25
109
|
function isNullableNumber(value) { return value === null || isNumber(value); }
|
|
26
110
|
export function encodeComputerFrame(frame) {
|
|
27
|
-
|
|
111
|
+
const encoded = JSON.stringify(frame);
|
|
112
|
+
if (Buffer.byteLength(encoded, 'utf8') > MAX_COMPUTER_FRAME_BYTES)
|
|
113
|
+
throw new TypeError('Computer frame too large');
|
|
114
|
+
return encoded;
|
|
28
115
|
}
|
|
29
116
|
export function decodeComputerFrame(text) {
|
|
117
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_COMPUTER_FRAME_BYTES)
|
|
118
|
+
throw new TypeError('Computer frame too large');
|
|
30
119
|
return decodeChannelFrame(text, DISCRIMINATORS, isComputerFrame);
|
|
31
120
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Native Qoder does not forward background child SDK messages through ACP. Read only the
|
|
2
|
+
* exact transcript named by a validated root launch receipt; never scan or infer ownership. */
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { open } from 'node:fs/promises';
|
|
5
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
6
|
+
const id = (value) => typeof value === 'string' && /^[a-zA-Z0-9_-]{1,160}$/.test(value);
|
|
7
|
+
const LIMIT = 2 * 1024 * 1024;
|
|
8
|
+
export class BackgroundProcess {
|
|
9
|
+
options;
|
|
10
|
+
offset = 0;
|
|
11
|
+
tail = Buffer.alloc(0);
|
|
12
|
+
skipLine = false;
|
|
13
|
+
seen = new Set();
|
|
14
|
+
tools = new Map();
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
async poll() {
|
|
19
|
+
const file = await open(this.options.path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
20
|
+
try {
|
|
21
|
+
const stat = await file.stat();
|
|
22
|
+
if (!stat.isFile())
|
|
23
|
+
return true;
|
|
24
|
+
if (stat.size < this.offset) {
|
|
25
|
+
this.offset = 0;
|
|
26
|
+
this.tail = Buffer.alloc(0);
|
|
27
|
+
this.skipLine = false;
|
|
28
|
+
}
|
|
29
|
+
const end = Math.min(stat.size, this.offset + 4 * 1024 * 1024);
|
|
30
|
+
while (this.offset < end) {
|
|
31
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, end - this.offset));
|
|
32
|
+
const { bytesRead } = await file.read(chunk, 0, chunk.length, this.offset);
|
|
33
|
+
if (!bytesRead)
|
|
34
|
+
break;
|
|
35
|
+
this.offset += bytesRead;
|
|
36
|
+
this.consumeBytes(chunk.subarray(0, bytesRead));
|
|
37
|
+
}
|
|
38
|
+
return this.offset >= stat.size;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await file.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
consumeBytes(bytes) {
|
|
45
|
+
let start = 0;
|
|
46
|
+
for (let end = bytes.indexOf(10); end >= 0; end = bytes.indexOf(10, start)) {
|
|
47
|
+
const part = bytes.subarray(start, end);
|
|
48
|
+
if (!this.skipLine && this.tail.length + part.length <= LIMIT) {
|
|
49
|
+
try {
|
|
50
|
+
this.consume(object(JSON.parse(Buffer.concat([this.tail, part]).toString('utf8'))));
|
|
51
|
+
}
|
|
52
|
+
catch { /* not evidence */ }
|
|
53
|
+
}
|
|
54
|
+
this.tail = Buffer.alloc(0);
|
|
55
|
+
this.skipLine = false;
|
|
56
|
+
start = end + 1;
|
|
57
|
+
}
|
|
58
|
+
const part = bytes.subarray(start);
|
|
59
|
+
if (!this.skipLine && this.tail.length + part.length <= LIMIT)
|
|
60
|
+
this.tail = Buffer.concat([this.tail, part]);
|
|
61
|
+
else {
|
|
62
|
+
this.tail = Buffer.alloc(0);
|
|
63
|
+
this.skipLine = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
consume(row) {
|
|
67
|
+
const o = this.options;
|
|
68
|
+
if (row.sessionId !== o.sessionId || row.cwd !== o.cwd || row.agentId !== o.agentId ||
|
|
69
|
+
row.parent_tool_use_id !== o.parentToolUseId || row.isSidechain !== true || row.version !== '1.1.61' ||
|
|
70
|
+
!id(row.uuid) || this.seen.has(row.uuid) || this.seen.size >= 4096)
|
|
71
|
+
return;
|
|
72
|
+
this.seen.add(row.uuid);
|
|
73
|
+
const content = object(row.message).content;
|
|
74
|
+
if (!Array.isArray(content))
|
|
75
|
+
return;
|
|
76
|
+
for (const block of content.map(object)) {
|
|
77
|
+
if (row.type === 'assistant' && block.type === 'tool_use' && id(block.id) && typeof block.name === 'string') {
|
|
78
|
+
if (this.tools.has(block.id) || this.tools.size >= 4096)
|
|
79
|
+
continue;
|
|
80
|
+
const input = object(block.input);
|
|
81
|
+
this.tools.set(block.id, { name: block.name, input });
|
|
82
|
+
this.emit('tool_call', { toolCallId: block.id, name: block.name, title: block.name,
|
|
83
|
+
kind: block.name === 'Read' ? 'read' : block.name === 'Bash' ? 'execute' : block.name === 'Agent' ? 'think' : 'other',
|
|
84
|
+
rawInput: input, status: 'pending' }, block.name);
|
|
85
|
+
}
|
|
86
|
+
else if (row.type === 'user' && block.type === 'tool_result' && id(block.tool_use_id)) {
|
|
87
|
+
const tool = this.tools.get(block.tool_use_id);
|
|
88
|
+
if (!tool)
|
|
89
|
+
continue;
|
|
90
|
+
const raw = row.toolUseResult ?? block.content;
|
|
91
|
+
const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content ?? '');
|
|
92
|
+
this.emit('tool_call_update', { toolCallId: block.tool_use_id, name: tool.name,
|
|
93
|
+
status: block.is_error === true ? 'failed' : 'completed', rawOutput: raw,
|
|
94
|
+
content: [{ type: 'content', content: { type: 'text', text } }] }, tool.name);
|
|
95
|
+
}
|
|
96
|
+
else if (row.type === 'assistant' && ((block.type === 'text' && typeof block.text === 'string') ||
|
|
97
|
+
(block.type === 'thinking' && typeof block.thinking === 'string'))) {
|
|
98
|
+
this.emit(block.type === 'text' ? 'agent_message_chunk' : 'agent_thought_chunk', { content: { type: 'text', text: block.type === 'text' ? block.text : block.thinking } });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
emit(eventName, payload, toolName) {
|
|
103
|
+
this.options.emit({ eventName, payload: { ...payload, _meta: { qoder: { version: 1, source: 'correction',
|
|
104
|
+
parentToolUseId: this.options.parentToolUseId, route: 'child', ...(toolName ? { toolName } : {}) } } } });
|
|
105
|
+
}
|
|
106
|
+
}
|