@platless/axom-cli 0.1.0 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +132 -0
  4. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  7. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  8. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  9. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  10. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  11. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/nightly-task.md +152 -0
  15. package/dist/axom-patch/cli/pi-steering.json +37 -0
  16. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  17. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
  18. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  19. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  20. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  21. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
  24. package/dist/axom-patch/cli/qoder-foreground-output.md +42 -0
  25. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  26. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  27. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  28. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  29. package/dist/axom-patch/cli/runtime.json +63 -0
  30. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  31. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  32. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  33. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  34. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  35. package/dist/axom-patch/cli/zcode-models.json +38 -0
  36. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  37. package/dist/cli.js +36 -1
  38. package/dist/commands/auth.js +4 -13
  39. package/dist/commands/execute.js +4 -4
  40. package/dist/commands/record.js +6 -6
  41. package/dist/commands/struct.js +4 -4
  42. package/dist/commands/workstation.js +2 -2
  43. package/dist/credentials.js +2 -2
  44. package/dist/install/claude/commands.js +13 -13
  45. package/dist/keepers/acp/keeper.mjs +3083 -0
  46. package/dist/keepers/acp/manifest.json +9 -0
  47. package/dist/keepers/terminal/keeper.mjs +8469 -0
  48. package/dist/keepers/terminal/manifest.json +9 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  63. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  64. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  65. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  66. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  67. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  68. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  69. package/dist/mcp.js +32 -14
  70. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  71. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  72. package/dist/test/keeperKit.js +67 -0
  73. package/dist/workstation/acp/acpAgents.js +2514 -681
  74. package/dist/workstation/acp/acpClient.js +95 -318
  75. package/dist/workstation/acp/activityBusy.js +43 -0
  76. package/dist/workstation/acp/activityReporting.js +54 -0
  77. package/dist/workstation/acp/adoption.js +28 -356
  78. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  79. package/dist/workstation/acp/binaryInstall.js +100 -27
  80. package/dist/workstation/acp/builds/gc.js +245 -0
  81. package/dist/workstation/acp/builds/install/binary.js +28 -0
  82. package/dist/workstation/acp/builds/install/npm.js +235 -0
  83. package/dist/workstation/acp/builds/install/uv.js +254 -0
  84. package/dist/workstation/acp/builds/migrate.js +195 -0
  85. package/dist/workstation/acp/builds/model.js +128 -0
  86. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  87. package/dist/workstation/acp/builds/store.js +352 -0
  88. package/dist/workstation/acp/commandTableProbe.js +68 -0
  89. package/dist/workstation/acp/configOptions.js +103 -1
  90. package/dist/workstation/acp/delegationMcp.js +24 -0
  91. package/dist/workstation/acp/envSecrets.js +27 -0
  92. package/dist/workstation/acp/initializeResponse.js +285 -0
  93. package/dist/workstation/acp/installErrors.js +12 -2
  94. package/dist/workstation/acp/installProbe.js +1 -1
  95. package/dist/workstation/acp/managedPatches.js +144 -0
  96. package/dist/workstation/acp/outbox.js +330 -0
  97. package/dist/workstation/acp/patchAssets.js +80 -0
  98. package/dist/workstation/acp/permission.js +335 -37
  99. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  100. package/dist/workstation/acp/processor/commands.js +35 -0
  101. package/dist/workstation/acp/processor/control.js +355 -0
  102. package/dist/workstation/acp/processor/emit.js +26 -0
  103. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  104. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  105. package/dist/workstation/acp/processor/origins.js +48 -0
  106. package/dist/workstation/acp/processor/output.js +67 -0
  107. package/dist/workstation/acp/processor/projection.js +42 -0
  108. package/dist/workstation/acp/processor/record.js +128 -0
  109. package/dist/workstation/acp/processor/runtime.js +181 -0
  110. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  111. package/dist/workstation/acp/processor/session.js +525 -0
  112. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  113. package/dist/workstation/acp/processor/state.js +74 -0
  114. package/dist/workstation/acp/processor/step.js +459 -0
  115. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  116. package/dist/workstation/acp/processor/test/harness.js +120 -0
  117. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  118. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  119. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  120. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  121. package/dist/workstation/acp/processor/transport/local.js +269 -0
  122. package/dist/workstation/acp/processor/transport/types.js +78 -0
  123. package/dist/workstation/acp/processor/updates.js +364 -0
  124. package/dist/workstation/acp/processorSession.js +1512 -0
  125. package/dist/workstation/acp/qoderStartup.js +45 -0
  126. package/dist/workstation/acp/registry/catalog.js +385 -4
  127. package/dist/workstation/acp/registry/detect.js +55 -0
  128. package/dist/workstation/acp/registry/launch.js +61 -119
  129. package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
  130. package/dist/workstation/acp/steering.js +58 -0
  131. package/dist/workstation/acp/switching.js +114 -0
  132. package/dist/workstation/acp/terminalLogin.js +171 -0
  133. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  134. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  135. package/dist/workstation/acp/test/daemonLife.js +106 -0
  136. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  137. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  138. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  139. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  140. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  141. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  142. package/dist/workstation/acp/test/raw/sources.js +249 -0
  143. package/dist/workstation/acp/test/realAdapter.js +377 -0
  144. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  145. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  146. package/dist/workstation/acp/timedCommand.js +4 -2
  147. package/dist/workstation/acp/uvxInstall.js +72 -122
  148. package/dist/workstation/acp/validate.js +33 -5
  149. package/dist/workstation/agentFlow.js +103 -0
  150. package/dist/workstation/agentSessions.js +1686 -348
  151. package/dist/workstation/channels/agent.js +14 -6
  152. package/dist/workstation/channels/computer.js +203 -7
  153. package/dist/workstation/channels/computerExecs.js +200 -0
  154. package/dist/workstation/channels/management.js +86 -6
  155. package/dist/workstation/config.js +3 -1
  156. package/dist/workstation/connection/encryptedChannel.js +88 -21
  157. package/dist/workstation/daemon.js +309 -6
  158. package/dist/workstation/daemonctl.js +28 -2
  159. package/dist/workstation/desktop/binding.js +36 -0
  160. package/dist/workstation/desktop/bridge.js +212 -0
  161. package/dist/workstation/desktop/flow-control.js +52 -0
  162. package/dist/workstation/desktop/focused-text.js +130 -0
  163. package/dist/workstation/desktop/protocol.js +35 -0
  164. package/dist/workstation/exec/attach.js +126 -0
  165. package/dist/workstation/exec/command.js +40 -0
  166. package/dist/workstation/exec/durable.js +517 -0
  167. package/dist/workstation/exec/fallback.js +106 -0
  168. package/dist/workstation/exec/policy.js +837 -0
  169. package/dist/workstation/exec/shaping.js +211 -0
  170. package/dist/workstation/exec.js +3 -112
  171. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  172. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  173. package/dist/workstation/files/client.js +455 -0
  174. package/dist/workstation/files/errors.js +28 -0
  175. package/dist/workstation/files/fence.js +350 -0
  176. package/dist/workstation/files/find/fileIndex.js +153 -0
  177. package/dist/workstation/files/find/fuzzy.js +155 -0
  178. package/dist/workstation/files/host.js +122 -0
  179. package/dist/workstation/files/hostProtocol.js +2 -0
  180. package/dist/workstation/files/media/cache.js +620 -0
  181. package/dist/workstation/files/media/encoding.js +308 -0
  182. package/dist/workstation/files/media/estimator.js +100 -0
  183. package/dist/workstation/files/media/media.js +325 -0
  184. package/dist/workstation/files/media/packedAudio.js +23 -0
  185. package/dist/workstation/files/media/scheduler.js +551 -0
  186. package/dist/workstation/files/media/types.js +5 -0
  187. package/dist/workstation/files/scope.js +45 -0
  188. package/dist/workstation/files/structured.js +284 -0
  189. package/dist/workstation/files/text.js +356 -0
  190. package/dist/workstation/files/uploads.js +351 -0
  191. package/dist/workstation/journal.js +65 -236
  192. package/dist/workstation/keepers/acp/client.js +101 -0
  193. package/dist/workstation/keepers/acp/envelope.js +62 -0
  194. package/dist/workstation/keepers/acp/host.js +419 -0
  195. package/dist/workstation/keepers/acp/main.js +18 -0
  196. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  197. package/dist/workstation/keepers/acp/params.js +137 -0
  198. package/dist/workstation/keepers/acp/protocol.js +66 -0
  199. package/dist/workstation/keepers/acp/recover.js +120 -0
  200. package/dist/workstation/keepers/acp/session.js +584 -0
  201. package/dist/workstation/keepers/acp/stderr.js +154 -0
  202. package/dist/workstation/keepers/kit/activity.js +103 -0
  203. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  204. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  205. package/dist/workstation/keepers/kit/bundle.js +120 -0
  206. package/dist/workstation/keepers/kit/client.js +432 -0
  207. package/dist/workstation/keepers/kit/identity.js +21 -0
  208. package/dist/workstation/keepers/kit/ipc.js +112 -0
  209. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  210. package/dist/workstation/keepers/kit/launcher.js +249 -0
  211. package/dist/workstation/keepers/kit/lock.js +123 -0
  212. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  213. package/dist/workstation/keepers/kit/paths.js +75 -0
  214. package/dist/workstation/keepers/kit/protocol.js +43 -0
  215. package/dist/workstation/keepers/kit/server.js +518 -0
  216. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  217. package/dist/workstation/keepers/legacy.js +296 -0
  218. package/dist/workstation/keepers/policy.js +576 -0
  219. package/dist/workstation/keepers/stop.js +34 -0
  220. package/dist/workstation/keepers/terminal/client.js +213 -0
  221. package/dist/workstation/keepers/terminal/exec.js +981 -0
  222. package/dist/workstation/keepers/terminal/generations.js +129 -0
  223. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  224. package/dist/workstation/keepers/terminal/main.js +43 -0
  225. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  226. package/dist/workstation/keepers/terminal/params.js +27 -0
  227. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  228. package/dist/workstation/keepers/terminal/pty.js +330 -0
  229. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  230. package/dist/workstation/protocol/accountLimits.js +47 -0
  231. package/dist/workstation/protocol/agent.js +201 -8
  232. package/dist/workstation/protocol/common.js +36 -0
  233. package/dist/workstation/protocol/computer.js +107 -7
  234. package/dist/workstation/protocol/management.js +50 -2
  235. package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
  236. package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
  237. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  238. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  239. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  240. package/dist/workstation/qoderAdapter/contract.js +78 -0
  241. package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
  242. package/dist/workstation/qoderAdapter/history.js +85 -0
  243. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  244. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  245. package/dist/workstation/qoderAdapter/main.js +95 -0
  246. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  247. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  248. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  249. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  250. package/dist/workstation/qoderAdapter/profile.js +93 -0
  251. package/dist/workstation/qoderAdapter/sdkDriver.js +48 -0
  252. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  253. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  254. package/dist/workstation/qoderAdapter/session.js +319 -0
  255. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  256. package/dist/workstation/qoderAdapter/subwork.js +85 -0
  257. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  258. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  259. package/dist/workstation/qoderAdapter/types.js +1 -0
  260. package/dist/workstation/qoderAdapter/usage.js +37 -0
  261. package/dist/workstation/register.js +4 -0
  262. package/dist/workstation/systemMetrics.js +110 -1
  263. package/dist/workstation/tools/browser.js +394 -0
  264. package/dist/workstation/tools/client.js +277 -0
  265. package/dist/workstation/tools/info.js +50 -0
  266. package/dist/workstation/tools/keeperProcess.js +124 -0
  267. package/dist/workstation/tools/router.js +55 -0
  268. package/dist/workstation/tools/terminalRouter.js +222 -0
  269. package/dist/workstation/tools/validation.js +86 -0
  270. package/dist/workstation/update/coordinator.js +32 -0
  271. package/dist/workstation/update/npmInstaller.js +2 -0
  272. package/dist/workstation/update/ptyPreflight.js +45 -0
  273. package/dist/workstation/update/taskRegistry.js +141 -23
  274. package/dist/workstation/update/watchdog.js +66 -0
  275. package/package.json +20 -7
  276. package/skills/axom/SKILL.md +55 -17
  277. package/skills/axom-definition/SKILL.md +32 -32
  278. package/skills/axom-platform/SKILL.md +39 -39
  279. package/dist/workstation/acp/acpSession.js +0 -1047
  280. package/dist/workstation/acp/contract.js +0 -494
  281. package/dist/workstation/acp/draftInterceptor.js +0 -154
