@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
|
@@ -35,20 +35,29 @@
|
|
|
35
35
|
*
|
|
36
36
|
* `elicitation/create`'s `mode` union is **open** (`"form" | "url" | string`, per
|
|
37
37
|
* `CreateElicitationRequest`'s schema comment: "Custom or future elicitation mode" — a bare `string`
|
|
38
|
-
* arm, unlike `session/update`'s closed 15-member union). Axom's wire vocabulary
|
|
38
|
+
* arm, unlike `session/update`'s closed 15-member union). Axom's wire vocabulary has the
|
|
39
39
|
* `elicitation_request`/`elicitation` pair (Plan B renamed the platform's own interaction events onto
|
|
40
|
-
* ACP's words)
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* `
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
40
|
+
* ACP's words); both are answerable end to end now, but along different paths. `elicitation_request`
|
|
41
|
+
* (`TimelineEvent.ElicitationRequested`) is what {@link handleFormElicitation} raises for `'form'`
|
|
42
|
+
* mode (this task, Task 18) — `TimelineEventWire.decodeElicitationRequested` rebuilds it, lands a
|
|
43
|
+
* `WorkItemKind.USER_INTERACTION` item, parks the run, and the user's answer travels back over its own
|
|
44
|
+
* frame, `AgentFrame.AgentElicitationAnswer` (Task 17), through `AcpSession.answerElicitation`. Bare
|
|
45
|
+
* `elicitation` (`UserInteractionRequested`) is the *platform's own* agents asking a question — a
|
|
46
|
+
* named decode branch since `TimelineEventWire.kt:127` — and answers through `ExternalAgentSessionRuntime
|
|
47
|
+
* .execute` forwarding the reply as a normal `channel.prompt(runId, context.userText, ...)`; a bridged
|
|
48
|
+
* agent's `elicitation/create` never produces this one.
|
|
49
|
+
*
|
|
50
|
+
* `'form'`'s wire capability (`acpClient.ts`'s `clientCapabilities.elicitation.form`) is what makes the
|
|
51
|
+
* whole thing reachable in the first place: `claude-agent-acp` puts `AskUserQuestion` into
|
|
52
|
+
* `disallowedTools` (`dist/acp-agent.js:5959`) unless it is declared, so before that line existed the
|
|
53
|
+
* model had no tool to call `elicitation/create` from at all. {@link handleFormElicitation} answers it
|
|
54
|
+
* following {@link handleRequestPermission}'s own shape — see that function's doc comment for the
|
|
55
|
+
* four properties carried over deliberately, and {@link handleFormElicitation}'s own doc comment for
|
|
56
|
+
* the one place the two diverge (a schema outside Axom's profile still raises the card before
|
|
57
|
+
* declining, rather than being refused in silence the way an empty form is). The open `default` arm
|
|
58
|
+
* still declines everything the SDK or a nonstandard agent sends outside `'form'`/`'url'`, for the
|
|
59
|
+
* same reason it always has: it has nowhere to go, and declining is what keeps it defensible — it
|
|
60
|
+
* never hangs the agent and it never fabricates a user's consent for input nobody was ever shown.
|
|
52
61
|
*
|
|
53
62
|
* `'url'` is different: it is the shape some agents use to hand a sign-in link to the reader (Codex's
|
|
54
63
|
* device-code flow reaches it through `elicitation/create` once `elicitation.url` is declared in
|
|
@@ -113,10 +122,9 @@ function toolCallPayload(toolCall) {
|
|
|
113
122
|
* ACP `RequestPermissionRequest` (`types.gen.d.ts:108`) on Axom's wire: `{toolCall, options}` plus
|
|
114
123
|
* `requestId`.
|
|
115
124
|
*
|
|
116
|
-
* `requestId` is
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* already being delivered on a specific run.
|
|
125
|
+
* `requestId` is a host-derived resolver token — {@link permissionRequestId}, the same on the live
|
|
126
|
+
* path and in the ACP processor. The raw JSON-RPC ID stays local to its connection; exact `sessionId`
|
|
127
|
+
* travels in the wire event's `acpSessionId` origin field.
|
|
120
128
|
*
|
|
121
129
|
* **`toolCall` is nested, not flattened.** The flattened version lifted the tool's
|
|
122
130
|
* `name`/`title`/`kind`/`arguments` to the top level, which put `name` (the tool's programmatic name,
|
|
@@ -127,9 +135,10 @@ function toolCallPayload(toolCall) {
|
|
|
127
135
|
* `toolCall.title`, and the renderer computes it from the same field now
|
|
128
136
|
* (`WorkTimelineInteractions.foldPermission`) instead of storing a second, drifting copy.
|
|
129
137
|
*/
|
|
130
|
-
function buildPermissionRequestedEvent(input) {
|
|
138
|
+
export function buildPermissionRequestedEvent(input) {
|
|
131
139
|
return {
|
|
132
140
|
eventName: 'permission_request',
|
|
141
|
+
...(input.sessionId !== undefined ? { acpSessionId: input.sessionId } : {}),
|
|
133
142
|
payload: {
|
|
134
143
|
requestId: input.requestId,
|
|
135
144
|
toolCall: toolCallPayload(input.toolCall),
|
|
@@ -155,7 +164,7 @@ function buildPermissionRequestedEvent(input) {
|
|
|
155
164
|
* `sink.answer` — the agent gave up on this specific request, so nothing should still expect an
|
|
156
165
|
* answer for it. Session-level abandonment (the session closing, or the connection dying) is instead
|
|
157
166
|
* `AcpSession`'s own job (`registerPendingApproval` resolving immediately when already closed, and
|
|
158
|
-
* `runReadLoop` draining on an aborted connection) — see `acpSession.ts
|
|
167
|
+
* `runReadLoop` draining on an aborted connection) — see the old `acpSession.ts` (deleted in Task 29).
|
|
159
168
|
*
|
|
160
169
|
* The eventual answer is whatever string reaches `sink`'s registered resolver: normally one of
|
|
161
170
|
* `input.options`' own `optionId`s (relayed from the user through `AcpSession.answer`), but both
|
|
@@ -194,14 +203,17 @@ export async function handleRequestPermission(input, sink) {
|
|
|
194
203
|
};
|
|
195
204
|
sink.registerPendingApproval(input.requestId, (outcome) => {
|
|
196
205
|
settle(optionIds.has(outcome) ? { outcome: 'answered', optionId: outcome } : { outcome: 'rejected' });
|
|
197
|
-
});
|
|
206
|
+
}, input.sessionId === undefined ? undefined : { originNativeSessionId: input.sessionId, rpcRequestId: input.rpcRequestId ?? input.requestId });
|
|
207
|
+
if (settled)
|
|
208
|
+
return;
|
|
198
209
|
input.signal?.addEventListener('abort', () => {
|
|
199
210
|
settle({ outcome: 'rejected' });
|
|
200
211
|
// Purge the now-moot pending entry so a stray later answer doesn't linger against a request
|
|
201
212
|
// the agent already gave up on. The `settle` above has already fixed this promise's outcome
|
|
202
213
|
// (the `settled` guard makes the resolver this triggers a no-op), so the value passed here
|
|
203
214
|
// only has to avoid accidentally matching a real option id.
|
|
204
|
-
sink.
|
|
215
|
+
const cancel = sink.cancelPendingApproval?.bind(sink) ?? sink.answer.bind(sink);
|
|
216
|
+
cancel(input.requestId, unmatchableAnswerFor(input.options));
|
|
205
217
|
}, { once: true });
|
|
206
218
|
sink.emitEvent(event);
|
|
207
219
|
});
|
|
@@ -211,26 +223,35 @@ function declineElicitation() {
|
|
|
211
223
|
return { action: 'decline' };
|
|
212
224
|
}
|
|
213
225
|
/**
|
|
214
|
-
* Answer `elicitation/create`. `'url'` accepts and hands the URL out
|
|
215
|
-
* when both hold**: the request itself actually carries a string
|
|
216
|
-
* nonconforming agent that omits either gets declined, never
|
|
217
|
-
* real `
|
|
218
|
-
*
|
|
219
|
-
*
|
|
226
|
+
* Answer `elicitation/create`, dispatching on `request.mode`. `'url'` accepts and hands the URL out
|
|
227
|
+
* through `ctx.present` **only when both hold**: the request itself actually carries a string
|
|
228
|
+
* `elicitationId`/`url` (a nonconforming agent that omits either gets declined, never
|
|
229
|
+
* `present(undefined, undefined)`), and a real `ctx.present` is wired (see
|
|
230
|
+
* {@link PresentElicitationUrl}'s doc comment — its absence means declining, not accepting into a
|
|
231
|
+
* void). Accepting here does **not** mean the user finished anything; it means "the URL has been put
|
|
220
232
|
* in front of them", matching ACP's own `accept` semantics for this mode — which is exactly why both
|
|
221
|
-
* guards exist: an `accept` this module cannot back with a shown URL would make that claim false.
|
|
233
|
+
* guards exist: an `accept` this module cannot back with a shown URL would make that claim false. This
|
|
234
|
+
* branch, and this doc paragraph, are unchanged by Task 18 — only `'form'` moved.
|
|
235
|
+
*
|
|
236
|
+
* `'form'` is answered by {@link handleFormElicitation} — see its own doc comment, which is also
|
|
237
|
+
* where the four properties it deliberately carries over from {@link handleRequestPermission} are
|
|
238
|
+
* listed, and the one place it diverges.
|
|
222
239
|
*
|
|
223
240
|
* The `switch` has a real `default`, not just `'form'`/`'url'` cases, because `CreateElicitationRequest['mode']`
|
|
224
241
|
* is `"form" | "url" | string` — an **open** union (unlike `session/update`'s closed 15-member one).
|
|
225
242
|
* Falling through with no default would mean the client's response is `undefined` for any mode the SDK
|
|
226
243
|
* or a nonstandard agent invents next, sending a malformed JSON-RPC result the agent may wait on
|
|
227
244
|
* forever instead of a clean, immediate decline.
|
|
245
|
+
*
|
|
246
|
+
* `sink` is threaded through every mode uniformly (`registerPermissionHandlers` always has one, one
|
|
247
|
+
* per connection — see `PermissionSink`'s own doc comment) even though only `'form'` reads it; passing
|
|
248
|
+
* it unconditionally here is simpler than a second entry point that exists solely to omit an unused
|
|
249
|
+
* parameter, and every arm below is still free to ignore it.
|
|
228
250
|
*/
|
|
229
|
-
export function handleCreateElicitation(request,
|
|
251
|
+
export async function handleCreateElicitation(request, ctx, sink) {
|
|
230
252
|
switch (request.mode) {
|
|
231
253
|
case 'form':
|
|
232
|
-
|
|
233
|
-
return declineElicitation();
|
|
254
|
+
return handleFormElicitation(request, ctx, sink);
|
|
234
255
|
case 'url': {
|
|
235
256
|
const { elicitationId, url } = request;
|
|
236
257
|
if (typeof elicitationId !== 'string' || typeof url !== 'string') {
|
|
@@ -239,13 +260,13 @@ export function handleCreateElicitation(request, deps) {
|
|
|
239
260
|
// path. The elicitation still gets answered (declined), never left to hang.
|
|
240
261
|
return declineElicitation();
|
|
241
262
|
}
|
|
242
|
-
if (!
|
|
263
|
+
if (!ctx.present) {
|
|
243
264
|
// Nobody to show it to yet — see `PresentElicitationUrl`'s doc comment. Declining here is
|
|
244
265
|
// what an agent (or an MCP server proxied through one) raising this mid-turn can actually
|
|
245
266
|
// act on, unlike an `accept` that then goes nowhere.
|
|
246
267
|
return declineElicitation();
|
|
247
268
|
}
|
|
248
|
-
|
|
269
|
+
ctx.present(elicitationId, url);
|
|
249
270
|
return { action: 'accept' };
|
|
250
271
|
}
|
|
251
272
|
default:
|
|
@@ -255,6 +276,241 @@ export function handleCreateElicitation(request, deps) {
|
|
|
255
276
|
return declineElicitation();
|
|
256
277
|
}
|
|
257
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Is `key` a free-text companion the server will *fold away* (`<key>_custom`,
|
|
281
|
+
* `ElicitationRequest.CUSTOM_SUFFIX` on the Kotlin side, `claude-agent-acp`'s own
|
|
282
|
+
* `question_<n>_custom` shape) rather than something the reader gets a row for? A folded companion
|
|
283
|
+
* must not be counted when deciding whether a schema has anything to draw at all.
|
|
284
|
+
*
|
|
285
|
+
* **The suffix alone is not the rule — the owner has to exist.** This used to return `true` for any
|
|
286
|
+
* `_custom`-suffixed key, which matched a server that *dropped* an ownerless one. Task 9 (ruling
|
|
287
|
+
* R15) changed that: `elicitationRequestFromPayload` now lands an ownerless `_custom` key as a
|
|
288
|
+
* read-only `UNPRESENTABLE` entry instead of discarding it, so a schema made *entirely* of such keys
|
|
289
|
+
* decodes into a real (if all-read-only) card. Measured against the real decoder: `{"foo_custom":
|
|
290
|
+
* {"type":"string"}}` alone → `fields=[foo_custom:UNPRESENTABLE]`, not `null`. With the old
|
|
291
|
+
* suffix-only rule this function returned `[]` for that schema and {@link handleFormElicitation}'s
|
|
292
|
+
* empty-form guard declined it **without ever raising a card** — the card silently never appears and
|
|
293
|
+
* the agent gets a decline it did not earn, which is the "too narrow" half of the drift R24 is about.
|
|
294
|
+
*
|
|
295
|
+
* Checking that the owner is *also a property* is deliberately weaker than the server's own rule
|
|
296
|
+
* (which additionally requires the owner to be a single-/multi-select, ruling R13), and the weakness
|
|
297
|
+
* only ever runs in the safe direction: it can under-count fields (a `_custom` key whose owner is a
|
|
298
|
+
* scalar is folded here but is a read-only row server-side) and never over-count. Under-counting
|
|
299
|
+
* cannot wrongly reach zero — for it to fold a key away, the owner has to be a property, and that
|
|
300
|
+
* owner is itself counted. Zero here therefore still means zero server-side, which is the only thing
|
|
301
|
+
* the empty-form guard reads.
|
|
302
|
+
*/
|
|
303
|
+
function isFoldedCompanionKey(key, properties) {
|
|
304
|
+
if (!key.endsWith('_custom'))
|
|
305
|
+
return false;
|
|
306
|
+
const owner = key.slice(0, -'_custom'.length);
|
|
307
|
+
return owner.length > 0 && Object.prototype.hasOwnProperty.call(properties, owner);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The property keys of a `'form'` request's `requestedSchema.properties` that the reader will
|
|
311
|
+
* actually get a row for — `[]` for anything that isn't a legible `{properties: {...}}` object,
|
|
312
|
+
* including a missing `requestedSchema` altogether. This alone decides
|
|
313
|
+
* {@link handleFormElicitation}'s empty-form guard: zero keys here is the same "nothing legible
|
|
314
|
+
* could ever be raised" shape as `handleRequestPermission`'s `options.length === 0`, regardless of
|
|
315
|
+
* *why* there are zero (no schema, an empty `properties`, or a `properties` that is entirely folded
|
|
316
|
+
* companions).
|
|
317
|
+
*
|
|
318
|
+
* All three of those are the shapes the server-side decoder still drops outright — measured, not
|
|
319
|
+
* inferred: `{"properties":{}}`, `properties` that isn't an object, and a missing `requestedSchema`
|
|
320
|
+
* each return `null` from `elicitationRequestFromPayload`, because `MODE_FORM` requires
|
|
321
|
+
* `fields.isNotEmpty()`. Everything else it now decodes (see {@link handleFormElicitation}).
|
|
322
|
+
*/
|
|
323
|
+
export function elicitationFieldKeys(requestedSchema) {
|
|
324
|
+
const properties = requestedSchema?.properties;
|
|
325
|
+
if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
|
|
326
|
+
return [];
|
|
327
|
+
const record = properties;
|
|
328
|
+
return Object.keys(record).filter((key) => !isFoldedCompanionKey(key, record));
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Kotlin's `String.isBlank()`, restated: empty, or entirely whitespace. `validate.ts`'s
|
|
332
|
+
* `nonEmptyString` (`.length > 0`) does **not** catch `" "` — `ElicitationRequest.init`'s
|
|
333
|
+
* `require(message.isNotBlank())` does, and so must this: a blank
|
|
334
|
+
* `message` throws inside `elicitationRequestFromPayload`'s `runCatching`. It is a **request-level**
|
|
335
|
+
* invariant, so unlike a malformed property it has no read-only fallback to degrade into — the whole
|
|
336
|
+
* request is dropped server-side, which is why this check survived Task 9's removal of the
|
|
337
|
+
* per-property profile predicate (see the ruling-R24 block below).
|
|
338
|
+
*/
|
|
339
|
+
export function isBlank(value) {
|
|
340
|
+
return value.trim().length === 0;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* **Ruling R24, restated after Task 9 (D5′) — the predicate this block used to hold is gone because
|
|
344
|
+
* the server-side gap it predicted is gone.**
|
|
345
|
+
*
|
|
346
|
+
* There used to be an `isAxomProfileProperty`/`isAxomProfileSchema` pair here, a hand-written mirror
|
|
347
|
+
* of every `error()`/`require()` on `elicitationRequestFromPayload`'s answerable path. It existed
|
|
348
|
+
* because **one** malformed property took down the **whole** `elicitation_request` server-side: the
|
|
349
|
+
* throw escaped to that function's outer `runCatching`, the event was dropped, and the agent blocked
|
|
350
|
+
* on `elicitation/create` forever with nothing on screen to explain it. Declining before sending was
|
|
351
|
+
* the lesser evil — the agent at least unblocked — but it bought that by making the card never
|
|
352
|
+
* appear, which is the *other* silent failure.
|
|
353
|
+
*
|
|
354
|
+
* Task 9 removed the cause. Every one of those shapes now degrades to a single read-only
|
|
355
|
+
* `UNPRESENTABLE` entry inside an otherwise normal card (`ElicitationField`'s new kind; the
|
|
356
|
+
* per-property `runCatching` in `elicitationRequestFromPayload`), so there is nothing left at the
|
|
357
|
+
* *property* level for this client to predict. **Measured against the real Kotlin decoder, not read
|
|
358
|
+
* off the parser** — every payload `permission.test.ts` exercises was run through
|
|
359
|
+
* `elicitationRequestFromPayload` and only these still return `null`, all of them request-level and
|
|
360
|
+
* all of them already covered by the two guards {@link handleFormElicitation} keeps:
|
|
361
|
+
*
|
|
362
|
+
* | shape | verdict | guard that still catches it |
|
|
363
|
+
* |---|---|---|
|
|
364
|
+
* | **whitespace-only** `message` (`" "`) | `null` (`ElicitationRequest.init`) | {@link isBlank}, emit-then-decline |
|
|
365
|
+
* | **empty** `message` (`""`) | `null`, same requirement | `validateWireEvent` rejects the event first → decline, **no card** |
|
|
366
|
+
* | `properties: {}` | `null` (`MODE_FORM` needs `fields.isNotEmpty()`) | {@link elicitationFieldKeys} → `[]` |
|
|
367
|
+
* | `properties` not an object | `null`, same requirement | {@link elicitationFieldKeys} → `[]` |
|
|
368
|
+
* | `requestedSchema` absent | `null`, same requirement | {@link elicitationFieldKeys} → `[]` |
|
|
369
|
+
*
|
|
370
|
+
* Final review F7 split that first row in two, because merging them wrote the verdict backwards for
|
|
371
|
+
* half of it: `""` never reaches emit-then-decline at all. {@link validateWireEvent} runs first
|
|
372
|
+
* (`decodeElicitationRequested`'s `nonEmptyString`, `validate.ts:164`) and rejects the event, so the
|
|
373
|
+
* reader gets **no card** — precisely the outcome emit-then-decline exists to avoid. Only
|
|
374
|
+
* *whitespace* makes it past that gate and into the path this table describes; the {@link isBlank}
|
|
375
|
+
* KDoc had the reason right all along (`nonEmptyString` does not see `" "`), it was this row that
|
|
376
|
+
* folded the two cases into one sentence.
|
|
377
|
+
*
|
|
378
|
+
* Everything else decodes, including every shape this file used to decline: duplicate `const`s, a
|
|
379
|
+
* blank option `title`, a candidate-less `type: "array"`, an unknown or `_`-prefixed `type`, a
|
|
380
|
+
* scalar carrying candidates or a malformed `oneOf`/`items`/`anyOf`, a duplicate untitled `enum`
|
|
381
|
+
* value, a property that isn't an object at all, a blank property key, and an ownerless `_custom`
|
|
382
|
+
* key. Each becomes a visible read-only row; the reader can answer the rest of the card and submit,
|
|
383
|
+
* or press Skip (ruling D4/R19) to send `{action: 'decline'}` themselves.
|
|
384
|
+
*
|
|
385
|
+
* **The rule itself has not changed, only the answer it currently produces.** It is keyed on server
|
|
386
|
+
* behaviour: whenever whatever decodes a `CreateElicitationRequest` server-side starts *rejecting* a
|
|
387
|
+
* shape this client forwards, a predicate has to come back here in the same commit — otherwise the
|
|
388
|
+
* agent parks on `elicitation/create` forever waiting for an answer that cannot be produced. And if
|
|
389
|
+
* a reintroduced predicate is ever narrower than the decoder, the card silently never appears and
|
|
390
|
+
* the agent gets a decline it did not earn. Both directions have shipped bugs (Task 8, Task 8.5);
|
|
391
|
+
* measure them against the real decoder rather than against a reading of this file.
|
|
392
|
+
*/
|
|
393
|
+
/**
|
|
394
|
+
* ACP `CreateElicitationRequest`'s `'form'` arm folded onto Axom's `elicitation_request` wire shape —
|
|
395
|
+
* the mirror of `buildPermissionRequestedEvent` above, and the payload
|
|
396
|
+
* `TimelineEventWire.decodeElicitationRequested`/`elicitationRequestFromPayload` expect.
|
|
397
|
+
*
|
|
398
|
+
* `requestId` is `ctx.requestId` — the JSON-RPC envelope id — not ACP's own optional
|
|
399
|
+
* `ElicitationRequestScope.requestId` field on `request`, for the identical reason
|
|
400
|
+
* `buildPermissionRequestedEvent` reads its `requestId` off `PermissionRequestInput` rather than the
|
|
401
|
+
* request body: ACP correlates the eventual answer through the envelope, and an event stream has none.
|
|
402
|
+
*
|
|
403
|
+
* `requestedSchema` is forwarded **verbatim** — Global Constraint 3, "transport, not translation" (see
|
|
404
|
+
* `toolCallPayload`'s doc comment for the same rule applied to `ToolCallUpdate`): this function does
|
|
405
|
+
* not narrow a field to fit Axom's profile, and it does not strip a field outside it either. The wire
|
|
406
|
+
* event always carries exactly what the agent asked; {@link handleFormElicitation} is what decides,
|
|
407
|
+
* from that same schema, whether to wait for an answer or decline right after raising the card.
|
|
408
|
+
*/
|
|
409
|
+
export function buildElicitationRequestedEvent(request, requestId) {
|
|
410
|
+
return {
|
|
411
|
+
eventName: 'elicitation_request',
|
|
412
|
+
payload: {
|
|
413
|
+
requestId,
|
|
414
|
+
...(typeof request.toolCallId === 'string' ? { toolCallId: request.toolCallId } : {}),
|
|
415
|
+
request: {
|
|
416
|
+
mode: request.mode,
|
|
417
|
+
message: request.message,
|
|
418
|
+
requestedSchema: request.requestedSchema,
|
|
419
|
+
},
|
|
420
|
+
answer: null,
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* `'form'`-mode `elicitation/create` — the counterpart to {@link handleRequestPermission} this
|
|
426
|
+
* module's class doc comment promises. Four properties carried over deliberately:
|
|
427
|
+
*
|
|
428
|
+
* 1. **Every path settles.** The `Promise` below always resolves — including the two guard clauses
|
|
429
|
+
* above it, both still inside this `async function` — because the agent is blocked on
|
|
430
|
+
* `session/prompt` until it does.
|
|
431
|
+
* 2. **An empty form is refused before it is raised**, mirroring `handleRequestPermission`'s
|
|
432
|
+
* `options.length === 0` hard refusal: a `requestedSchema` with zero answerable fields (missing
|
|
433
|
+
* entirely, `properties: {}`, or `properties` that is entirely `_custom` companions — see
|
|
434
|
+
* {@link elicitationFieldKeys}) is a card nobody could ever answer, which is worse than no card.
|
|
435
|
+
* 3. **`ctx.signal` aborting settles `{action: 'cancel'}` and purges the pending entry** — ACP's own
|
|
436
|
+
* real cancellation outcome (see `AcpSession`'s `ABANDONED_ELICITATION_ANSWER` doc comment for why
|
|
437
|
+
* this needs no randomized sentinel the way `handleRequestPermission`'s purge value does), so a
|
|
438
|
+
* card nobody is listening to answer does not linger in `AcpSession`'s pending map.
|
|
439
|
+
* 4. **The open `default` arm stays reachable** — one level up, in {@link handleCreateElicitation}'s
|
|
440
|
+
* own `switch`, not repeated here.
|
|
441
|
+
*
|
|
442
|
+
* **Where this diverges from the template**, deliberately: a blank `message` ({@link isBlank}) still
|
|
443
|
+
* **raises the card** before declining — the question is legible, `ElicitationRequest.init` just
|
|
444
|
+
* rejects the message server-side. It declines only *after* raising, instead of the guard-clause
|
|
445
|
+
* silence `handleRequestPermission` uses for its one hard-refusal case. Dropping the event here
|
|
446
|
+
* instead would leave the user seeing neither the question the agent asked nor any reason nothing
|
|
447
|
+
* happened (spec §12 D5′). This is the *opposite* of the empty-form case above, which has nothing
|
|
448
|
+
* legible to draw at all — raising a card for it would show the user a form with no fields on it,
|
|
449
|
+
* not a degraded but real question.
|
|
450
|
+
*
|
|
451
|
+
* **A schema with fields Axom cannot render no longer takes this exit** (Task 9, ruling R24): those
|
|
452
|
+
* decode into read-only rows server-side, so this handler waits for a real answer — which the reader
|
|
453
|
+
* supplies either by answering the rest of the card or by pressing Skip (`{action: 'decline'}`,
|
|
454
|
+
* ruling D4). See the ruling-R24 block above for the measured list of what still fails to decode.
|
|
455
|
+
*/
|
|
456
|
+
async function handleFormElicitation(request, ctx, sink) {
|
|
457
|
+
const requestId = ctx.requestId;
|
|
458
|
+
if (!sink || !requestId) {
|
|
459
|
+
// Defensive only: `registerPermissionHandlers` — the one real caller — always supplies both,
|
|
460
|
+
// reading `requestId` off the JSON-RPC envelope. Nothing to raise a card through or correlate an
|
|
461
|
+
// answer with, so there is nothing left to do but decline, same as the empty-form case below.
|
|
462
|
+
return declineElicitation();
|
|
463
|
+
}
|
|
464
|
+
if (ctx.signal?.aborted) {
|
|
465
|
+
// Mirrors `handleRequestPermission`'s identical early check: a listener added *after* `abort()`
|
|
466
|
+
// already fired never sees it (`AbortSignal` does not replay past events), so without this guard
|
|
467
|
+
// the mid-flight listener below would never fire and this would hang forever instead of settling.
|
|
468
|
+
return { action: 'cancel' };
|
|
469
|
+
}
|
|
470
|
+
const fieldKeys = elicitationFieldKeys(request.requestedSchema);
|
|
471
|
+
if (fieldKeys.length === 0) {
|
|
472
|
+
return declineElicitation();
|
|
473
|
+
}
|
|
474
|
+
const event = buildElicitationRequestedEvent(request, requestId);
|
|
475
|
+
const rejection = validateWireEvent(event);
|
|
476
|
+
if (rejection) {
|
|
477
|
+
// Defensive only, same reasoning as `handleRequestPermission`'s identical check: every field here
|
|
478
|
+
// is derived from ACP's own typed request, so a conforming agent can never actually trip this.
|
|
479
|
+
console.error(`[acp] refusing to raise an elicitation card that fails validateWireEvent: ${rejection.reason}`);
|
|
480
|
+
return declineElicitation();
|
|
481
|
+
}
|
|
482
|
+
if (isBlank(request.message)) {
|
|
483
|
+
// Legible, just not collectible here — raise it, then decline. See this function's own doc
|
|
484
|
+
// comment for why this is not the same as the empty-form guard above. A blank `message` is the
|
|
485
|
+
// **only** reason left for this branch: `ElicitationRequest.init`'s `require(message.isNotBlank())`
|
|
486
|
+
// is a request-level invariant, so the throw takes the whole request down with no read-only
|
|
487
|
+
// fallback to catch it, and `validate.ts`'s own `nonEmptyString` (`.length > 0`) does not see
|
|
488
|
+
// `" "`. Every *property*-level reason that used to route here now decodes into a read-only row
|
|
489
|
+
// instead — see the ruling-R24 block above for the measured table.
|
|
490
|
+
sink.emitEvent(event);
|
|
491
|
+
return declineElicitation();
|
|
492
|
+
}
|
|
493
|
+
return new Promise((resolve) => {
|
|
494
|
+
let settled = false;
|
|
495
|
+
const settle = (response) => {
|
|
496
|
+
if (settled)
|
|
497
|
+
return;
|
|
498
|
+
settled = true;
|
|
499
|
+
resolve(response);
|
|
500
|
+
};
|
|
501
|
+
sink.registerPendingElicitation(requestId, settle);
|
|
502
|
+
ctx.signal?.addEventListener('abort', () => {
|
|
503
|
+
settle({ action: 'cancel' });
|
|
504
|
+
// Purge the now-moot pending entry, exactly like `handleRequestPermission`'s own purge call —
|
|
505
|
+
// the `settled` guard above already makes a later real answer a no-op for this promise, but
|
|
506
|
+
// without this the entry would linger in `AcpSession.pendingElicitations` forever. `'cancel'`
|
|
507
|
+
// is ACP's own real cancellation outcome, so — unlike `handleRequestPermission`'s
|
|
508
|
+
// `unmatchableAnswerFor` — there is no collision to guard against here.
|
|
509
|
+
sink.answerElicitation(requestId, { action: 'cancel' });
|
|
510
|
+
}, { once: true });
|
|
511
|
+
sink.emitEvent(event);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
258
514
|
/**
|
|
259
515
|
* `elicitation/complete` fires only for an elicitation the client previously accepted — now the `'url'`
|
|
260
516
|
* mode, once the agent considers that round trip finished. This turns it into a `dismiss` call so
|
|
@@ -265,6 +521,35 @@ export function handleCreateElicitation(request, deps) {
|
|
|
265
521
|
export function handleElicitationComplete(notification, deps) {
|
|
266
522
|
deps.dismiss(notification.elicitationId);
|
|
267
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* The card id of one `session/request_permission`: `perm:<hostGeneration>:<JSON agent id>` (spec
|
|
526
|
+
* 2026-10-11 §5.4).
|
|
527
|
+
*
|
|
528
|
+
* ## Why derived, not random
|
|
529
|
+
*
|
|
530
|
+
* It used to be `randomUUID()` per invocation. A card id the server is already showing must still
|
|
531
|
+
* resolve after the daemon restarts underneath a live adapter, and a random id cannot: the re-presented
|
|
532
|
+
* request would get a new one, `settleApproval` ignores unknown ids, and the user's answer to the
|
|
533
|
+
* visible card would be lost (analysis A §4). Derived from the request itself, the same request gets
|
|
534
|
+
* the same id in every daemon generation, and the ACP processor — which may not draw randomness at
|
|
535
|
+
* all (`processor/guard.test.ts`) — computes it with this very function.
|
|
536
|
+
*
|
|
537
|
+
* ## Why it is still unique
|
|
538
|
+
*
|
|
539
|
+
* - `hostGeneration` identifies one adapter process: the keeper's `spawnId` on the processor path,
|
|
540
|
+
* `AgentRuntimeDiagnostics.generation` (fresh per `connectAgent`) on today's live path — the same
|
|
541
|
+
* value published as `callbackContext.hostGeneration`. A reconnect is a new process, so a new
|
|
542
|
+
* generation: the agent's ids restart at 0 there without colliding with an earlier card.
|
|
543
|
+
* - Within one process the agent never reuses a pending JSON-RPC id. `JSON.stringify` keeps the
|
|
544
|
+
* number `1` and the string `"1"` apart.
|
|
545
|
+
*
|
|
546
|
+
* Kept short enough for `AcpSession.registerPendingApproval`'s 256-byte rule for every id a real
|
|
547
|
+
* adapter sends; a pathological id longer than ~200 bytes is refused there, as an overlong
|
|
548
|
+
* `rpcRequestId` already was.
|
|
549
|
+
*/
|
|
550
|
+
export function permissionRequestId(hostGeneration, agentRequestId) {
|
|
551
|
+
return `perm:${hostGeneration}:${JSON.stringify(agentRequestId)}`;
|
|
552
|
+
}
|
|
268
553
|
/**
|
|
269
554
|
* Register this module's three handlers onto `app`. Must run inside `connectAgent`'s `configure`
|
|
270
555
|
* hook — see the file doc comment for why a post-connect `.onRequest`/`.onNotification` would compile
|
|
@@ -278,16 +563,23 @@ export function handleElicitationComplete(notification, deps) {
|
|
|
278
563
|
* the call site instead. Its own `present` may still be omitted (see {@link PresentElicitationUrl}) —
|
|
279
564
|
* that is `acpAgents.ts`'s session connection saying, on purpose, that a `url` elicitation raised
|
|
280
565
|
* outside a login has nowhere to go, and it declines rather than accepting into nothing.
|
|
566
|
+
*
|
|
567
|
+
* `connection.generation` is the adapter process's generation — `connectAgent` hands it to its
|
|
568
|
+
* `configure` hook — and scopes the card ids {@link permissionRequestId} derives.
|
|
281
569
|
*/
|
|
282
|
-
export function registerPermissionHandlers(app, sink, elicitationDeps) {
|
|
570
|
+
export function registerPermissionHandlers(app, sink, elicitationDeps, connection) {
|
|
283
571
|
app
|
|
284
572
|
.onRequest(methods.client.session.requestPermission, async (ctx) => {
|
|
285
|
-
|
|
573
|
+
if (typeof ctx.params.sessionId !== 'string' || !ctx.params.sessionId.trim()) {
|
|
574
|
+
console.error(JSON.stringify({ component: 'acp', error: 'invalid_permission_origin', rpcRequestId: String(ctx.requestId) }));
|
|
575
|
+
return { outcome: { outcome: 'cancelled' } };
|
|
576
|
+
}
|
|
577
|
+
const decision = await handleRequestPermission({ requestId: permissionRequestId(connection.generation, ctx.requestId), rpcRequestId: String(ctx.requestId), sessionId: ctx.params.sessionId, toolCall: ctx.params.toolCall, options: ctx.params.options, signal: ctx.signal }, sink);
|
|
286
578
|
return {
|
|
287
579
|
outcome: decision.outcome === 'answered' ? { outcome: 'selected', optionId: decision.optionId } : { outcome: 'cancelled' },
|
|
288
580
|
};
|
|
289
581
|
})
|
|
290
|
-
.onRequest(methods.client.elicitation.create, (ctx) => handleCreateElicitation(ctx.params, elicitationDeps))
|
|
582
|
+
.onRequest(methods.client.elicitation.create, (ctx) => handleCreateElicitation(ctx.params, { present: elicitationDeps.present, requestId: String(ctx.requestId), signal: ctx.signal }, sink))
|
|
291
583
|
.onNotification(methods.client.elicitation.complete, (ctx) => handleElicitationComplete(ctx.params, elicitationDeps));
|
|
292
584
|
}
|
|
293
585
|
/**
|
|
@@ -309,8 +601,14 @@ export function createDeferredPermissionSink() {
|
|
|
309
601
|
return {
|
|
310
602
|
sink: {
|
|
311
603
|
emitEvent: (event) => requireBound().emitEvent(event),
|
|
312
|
-
registerPendingApproval: (requestId, resolve) => requireBound().registerPendingApproval(requestId, resolve),
|
|
604
|
+
registerPendingApproval: (requestId, resolve, address) => requireBound().registerPendingApproval(requestId, resolve, address),
|
|
605
|
+
cancelPendingApproval: (requestId, outcome) => {
|
|
606
|
+
const target = requireBound();
|
|
607
|
+
(target.cancelPendingApproval?.bind(target) ?? target.answer.bind(target))(requestId, outcome);
|
|
608
|
+
},
|
|
313
609
|
answer: (requestId, outcome) => requireBound().answer(requestId, outcome),
|
|
610
|
+
registerPendingElicitation: (requestId, resolve) => requireBound().registerPendingElicitation(requestId, resolve),
|
|
611
|
+
answerElicitation: (requestId, response) => requireBound().answerElicitation(requestId, response),
|
|
314
612
|
},
|
|
315
613
|
bind(target) {
|
|
316
614
|
// Review finding (task-5, item 4): silently replacing an already-bound sink on a second call
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Processor checkpoints (spec 2026-10-11 §5.5): the state after record `offset`, as JSON.
|
|
3
|
+
*
|
|
4
|
+
* ## Two versions, two questions
|
|
5
|
+
*
|
|
6
|
+
* - `schema` answers "can this daemon **read** it?". An unknown schema throws
|
|
7
|
+
* {@link CheckpointUnreadableError}; the caller then replays from the session's start (outbox and
|
|
8
|
+
* watermark cover the frames already sent), or, if the log was truncated, marks the session
|
|
9
|
+
* degraded and switches it at the next open (§5.5.6). Readability of older schemas is a test-guarded
|
|
10
|
+
* promise: never change the meaning of an existing field without bumping `schema` and adding a
|
|
11
|
+
* reader for the old one.
|
|
12
|
+
* - `processorVersion` answers "would **this** code have produced the same frames?". Bump it whenever
|
|
13
|
+
* `step` can emit different output for the same records. A readable checkpoint from another
|
|
14
|
+
* processor version is still restored — that is the whole point of cutting a head checkpoint at
|
|
15
|
+
* upgrade — but the caller must not count replay-vs-outbox differences as divergence then.
|
|
16
|
+
*
|
|
17
|
+
* Checkpoints are cut only on record boundaries (a synchronous `step` leaves no half-processed record)
|
|
18
|
+
* and carry no secrets (`state.ts`).
|
|
19
|
+
*/
|
|
20
|
+
import { PROCESSOR_STATE_SCHEMA } from './state.js';
|
|
21
|
+
/**
|
|
22
|
+
* Bump when `step`'s output for the same records changes. 2: Task 17a (turns, cards, `seq`; prompt
|
|
23
|
+
* methods refused by `rpc.request`). 3: Task 17b (`session/update` mapped, the open session bound
|
|
24
|
+
* from its response, config/steer/levels; events validated before numbering). 4: commands carry
|
|
25
|
+
* their `spawnId`; `processor.history_end` carries `droppedCount`; a prompt closes open history.
|
|
26
|
+
* 5: Task 17c (the callback snapshot in the runtime diagnostics and its revision bumps; Qoder SDK
|
|
27
|
+
* cards carry a `callbackContext`; the Qoder adapter boundaries; side-channel notes). 6: a process call's
|
|
28
|
+
* result waits for its child's first status; ended/delivered calls keep no dead data.
|
|
29
|
+
* 7: Task 20 — the `agent_install_outcome` note becomes a `step` event (it was an unknown note).
|
|
30
|
+
* 8: Task 21 — a level update after a load's response is folded live, not as history; the `agent_output_gap` note is a `step`.
|
|
31
|
+
*/
|
|
32
|
+
export const PROCESSOR_VERSION = 8;
|
|
33
|
+
export const CHECKPOINT_SCHEMA = 1;
|
|
34
|
+
export class CheckpointUnreadableError extends Error {
|
|
35
|
+
}
|
|
36
|
+
function clone(value) {
|
|
37
|
+
return JSON.parse(JSON.stringify(value));
|
|
38
|
+
}
|
|
39
|
+
/** A detached copy of `state`, so later steps cannot reach into a checkpoint already handed out. */
|
|
40
|
+
export function checkpointOf(state) {
|
|
41
|
+
return { schema: CHECKPOINT_SCHEMA, processorVersion: PROCESSOR_VERSION, offset: state.offset, state: clone(state) };
|
|
42
|
+
}
|
|
43
|
+
export function encodeCheckpoint(checkpoint) {
|
|
44
|
+
return JSON.stringify(checkpoint);
|
|
45
|
+
}
|
|
46
|
+
function isRecord(value) {
|
|
47
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
/** Parse and check a checkpoint blob (string or already-parsed JSON). */
|
|
50
|
+
export function decodeCheckpoint(blob) {
|
|
51
|
+
let value = blob;
|
|
52
|
+
if (typeof blob === 'string') {
|
|
53
|
+
try {
|
|
54
|
+
value = JSON.parse(blob);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new CheckpointUnreadableError('checkpoint is not JSON');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!isRecord(value))
|
|
61
|
+
throw new CheckpointUnreadableError('checkpoint is not an object');
|
|
62
|
+
if (value.schema !== CHECKPOINT_SCHEMA)
|
|
63
|
+
throw new CheckpointUnreadableError(`unknown checkpoint schema ${JSON.stringify(value.schema)}`);
|
|
64
|
+
const { processorVersion, offset, state } = value;
|
|
65
|
+
if (typeof processorVersion !== 'number' || !Number.isInteger(processorVersion))
|
|
66
|
+
throw new CheckpointUnreadableError('checkpoint has no processorVersion');
|
|
67
|
+
if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < -1)
|
|
68
|
+
throw new CheckpointUnreadableError('checkpoint offset is invalid');
|
|
69
|
+
if (!isRecord(state) || state.schema !== PROCESSOR_STATE_SCHEMA)
|
|
70
|
+
throw new CheckpointUnreadableError('checkpoint state schema is unknown');
|
|
71
|
+
if (state.offset !== offset)
|
|
72
|
+
throw new CheckpointUnreadableError('checkpoint offset disagrees with its state');
|
|
73
|
+
for (const key of ['process', 'initialize', 'outbound', 'inbound', 'diagnostics', 'permissions', 'elicitations',
|
|
74
|
+
'permissionOrigins', 'permissionParents', 'unresolvedPermissionOrigins', 'open', 'mapperScopes', 'message', 'steers',
|
|
75
|
+
'asyncTasks', 'runtime', 'callbacks', 'boundaries']) {
|
|
76
|
+
if (!isRecord(state[key]))
|
|
77
|
+
throw new CheckpointUnreadableError(`checkpoint state has no ${key}`);
|
|
78
|
+
}
|
|
79
|
+
if (typeof state.seq !== 'number' || !Array.isArray(state.turns) || !Array.isArray(state.settledPermissions)) {
|
|
80
|
+
throw new CheckpointUnreadableError('checkpoint state has no session layer (seq/turns)');
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(state.steered) || !('configOptions' in state) || !('delegation' in state) || !('history' in state)
|
|
83
|
+
|| !('activity' in state)) {
|
|
84
|
+
throw new CheckpointUnreadableError('checkpoint state has no open-session layer (Task 17b)');
|
|
85
|
+
}
|
|
86
|
+
return { schema: CHECKPOINT_SCHEMA, processorVersion, offset, state: clone(state) };
|
|
87
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The line to write for `command`: `fill`ed from `resolve`, or `undefined` if its blocks are gone. */
|
|
2
|
+
export function materializeLine(command, resolve) {
|
|
3
|
+
if (command.fill === undefined)
|
|
4
|
+
return command.line;
|
|
5
|
+
const blocks = resolve?.(command.fill.blocksRef);
|
|
6
|
+
if (blocks === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
const message = JSON.parse(command.line);
|
|
9
|
+
// Assigning an existing key keeps its position: the line keeps the SDK's key order.
|
|
10
|
+
message.params.prompt = blocks;
|
|
11
|
+
return JSON.stringify(message);
|
|
12
|
+
}
|
|
13
|
+
export function executeDownCommands(sink, commands, resolve) {
|
|
14
|
+
const written = [];
|
|
15
|
+
const dropped = [];
|
|
16
|
+
const refused = [];
|
|
17
|
+
const unresolved = [];
|
|
18
|
+
for (const command of commands) {
|
|
19
|
+
if (sink.hasDown(command.cause)) {
|
|
20
|
+
dropped.push(command);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const line = materializeLine(command, resolve);
|
|
24
|
+
if (line === undefined) {
|
|
25
|
+
unresolved.push(command);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const { fill: _fill, ...rest } = command;
|
|
29
|
+
if (sink.writeDown({ ...rest, line }) === 'refused')
|
|
30
|
+
refused.push(command);
|
|
31
|
+
else
|
|
32
|
+
written.push(command);
|
|
33
|
+
}
|
|
34
|
+
return { written, dropped, refused, unresolved };
|
|
35
|
+
}
|