@platless/axom-cli 0.3.13 → 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/dist/axom-patch/cli/README.md +30 -8
- 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-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
- package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
- package/dist/axom-patch/cli/qoder-steering.json +15 -15
- package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
- package/dist/axom-patch/cli/runtime.json +34 -8
- 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 +33 -6
- 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/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 +2163 -611
- package/dist/workstation/acp/acpClient.js +60 -497
- package/dist/workstation/acp/adoption.js +10 -396
- 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/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 +76 -160
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +20 -3
- package/dist/workstation/acp/permission.js +42 -10
- 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 +1 -1
- package/dist/workstation/acp/registry/catalog.js +154 -32
- package/dist/workstation/acp/registry/launch.js +61 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +114 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- 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/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/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1324 -474
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +84 -4
- package/dist/workstation/daemon.js +288 -7
- package/dist/workstation/daemonctl.js +28 -2
- 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/{fileOps.js → files/text.js} +117 -38
- 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/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +48 -4
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +50 -2
- package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
- package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
- package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
- package/dist/workstation/qoderAdapter/history.js +1 -3
- package/dist/workstation/qoderAdapter/main.js +4 -2
- package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
- package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
- package/dist/workstation/qoderAdapter/session.js +1 -1
- package/dist/workstation/qoderAdapter/subwork.js +15 -7
- package/dist/workstation/systemMetrics.js +110 -1
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/package.json +10 -7
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `initialize` handshake, as data: the params this client sends and what it reads out of the
|
|
3
|
+
* agent's reply.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is its own module
|
|
6
|
+
*
|
|
7
|
+
* Two readers must agree on every byte of this: `connectAgent` (the live SDK path) and the
|
|
8
|
+
* deterministic ACP processor (`processor/`, spec 2026-10-11 §5.4), which parses the same reply off
|
|
9
|
+
* the keeper's raw log inside `step`. `step`'s import graph may not reach anything that spawns a
|
|
10
|
+
* process, reads the clock or draws a random id (`processor/guard.test.ts`), and `acpClient.ts` does
|
|
11
|
+
* all three — so the pure half lives here and `acpClient.ts` re-exports every name it used to own.
|
|
12
|
+
* One implementation rather than two kept equal by a test: the equivalence test in
|
|
13
|
+
* `processor/initialize.test.ts` then guards the processor's *wiring* (the right line, the right
|
|
14
|
+
* result), not a second copy of these rules.
|
|
15
|
+
*
|
|
16
|
+
* The Qoder runtime profile in `_meta` is deliberately not read here: it belongs to the connection's
|
|
17
|
+
* runtime diagnostics, which the processor takes over in Task 17b/17c.
|
|
18
|
+
*/
|
|
19
|
+
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
|
|
20
|
+
import { airCapabilityMeta, asSdkClientCapabilities, AIR_ASYNC_TASKS_CAPABILITY, AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, DRAFT_SUBAGENTS_CAPABILITY, } from './draftSubagents.js';
|
|
21
|
+
import { metaTerminalAuthArgs, terminalLoginSpecOf } from './terminalLoginSpec.js';
|
|
22
|
+
/** The `initialize` params this client sends — the same object on the live path and from the processor. */
|
|
23
|
+
export function initializeRequestParams() {
|
|
24
|
+
return {
|
|
25
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
26
|
+
// Three things are declared here, and they are not interchangeable — see
|
|
27
|
+
// `draftSubagents.ts` for the tier each one sits in.
|
|
28
|
+
//
|
|
29
|
+
// 1. `subagents: {}` is the **canonical** draft field from ACP RFD #1992. Both official
|
|
30
|
+
// adapters gate native subagent sessions on it, and both answer with
|
|
31
|
+
// `agentCapabilities.sessionCapabilities.subagents` (measured against each).
|
|
32
|
+
//
|
|
33
|
+
// 2. The AIR list is sent as well, for two separate reasons. It is the *only* channel for
|
|
34
|
+
// `asyncTasks` — nothing canonical exists for background work — and it is the fallback
|
|
35
|
+
// for `nativeSubagentSessions`, because a released SDK sitting between us and an agent
|
|
36
|
+
// can strip a draft field it has no type for (upstream says so in #1017's compatibility
|
|
37
|
+
// note). Declaring both is not belt-and-braces: they cover different failure modes.
|
|
38
|
+
//
|
|
39
|
+
// 3. `subagent-transcript: true` stays. It is claude-agent-acp's older vendor key
|
|
40
|
+
// (`dist/acp-agent.js:155-158`), and it still governs the **fallback** path taken when
|
|
41
|
+
// native subagent sessions are not negotiated — an older adapter, or a stripped
|
|
42
|
+
// capability. Without it that path strips every subagent's text and thinking
|
|
43
|
+
// (`stripSubagentTextAndThinking`, `:3926`), leaving nothing but tool-call wreckage.
|
|
44
|
+
// Adapters that have never heard of any of these keys are required by the spec to
|
|
45
|
+
// ignore them.
|
|
46
|
+
clientCapabilities: asSdkClientCapabilities({
|
|
47
|
+
[DRAFT_SUBAGENTS_CAPABILITY]: {},
|
|
48
|
+
// `ClientSessionCapabilities.configOptions` (`types.gen.d.ts:4306`), part of the schema
|
|
49
|
+
// proper — unlike `subagents` above, this survives the pinned SDK's `zInitializeRequest`
|
|
50
|
+
// stripping because it's a known field, not a draft one. `{}` for `boolean` tells the
|
|
51
|
+
// agent it may send `type: "boolean"` entries in `SessionConfigOption` and that this
|
|
52
|
+
// client will send `session/set_config_option` with a boolean `value`
|
|
53
|
+
// (`SessionConfigOptionsCapabilities`, `types.gen.d.ts:4333-4353`); without it an agent
|
|
54
|
+
// with a boolean toggle downgrades it to a two-value select.
|
|
55
|
+
session: { configOptions: { boolean: {} } },
|
|
56
|
+
// `ClientCapabilities.elicitation` (`types.gen.d.ts:4479`) is a known field of the schema
|
|
57
|
+
// proper, not a draft one, so it survives `zInitializeRequest` stripping as-is. `{}` for
|
|
58
|
+
// `url` (`ElicitationUrlCapabilities`, `:4616`) means the client can put a URL in front of
|
|
59
|
+
// the user; this is the precondition for the whole device-code login shape to exist at all
|
|
60
|
+
// — Codex's own `getCodexAuthMethods` gates its `chat-gpt-device-code` auth method on
|
|
61
|
+
// `clientCapabilities?.elicitation?.url != null`. Without declaring it, Codex never offers
|
|
62
|
+
// that method and `elicitation/create` with `mode: 'url'` never arrives to answer (see
|
|
63
|
+
// `permission.ts`'s `handleCreateElicitation`).
|
|
64
|
+
//
|
|
65
|
+
// `form` (`ElicitationFormCapabilities`, `types.gen.d.ts:4455`) is what keeps Claude's
|
|
66
|
+
// AskUserQuestion alive: without it the adapter puts the tool in `disallowedTools`
|
|
67
|
+
// (`claude-agent-acp/dist/acp-agent.js:5959`), so the model never has it at all — the
|
|
68
|
+
// symptom is prose questions, not an unrendered card. This is the trigger for the whole
|
|
69
|
+
// elicitation-standardization plan: everything through Task 17 built the machinery to
|
|
70
|
+
// answer `elicitation/create` once it arrives; this line is what makes it arrive at all.
|
|
71
|
+
elicitation: { url: {}, form: {} },
|
|
72
|
+
_meta: {
|
|
73
|
+
// The convention Zed introduced and the ACP adapters adopted: an agent may gate its terminal
|
|
74
|
+
// sign-in methods on this key — claude-agent-acp advertises `claude-ai-login` / `console-login`
|
|
75
|
+
// (each with a `_meta["terminal-auth"]` naming the pipe-friendly invocation) only when the
|
|
76
|
+
// client declares it; pi-acp always returns its method but attaches the same block only then.
|
|
77
|
+
// We DO host those invocations — `terminalLogin.ts` runs them with pipes, sniffs the OAuth URL
|
|
78
|
+
// out of the output and surfaces it as a login link (spec 2026-09-24 D6/D7/D9) — so declaring
|
|
79
|
+
// this is a statement of fact, not an aspiration. Same-machine only: a hosted login's
|
|
80
|
+
// localhost OAuth callback must reach the workstation's own listener, which a browser opened
|
|
81
|
+
// on another machine cannot do; the standard `authenticate` methods (device-code style) are
|
|
82
|
+
// unaffected and never needed this key.
|
|
83
|
+
'terminal-auth': true,
|
|
84
|
+
'subagent-transcript': true,
|
|
85
|
+
...airCapabilityMeta(AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether a JSON-RPC error code is one a frame may carry: an integer inside the Kotlin twin's `Int`
|
|
92
|
+
* range. See `acpClient.ts` `jsonRpcErrorCode` for why a malformed code is reported as no code at
|
|
93
|
+
* all. Split out so the processor, which sees the peer's `{code}` as plain JSON rather than as the
|
|
94
|
+
* SDK's `RequestError`, applies the identical rule.
|
|
95
|
+
*/
|
|
96
|
+
export function jsonRpcErrorCodeOf(code) {
|
|
97
|
+
if (typeof code !== 'number' || !Number.isInteger(code))
|
|
98
|
+
return undefined;
|
|
99
|
+
return code >= -2_147_483_648 && code <= 2_147_483_647 ? code : undefined;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Read an `initialize` result exactly the way `connectAgent` always has.
|
|
103
|
+
*
|
|
104
|
+
* The protocol version is checked **first** and nothing else is read when it is wrong — Global
|
|
105
|
+
* Constraint 1 is v1-only and a reply for another version is not ours to interpret. Malformed input
|
|
106
|
+
* throws where it always threw (a non-array `authMethods`, a non-object result); callers that must
|
|
107
|
+
* not throw (the processor) catch it themselves, so the live path keeps its exact behaviour.
|
|
108
|
+
*/
|
|
109
|
+
export function parseInitializeResult(initResponse) {
|
|
110
|
+
if (initResponse.protocolVersion !== PROTOCOL_VERSION) {
|
|
111
|
+
return { protocolSupported: false, protocolVersion: initResponse.protocolVersion };
|
|
112
|
+
}
|
|
113
|
+
const sessionCapabilities = initResponse.agentCapabilities?.sessionCapabilities;
|
|
114
|
+
const capabilities = {
|
|
115
|
+
// Capability markers are empty objects (`{ _meta? }`); supplying `{}` means "supported", and
|
|
116
|
+
// omitting the field (or explicit `null`) means "not supported". A truthiness check reads `{}`
|
|
117
|
+
// correctly by accident but reads `null`/`undefined` correctly too, which hides the real bug:
|
|
118
|
+
// this must stay an explicit `!= null`, never `Boolean(...)` or a bare truthy test.
|
|
119
|
+
list: sessionCapabilities?.list != null,
|
|
120
|
+
delete: sessionCapabilities?.delete != null,
|
|
121
|
+
resume: sessionCapabilities?.resume != null,
|
|
122
|
+
loadSession: initResponse.agentCapabilities?.loadSession === true,
|
|
123
|
+
logout: initResponse.agentCapabilities?.auth?.logout != null,
|
|
124
|
+
/**
|
|
125
|
+
* ACP `PromptCapabilities.image` (`types.gen.d.ts:1601`): "Agent supports
|
|
126
|
+
* [`ContentBlock::Image`]". Every field of that type is optional, so an agent that says
|
|
127
|
+
* nothing supports text only -- `=== true` rather than `!== false`.
|
|
128
|
+
*
|
|
129
|
+
* Verified against codex-acp 1.7.0, which declares `{embeddedContext: true, image: true}`.
|
|
130
|
+
* `audio` is deliberately not read: no agent we target declares it, and an unread capability
|
|
131
|
+
* is a field nobody can act on.
|
|
132
|
+
*/
|
|
133
|
+
promptImage: initResponse.agentCapabilities?.promptCapabilities?.image === true,
|
|
134
|
+
mcpHttp: initResponse.agentCapabilities?.mcpCapabilities?.http === true,
|
|
135
|
+
};
|
|
136
|
+
// Honour the advertised capability, not what the agent happens to route: some adapters (e.g.
|
|
137
|
+
// codex-acp routing `session/fork`) implement more than they declare in `agentCapabilities`.
|
|
138
|
+
// Reading `capabilities` above off `initResponse` — never probing the agent for what it accepts
|
|
139
|
+
// — is what keeps that distinction intact.
|
|
140
|
+
const advertisedAuthMethods = initResponse.authMethods ?? [];
|
|
141
|
+
const authMethods = advertisedAuthMethods
|
|
142
|
+
.map(toAuthMethodSnapshot)
|
|
143
|
+
.filter((method) => method !== null);
|
|
144
|
+
const terminalLogins = [];
|
|
145
|
+
for (const method of advertisedAuthMethods) {
|
|
146
|
+
const spec = terminalLoginSpecOf(method);
|
|
147
|
+
if (spec && typeof method.id === 'string') {
|
|
148
|
+
// A duplicate id keeps its first position and takes the later spec — exactly `Map.set`.
|
|
149
|
+
const existing = terminalLogins.findIndex(([id]) => id === method.id);
|
|
150
|
+
if (existing >= 0)
|
|
151
|
+
terminalLogins[existing] = [method.id, spec];
|
|
152
|
+
else
|
|
153
|
+
terminalLogins.push([method.id, spec]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
protocolSupported: true,
|
|
158
|
+
capabilities,
|
|
159
|
+
authMethods,
|
|
160
|
+
droppedAuthMethods: advertisedAuthMethods.length - authMethods.length,
|
|
161
|
+
terminalLogins,
|
|
162
|
+
...(initResponse.agentCapabilities?._meta?.['axom/accountRateLimits']?.read === true ? { supportsAccountLimits: true, accountLimitsVersion: initResponse.agentCapabilities?._meta?.['axom/accountRateLimits']?.version ?? 1 } : {}),
|
|
163
|
+
supportsSteering: steeringAdvertised(initResponse._meta),
|
|
164
|
+
steeringTimings: steeringTimings(initResponse._meta),
|
|
165
|
+
agentInfo: initResponse.agentInfo ?? undefined,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Whether an `InitializeResponse._meta` advertises the `_session/steering` extension.
|
|
170
|
+
*
|
|
171
|
+
* ## `_session/steering` is not ACP
|
|
172
|
+
*
|
|
173
|
+
* It is absent from the schema this client is pinned to (`grep steer
|
|
174
|
+
* node_modules/@agentclientprotocol/sdk/dist/schema/types.gen.d.ts` returns nothing). It is an
|
|
175
|
+
* extension `claude-agent-acp` implements: the method constant lives at its `acp-agent.js@0.76.0:164`
|
|
176
|
+
* and its own comment calls it a *"Custom (extension) request method ... Named `_session/steering`
|
|
177
|
+
* per the agreed ACP steering wire protocol"*. The leading underscore is ACP's extension namespace.
|
|
178
|
+
*
|
|
179
|
+
* ## Why the top-level `_meta` specifically
|
|
180
|
+
*
|
|
181
|
+
* The adapter advertises it at `InitializeResponse._meta.steering.supported`, a **sibling of
|
|
182
|
+
* `agentCapabilities`** (`acp-agent.js@0.76.0:944-950`), not a key inside it. `_meta` is typed
|
|
183
|
+
* `{[key: string]: unknown} | null` (`types.gen.d.ts:1498-1500`), so every hop down has to be
|
|
184
|
+
* narrowed rather than indexed through a cast.
|
|
185
|
+
*
|
|
186
|
+
* Strictly `=== true`. An agent that sends `steering: {}` (the shape ACP uses for its *own*
|
|
187
|
+
* capability markers) has not said `supported`, and this extension does not define that spelling —
|
|
188
|
+
* reading it as a yes would be this client inventing a wire contract.
|
|
189
|
+
*/
|
|
190
|
+
export function steeringAdvertised(meta) {
|
|
191
|
+
if (typeof meta !== 'object' || meta === null)
|
|
192
|
+
return false;
|
|
193
|
+
const steering = meta.steering;
|
|
194
|
+
if (typeof steering !== 'object' || steering === null)
|
|
195
|
+
return false;
|
|
196
|
+
return steering.supported === true;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The delivery timings the `_session/steering` extension defines — when the agent hands an injected
|
|
200
|
+
* message to the model. `next` is the only lossless one: `now` aborts the running generation and
|
|
201
|
+
* discards whatever tool calls were still outstanding.
|
|
202
|
+
*/
|
|
203
|
+
export const STEER_TIMINGS = ['now', 'next', 'later'];
|
|
204
|
+
/**
|
|
205
|
+
* Which delivery timings an `InitializeResponse._meta` says the agent accepts —
|
|
206
|
+
* `_meta.steering.timings`, the sibling of the `supported` flag {@link steeringAdvertised} reads,
|
|
207
|
+
* and read from the **top-level** `_meta` for exactly the same reason.
|
|
208
|
+
*
|
|
209
|
+
* ## Empty is the answer, not the absence of one
|
|
210
|
+
*
|
|
211
|
+
* An agent that declares no timings has told us we may not name one. That is not "assume the
|
|
212
|
+
* default" — the extension ignores an unrecognised `priority` **silently** (the measurement behind
|
|
213
|
+
* ruling N4: sending `next` to today's adapters lands as `now`, which throws away the user's
|
|
214
|
+
* in-flight work). So an empty result must reach the caller as "do not specify a timing at all",
|
|
215
|
+
* and today — before the upstream PR that makes adapters broadcast this — every agent in the wild
|
|
216
|
+
* produces exactly that. Steady state, not a bug.
|
|
217
|
+
*
|
|
218
|
+
* ## Unknown values are dropped, never rejected
|
|
219
|
+
*
|
|
220
|
+
* A timing this build has never heard of is skipped and the known ones beside it survive. This
|
|
221
|
+
* client is deployed on user machines and can be older than both the agent it launches and the
|
|
222
|
+
* server it reports to; throwing (or discarding the whole array) on a value from a future version
|
|
223
|
+
* of the extension would take steering away from an agent that does support it. Non-strings are
|
|
224
|
+
* dropped the same way: `_meta` is typed `{[key: string]: unknown}`, so nothing here is guaranteed.
|
|
225
|
+
*
|
|
226
|
+
* Duplicates collapse — the wire shape is a JSON array but the fact is a set.
|
|
227
|
+
*/
|
|
228
|
+
export function steeringTimings(meta) {
|
|
229
|
+
if (typeof meta !== 'object' || meta === null)
|
|
230
|
+
return [];
|
|
231
|
+
const steering = meta.steering;
|
|
232
|
+
if (typeof steering !== 'object' || steering === null)
|
|
233
|
+
return [];
|
|
234
|
+
const declared = steering.timings;
|
|
235
|
+
if (!Array.isArray(declared))
|
|
236
|
+
return [];
|
|
237
|
+
const known = [];
|
|
238
|
+
for (const entry of declared) {
|
|
239
|
+
if (typeof entry !== 'string')
|
|
240
|
+
continue;
|
|
241
|
+
if (!STEER_TIMINGS.includes(entry))
|
|
242
|
+
continue;
|
|
243
|
+
if (known.includes(entry))
|
|
244
|
+
continue;
|
|
245
|
+
known.push(entry);
|
|
246
|
+
}
|
|
247
|
+
return known;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* One `initialize` auth method, or `null` when the agent did not advertise a usable one.
|
|
251
|
+
*
|
|
252
|
+
* `initResponse` is another process's JSON that arrived over a pipe; `AuthMethod` is what this
|
|
253
|
+
* client *asked* for, not what it was handed. Both variants of that union make `id` and `name`
|
|
254
|
+
* required (`@agentclientprotocol/sdk` `types.gen.d.ts:2241-2246` for `AuthMethodTerminal`,
|
|
255
|
+
* `:2281-2286` for `AuthMethodAgent`), so an agent that omits them is off-spec -- but nothing
|
|
256
|
+
* between its stdout and here checks, and the snapshot then carries `undefined` in two positions
|
|
257
|
+
* its own type declares non-optional. That lie does not stay local: `JSON.stringify` erases an
|
|
258
|
+
* `undefined` value entirely, so the object passes every `typeof x.id` check in this process and
|
|
259
|
+
* still reaches a peer with the keys missing.
|
|
260
|
+
*
|
|
261
|
+
* `agoragentic-acp` advertises exactly `{type: 'terminal', description: '…'}`. Observing it once
|
|
262
|
+
* put a malformed `AuthMethodInfo` in the roster, which rides in every subsequent
|
|
263
|
+
* `management_hello`, which the server then cannot decode -- and a hello the server cannot decode
|
|
264
|
+
* closes the socket with nothing said, so the workstation retried into a permanent OFFLINE.
|
|
265
|
+
*
|
|
266
|
+
* Dropping rather than repairing: `id` is the value `authenticate` is called with, so inventing
|
|
267
|
+
* one would trade a missing sign-in method for a broken one.
|
|
268
|
+
*/
|
|
269
|
+
export function toAuthMethodSnapshot(method) {
|
|
270
|
+
if (typeof method.id !== 'string' || typeof method.name !== 'string')
|
|
271
|
+
return null;
|
|
272
|
+
// Terminal by shape (spec 2026-09-24 D6): a top-level `type: "terminal"`, or a `_meta
|
|
273
|
+
// ["terminal-auth"]` object from an agent that advertises its terminal login only there.
|
|
274
|
+
const terminal = terminalLoginSpecOf(method) !== undefined;
|
|
275
|
+
const args = 'args' in method && Array.isArray(method.args) ? method.args : terminal ? metaTerminalAuthArgs(method) : undefined;
|
|
276
|
+
return {
|
|
277
|
+
id: method.id,
|
|
278
|
+
name: method.name,
|
|
279
|
+
...(terminal ? { type: 'terminal' } : {}),
|
|
280
|
+
...(method.description != null ? { description: method.description } : {}),
|
|
281
|
+
// `args` only exists on a terminal method — top-level, or under `_meta["terminal-auth"]` when
|
|
282
|
+
// that is the only place the agent put it. Never the `_meta` command: see `terminalLogins`.
|
|
283
|
+
...(args ? { args } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
* built.
|
|
17
17
|
*
|
|
18
18
|
* Three shapes, matching the three things a failed install can mean (see `acpClient.ts`'s
|
|
19
|
-
* `
|
|
19
|
+
* `stderrShowsMissingPackageVersion` — the one predicate every ETARGET
|
|
20
20
|
* check in this codebase reuses rather than re-deriving):
|
|
21
21
|
*
|
|
22
22
|
* 1. An ordinary failure with no ETARGET in sight: a network hiccup, a full disk, a missing archive
|
|
23
23
|
* tool, a timeout. {@link friendlyOrdinaryInstallError}.
|
|
24
24
|
* 2. The registry pins a version that is not published upstream, and the unpinned retry
|
|
25
|
-
* (`
|
|
25
|
+
* (`AcpAgents.installPristineWithFallback`) that tries to work around it fails too.
|
|
26
26
|
* {@link friendlyPinnedVersionUnavailableAndFallbackFailed}.
|
|
27
27
|
* 3. The same missing pin, but the unpinned retry succeeds — not a failure at all, so it produces no
|
|
28
28
|
* message here. It produces a {@link PinnedVersionUnavailable} fact instead, which
|
|
@@ -87,3 +87,13 @@ export function friendlyPinnedVersionUnavailableAndFallbackFailed(agentId, pinne
|
|
|
87
87
|
return (`The registry pins '${agentId}' to version ${pinned}, which is not published. Installing the ` +
|
|
88
88
|
`latest available version instead failed too: ${shortDetail(fallbackDetail)}.${reassurance(existingVersion)}`);
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* A uv-distributed adapter, and `uv` itself is not on this workstation (the spawn failed `ENOENT`). Not a
|
|
92
|
+
* transient failure, so not "try again in a moment": the reader has to install uv first. Raised only by
|
|
93
|
+
* an Install that tried to run `uv` — scanning never checks for it.
|
|
94
|
+
*/
|
|
95
|
+
export function friendlyUvMissingError(agentId, existingVersion) {
|
|
96
|
+
return (`Could not install '${agentId}': uv is not installed on this workstation, and this agent is distributed ` +
|
|
97
|
+
`as a Python tool installed with uv. Install uv (https://docs.astral.sh/uv/getting-started/installation/) ` +
|
|
98
|
+
`so the workstation's service can find it on its PATH, then install the agent again.${reassurance(existingVersion)}`);
|
|
99
|
+
}
|
|
@@ -312,7 +312,7 @@ export function installNpxPackage(spec, options = {}) {
|
|
|
312
312
|
// 都没有时 `npxCacheVersion` 同样是 `undefined` —— 两个 `undefined` 相等,若不单独守住这一条,
|
|
313
313
|
// 一个从未被任何 npx 调用装过的包名,只要没剥版本前先被查过缓存,就会在这里被判定成"已经装好",
|
|
314
314
|
// 一次 npx 都不用起。registry 自己从不发不钉版本的 spec(见 `registry/launch.ts` 的说明),这条分支
|
|
315
|
-
//
|
|
315
|
+
// 过去因此从没被真正走到过;旧的 npx 安装期回退——
|
|
316
316
|
// 是第一个会把不带版本的 spec 递进来的调用方,伪装成功比真的失败更糟:界面会说"已安装",磁盘上
|
|
317
317
|
// 其实什么都没有。
|
|
318
318
|
const pinnedVersion = npmPackageVersion(spec);
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Automatic adapter patches, owned by Axom, applied only at **install** time to a copy of the
|
|
3
|
+
* daemon-owned pristine tree (spec §6.2/§3.3). The pristine tree is never patched in place, and
|
|
4
|
+
* nothing here ever runs at launch except {@link verifyPatchedFiles}, the tamper check.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { constants } from 'node:fs';
|
|
8
|
+
import { cp, readFile, writeFile } from 'node:fs/promises';
|
|
4
9
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
10
|
+
import { inside } from './builds/model.js';
|
|
6
11
|
import { packagePatchMatch, packagePatches } from './patchAssets.js';
|
|
7
12
|
const sha = (content) => createHash('sha256').update(content).digest('hex');
|
|
8
|
-
const pending = new Map();
|
|
9
13
|
const warned = new Set();
|
|
10
14
|
function warnOnce(key, message) {
|
|
11
15
|
if (warned.has(key))
|
|
@@ -13,7 +17,7 @@ function warnOnce(key, message) {
|
|
|
13
17
|
warned.add(key);
|
|
14
18
|
console.warn(message);
|
|
15
19
|
}
|
|
16
|
-
function
|
|
20
|
+
function within(root, relative) {
|
|
17
21
|
const target = path.resolve(root, relative);
|
|
18
22
|
if (!target.startsWith(path.resolve(root) + path.sep))
|
|
19
23
|
throw new Error('Axom patch path escapes its package');
|
|
@@ -28,7 +32,7 @@ export async function applyPackagePatches(packageDir, recipes) {
|
|
|
28
32
|
throw new Error(`Axom patch '${recipe.id}' does not support ${metadata.name}@${metadata.version}`);
|
|
29
33
|
}
|
|
30
34
|
for (const edit of recipe.edits) {
|
|
31
|
-
const filename =
|
|
35
|
+
const filename = within(packageDir, edit.file);
|
|
32
36
|
const original = await readFile(filename, 'utf8');
|
|
33
37
|
if (sha(original) !== edit.before_sha256 || !edit.before || original.split(edit.before).length !== 2) {
|
|
34
38
|
throw new Error(`Axom patch '${recipe.id}': original bytes changed in ${edit.file}`);
|
|
@@ -49,7 +53,7 @@ async function originalsFit(packageDir, recipe) {
|
|
|
49
53
|
let content = files.get(edit.file);
|
|
50
54
|
if (content === undefined) {
|
|
51
55
|
try {
|
|
52
|
-
content = await readFile(
|
|
56
|
+
content = await readFile(within(packageDir, edit.file), 'utf8');
|
|
53
57
|
}
|
|
54
58
|
catch {
|
|
55
59
|
return false;
|
|
@@ -61,168 +65,80 @@ async function originalsFit(packageDir, recipe) {
|
|
|
61
65
|
}
|
|
62
66
|
return true;
|
|
63
67
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
for (const entry of entries) {
|
|
74
|
-
if (!entry.isDirectory())
|
|
75
|
-
continue;
|
|
76
|
-
const modules = path.join(root, entry.name, 'node_modules');
|
|
77
|
-
try {
|
|
78
|
-
const metadata = JSON.parse(await readFile(inside(modules, name + '/package.json'), 'utf8'));
|
|
79
|
-
if (metadata.name === name && metadata.version === version)
|
|
80
|
-
return modules;
|
|
81
|
-
}
|
|
82
|
-
catch { /* Interrupted/other version of npm's cache. */ }
|
|
83
|
-
}
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
/** Called by Install/Update and every real launch, including adapters installed before this CLI. */
|
|
87
|
-
export async function preparePatchedLaunch(agent, launch, adapterRoot, probe, options = {}) {
|
|
88
|
-
const distribution = agent.distribution.npx;
|
|
89
|
-
if (!distribution)
|
|
90
|
-
return launch;
|
|
91
|
-
const name = npmPackageName(distribution.package);
|
|
92
|
-
const version = npmPackageVersion(distribution.package);
|
|
93
|
-
const candidates = (options.recipes ?? packagePatches).filter((patch) => patch.package === name);
|
|
68
|
+
/**
|
|
69
|
+
* The recipes this CLI applies to `name@version`, read against the **pristine** package at
|
|
70
|
+
* `pristinePackageDir`: exact-version recipes always (their bytes are a promise — a mismatch fails the
|
|
71
|
+
* patch step loudly), range recipes only when the pristine bytes fit (otherwise upstream runs, with a
|
|
72
|
+
* warning). An empty answer means the build is the pristine tree itself.
|
|
73
|
+
*/
|
|
74
|
+
export async function selectPackagePatches(name, version, pristinePackageDir, recipes = packagePatches) {
|
|
75
|
+
const candidates = recipes.filter((patch) => patch.package === name);
|
|
94
76
|
const matched = candidates.flatMap((recipe) => {
|
|
95
|
-
const match =
|
|
77
|
+
const match = packagePatchMatch(recipe, version);
|
|
96
78
|
return match ? [{ recipe, match }] : [];
|
|
97
79
|
});
|
|
98
|
-
if (matched.length && (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name) ||
|
|
80
|
+
if (matched.length && (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name) || !/^[0-9a-z.+-]+$/i.test(version))) {
|
|
99
81
|
throw new Error('Invalid package identity for Axom adapter patch');
|
|
100
82
|
}
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (match !== 'range')
|
|
107
|
-
continue;
|
|
108
|
-
if (await originalsFit(inside(modules, name), recipe))
|
|
109
|
-
fitting.add(recipe);
|
|
110
|
-
else
|
|
111
|
-
warnOnce(`${recipe.id}@${distribution.package}`, `[axom-patch] ${recipe.id} does not fit ${distribution.package}; using the upstream package. Review the patch inventory.`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const recipes = matched.filter(({ recipe, match }) => match === 'exact' || fitting.has(recipe)).map(({ recipe }) => recipe);
|
|
115
|
-
if (!recipes.length) {
|
|
116
|
-
if (candidates.length && !matched.length) {
|
|
117
|
-
warnOnce(distribution.package, `[axom-patch] No verified patch for ${distribution.package}; using the upstream package. Review the patch inventory.`);
|
|
83
|
+
const selected = [];
|
|
84
|
+
for (const { recipe, match } of matched) {
|
|
85
|
+
if (match === 'exact') {
|
|
86
|
+
selected.push(recipe);
|
|
87
|
+
continue;
|
|
118
88
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const existing = pending.get(target);
|
|
124
|
-
if (existing) {
|
|
125
|
-
await existing;
|
|
126
|
-
return makeLaunch();
|
|
127
|
-
}
|
|
128
|
-
const preparation = prepare();
|
|
129
|
-
pending.set(target, preparation);
|
|
130
|
-
try {
|
|
131
|
-
await preparation;
|
|
132
|
-
return await makeLaunch();
|
|
89
|
+
if (await originalsFit(pristinePackageDir, recipe))
|
|
90
|
+
selected.push(recipe);
|
|
91
|
+
else
|
|
92
|
+
warnOnce(`${recipe.id}@${name}@${version}`, `[axom-patch] ${recipe.id} does not fit ${name}@${version}; using the upstream package. Review the patch inventory.`);
|
|
133
93
|
}
|
|
134
|
-
|
|
135
|
-
|
|
94
|
+
if (!selected.length && candidates.length && !matched.length) {
|
|
95
|
+
warnOnce(`${name}@${version}`, `[axom-patch] No verified patch for ${name}@${version}; using the upstream package. Review the patch inventory.`);
|
|
136
96
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
97
|
+
return selected;
|
|
98
|
+
}
|
|
99
|
+
/** The identity of a recipe set: any recipe change is a new digest, hence a new build. */
|
|
100
|
+
export function patchDigest(recipes) {
|
|
101
|
+
return sha(JSON.stringify(recipes));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Copy the pristine `node_modules` (reflink where the filesystem supports it — `COPYFILE_FICLONE`
|
|
105
|
+
* falls back to an ordinary copy by itself; relative symlinks such as `.bin/*` are kept relative so
|
|
106
|
+
* the copy never points back into pristine) into `stagedRoot`, patch the copy, and write a receipt.
|
|
107
|
+
* Returns the sha256 of every patched file, relative to the package directory.
|
|
108
|
+
*/
|
|
109
|
+
export async function createPatchedTree(pristineRoot, stagedRoot, name, version, recipes) {
|
|
110
|
+
await cp(path.join(pristineRoot, 'node_modules'), path.join(stagedRoot, 'node_modules'), {
|
|
111
|
+
recursive: true, verbatimSymlinks: true, mode: constants.COPYFILE_FICLONE,
|
|
112
|
+
});
|
|
113
|
+
const packageDir = inside(path.join(stagedRoot, 'node_modules'), name);
|
|
114
|
+
const files = await applyPackagePatches(packageDir, recipes);
|
|
115
|
+
const expected = expectedPatchedFiles(recipes);
|
|
116
|
+
if (JSON.stringify(sortKeys(files)) !== JSON.stringify(sortKeys(expected)))
|
|
117
|
+
throw new Error('Incomplete Axom adapter patch');
|
|
118
|
+
await writeFile(path.join(stagedRoot, 'axom-patches.json'), JSON.stringify({
|
|
119
|
+
digest: patchDigest(recipes), package: name, version, patches: recipes.map((recipe) => recipe.id), files,
|
|
120
|
+
}) + '\n');
|
|
121
|
+
console.info(`[axom-patch] Applied ${recipes.map((recipe) => recipe.id).join(', ')} to ${name}@${version}`);
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
/** The files a recipe set rewrites and their expected sha256, derived from the shipped recipes. */
|
|
125
|
+
export function expectedPatchedFiles(recipes) {
|
|
126
|
+
return Object.fromEntries(recipes.flatMap((recipe) => recipe.edits.map((edit) => [edit.file, edit.after_sha256])));
|
|
127
|
+
}
|
|
128
|
+
function sortKeys(record) {
|
|
129
|
+
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
130
|
+
}
|
|
131
|
+
/** The launch-time tamper check: every patched file still hashes to what was recorded at install. */
|
|
132
|
+
export async function verifyPatchedFiles(packageDir, files) {
|
|
133
|
+
for (const [file, hash] of Object.entries(files)) {
|
|
134
|
+
let content;
|
|
141
135
|
try {
|
|
142
|
-
|
|
136
|
+
content = await readFile(within(packageDir, file));
|
|
143
137
|
}
|
|
144
|
-
catch
|
|
145
|
-
|
|
146
|
-
throw error;
|
|
147
|
-
}
|
|
148
|
-
if (receipt) {
|
|
149
|
-
if (receipt.digest !== digest)
|
|
150
|
-
throw new Error('Axom adapter patch receipt does not match this CLI');
|
|
151
|
-
// Derive expected files from the shipped recipe, never from a writable receipt alone.
|
|
152
|
-
const expected = Object.fromEntries(recipes.flatMap((recipe) => recipe.edits.map((edit) => [edit.file, edit.after_sha256])));
|
|
153
|
-
if (JSON.stringify(receipt.files) !== JSON.stringify(expected))
|
|
154
|
-
throw new Error('Incomplete Axom adapter patch receipt');
|
|
155
|
-
for (const [file, hash] of Object.entries(expected)) {
|
|
156
|
-
if (sha(await readFile(inside(packageDir, file))) !== hash)
|
|
157
|
-
throw new Error(`Axom patched adapter was modified: ${file}`);
|
|
158
|
-
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error(`Axom patched adapter is missing ${file}`);
|
|
159
140
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
await stat(target);
|
|
163
|
-
throw new Error('Incomplete Axom patched adapter directory; refusing to use it');
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
if (error.code !== 'ENOENT')
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
const modules = await pristineModules();
|
|
170
|
-
await mkdir(path.dirname(target), { recursive: true });
|
|
171
|
-
const temporary = target + '.staging-' + randomUUID();
|
|
172
|
-
try {
|
|
173
|
-
// Dependencies and .bin are part of the adapter environment; clone them as well.
|
|
174
|
-
await cp(modules, path.join(temporary, 'node_modules'), { recursive: true, dereference: true });
|
|
175
|
-
const files = await applyPackagePatches(inside(path.join(temporary, 'node_modules'), name), recipes);
|
|
176
|
-
await writeFile(path.join(temporary, 'axom-patches.json'), JSON.stringify({
|
|
177
|
-
digest, package: name, version, patches: recipes.map((recipe) => recipe.id), files,
|
|
178
|
-
}) + '\n');
|
|
179
|
-
try {
|
|
180
|
-
await rename(temporary, target);
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
// A second daemon may have published the same immutable copy while we built ours.
|
|
184
|
-
if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
|
|
185
|
-
throw error;
|
|
186
|
-
return prepare();
|
|
187
|
-
}
|
|
188
|
-
console.info(`[axom-patch] Applied ${recipes.map((recipe) => recipe.id).join(', ')} to ${name}@${version}`);
|
|
189
|
-
}
|
|
190
|
-
finally {
|
|
191
|
-
await rm(temporary, { recursive: true, force: true });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
async function pristineModules() {
|
|
196
|
-
let modules = await cachedModules(name, version, probe);
|
|
197
|
-
if (!modules) {
|
|
198
|
-
const installed = await installNpxPackage(distribution.package, {
|
|
199
|
-
env: probe, timeoutMs: options.timeoutMs, npxCommand: options.npxCommand,
|
|
200
|
-
});
|
|
201
|
-
if (!installed.ok)
|
|
202
|
-
throw new Error(installed.error);
|
|
203
|
-
modules = await cachedModules(name, version, probe);
|
|
204
|
-
}
|
|
205
|
-
if (!modules)
|
|
206
|
-
throw new Error(`Installed package ${distribution.package} is absent from npm's cache`);
|
|
207
|
-
return modules;
|
|
208
|
-
}
|
|
209
|
-
async function makeLaunch() {
|
|
210
|
-
const packageDir = inside(path.join(target, 'node_modules'), name);
|
|
211
|
-
const metadata = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
|
|
212
|
-
if (metadata.name !== name || metadata.version !== version)
|
|
213
|
-
throw new Error('Patched adapter package identity changed');
|
|
214
|
-
const selectedBins = [...new Set(recipes.map(recipe => recipe.bin).filter((bin) => bin !== undefined))];
|
|
215
|
-
if (selectedBins.length > 1)
|
|
216
|
-
throw new Error('Axom patches disagree about the adapter executable');
|
|
217
|
-
const binaries = selectedBins.length ? [metadata.bin?.[selectedBins[0]]] :
|
|
218
|
-
typeof metadata.bin === 'string' ? [metadata.bin] : Object.values(metadata.bin ?? {});
|
|
219
|
-
if (binaries.length !== 1 || typeof binaries[0] !== 'string')
|
|
220
|
-
throw new Error('Patched adapter has no unambiguous executable');
|
|
221
|
-
const entry = inside(packageDir, binaries[0]);
|
|
222
|
-
if (!(await stat(entry)).isFile())
|
|
223
|
-
throw new Error('Patched adapter executable is missing');
|
|
224
|
-
const env = { ...launch.env };
|
|
225
|
-
env.PATH = path.join(target, 'node_modules', '.bin') + path.delimiter + (env.PATH ?? probe.path ?? process.env.PATH ?? '');
|
|
226
|
-
return { command: process.execPath, args: [entry, ...(distribution.args ?? [])], env };
|
|
141
|
+
if (sha(content) !== hash)
|
|
142
|
+
throw new Error(`Axom patched adapter was modified: ${file}`);
|
|
227
143
|
}
|
|
228
144
|
}
|