@@ -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
- return { accessToken, refreshToken, expiresInSeconds: response.expires_in };
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;
@@ -0,0 +1,47 @@
1
+ /** Bounded, plain-JS wire validation: usable inside the deterministic ACP processor. */
2
+ export const ACCOUNT_LIMITS_READ_TIMEOUT_MS = 180_000;
3
+ const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
4
+ const text = (v, min = 0, max = 160) => typeof v === 'string' && v.length >= min && v.length <= max;
5
+ const time = (v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(v) && Number.isFinite(new Date(v).getTime());
6
+ const oneOf = (v, values) => typeof v === 'string' && values.includes(v);
7
+ function parse(value) {
8
+ const valid = object(value) && value.schemaVersion === 1 && text(value.provider, 1, 64)
9
+ && (value.updateMode === undefined || oneOf(value.updateMode, ['snapshot', 'delta']))
10
+ && oneOf(value.billingMode, ['subscription', 'non_subscription', 'unknown'])
11
+ && oneOf(value.availability, ['available', 'unavailable', 'unknown', 'not_applicable']) && time(value.observedAt)
12
+ && (value.plan === null || object(value.plan) && text(value.plan.id) && text(value.plan.label))
13
+ && (value.accountKey === undefined || text(value.accountKey, 0, 128))
14
+ && Array.isArray(value.limits) && value.limits.length <= 32 && value.limits.every(row => object(row)
15
+ && text(row.id, 1) && text(row.label) && oneOf(row.kind, ['subscription', 'spend', 'rate'])
16
+ && (row.windowSeconds === null || typeof row.windowSeconds === 'number' && Number.isInteger(row.windowSeconds) && row.windowSeconds > 0)
17
+ && (row.period == null || text(row.period))
18
+ && (row.key === undefined || row.key === null || text(row.key, 1, 64))
19
+ && (row.usedPercent === null || typeof row.usedPercent === 'number' && Number.isFinite(row.usedPercent) && row.usedPercent >= 0 && row.usedPercent <= 100)
20
+ && (row.resetsAt === null || time(row.resetsAt)) && oneOf(row.status, ['allowed', 'warning', 'blocked', 'unknown'])
21
+ && (row.active == null || typeof row.active === 'boolean') && (row.scope == null || object(row.scope))
22
+ && ['used', 'limit', 'unit', 'currency'].every(key => row[key] === undefined || text(row[key]))
23
+ && (row.exponent === undefined || typeof row.exponent === 'number' && Number.isInteger(row.exponent) && row.exponent >= 0 && row.exponent <= 12))
24
+ && new Set(value.limits.map(row => row.id)).size === value.limits.length
25
+ && Array.isArray(value.balances) && value.balances.length <= 16 && value.balances.every(row => object(row)
26
+ && text(row.id, 1) && text(row.label) && text(row.unit) && (row.remaining === null || text(row.remaining))
27
+ && (row.currency === undefined || text(row.currency)) && (row.unlimited === undefined || typeof row.unlimited === 'boolean')
28
+ && (row.exponent === undefined || typeof row.exponent === 'number' && Number.isInteger(row.exponent) && row.exponent >= 0 && row.exponent <= 12))
29
+ && new Set(value.balances.map(row => row.id)).size === value.balances.length
30
+ && (value._meta === undefined || object(value._meta)) && JSON.stringify(value).length <= 65_536;
31
+ if (!valid)
32
+ throw new Error('Invalid account quota snapshot');
33
+ return { ...value, _meta: value._meta ?? {} };
34
+ }
35
+ export const accountLimitsSchema = {
36
+ parse,
37
+ safeParse(value) {
38
+ try {
39
+ return { success: true, data: parse(value) };
40
+ }
41
+ catch {
42
+ return { success: false, error: new Error('Invalid account quota snapshot') };
43
+ }
44
+ },
45
+ };
46
+ export const ACCOUNT_LIMITS_READ = '_axom/account/rateLimits/read';
47
+ export const ACCOUNT_LIMITS_UPDATED = '_axom/account/rateLimits/updated';
@@ -1,6 +1,26 @@
1
+ import { accountLimitsSchema } from './accountLimits.js';
2
+ import { isRuntimeDiagnostics } from '../qoderAdapter/profile.js';
1
3
  import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableAuthMethodInfoArray, isNullableString, isNumber, isObject, isString, isStringArray, isStringRecord, } from './common.js';
