@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.
- package/README.md +1 -1
- package/dist/authScopes.js +19 -0
- package/dist/axom-patch/cli/README.md +132 -0
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +120 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/nightly-task.md +152 -0
- package/dist/axom-patch/cli/pi-steering.json +37 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +38 -0
- package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +61 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +82 -0
- package/dist/axom-patch/cli/qoder-foreground-output.md +42 -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 +63 -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/axom-patch/cli/zcode-account-limits.json +31 -0
- package/dist/axom-patch/cli/zcode-models.json +38 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
- package/dist/cli.js +36 -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/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +32 -14
- package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
- package/dist/runtime-deps/qoder-sdk/package.json +8 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2514 -681
- package/dist/workstation/acp/acpClient.js +95 -318
- package/dist/workstation/acp/activityBusy.js +43 -0
- package/dist/workstation/acp/activityReporting.js +54 -0
- package/dist/workstation/acp/adoption.js +28 -356
- package/dist/workstation/acp/authUrlSniffer.js +36 -0
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +235 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/delegationMcp.js +24 -0
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +285 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +144 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +80 -0
- package/dist/workstation/acp/permission.js +335 -37
- package/dist/workstation/acp/processor/checkpoint.js +87 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +355 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +181 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +525 -0
- package/dist/workstation/acp/processor/sideChannel.js +80 -0
- package/dist/workstation/acp/processor/state.js +74 -0
- package/dist/workstation/acp/processor/step.js +459 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +364 -0
- package/dist/workstation/acp/processorSession.js +1512 -0
- package/dist/workstation/acp/qoderStartup.js +45 -0
- package/dist/workstation/acp/registry/catalog.js +385 -4
- package/dist/workstation/acp/registry/detect.js +55 -0
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +56 -27
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +171 -0
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +185 -9
- package/dist/workstation/acp/test/keeperWorld.js +267 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +249 -0
- package/dist/workstation/acp/test/realAdapter.js +377 -0
- package/dist/workstation/acp/test/scriptAgent.js +206 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/acp/validate.js +33 -5
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1686 -348
- package/dist/workstation/channels/agent.js +14 -6
- package/dist/workstation/channels/computer.js +203 -7
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +86 -6
- package/dist/workstation/config.js +3 -1
- package/dist/workstation/connection/encryptedChannel.js +88 -21
- package/dist/workstation/daemon.js +309 -6
- package/dist/workstation/daemonctl.js +28 -2
- 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/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/files/text.js +356 -0
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +120 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +249 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +75 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/oauth/deviceAuthorization.js +21 -12
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +201 -8
- package/dist/workstation/protocol/common.js +36 -0
- package/dist/workstation/protocol/computer.js +107 -7
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +118 -0
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +193 -0
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
- package/dist/workstation/qoderAdapter/contract.js +78 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +36 -0
- package/dist/workstation/qoderAdapter/history.js +85 -0
- package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
- package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
- package/dist/workstation/qoderAdapter/main.js +95 -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 +48 -0
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +319 -0
- package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
- package/dist/workstation/qoderAdapter/subwork.js +85 -0
- package/dist/workstation/qoderAdapter/taskControl.js +38 -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/systemMetrics.js +110 -1
- package/dist/workstation/tools/browser.js +394 -0
- package/dist/workstation/tools/client.js +277 -0
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +124 -0
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +86 -0
- package/dist/workstation/update/coordinator.js +32 -0
- package/dist/workstation/update/npmInstaller.js +2 -0
- package/dist/workstation/update/ptyPreflight.js +45 -0
- package/dist/workstation/update/taskRegistry.js +141 -23
- package/dist/workstation/update/watchdog.js +66 -0
- package/package.json +20 -7
- package/skills/axom/SKILL.md +55 -17
- package/skills/axom-definition/SKILL.md +32 -32
- package/skills/axom-platform/SKILL.md +39 -39
- package/dist/workstation/acp/acpSession.js +0 -1047
- package/dist/workstation/acp/contract.js +0 -494
- package/dist/workstation/acp/draftInterceptor.js +0 -154
|
@@ -3,10 +3,13 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { Readable, Writable } from 'node:stream';
|
|
4
4
|
import { client, ndJsonStream, PROTOCOL_VERSION, RequestError } from '@agentclientprotocol/sdk';
|
|
5
5
|
import { sniffAuthUrl } from './authUrlSniffer.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
}
|
|
6
|
+
import { overlayEnvSecrets } from './envSecrets.js';
|
|
7
|
+
import { secretForms } from '../keepers/acp/stderr.js';
|
|
8
|
+
import { initializeRequestParams, jsonRpcErrorCodeOf, parseInitializeResult, } from './initializeResponse.js';
|
|
9
|
+
import { withPathPrefix } from './registry/launch.js';
|
|
10
|
+
// Moved to `initializeResponse.ts` (shared with the deterministic processor); re-exported so every
|
|
11
|
+
// existing importer of these names is unchanged.
|
|
12
|
+
export { STEER_TIMINGS, steeringAdvertised, steeringTimings, toAuthMethodSnapshot, } from './initializeResponse.js';
|
|
10
13
|
/**
|
|
11
14
|
* A subprocess failed to start, or died before it finished responding to `initialize`.
|
|
12
15
|
*
|
|
@@ -14,8 +17,7 @@ export class UnsupportedNodeVersionError extends Error {
|
|
|
14
17
|
* 上写的那几行 —— npx 的 `npm error code ETARGET`、adapter 的崩溃栈、代理的拒绝。这里以前是
|
|
15
18
|
* `stdio: 'inherit'`,那几行只落进 daemon.log,调用方拿到的是一句
|
|
16
19
|
* "exited before responding to initialize (code=1)",服务端那头再转手就成了一条内容为空的
|
|
17
|
-
* "ACP connection closed" ——
|
|
18
|
-
* 需要的是证据,不是从「管子断了」倒推出来的猜测。
|
|
20
|
+
* "ACP connection closed" —— 排查的人手上什么都没有。
|
|
19
21
|
*/
|
|
20
22
|
export class AgentSpawnError extends Error {
|
|
21
23
|
stderr;
|
|
@@ -45,156 +47,15 @@ export class AgentInitializeTimeoutError extends Error {
|
|
|
45
47
|
*/
|
|
46
48
|
const NPM_ETARGET = /(?:^|[^A-Z])ETARGET(?![A-Z])/;
|
|
47
49
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* `InstallOutcome.error`/`stderr` 字符串,不是这两个类里的一个实例,没有 `.stderr` 字段可供
|
|
51
|
-
* {@link isMissingPackageVersion} 去 `instanceof` 窄化。两个调用点都要认同一件事——npm 自己打在
|
|
52
|
-
* stderr 上的 `ETARGET` 错误码——所以正则只在这一处,两个调用点都直接测字符串,而不是各自维护一份。
|
|
50
|
+
* 判定规则本身:npm 自己打在 stderr 上的 `ETARGET` 错误码。读者是安装期的回退
|
|
51
|
+
* (`AcpAgents.installPristineWithFallback`)——启动期不再有回退,启动从不解析版本(spec 2026-10-11 §6.2)。
|
|
53
52
|
*/
|
|
54
53
|
export function stderrShowsMissingPackageVersion(stderr) {
|
|
55
54
|
return NPM_ETARGET.test(stderr);
|
|
56
55
|
}
|
|
57
|
-
/**
|
|
58
|
-
* 这次启动失败,是不是因为 registry 钉的版本 npm 根本没有。
|
|
59
|
-
*
|
|
60
|
-
* 只看 {@link AgentSpawnError.stderr} / {@link AgentInitializeTimeoutError.stderr} 里的原始文本 ——
|
|
61
|
-
* 超时也算进来是因为实测过:npx 在一台慢机器上可以把 20s 的 initialize 预算耗光之后才让子进程死掉,
|
|
62
|
-
* 那时候 ETARGET 早就写在 stderr 上了,只是赢下 race 的是计时器。
|
|
63
|
-
*/
|
|
64
|
-
export function isMissingPackageVersion(error) {
|
|
65
|
-
if (!(error instanceof AgentSpawnError) && !(error instanceof AgentInitializeTimeoutError))
|
|
66
|
-
return false;
|
|
67
|
-
return stderrShowsMissingPackageVersion(error.stderr);
|
|
68
|
-
}
|
|
69
56
|
/** The agent negotiated a protocol version this client does not speak. */
|
|
70
57
|
export class UnsupportedProtocolVersionError extends Error {
|
|
71
58
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Minimum Node.js major version a known agent package requires, keyed by its npx package name
|
|
74
|
-
* (unversioned — matched against both a bare specifier and a version-pinned one; see
|
|
75
|
-
* `argMatchesPackage`).
|
|
76
|
-
*
|
|
77
|
-
* The registry has no field for this — verified fact, not an oversight to fix here. The only place
|
|
78
|
-
* a Node requirement is declared is the agent package's own `package.json#engines`, and `npx` does
|
|
79
|
-
* not enforce it: it just runs the package under whatever `node` is already on PATH. Getting this
|
|
80
|
-
* wrong means the user sees a subprocess crash stack instead of an actionable message (ruling R7),
|
|
81
|
-
* so this table exists to catch the one adapter known, as of 2026-08-21, to need it.
|
|
82
|
-
*/
|
|
83
|
-
const KNOWN_MIN_NODE_MAJOR = [
|
|
84
|
-
{ packageName: '@agentclientprotocol/claude-agent-acp', minMajor: 22 },
|
|
85
|
-
];
|
|
86
|
-
/**
|
|
87
|
-
* True when `arg` is exactly `packageName`, or `packageName` followed by an `@version` suffix.
|
|
88
|
-
* `arg.startsWith(packageName)` alone would also match an unrelated package that merely shares a
|
|
89
|
-
* prefix (e.g. `@agentclientprotocol/claude-agent-acp-extra`); anchoring on an exact match or an
|
|
90
|
-
* `@` boundary rules that out while still matching ruling R3's user-supplied, unpinned
|
|
91
|
-
* `command`/`args` override, which has no version suffix to key off at all.
|
|
92
|
-
*/
|
|
93
|
-
function argMatchesPackage(arg, packageName) {
|
|
94
|
-
return arg === packageName || arg.startsWith(`${packageName}@`);
|
|
95
|
-
}
|
|
96
|
-
/** Exported for tests: derives the minimum Node major version a launch spec requires, if known. */
|
|
97
|
-
export function requiredNodeMajorFor(spec) {
|
|
98
|
-
const match = KNOWN_MIN_NODE_MAJOR.find((entry) => spec.args.some((arg) => argMatchesPackage(arg, entry.packageName)));
|
|
99
|
-
return match?.minMajor;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Throws a message a user can act on when this machine's Node is older than a known agent
|
|
103
|
-
* requires. `nodeVersion` defaults to the real running Node; it takes an explicit override so tests
|
|
104
|
-
* can exercise both sides of the gate without depending on the host's actual Node version —
|
|
105
|
-
* `process.versions.node` is a frozen property and cannot be stubbed.
|
|
106
|
-
*/
|
|
107
|
-
export function assertNodeVersionSupported(spec, nodeVersion = process.versions.node) {
|
|
108
|
-
const required = requiredNodeMajorFor(spec);
|
|
109
|
-
if (required === undefined)
|
|
110
|
-
return;
|
|
111
|
-
const currentMajor = Number.parseInt(nodeVersion.split('.')[0] ?? '', 10);
|
|
112
|
-
if (Number.isNaN(currentMajor) || currentMajor < required) {
|
|
113
|
-
throw new UnsupportedNodeVersionError(`This agent requires Node.js >= ${required}, but this machine is running Node.js ${nodeVersion}. ` +
|
|
114
|
-
'Install a newer Node.js and try again.');
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Whether an `InitializeResponse._meta` advertises the `_session/steering` extension.
|
|
119
|
-
*
|
|
120
|
-
* ## `_session/steering` is not ACP
|
|
121
|
-
*
|
|
122
|
-
* It is absent from the schema this client is pinned to (`grep steer
|
|
123
|
-
* node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts` returns nothing). It is an
|
|
124
|
-
* extension `claude-agent-acp` implements: the method constant lives at its `acp-agent.js@0.76.0:164`
|
|
125
|
-
* and its own comment calls it a *"Custom (extension) request method ... Named `_session/steering`
|
|
126
|
-
* per the agreed ACP steering wire protocol"*. The leading underscore is ACP's extension namespace.
|
|
127
|
-
*
|
|
128
|
-
* ## Why the top-level `_meta` specifically
|
|
129
|
-
*
|
|
130
|
-
* The adapter advertises it at `InitializeResponse._meta.steering.supported`, a **sibling of
|
|
131
|
-
* `agentCapabilities`** (`acp-agent.js@0.76.0:944-950`), not a key inside it. `_meta` is typed
|
|
132
|
-
* `{[key: string]: unknown} | null` (`types.gen.d.ts:1498-1500`), so every hop down has to be
|
|
133
|
-
* narrowed rather than indexed through a cast.
|
|
134
|
-
*
|
|
135
|
-
* Strictly `=== true`. An agent that sends `steering: {}` (the shape ACP uses for its *own*
|
|
136
|
-
* capability markers) has not said `supported`, and this extension does not define that spelling —
|
|
137
|
-
* reading it as a yes would be this client inventing a wire contract.
|
|
138
|
-
*/
|
|
139
|
-
export function steeringAdvertised(meta) {
|
|
140
|
-
if (typeof meta !== 'object' || meta === null)
|
|
141
|
-
return false;
|
|
142
|
-
const steering = meta.steering;
|
|
143
|
-
if (typeof steering !== 'object' || steering === null)
|
|
144
|
-
return false;
|
|
145
|
-
return steering.supported === true;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* The delivery timings the `_session/steering` extension defines — when the agent hands an injected
|
|
149
|
-
* message to the model. `next` is the only lossless one: `now` aborts the running generation and
|
|
150
|
-
* discards whatever tool calls were still outstanding.
|
|
151
|
-
*/
|
|
152
|
-
export const STEER_TIMINGS = ['now', 'next', 'later'];
|
|
153
|
-
/**
|
|
154
|
-
* Which delivery timings an `InitializeResponse._meta` says the agent accepts —
|
|
155
|
-
* `_meta.steering.timings`, the sibling of the `supported` flag {@link steeringAdvertised} reads,
|
|
156
|
-
* and read from the **top-level** `_meta` for exactly the same reason.
|
|
157
|
-
*
|
|
158
|
-
* ## Empty is the answer, not the absence of one
|
|
159
|
-
*
|
|
160
|
-
* An agent that declares no timings has told us we may not name one. That is not "assume the
|
|
161
|
-
* default" — the extension ignores an unrecognised `priority` **silently** (the measurement behind
|
|
162
|
-
* ruling N4: sending `next` to today's adapters lands as `now`, which throws away the user's
|
|
163
|
-
* in-flight work). So an empty result must reach the caller as "do not specify a timing at all",
|
|
164
|
-
* and today — before the upstream PR that makes adapters broadcast this — every agent in the wild
|
|
165
|
-
* produces exactly that. Steady state, not a bug.
|
|
166
|
-
*
|
|
167
|
-
* ## Unknown values are dropped, never rejected
|
|
168
|
-
*
|
|
169
|
-
* A timing this build has never heard of is skipped and the known ones beside it survive. This
|
|
170
|
-
* client is deployed on user machines and can be older than both the agent it launches and the
|
|
171
|
-
* server it reports to; throwing (or discarding the whole array) on a value from a future version
|
|
172
|
-
* of the extension would take steering away from an agent that does support it. Non-strings are
|
|
173
|
-
* dropped the same way: `_meta` is typed `{[key: string]: unknown}`, so nothing here is guaranteed.
|
|
174
|
-
*
|
|
175
|
-
* Duplicates collapse — the wire shape is a JSON array but the fact is a set.
|
|
176
|
-
*/
|
|
177
|
-
export function steeringTimings(meta) {
|
|
178
|
-
if (typeof meta !== 'object' || meta === null)
|
|
179
|
-
return [];
|
|
180
|
-
const steering = meta.steering;
|
|
181
|
-
if (typeof steering !== 'object' || steering === null)
|
|
182
|
-
return [];
|
|
183
|
-
const declared = steering.timings;
|
|
184
|
-
if (!Array.isArray(declared))
|
|
185
|
-
return [];
|
|
186
|
-
const known = [];
|
|
187
|
-
for (const entry of declared) {
|
|
188
|
-
if (typeof entry !== 'string')
|
|
189
|
-
continue;
|
|
190
|
-
if (!STEER_TIMINGS.includes(entry))
|
|
191
|
-
continue;
|
|
192
|
-
if (known.includes(entry))
|
|
193
|
-
continue;
|
|
194
|
-
known.push(entry);
|
|
195
|
-
}
|
|
196
|
-
return known;
|
|
197
|
-
}
|
|
198
59
|
/**
|
|
199
60
|
* The JSON-RPC error code an agent answered with, or `undefined` when the failure was not one an
|
|
200
61
|
* agent answered at all.
|
|
@@ -225,10 +86,7 @@ export function steeringTimings(meta) {
|
|
|
225
86
|
export function jsonRpcErrorCode(error) {
|
|
226
87
|
if (!(error instanceof RequestError))
|
|
227
88
|
return undefined;
|
|
228
|
-
|
|
229
|
-
if (!Number.isInteger(code))
|
|
230
|
-
return undefined;
|
|
231
|
-
return code >= -2_147_483_648 && code <= 2_147_483_647 ? code : undefined;
|
|
89
|
+
return jsonRpcErrorCodeOf(error.code);
|
|
232
90
|
}
|
|
233
91
|
function translateSpawnError(command, error, stderr) {
|
|
234
92
|
if (error.code === 'ENOENT') {
|
|
@@ -251,7 +109,7 @@ const STDERR_TAIL_LIMIT = 8_192;
|
|
|
251
109
|
*/
|
|
252
110
|
const STDERR_FLUSH_GRACE_MS = 250;
|
|
253
111
|
/**
|
|
254
|
-
* 把子进程的 stderr
|
|
112
|
+
* 把子进程的 stderr 接出来:普通连接原样转发,托管 MCP 请求后的输出逐行脱敏,并留一份有界的安全尾巴给报错带上,
|
|
255
113
|
* 一边逐行喂给 {@link sniffAuthUrl} —— 有的 agent(实测 antigravity)不走 `elicitation/create` 递
|
|
256
114
|
* 登录链接,而是直接打到 stderr 上(见 `authUrlSniffer.ts` 的文件头注释)。
|
|
257
115
|
*
|
|
@@ -269,9 +127,41 @@ const STDERR_FLUSH_GRACE_MS = 250;
|
|
|
269
127
|
*/
|
|
270
128
|
function captureChildStderr(stream, present) {
|
|
271
129
|
if (!stream)
|
|
272
|
-
return { text: () => '', flushed: () => Promise.resolve() };
|
|
130
|
+
return { text: () => '', flushed: () => Promise.resolve(), redactValue: () => { } };
|
|
273
131
|
let tail = '';
|
|
274
132
|
let lineBuffer = '';
|
|
133
|
+
let protectedLine = '';
|
|
134
|
+
let droppingLongLine = false;
|
|
135
|
+
/** {@link ChildStderr.redactValue}'s forms. */
|
|
136
|
+
const quiet = new Set();
|
|
137
|
+
const redactAll = (text) => {
|
|
138
|
+
let safe = text;
|
|
139
|
+
for (const secret of quiet)
|
|
140
|
+
safe = safe.replaceAll(secret, '[REDACTED]');
|
|
141
|
+
return safe;
|
|
142
|
+
};
|
|
143
|
+
const forward = (value) => {
|
|
144
|
+
tail = (tail + value).slice(-STDERR_TAIL_LIMIT);
|
|
145
|
+
process.stderr.write(value);
|
|
146
|
+
};
|
|
147
|
+
const forwardProtected = (value) => {
|
|
148
|
+
protectedLine += value;
|
|
149
|
+
let newline;
|
|
150
|
+
while ((newline = protectedLine.indexOf('\n')) >= 0) {
|
|
151
|
+
if (droppingLongLine) {
|
|
152
|
+
droppingLongLine = false;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
forward(redactAll(protectedLine.slice(0, newline + 1)));
|
|
156
|
+
}
|
|
157
|
+
protectedLine = protectedLine.slice(newline + 1);
|
|
158
|
+
}
|
|
159
|
+
if (protectedLine.length > STDERR_TAIL_LIMIT) {
|
|
160
|
+
protectedLine = '';
|
|
161
|
+
droppingLongLine = true;
|
|
162
|
+
forward('[acp] child stderr line omitted\n');
|
|
163
|
+
}
|
|
164
|
+
};
|
|
275
165
|
// One login can print its own URL more than once — the ordinary shape of an OAuth CLI is "here's
|
|
276
166
|
// the link" immediately followed by "if your browser didn't open, here it is again", verbatim
|
|
277
167
|
// the same URL. Without this, that becomes two permanent, identical, un-retractable cards for a
|
|
@@ -315,19 +205,36 @@ function captureChildStderr(stream, present) {
|
|
|
315
205
|
};
|
|
316
206
|
stream.on('data', (chunk) => {
|
|
317
207
|
const text = chunk.toString();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
208
|
+
if (quiet.size > 0) {
|
|
209
|
+
forwardProtected(text);
|
|
210
|
+
scanForAuthUrl(text);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
tail = (tail + text).slice(-STDERR_TAIL_LIMIT);
|
|
214
|
+
process.stderr.write(chunk);
|
|
215
|
+
scanForAuthUrl(text);
|
|
216
|
+
}
|
|
321
217
|
});
|
|
322
218
|
// 读端出错(SIGKILL 掉子进程时管子会断)没有谁需要处理,但一个没有监听器的 `'error'` 会把整个
|
|
323
219
|
// 守护进程掀翻 —— 这行不是防御性代码,是 EventEmitter 的硬性要求。
|
|
324
220
|
stream.on('error', () => { });
|
|
325
221
|
// A final unterminated line (no trailing `\n` before the process exits) is still a real line —
|
|
326
222
|
// scan whatever is left in the buffer once the stream itself says there is no more coming.
|
|
327
|
-
void ended.then(() =>
|
|
223
|
+
void ended.then(() => {
|
|
224
|
+
if (protectedLine && !droppingLongLine)
|
|
225
|
+
forward(redactAll(protectedLine));
|
|
226
|
+
scanForAuthUrl('\n');
|
|
227
|
+
});
|
|
328
228
|
return {
|
|
329
229
|
text: () => tail,
|
|
330
230
|
flushed: (ms) => settledWithin(ended, ms).then(() => undefined),
|
|
231
|
+
redactValue: (value) => {
|
|
232
|
+
// Longest first, so a form containing another is removed whole (Set order is insertion order).
|
|
233
|
+
const all = [...quiet, ...secretForms(value)].sort((a, b) => b.length - a.length);
|
|
234
|
+
quiet.clear();
|
|
235
|
+
for (const form of all)
|
|
236
|
+
quiet.add(form);
|
|
237
|
+
},
|
|
331
238
|
};
|
|
332
239
|
}
|
|
333
240
|
/**
|
|
@@ -350,7 +257,7 @@ function rejectionWithin(failure, ms) {
|
|
|
350
257
|
});
|
|
351
258
|
});
|
|
352
259
|
}
|
|
353
|
-
/** 报错正文里只放最后几行;完整尾巴留在 `.stderr`
|
|
260
|
+
/** 报错正文里只放最后几行;完整尾巴留在 `.stderr` 上给程序读。 */
|
|
354
261
|
function lastStderrLines(text, count) {
|
|
355
262
|
return text.split('\n').map((line) => line.trim()).filter(Boolean).slice(-count).join(' | ');
|
|
356
263
|
}
|
|
@@ -451,94 +358,41 @@ export async function closeAgentTransport(child, connection, graceMs = CHILD_EXI
|
|
|
451
358
|
connection.close();
|
|
452
359
|
await connection.closed.catch(() => { });
|
|
453
360
|
}
|
|
454
|
-
/**
|
|
455
|
-
* One `initialize` auth method, or `null` when the agent did not advertise a usable one.
|
|
456
|
-
*
|
|
457
|
-
* `initResponse` is another process's JSON that arrived over a pipe; `AuthMethod` is what this
|
|
458
|
-
* client *asked* for, not what it was handed. Both variants of that union make `id` and `name`
|
|
459
|
-
* required (`@agentclientprotocol/sdk` `types.gen.d.ts:2241-2246` for `AuthMethodTerminal`,
|
|
460
|
-
* `:2281-2286` for `AuthMethodAgent`), so an agent that omits them is off-spec -- but nothing
|
|
461
|
-
* between its stdout and here checks, and the snapshot then carries `undefined` in two positions
|
|
462
|
-
* its own type declares non-optional. That lie does not stay local: `JSON.stringify` erases an
|
|
463
|
-
* `undefined` value entirely, so the object passes every `typeof x.id` check in this process and
|
|
464
|
-
* still reaches a peer with the keys missing.
|
|
465
|
-
*
|
|
466
|
-
* `agoragentic-acp` advertises exactly `{type: 'terminal', description: '…'}`. Observing it once
|
|
467
|
-
* put a malformed `AuthMethodInfo` in the roster, which rides in every subsequent
|
|
468
|
-
* `management_hello`, which the server then cannot decode -- and a hello the server cannot decode
|
|
469
|
-
* closes the socket with nothing said, so the workstation retried into a permanent OFFLINE.
|
|
470
|
-
*
|
|
471
|
-
* Dropping rather than repairing: `id` is the value `authenticate` is called with, so inventing
|
|
472
|
-
* one would trade a missing sign-in method for a broken one.
|
|
473
|
-
*/
|
|
474
|
-
function toAuthMethodSnapshot(method) {
|
|
475
|
-
if (typeof method.id !== 'string' || typeof method.name !== 'string')
|
|
476
|
-
return null;
|
|
477
|
-
return {
|
|
478
|
-
id: method.id,
|
|
479
|
-
name: method.name,
|
|
480
|
-
...('type' in method && method.type === 'terminal' ? { type: 'terminal' } : {}),
|
|
481
|
-
...(method.description != null ? { description: method.description } : {}),
|
|
482
|
-
// `args` only exists on `AuthMethodTerminal` — an `agent`-type method has no such field, and
|
|
483
|
-
// `'args' in method` is false for it, which is exactly "nothing to show" (see `AgentAuthMethod`).
|
|
484
|
-
...('args' in method && Array.isArray(method.args) ? { args: method.args } : {}),
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
361
|
const DEFAULT_INITIALIZE_TIMEOUT_MS = 30_000;
|
|
488
362
|
/**
|
|
489
363
|
* Spawns an ACP agent subprocess, connects to it over ndjson stdio, and negotiates capabilities
|
|
490
364
|
* via `initialize`. `env` and `agentEnv` are overlay layers on top of the current process
|
|
491
365
|
* environment, not full environments (existing contract carried over from the deleted bridge
|
|
492
366
|
* transport): if `PATH` is present in either, it has already been resolved by the caller and must
|
|
493
|
-
* not be prefixed with anything else here
|
|
367
|
+
* not be prefixed with anything else here — except the spec's own `pathPrefix` (a build's
|
|
368
|
+
* `node_modules/.bin`), which the adapter needs whatever `PATH` it is given.
|
|
494
369
|
*/
|
|
495
370
|
export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
496
|
-
|
|
371
|
+
// No Node gate here any more: a build's `engines.node` is checked against the Node that runs it when
|
|
372
|
+
// the spec is made (`launchForBuild`, `builds/nodeGate.ts`); a config `command` override is the user's own.
|
|
373
|
+
// A build's own PATH prefix (`node_modules/.bin`) must survive a server- or agent-supplied PATH.
|
|
374
|
+
const processEnv = withPathPrefix({ ...process.env, ...spec.env, ...env, ...agentEnv }, spec.pathPrefix);
|
|
497
375
|
const child = spawn(spec.command, spec.args, {
|
|
498
376
|
// stderr 是 `pipe` 而不是 `inherit`,而且**依然不接进协议流** —— 它被 `captureChildStderr`
|
|
499
377
|
// 单独读走:原样转发到 daemon.log(`inherit` 以前免费提供的那件事),同时留一份尾巴给启动期的
|
|
500
378
|
// 报错。启动失败时 npm 写在这里的文字是唯一的一手证据。
|
|
501
379
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
502
|
-
env:
|
|
380
|
+
env: processEnv,
|
|
503
381
|
});
|
|
504
382
|
const stderrTail = captureChildStderr(child.stderr, options.present);
|
|
383
|
+
for (const value of overlayEnvSecrets({ env, agentEnv }))
|
|
384
|
+
stderrTail.redactValue(value);
|
|
505
385
|
const lifecycle = watchChildLifecycle(child, spec.command, stderrTail);
|
|
506
386
|
// Argument order reads backwards from what the names suggest: argument 1 is the child's stdin
|
|
507
387
|
// (we write to it), argument 2 is the child's stdout (we read from it). Swapping them still
|
|
508
388
|
// compiles and still "connects" — the initialize request just never arrives, so the failure mode
|
|
509
389
|
// is a hang, not an error.
|
|
510
|
-
|
|
511
|
-
// passed in, because the interceptor has to be in place before `initialize` — the agent may send
|
|
512
|
-
// its first update before anyone has had a chance to register anything.
|
|
513
|
-
let ownSessionId = null;
|
|
514
|
-
let draftSink = null;
|
|
515
|
-
// Sessions this connection handles by some route other than an `AcpSession` — today only
|
|
516
|
-
// adoption's replay. Kept beside `ownSessionId` rather than folded into it because they mean
|
|
517
|
-
// different things: one is "the session we drive", these are "sessions someone else on this
|
|
518
|
-
// connection is already reading".
|
|
519
|
-
const knownSessionIds = new Set();
|
|
520
|
-
const stream = ndJsonStream(Writable.toWeb(child.stdin),
|
|
521
|
-
// The child's stdout, minus the notifications the pinned SDK cannot deliver. Everything else
|
|
522
|
-
// reaches it byte-for-byte; see `draftInterceptor.ts` for the measurements behind this.
|
|
523
|
-
interceptDraftUpdates(Readable.toWeb(child.stdout), {
|
|
524
|
-
// Unbound means unknown, and unknown must not be claimed as ours: treating a session we have
|
|
525
|
-
// not been told about as our own would send its frames to the SDK, which drops them silently.
|
|
526
|
-
// Diverting is the recoverable direction — the sink can decide, and it can log.
|
|
527
|
-
isOwnSession: (sessionId) => (ownSessionId !== null && sessionId === ownSessionId) || knownSessionIds.has(sessionId),
|
|
528
|
-
onDiverted: (update) => {
|
|
529
|
-
if (draftSink) {
|
|
530
|
-
draftSink(update);
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
// No sink yet: say so once per update rather than dropping in silence. This is reachable
|
|
534
|
-
// only before the session is bound, which is before any prompt, so it should stay rare —
|
|
535
|
-
// and if it stops being rare, that is worth seeing.
|
|
536
|
-
console.warn(`[acp] dropped a ${update.reason} session/update for '${update.sessionId}': ` +
|
|
537
|
-
'no session is bound to this connection yet');
|
|
538
|
-
},
|
|
539
|
-
}));
|
|
390
|
+
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
540
391
|
const app = client({ name: 'axom' });
|
|
541
|
-
|
|
392
|
+
// This adapter process's generation: the scope `permission.ts`'s `permissionRequestId` derives card ids in (a sign-in's
|
|
393
|
+
// connection registers the permission handlers).
|
|
394
|
+
const generation = randomUUID();
|
|
395
|
+
options.configure?.(app, { generation });
|
|
542
396
|
const connection = app.connect(stream);
|
|
543
397
|
const timeoutMs = options.initializeTimeoutMs ?? DEFAULT_INITIALIZE_TIMEOUT_MS;
|
|
544
398
|
let timeoutHandle;
|
|
@@ -555,53 +409,7 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
555
409
|
let initResponse;
|
|
556
410
|
try {
|
|
557
411
|
initResponse = await Promise.race([
|
|
558
|
-
connection.agent.request('initialize',
|
|
559
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
560
|
-
// Three things are declared here, and they are not interchangeable — see
|
|
561
|
-
// `draftSubagents.ts` for the tier each one sits in.
|
|
562
|
-
//
|
|
563
|
-
// 1. `subagents: {}` is the **canonical** draft field from ACP RFD #1992. Both official
|
|
564
|
-
// adapters gate native subagent sessions on it, and both answer with
|
|
565
|
-
// `agentCapabilities.sessionCapabilities.subagents` (measured against each).
|
|
566
|
-
//
|
|
567
|
-
// 2. The AIR list is sent as well, for two separate reasons. It is the *only* channel for
|
|
568
|
-
// `asyncTasks` — nothing canonical exists for background work — and it is the fallback
|
|
569
|
-
// for `nativeSubagentSessions`, because a released SDK sitting between us and an agent
|
|
570
|
-
// can strip a draft field it has no type for (upstream says so in #1017's compatibility
|
|
571
|
-
// note). Declaring both is not belt-and-braces: they cover different failure modes.
|
|
572
|
-
//
|
|
573
|
-
// 3. `subagent-transcript: true` stays. It is claude-agent-acp's older vendor key
|
|
574
|
-
// (`dist/acp-agent.js:155-158`), and it still governs the **fallback** path taken when
|
|
575
|
-
// native subagent sessions are not negotiated — an older adapter, or a stripped
|
|
576
|
-
// capability. Without it that path strips every subagent's text and thinking
|
|
577
|
-
// (`stripSubagentTextAndThinking`, `:3926`), leaving nothing but tool-call wreckage.
|
|
578
|
-
// Adapters that have never heard of any of these keys are required by the spec to
|
|
579
|
-
// ignore them.
|
|
580
|
-
clientCapabilities: asSdkClientCapabilities({
|
|
581
|
-
[DRAFT_SUBAGENTS_CAPABILITY]: {},
|
|
582
|
-
// `ClientSessionCapabilities.configOptions` (`types.gen.d.ts:4306`), part of the schema
|
|
583
|
-
// proper — unlike `subagents` above, this survives the pinned SDK's `zInitializeRequest`
|
|
584
|
-
// stripping because it's a known field, not a draft one. `{}` for `boolean` tells the
|
|
585
|
-
// agent it may send `type: "boolean"` entries in `SessionConfigOption` and that this
|
|
586
|
-
// client will send `session/set_config_option` with a boolean `value`
|
|
587
|
-
// (`SessionConfigOptionsCapabilities`, `types.gen.d.ts:4333-4353`); without it an agent
|
|
588
|
-
// with a boolean toggle downgrades it to a two-value select.
|
|
589
|
-
session: { configOptions: { boolean: {} } },
|
|
590
|
-
// `ClientCapabilities.elicitation` (`types.gen.d.ts:4479`) is a known field of the schema
|
|
591
|
-
// proper, not a draft one, so it survives `zInitializeRequest` stripping as-is. `{}` for
|
|
592
|
-
// `url` (`ElicitationUrlCapabilities`, `:4616`) means the client can put a URL in front of
|
|
593
|
-
// the user; this is the precondition for the whole device-code login shape to exist at all
|
|
594
|
-
// — Codex's own `getCodexAuthMethods` gates its `chat-gpt-device-code` auth method on
|
|
595
|
-
// `clientCapabilities?.elicitation?.url != null`. Without declaring it, Codex never offers
|
|
596
|
-
// that method and `elicitation/create` with `mode: 'url'` never arrives to answer (see
|
|
597
|
-
// `permission.ts`'s `handleCreateElicitation`).
|
|
598
|
-
elicitation: { url: {} },
|
|
599
|
-
_meta: {
|
|
600
|
-
'subagent-transcript': true,
|
|
601
|
-
...airCapabilityMeta(AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
|
|
602
|
-
},
|
|
603
|
-
}),
|
|
604
|
-
}),
|
|
412
|
+
connection.agent.request('initialize', initializeRequestParams()),
|
|
605
413
|
lifecycle.failure,
|
|
606
414
|
timeout,
|
|
607
415
|
]);
|
|
@@ -622,68 +430,37 @@ export async function connectAgent(spec, env, agentEnv, options = {}) {
|
|
|
622
430
|
}
|
|
623
431
|
// Global Constraint 1 is v1-only; the schema calls for disconnecting when the agent didn't
|
|
624
432
|
// accept the version this client sent.
|
|
625
|
-
|
|
433
|
+
const reading = parseInitializeResult(initResponse);
|
|
434
|
+
if (!reading.protocolSupported) {
|
|
626
435
|
const error = new UnsupportedProtocolVersionError(`Agent '${spec.command}' negotiated protocol version ${initResponse.protocolVersion}, ` +
|
|
627
436
|
`but this client only supports version ${PROTOCOL_VERSION}.`);
|
|
628
437
|
connection.close(error);
|
|
629
438
|
child.kill();
|
|
630
439
|
throw error;
|
|
631
440
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
// this must stay an explicit `!= null`, never `Boolean(...)` or a bare truthy test.
|
|
638
|
-
list: sessionCapabilities?.list != null,
|
|
639
|
-
resume: sessionCapabilities?.resume != null,
|
|
640
|
-
loadSession: initResponse.agentCapabilities?.loadSession === true,
|
|
641
|
-
logout: initResponse.agentCapabilities?.auth?.logout != null,
|
|
642
|
-
/**
|
|
643
|
-
* ACP `PromptCapabilities.image` (`types.gen.d.ts:1601`): "Agent supports
|
|
644
|
-
* [`ContentBlock::Image`]". Every field of that type is optional, so an agent that says
|
|
645
|
-
* nothing supports text only -- `=== true` rather than `!== false`.
|
|
646
|
-
*
|
|
647
|
-
* Verified against codex-acp 1.7.0, which declares `{embeddedContext: true, image: true}`.
|
|
648
|
-
* `audio` is deliberately not read: no agent we target declares it, and an unread capability
|
|
649
|
-
* is a field nobody can act on.
|
|
650
|
-
*/
|
|
651
|
-
promptImage: initResponse.agentCapabilities?.promptCapabilities?.image === true,
|
|
652
|
-
};
|
|
653
|
-
// Honour the advertised capability, not what the agent happens to route: some adapters (e.g.
|
|
654
|
-
// codex-acp routing `session/fork`) implement more than they declare in `agentCapabilities`.
|
|
655
|
-
// Reading `capabilities` above off `initResponse` — never probing the agent for what it accepts
|
|
656
|
-
// — is what keeps that distinction intact.
|
|
657
|
-
const advertisedAuthMethods = initResponse.authMethods ?? [];
|
|
658
|
-
const authMethods = advertisedAuthMethods
|
|
659
|
-
.map(toAuthMethodSnapshot)
|
|
660
|
-
.filter((method) => method !== null);
|
|
661
|
-
if (authMethods.length !== advertisedAuthMethods.length) {
|
|
441
|
+
// Capabilities, auth methods and steering are read in `initializeResponse.ts`, the one reading the
|
|
442
|
+
// deterministic processor shares — see that file for the rules.
|
|
443
|
+
const { capabilities, authMethods, droppedAuthMethods } = reading;
|
|
444
|
+
const terminalLogins = new Map(reading.terminalLogins);
|
|
445
|
+
if (droppedAuthMethods > 0) {
|
|
662
446
|
// Named and counted rather than dropped in silence: the user loses a sign-in method they can
|
|
663
447
|
// see advertised in the agent's own docs, and the only honest answer to "why" is which agent
|
|
664
448
|
// sent what. `agentInfo.name` first because most agents launch through `npx`/`uvx` and
|
|
665
449
|
// `spec.command` would then name the launcher, which identifies none of the ~40 registry
|
|
666
450
|
// entries. See `toAuthMethodSnapshot` for what accepting these costs instead.
|
|
667
451
|
console.warn(`[acp] agent '${initResponse.agentInfo?.name ?? spec.command}' advertised ` +
|
|
668
|
-
`${
|
|
452
|
+
`${droppedAuthMethods} auth method(s) without the required ` +
|
|
669
453
|
'id/name; ignoring them');
|
|
670
454
|
}
|
|
671
455
|
return {
|
|
672
456
|
capabilities,
|
|
673
457
|
authMethods,
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
458
|
+
terminalLogins,
|
|
459
|
+
supportsAccountLimits: reading.supportsAccountLimits,
|
|
460
|
+
supportsSteering: reading.supportsSteering,
|
|
461
|
+
steeringTimings: reading.steeringTimings,
|
|
462
|
+
agentInfo: reading.agentInfo,
|
|
677
463
|
connection,
|
|
678
|
-
agentProcessAlive: () => child.exitCode === null && child.signalCode === null,
|
|
679
|
-
bindDraftUpdates(sessionId, sink) {
|
|
680
|
-
ownSessionId = sessionId;
|
|
681
|
-
draftSink = sink;
|
|
682
|
-
},
|
|
683
|
-
markSessionKnown(sessionId) {
|
|
684
|
-
knownSessionIds.add(sessionId);
|
|
685
|
-
return () => knownSessionIds.delete(sessionId);
|
|
686
|
-
},
|
|
687
464
|
// Child first, streams second — see `closeAgentTransport` for why that order is the fix and not
|
|
688
465
|
// an incidental detail.
|
|
689
466
|
close: () => closeAgentTransport(child, connection),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「这条会话的 agent 自己说它在忙吗」——一个**弱**判据,不是归一化器。
|
|
3
|
+
*
|
|
4
|
+
* 真正的 vendor 原文 → 规范三态只发生在服务端 `axom-core` 的 `agentActivityOf`
|
|
5
|
+
* (前序 spec 裁决 L1:归一化只在一处)。这里要回答的问题窄得多,答案只有两种,
|
|
6
|
+
* 而且两边对「读不懂」的处置是同一条推理:服务端答 `null`(保留上一个已知状态,不写库),
|
|
7
|
+
* 这里答 `false`(本来源不主张忙碌,判据退回 `liveness.turnRunning`)。两边都不把未知
|
|
8
|
+
* 伪装成已知——答 `true` 会造出一个永不消失的假忙碌,正是本轮要消灭的东西。
|
|
9
|
+
*
|
|
10
|
+
* 两个键分属两套词汇表(claude 的 `state`、codex 的 `type`),**不做或运算**:
|
|
11
|
+
* `state` 在就只看 `state`。混读等于承认一份原文可以同时是两种方言。
|
|
12
|
+
*/
|
|
13
|
+
export function activityIsBusy(raw) {
|
|
14
|
+
if (!raw)
|
|
15
|
+
return false;
|
|
16
|
+
if (typeof raw.state === 'string')
|
|
17
|
+
return raw.state === 'running' || raw.state === 'requires_action';
|
|
18
|
+
if (typeof raw.type === 'string')
|
|
19
|
+
return raw.type === 'active' && !codexBlockedOnHumanOnly(raw);
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* codex 的 `activeFlags` 里表示「agent 正被人挡住、没有在算」的标记。
|
|
24
|
+
*
|
|
25
|
+
* **和会话状态问的不是同一个问题。** 服务端的 `agentActivityOf` 要答「vendor 说它在干什么」,
|
|
26
|
+
* 这里要答「排干时有没有在途工作会被重启破坏」。一条停在审批卡或提问上的会话,两个答案不同:
|
|
27
|
+
* 它确实处在 vendor 说的 `active`,但**没有任何在途工作**,重启它不损失什么。把这种会话算成忙,
|
|
28
|
+
* 会让一次优雅更新白等满 30 分钟排干再报 BUSY —— 正是本分支要消灭的那个形状,只是换了个入口。
|
|
29
|
+
* 2026-09-21 在真机上抓到的第一条 codex 原文就是这一种:
|
|
30
|
+
* `{"type":"active","activeFlags":["waitingOnUserInput"]}`。
|
|
31
|
+
*
|
|
32
|
+
* 值域的取证、证据等级,以及「不认识的标记为什么保持忙」的理由,见 `axom-core` 的
|
|
33
|
+
* `AgentActivityMapping.kt` 里 `CODEX_BLOCKED_ON_HUMAN_FLAGS` 的 KDoc —— 那里是权威说明,
|
|
34
|
+
* 这两处必须同时改,所以判据故意写成同一个形状(非空 + 每一项都认得)。
|
|
35
|
+
*/
|
|
36
|
+
const CODEX_BLOCKED_ON_HUMAN_FLAGS = new Set(['waitingOnApproval', 'waitingOnUserInput']);
|
|
37
|
+
/** `activeFlags` 非空**且每一项**都在 {@link CODEX_BLOCKED_ON_HUMAN_FLAGS} 里。 */
|
|
38
|
+
function codexBlockedOnHumanOnly(raw) {
|
|
39
|
+
const flags = raw.activeFlags;
|
|
40
|
+
if (!Array.isArray(flags) || flags.length === 0)
|
|
41
|
+
return false;
|
|
42
|
+
return flags.every((flag) => typeof flag === 'string' && CODEX_BLOCKED_ON_HUMAN_FLAGS.has(flag));
|
|
43
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两条厂商特定的活动状态采集路径。**只做搬运,不做归一化**——vendor→规范三态的映射
|
|
3
|
+
* 只发生在服务端 ingest 一处(裁决 L1:normalise in one place, server ingest)。
|
|
4
|
+
*
|
|
5
|
+
* 判 agent 用 `InitializeResponse.agentInfo.name`:ACP 的 `Implementation.name` 文档自述
|
|
6
|
+
* "intended for programmatic or logical use",而且用户改不了它(`agentName` 是工作站配置里的
|
|
7
|
+
* 条目名,随时可以改)。
|
|
8
|
+
*/
|
|
9
|
+
const CLAUDE_AGENT = '@agentclientprotocol/claude-agent-acp';
|
|
10
|
+
const CODEX_AGENT = '@agentclientprotocol/codex-acp';
|
|
11
|
+
/** 这个 agent 有没有配套的活动状态采集器;按 InitializeResponse.agentInfo.name 判。 */
|
|
12
|
+
export function reportsActivity(agentName) {
|
|
13
|
+
return agentName === CLAUDE_AGENT || agentName === CODEX_AGENT;
|
|
14
|
+
}
|
|
15
|
+
/** Certified AIR task-stop compatibility, independent of activity collection. These adapters
|
|
16
|
+
* implement `_session/async_task/stop`; this is NOT a native child-session cancel declaration.
|
|
17
|
+
* Keep this separate even while the certified provider sets happen to coincide.
|
|
18
|
+
*/
|
|
19
|
+
export function supportsAsyncTaskStop(agentName) {
|
|
20
|
+
return agentName === CLAUDE_AGENT || agentName === CODEX_AGENT;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* claude-agent-acp 的 `emitRawSDKMessages`(`acp-agent.d.ts:645`,0.75.1 起存在):
|
|
24
|
+
* 命中过滤器的原始 SDK 消息会以 `extNotification("_claude/sdkMessage", …)` 推回来。
|
|
25
|
+
* 只要状态那一种——过滤器是 `SDKMessageFilter { type, subtype? }`(`:609`)。
|
|
26
|
+
*/
|
|
27
|
+
export function newSessionMetaFor(agentName) {
|
|
28
|
+
if (agentName !== CLAUDE_AGENT)
|
|
29
|
+
return undefined;
|
|
30
|
+
return { claudeCode: { emitRawSDKMessages: [{ type: 'system', subtype: 'session_state_changed' }] } };
|
|
31
|
+
}
|
|
32
|
+
function asObject(value) {
|
|
33
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
34
|
+
? value
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
/** claude:把 _claude/sdkMessage 的 params 提成 vendor 原文,非状态消息返回 null。 */
|
|
38
|
+
export function claudeActivityRaw(params) {
|
|
39
|
+
const message = asObject(params?.message);
|
|
40
|
+
if (!message)
|
|
41
|
+
return null;
|
|
42
|
+
if (message.type !== 'system' || message.subtype !== 'session_state_changed')
|
|
43
|
+
return null;
|
|
44
|
+
// 整条消息原样带走,不挑字段:服务端要的是证据,而将来 SDK 给这条消息加字段时
|
|
45
|
+
// 这里不该是需要改的地方。
|
|
46
|
+
return message;
|
|
47
|
+
}
|
|
48
|
+
/** codex:把 session_info_update 的 _meta 提成 vendor 原文,没有状态返回 null。 */
|
|
49
|
+
export function codexActivityRaw(meta) {
|
|
50
|
+
const codex = asObject(meta?.codex);
|
|
51
|
+
if (!codex)
|
|
52
|
+
return null;
|
|
53
|
+
return asObject(codex.threadStatus);
|
|
54
|
+
}
|