2
4
  /** Current version and feature floors for the independently negotiated agent protocol. */
3
- export const AGENT_WIRE_VERSION = 14;
5
+ export const AGENT_WIRE_VERSION = 27;
6
+ /**
7
+ * `agent_switch_idle` / `agent_switch_idle_result` (spec 2026-10-11 §6.4, Task 25): "move idle sessions" of one agent to another
8
+ * adapter build. A server-to-client request: the server sends it only to a client advertising this version (an older one has no
9
+ * case for it and the request would only die at its timeout).
10
+ */
11
+ export const AGENT_WIRE_ACCOUNT_LIMITS_VERSION = 27;
12
+ export const AGENT_WIRE_SWITCH_IDLE_VERSION = 26;
13
+ /** `agent_delete_native` / `agent_native_deleted`: purge a session Axom created on an external agent. */
14
+ export const AGENT_WIRE_NATIVE_DELETE_VERSION = 24;
15
+ export const AGENT_WIRE_PROVIDER_TASK_VERSION = 23;
16
+ export const AGENT_WIRE_RUNTIME_PROFILE_VERSION = 22;
17
+ export const AGENT_WIRE_DELEGATION_RENEW_VERSION = 21;
18
+ export const AGENT_WIRE_TURN_IDENTITY_VERSION = 20;
19
+ export const AGENT_WIRE_DELEGATION_MCP_VERSION = 19;
20
+ /** Scoped replay/history is enabled only when both peers preserve native origin. */
21
+ export const AGENT_WIRE_SCOPED_ORIGIN_VERSION = 18;
22
+ /** agent_open.requireSessionLoad: reject unsupported/failed load without session/new fallback. */
23
+ export const AGENT_WIRE_STRICT_LOAD_VERSION = 17;
4
24
  export const AGENT_WIRE_MIN_VERSION = 1;
5
25
  export const AGENT_WIRE_SESSION_LOST_VERSION = 2;
6
26
  export const AGENT_WIRE_ADOPTION_VERSION = 3;
@@ -137,12 +157,65 @@ export const AGENT_WIRE_RELOADED_VERSION = 13;
137
157
  * is byte-for-byte the frame it always was and still goes out at {@link AGENT_WIRE_STEER_VERSION}.
138
158
  */
139
159
  export const AGENT_WIRE_STEER_BLOCKS_VERSION = 14;
160
+ /**
161
+ * The session activity-state push (`agent_session_state`) and the single-background-task stop
162
+ * request (`agent_async_task_stop`). **Both must be checked before sending — the check just lives
163
+ * in a different process for each, because the two frames do not share a sender.**
164
+ *
165
+ * ## Why both need a floor, not just one
166
+ *
167
+ * A version floor answers one question: *before sending, does the peer understand this frame?*
168
+ * These two frames travel opposite directions, so the check lives on opposite sides:
169
+ *
170
+ * - `agent_async_task_stop` is the **server's** request to this client. A v14 *client* has no case
171
+ * for it in `AGENT_FRAME_SHAPES` — the same shape as {@link AGENT_WIRE_STEER_VERSION} and
172
+ * {@link AGENT_WIRE_INSTALL_VERSION} — so the check lives on the Kotlin server (this constant's
173
+ * twin, `AgentChannelSession`, a later task), before every `agent_async_task_stop` it sends. There
174
+ * is nothing for this file to check on receipt: this daemon only ever handles the frame, never
175
+ * sends it.
176
+ * - `agent_session_state` is the **client's** push to the server, and getting this half wrong is
177
+ * what an earlier revision of this comment did — it argued "an old server just never sees the
178
+ * frame" was enough, which is only half the compatibility matrix and not the dangerous half. The
179
+ * dangerous direction is **a newer client talking to an older server**: this daemon is a
180
+ * separately-installed npm package on the user's machine, so the client upgrading ahead of the
181
+ * server is the ordinary update order here, not an edge case. A v15 client sending
182
+ * `agent_session_state` to a v14 server hits a decoder that throws on the unrecognised
183
+ * discriminator, and because the frame carries a top-level `sessionRef` and no `id`, the server's
184
+ * own recovery (`classifyUndecodableAgentFrame`) charges it to `FailSession` — declaring that
185
+ * session lost and forcing a reopen. This frame exists to be sent over and over, so on an
186
+ * unupgraded server every single push would force a session loss, and a run of them closes the
187
+ * whole agent channel — worse than the `agent_auth_url` incident this file already documents,
188
+ * since that frame carries no `sessionRef` and only ever reaches `Unattributable`.
189
+ *
190
+ * The correct precedent is {@link AGENT_WIRE_AUTH_URL_VERSION}, not
191
+ * {@link AGENT_WIRE_RELOADED_VERSION}: `agent_opened.reloaded` is an **optional key on a frame the
192
+ * peer already decodes**, safely absorbed by `ignoreUnknownKeys`, so nothing on the sending side
193
+ * needs to check anything. `agent_session_state` is a **new discriminator**, and a decoder throws
194
+ * on an unrecognised one before `ignoreUnknownKeys` ever gets a say — the same shape
195
+ * `agent_auth_url` is, and that constant's own floor is checked client-side (`serverAgentWire <
196
+ * AGENT_WIRE_AUTH_URL_VERSION`, see `agentSessions.ts`'s `onAuthenticate`/`present`/`dismiss`) for
197
+ * exactly this reason. `agent_session_state` follows the same rule: the client checks
198
+ * `this.serverAgentWire` before sending and **discards** the push below this floor, rather than
199
+ * erroring — a level nobody downstream can read is no worse than one that was never collected.
200
+ *
201
+ * So both frames are gated, on opposite sides: this file's own send path (a later task) must check
202
+ * `serverAgentWire` before every `agent_session_state`, and the Kotlin twin must check
203
+ * `clientAgentWire` before every `agent_async_task_stop`.
204
+ */
205
+ export const AGENT_WIRE_SESSION_STATE_VERSION = 15;
206
+ /**
207
+ * Elicitation answers (`agent_elicitation_answer`). Checked before sending, like
208
+ * {@link AGENT_WIRE_INSTALL_VERSION} and {@link AGENT_WIRE_CONFIG_OPTIONS_VERSION}: a peer below
209
+ * this floor has no case for the frame at all and would drop it, leaving the agent process blocked
210
+ * inside `elicitation/create` rather than ever reaching the user's answer.
211
+ */
212
+ export const AGENT_WIRE_ELICITATION_VERSION = 16;
140
213
  /**
141
214
  * Every agent frame's shape, as data rather than as a `switch`.
142
215
  *
143
216
  * Declarative on purpose: the required-field half of this table is the *only* statement anywhere of
144
217
  * 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
218
+ * (`protocol/agentRequiredFields.test.ts`'s `assertRequiredFieldsEnforced`) reads it directly to prove each one is really
146
219
  * enforced. Left as inline `hasRequired({...})` calls inside a `switch`, that list is unreadable from
147
220
  * a test, so the only way to check it would be a hand-maintained copy in the test file — which is
148
221
  * precisely how `agent_authenticate.methodId` came to have no guard at all while a report still
@@ -164,6 +237,7 @@ export const AGENT_FRAME_SHAPES = {
164
237
  optional: { sessions: (sessions) => Array.isArray(sessions) && sessions.every(isAgentResumeSession) },
165
238
  },
166
239
  heartbeat_ack: { required: { seq: isNumber } },
240
+ agent_reopen_request: { required: { sessionRef: isString, reason: isString } },
167
241
  agent_open: {
168
242
  required: {
169
243
  id: isString, sessionRef: isString, agentKey: isString, bridge: isString, cwd: isString, shell: isString,
@@ -171,7 +245,10 @@ export const AGENT_FRAME_SHAPES = {
171
245
  optional: {
172
246
  env: isStringRecord, pathPrepend: isStringArray, agentEnv: isStringRecord, command: isNullableString,
173
247
  args: isStringArray, nativeSessionId: isNullableString,
174
- resumeFromSeq: isNumber, backfill: isBoolean,
248
+ resumeFromSeq: isNumber, backfill: isBoolean, requireSessionLoad: isBoolean,
249
+ delegationMcp: (value) => isObject(value) &&
250
+ isString(value.url) && isStringRecord(value.headers) && isNumber(value.expiresAtEpochMs) &&
251
+ (value.bindingId === undefined || isString(value.bindingId)),
175
252
  },
176
253
  },
177
254
  agent_opened: {
@@ -180,6 +257,7 @@ export const AGENT_FRAME_SHAPES = {
180
257
  // `false` would be a second spelling of what its absence already says.
181
258
  optional: {
182
259
  nativeSessionId: isNullableString, error: isNullableString, supportsSteering: isVerifiedTrue,
260
+ runtimeDiagnostics: isRuntimeDiagnostics,
183
261
  // Plain strings, not a closed vocabulary: the sender already dropped everything it did not
184
262
  // recognise, and validating the values again here would make a future timing fail the frame
185
263
  // rather than be ignored — the opposite of what ruling N4 asks for.
@@ -187,16 +265,38 @@ export const AGENT_FRAME_SHAPES = {
187
265
  // `reloaded` is `true`-or-absent for the same reason as `supportsSteering`: a `false` would
188
266
  // be a second spelling of what its absence already says.
189
267
  reloaded: isVerifiedTrue,
268
+ delegationExpiresAtEpochMs: isNumber,
269
+ delegationRefreshPending: isVerifiedTrue,
270
+ delegationInstanceId: isString, delegationBindingId: isString, delegationRenewalRevision: isNumber,
190
271
  agentInfo: isAgentInfo, supportsImageInput: isVerifiedTrue, configOptions: isNullableSessionConfigOptionArray,
191
272
  // Same guard as `agent_verified.authMethods`, and the same absent-vs-`[]` reading: a
192
273
  // half-decoded method table is worse than none, because the reader would offer a login the
193
274
  // agent never named.
194
275
  authMethods: isNullableAuthMethodInfoArray,
195
276
  errorCode: isNullableJsonRpcErrorCode,
277
+ // `true`-or-absent, the same rule as `supportsSteering` above: absence already means "no
278
+ // collector wired up", so a `false` would be a second spelling of it.
279
+ reportsActivity: isVerifiedTrue,
280
+ supportsAsyncTaskStop: isVerifiedTrue,
281
+ // Vendor-verbatim and uninterpreted here (decision L1) -- any object shape is accepted, the
282
+ // same lenience `agent_event.payload` gets via `isRecord`.
283
+ activityRaw: isRecord,
284
+ // Task 21 (spec §5.5.4): `true`-or-absent, and the two id lists.
285
+ durable: isVerifiedTrue, pendingRequestIds: isStringArray, steeredIds: isStringArray,
196
286
  },
197
287
  },
198
- agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray } },
288
+ agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray, platformTurnId: isString } },
289
+ agent_delegation_renew: { required: { id: isString, sessionRef: isString, nativeSessionId: isString,
290
+ instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
291
+ agent_delegation_renewed: { required: { id: isString, ok: isBoolean }, optional: { error: isString,
292
+ nativeSessionId: isString, instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
199
293
  agent_answer: { required: { sessionRef: isString, requestId: isString, outcome: isString } },
294
+ // `content`'s interior is never validated here -- it is ACP's free-form elicitation payload, and
295
+ // this table is transport, not translator. `isRecord` only rejects a non-object.
296
+ agent_elicitation_answer: {
297
+ required: { sessionRef: isString, requestId: isString, action: isString },
298
+ optional: { content: isRecord },
299
+ },
200
300
  agent_interrupt: { required: { sessionRef: isString } },
201
301
  agent_close: { required: { sessionRef: isString } },
202
302
  agent_event: { required: { sessionRef: isString, seq: isNumber, eventName: isString, payload: isRecord } },
@@ -206,6 +306,8 @@ export const AGENT_FRAME_SHAPES = {
206
306
  stopReason: isString, nativeSessionId: isNullableString, error: isNullableString,
207
307
  authMethods: isNullableAuthMethodInfoArray,
208
308
  errorCode: isNullableJsonRpcErrorCode,
309
+ platformTurnId: isString,
310
+ executionEnded: isVerifiedTrue,
209
311
  },
210
312
  },
211
313
  agent_event_ack: { required: { sessionRef: isString, throughSeq: isNumber } },
@@ -214,6 +316,12 @@ export const AGENT_FRAME_SHAPES = {
214
316
  // to return its protocol-level `ok: false` response. The transport only requires that the
215
317
  // field is present; semantic validation remains at the request handler boundary.
216
318
  agent_list_native: { required: { id: isString, bridge: isString, query: () => true } },
319
+ // `query` is as lenient as `agent_list_native`'s, for the same reason.
320
+ agent_delete_native: { required: { id: isString, bridge: isString, query: () => true, nativeSessionIds: isStringArray } },
321
+ agent_native_deleted: {
322
+ required: { id: isString, ok: isBoolean },
323
+ optional: { results: isNativeDeleteResults, error: isNullableString },
324
+ },
217
325
  agent_native_sessions: {
218
326
  required: { id: isString, ok: isBoolean },
219
327
  optional: {
@@ -222,6 +330,9 @@ export const AGENT_FRAME_SHAPES = {
222
330
  },
223
331
  // Same lenience as `agent_list_native` above, for the same reason: the session manager wants to
224
332
  // answer a malformed query with its own `ok: false` rather than have the transport drop it.
333
+ agent_account_read: { required: { id: isString, bridge: isString, agentKey: isString, query: () => true } },
334
+ agent_account_read_result: { required: { id: isString, ok: isBoolean }, optional: { snapshot: (value) => accountLimitsSchema.safeParse(value).success, error: isString } },
335
+ agent_account_update: { required: { agentKey: isString, snapshot: (value) => accountLimitsSchema.safeParse(value).success } },
225
336
  agent_verify: {
226
337
  required: { id: isString, bridge: isString, query: () => true },
227
338
  // 与 `query` 那条**刻意的宽松**相反,这里是严格的:一份畸形的 query 还有个能回
@@ -290,17 +401,75 @@ export const AGENT_FRAME_SHAPES = {
290
401
  required: { id: isString, ok: isBoolean },
291
402
  optional: { configOptions: isNullableSessionConfigOptionArray, error: isNullableString },
292
403
  },
404
+ agent_session_state: { required: { sessionRef: isString, vendor: isString, raw: isRecord } },
405
+ agent_runtime_profile: { required: { sessionRef: isString, diagnostics: isRuntimeDiagnostics } },
406
+ agent_provider_task_event: { required: { sessionRef: isString, rootSessionId: isTaskId,
407
+ runtimeGeneration: isTaskId, taskId: isTaskId, uuid: isTaskId, state: isProviderTaskState,
408
+ live: isBoolean, stale: isBoolean }, optional: { taskType: isTaskType, background: isBoolean,
409
+ agentId: isTaskId, launchingToolUseId: isTaskId, status: isProviderTerminalState, summary: isTaskSummary } },
410
+ agent_provider_task_stop: { required: { id: isTaskId, sessionRef: isTaskId, rootSessionId: isTaskId,
411
+ runtimeGeneration: isTaskId, taskId: isTaskId, requestId: isTaskId } },
412
+ agent_provider_task_stopped: { required: { id: isTaskId, state: isProviderStopState },
413
+ optional: { error: isTaskSummary } },
414
+ agent_async_task_stop: { required: { sessionRef: isString, asyncTaskId: isString } },
415
+ agent_switch_idle: { required: { id: isString, bridge: isString }, optional: { toBuild: isString } },
416
+ agent_switch_idle_result: {
417
+ required: { id: isString, ok: isBoolean },
418
+ optional: { registered: isCount, immediate: isCount, unmovable: isSwitchIdleUnmovableArray, error: isNullableString },
419
+ },
293
420
  };
294
421
  const DISCRIMINATORS = new Set(Object.keys(AGENT_FRAME_SHAPES));
422
+ /**
423
+ * `agent_reopen_request{sessionRef, reason}` (spec 2026-10-11 §6.3, C→S): a session that is idle and has a pending switch (an adapter
424
+ * update, a keeper-acp generation change) asks the server to send `agent_open{requireSessionLoad:true}` with the session's fresh
425
+ * environment — a switch needs the spawn secrets only an `agent_open` carries. Task 24 decides when; the manager's
426
+ * `requestReopen` sends it, and only to a server advertising {@link AGENT_WIRE_REOPEN_REQUEST_VERSION}: an older server cannot
427
+ * decode it, and eight undecodable frames in a row close the agent channel (spec §10: there the switch waits for a natural reopen).
428
+ */
429
+ export const AGENT_WIRE_REOPEN_REQUEST_VERSION = 25;
430
+ /**
431
+ * The same version (Task 22): the server acknowledges as soon as a subagent relation releases the watermark it capped, and stops
432
+ * wasting its 32-event ack count on capped attempts — so a sent relation-pinned outbox entry may count against the sending
433
+ * window again (`SessionOutbox.inflight`). Below it the entries keep not counting once sent (fix round 1 of Task 20).
434
+ */
435
+ export const AGENT_WIRE_RELATION_ACK_VERSION = 25;
436
+ export const AGENT_REOPEN_REQUEST_SHAPE = AGENT_FRAME_SHAPES.agent_reopen_request;
437
+ export function agentReopenRequest(sessionRef, reason) {
438
+ return { t: 'agent_reopen_request', sessionRef, reason };
439
+ }
440
+ function isCount(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; }
441
+ function isSwitchIdleUnmovableArray(value) {
442
+ return Array.isArray(value) && value.every((entry) => isObject(entry) && isString(entry.sessionRef) && isString(entry.reason));
443
+ }
295
444
  function isRecord(value) { return isObject(value); }
445
+ function isTaskId(value) {
446
+ return typeof value === 'string' && value.length > 0 &&
447
+ Buffer.byteLength(value, 'utf8') <= 256 && !/[\x00-\x1f\x7f]/.test(value);
448
+ }
449
+ function isTaskType(value) { return isTaskId(value) && Buffer.byteLength(value, 'utf8') <= 128; }
450
+ function isTaskSummary(value) {
451
+ return typeof value === 'string' &&
452
+ Buffer.byteLength(value, 'utf8') <= 8192;
453
+ }
454
+ function isProviderTaskState(value) {
455
+ return value === 'pending' || value === 'paused' || value === 'running' || value === 'completed' ||
456
+ value === 'stopped' || value === 'failed' || value === 'unknown';
457
+ }
458
+ function isProviderTerminalState(value) {
459
+ return value === 'completed' || value === 'failed' || value === 'stopped';
460
+ }
461
+ function isProviderStopState(value) {
462
+ return value === 'accepted' || value === 'unknown' || value === 'rejected';
463
+ }
296
464
  function isAgentResumeSession(value) {
297
465
  return isObject(value)
298
- && hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean });
466
+ && hasRequired(value, { sessionRef: isString, journalMaxSeq: isNumber, agentAlive: isBoolean })
467
+ && (value.pendingRequestIds === undefined || isStringArray(value.pendingRequestIds));
299
468
  }
300
469
  /**
301
470
  * Accepts only the two outcomes this client can produce. `startedNewTurn` is refused on purpose: it
302
471
  * 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`.
472
+ * message the server has already put on its timeline — see `acp/steering.ts`'s `mapSteerOutcome`.
304
473
  */
305
474
  function isSteerOutcome(value) { return value === 'injected' || value === 'promptRequired'; }
306
475
  /** Accepts `true` and nothing else — the whole point of `authenticated` having no `false`. */
@@ -448,8 +617,17 @@ function isNativeSession(value) {
448
617
  nativeSessionId: isString, title: isString, updatedAtEpochMs: isNumber, cwd: isString,
449
618
  });
450
619
  }
620
+ function isNativeDeleteResult(value) {
621
+ return isObject(value)
622
+ && hasRequired(value, { nativeSessionId: isString, outcome: (o) => o === 'deleted' || o === 'unsupported' || o === 'failed' })
623
+ && hasOptional(value, { error: isNullableString });
624
+ }
625
+ function isNativeDeleteResults(value) {
626
+ return Array.isArray(value) && value.every(isNativeDeleteResult);
627
+ }
451
628
  function isEvent(value) {
452
- return isObject(value) && hasRequired(value, { eventName: isString, payload: isRecord });
629
+ return isObject(value) && hasRequired(value, { eventName: isString, payload: isRecord })
630
+ && hasOptional(value, { acpSessionId: isNullableString });
453
631
  }
454
632
  function isAgentFrame(value) {
455
633
  // Cast, not a narrowed lookup: `value.t` is unvetted wire input, so it may not be one of
@@ -588,11 +766,16 @@ export function outboundFailureReply(frame, reason) {
588
766
  switch (frame.t) {
589
767
  case 'agent_opened': return { t: 'agent_opened', id, ok: false, error };
590
768
  case 'agent_native_sessions': return { t: 'agent_native_sessions', id, ok: false, error };
769
+ case 'agent_native_deleted': return { t: 'agent_native_deleted', id, ok: false, error };
591
770
  case 'agent_verified': return { t: 'agent_verified', id, ok: false, error };
592
771
  case 'agent_authenticated': return { t: 'agent_authenticated', id, ok: false, error };
593
772
  case 'agent_installed': return { t: 'agent_installed', id, ok: false, error };
594
773
  case 'agent_steered': return { t: 'agent_steered', id, ok: false, error };
774
+ case 'agent_account_read_result': return { t: 'agent_account_read_result', id, ok: false, error };
595
775
  case 'agent_config_option_set': return { t: 'agent_config_option_set', id, ok: false, error };
776
+ case 'agent_delegation_renewed': return { t: 'agent_delegation_renewed', id, ok: false, error };
777
+ case 'agent_switch_idle_result': return { t: 'agent_switch_idle_result', id, ok: false, error };
778
+ case 'agent_provider_task_stopped': return { t: 'agent_provider_task_stopped', id, state: 'unknown', error };
596
779
  case 'agent_backfill_done':
597
780
  case 'agent_backfill_events': return { t: 'agent_backfill_done', id, ok: false, count: 0, error };
598
781
  default: return null;
@@ -602,5 +785,15 @@ export function encodeAgentFrame(frame) {
602
785
  return JSON.stringify(frame);
603
786
  }
604
787
  export function decodeAgentFrame(text) {
605
- return decodeChannelFrame(text, DISCRIMINATORS, isAgentFrame);
788
+ const frame = decodeChannelFrame(text, DISCRIMINATORS, isAgentFrame);
789
+ if (frame.t === 'agent_provider_task_event') {
790
+ if ((frame.state === 'completed' || frame.state === 'failed' || frame.state === 'stopped') && frame.live)
791
+ throw new TypeError('Invalid provider task terminal live state');
792
+ if (frame.status !== undefined &&
793
+ (frame.status !== frame.state || !['completed', 'failed', 'stopped'].includes(frame.status)))
794
+ throw new TypeError('Invalid provider task terminal status');
795
+ if (frame.summary !== undefined && frame.status === undefined)
796
+ throw new TypeError('Provider task summary requires terminal status');
797
+ }
798
+ return frame;
606
799
  }
@@ -23,6 +23,21 @@ 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
+ }
29
+ const isCount = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
30
+ export function isBridgeCurrentBuild(value) {
31
+ return isObject(value) && hasRequired(value, { id: isString, version: isString }) && hasOptional(value, { patched: isTrueOnly });
32
+ }
33
+ export function isBridgeBuild(value) {
34
+ return isObject(value)
35
+ && hasRequired(value, { id: isString, version: isString, sessions: isCount, idle: isCount, busy: isCount, unmovable: isCount })
36
+ && hasOptional(value, { patched: isTrueOnly, current: isTrueOnly, busyReasons: isStringArray, unmovableReasons: isStringArray });
37
+ }
38
+ export function isBridgeSwitchPending(value) {
39
+ return isObject(value) && hasRequired(value, { remaining: isCount }) && hasOptional(value, { toBuild: isString });
40
+ }
26
41
  export function isBridgeInfo(value) {
27
42
  return isObject(value)
28
43
  && hasRequired(value, { type: isString })
@@ -49,7 +64,10 @@ export function isBridgeInfo(value) {
49
64
  // string is all a peer sending it without the flag is asked to satisfy.
50
65
  platformTriple: isString,
51
66
  // `true` and nothing else, like `supportsSteering`: see `BridgeInfo.recommended`.
67
+ curated: isTrueOnly,
52
68
  recommended: isTrueOnly,
69
+ detected: isTrueOnly,
70
+ missingPrerequisite: isMissingPrerequisite,
53
71
  defaultName: isNullableString,
54
72
  defaultLabel: isNullableString,
55
73
  // Optional on purpose: see BridgeInfo's doc comment — absent is "unknown", and a guard that
@@ -60,6 +78,11 @@ export function isBridgeInfo(value) {
60
78
  // A peer that sent one would be publishing "we looked and it cannot steer" through a key
61
79
  // whose absence already says exactly that, so refusing it keeps one spelling per fact.
62
80
  supportsSteering: isTrueOnly,
81
+ supportsAccountLimits: isTrueOnly,
82
+ // Task 25 (spec §6.4): scan-time build data. Optional: absent is "not looked at".
83
+ currentBuild: isBridgeCurrentBuild,
84
+ builds: (value) => Array.isArray(value) && value.every(isBridgeBuild),
85
+ switchPending: isBridgeSwitchPending,
63
86
  });
64
87
  }
65
88
  export function isBridgeInfoArray(value) {
@@ -97,6 +120,19 @@ export function bridgesForWire(bridges, frame) {
97
120
  }
98
121
  return kept;
99
122
  }
123
+ /**
124
+ * The roster as a hello carries it: without the scan-only build fields (`currentBuild`/`builds`/`switchPending`, spec §6.4). The
125
+ * daemon keeps one cached roster, which a scan refreshes with those fields; a later reconnect's hello would otherwise carry a
126
+ * snapshot of sessions and pending moves that is stale by then. Only the scan answers them, fresh.
127
+ */
128
+ export function helloRoster(bridges) {
129
+ return bridges.map((bridge) => {
130
+ if (bridge.currentBuild === undefined && bridge.builds === undefined && bridge.switchPending === undefined)
131
+ return bridge;
132
+ const { currentBuild: _current, builds: _builds, switchPending: _pending, ...rest } = bridge;
133
+ return rest;
134
+ });
135
+ }
100
136
  function partitionWireSafeBridges(bridges) {
101
137
  let parsed;
102
138
  try {