@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
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opt-in real-adapter harness on the processor path (Task 30a). It replaces the real-adapter coverage Task 29 deleted with
|
|
3
|
+
* `AcpSession` (`qoderContract.test.ts`'s `AXOM_ACP_CONTRACT_COMMAND` certification, `providerLifecycleBaseline`,
|
|
4
|
+
* `qoderAdapter/acpHostIntegration`): every case opens through `AcpAgents.open` → `ProcessorSession`, over the local transport and
|
|
5
|
+
* over a real keeper-acp (the built bundle in `dist/keepers`), and reads what the daemon would — the session's frames and its raw log.
|
|
6
|
+
*
|
|
7
|
+
* ## Isolation
|
|
8
|
+
*
|
|
9
|
+
* Every run gets its own temp config dir (keeper-acp's sockets, raw logs and outboxes live there, never `~/.config/axom`), its own
|
|
10
|
+
* temp workspace and its own temp HOME. The adapter is started by a small launcher ({@link writeLauncher}) that:
|
|
11
|
+
* - puts it in a process group of its own (the owned-group cleanup of the deleted certification), and
|
|
12
|
+
* - replaces its environment with a minimal one read from a 0600 file in the run dir: PATH, the temp HOME, temp `CLAUDE_CONFIG_DIR`,
|
|
13
|
+
* `CODEX_HOME`, `XDG_CONFIG_HOME` (all created), the build's own launch env, and only the variables named in
|
|
14
|
+
* `AXOM_REAL_ADAPTER_ENV_PASS`. Nothing else of this process's environment (API keys, `CLAUDE_CODE_*`, tokens) reaches the
|
|
15
|
+
* adapter, and the environment is never in the spawn's argv (keeper-acp records argv in its label).
|
|
16
|
+
*
|
|
17
|
+
* A case that needs a logged-in adapter uses `AXOM_REAL_ADAPTER_AUTH_HOME` as HOME instead of a temp one: a directory the operator
|
|
18
|
+
* prepared for this purpose (e.g. `HOME=<dir> qodercli` once, then log in). The harness never defaults to the real HOME.
|
|
19
|
+
*
|
|
20
|
+
* ## Flags (all off: `npm test` runs only the hermetic cases and reports the rest as skipped, with the reason)
|
|
21
|
+
*
|
|
22
|
+
* | variable | effect |
|
|
23
|
+
* |---|---|
|
|
24
|
+
* | `AXOM_REAL_ADAPTER_HARNESS=1` | enables the real-adapter tiers at all |
|
|
25
|
+
* | `AXOM_REAL_ADAPTER_AGENTS=claude-acp,codex-acp` | registry adapters for the no-prompt tier, installed into the temp config dir |
|
|
26
|
+
* | `AXOM_ACP_CONTRACT_COMMAND` / `AXOM_ACP_CONTRACT_ARGS` | an adapter command (and a JSON argv, default `["--acp"]`): the no-prompt tier, and the Qoder certification's adapter |
|
|
27
|
+
* | `AXOM_REAL_ADAPTER_PROMPTS=1` | also runs the cases that send prompts (model usage, see each case's cost) |
|
|
28
|
+
* | `AXOM_REAL_ADAPTER_AUTH_HOME` | HOME for the prompt cases (required by them) |
|
|
29
|
+
* | `AXOM_REAL_ADAPTER_ENV_PASS=A,B` | extra variables passed through to the adapter (token-based auth) |
|
|
30
|
+
* | `AXOM_REAL_ADAPTER_TRANSPORTS=local,keeper` | which transports to run on (default both) |
|
|
31
|
+
* | `AXOM_QODER_SDK_QODERCLI=<path>` | the real SDK adapter tier: `node dist/cli.js qoder-acp --qodercli <path>` (prompts) |
|
|
32
|
+
*/
|
|
33
|
+
import { spawn } from 'node:child_process';
|
|
34
|
+
import { existsSync } from 'node:fs';
|
|
35
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
36
|
+
import { tmpdir } from 'node:os';
|
|
37
|
+
import path, { join } from 'node:path';
|
|
38
|
+
import { fileURLToPath } from 'node:url';
|
|
39
|
+
import { AcpKeeperClient } from '../../keepers/acp/client.js';
|
|
40
|
+
import { readKeeperManifest } from '../../keepers/kit/bundle.js';
|
|
41
|
+
import { keeperGen } from '../../keepers/kit/identity.js';
|
|
42
|
+
import { KeeperLauncher } from '../../keepers/kit/launcher.js';
|
|
43
|
+
import { killKeepersUnder } from '../../../test/keeperKit.js';
|
|
44
|
+
import { AcpAgents } from '../acpAgents.js';
|
|
45
|
+
import { KeeperTransport } from '../processor/transport/keeper.js';
|
|
46
|
+
import { LocalTransport } from '../processor/transport/local.js';
|
|
47
|
+
import { RawTruncatedError } from '../processor/transport/types.js';
|
|
48
|
+
import { startRawAgent } from '../processor/test/rawAgent.js';
|
|
49
|
+
import { watch } from './sessionFrames.js';
|
|
50
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
export const PACKAGE_ROOT = path.join(here, '..', '..', '..', '..');
|
|
52
|
+
export const REAL_KEEPER_DIST = path.join(PACKAGE_ROOT, 'dist', 'keepers');
|
|
53
|
+
const list = (value) => (value ?? '').split(',').map((part) => part.trim()).filter(Boolean);
|
|
54
|
+
export function realAdapterConfig(env = process.env) {
|
|
55
|
+
const transports = list(env.AXOM_REAL_ADAPTER_TRANSPORTS);
|
|
56
|
+
for (const name of transports)
|
|
57
|
+
if (name !== 'local' && name !== 'keeper')
|
|
58
|
+
throw new Error(`AXOM_REAL_ADAPTER_TRANSPORTS: unknown transport '${name}'`);
|
|
59
|
+
const args = env.AXOM_ACP_CONTRACT_ARGS ? JSON.parse(env.AXOM_ACP_CONTRACT_ARGS) : ['--acp'];
|
|
60
|
+
if (!Array.isArray(args) || !args.every((arg) => typeof arg === 'string'))
|
|
61
|
+
throw new Error('AXOM_ACP_CONTRACT_ARGS must be a JSON array of strings');
|
|
62
|
+
return {
|
|
63
|
+
enabled: env.AXOM_REAL_ADAPTER_HARNESS === '1',
|
|
64
|
+
prompts: env.AXOM_REAL_ADAPTER_PROMPTS === '1',
|
|
65
|
+
agents: list(env.AXOM_REAL_ADAPTER_AGENTS),
|
|
66
|
+
...(env.AXOM_ACP_CONTRACT_COMMAND ? { command: env.AXOM_ACP_CONTRACT_COMMAND } : {}),
|
|
67
|
+
args,
|
|
68
|
+
...(env.AXOM_REAL_ADAPTER_AUTH_HOME ? { authHome: env.AXOM_REAL_ADAPTER_AUTH_HOME } : {}),
|
|
69
|
+
envPass: list(env.AXOM_REAL_ADAPTER_ENV_PASS),
|
|
70
|
+
transports: transports.length > 0 ? transports : ['local', 'keeper'],
|
|
71
|
+
...(env.AXOM_QODER_SDK_QODERCLI ? { sdkQodercli: env.AXOM_QODER_SDK_QODERCLI } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Why a tier does not run (undefined: it runs). Shown as the skipped test's name. */
|
|
75
|
+
export function skipReason(config, tier) {
|
|
76
|
+
if (!config.enabled)
|
|
77
|
+
return 'set AXOM_REAL_ADAPTER_HARNESS=1 (see acp/test/realAdapter.ts)';
|
|
78
|
+
if (tier === 'no-prompt') {
|
|
79
|
+
return config.command || config.agents.length > 0 ? undefined : 'set AXOM_ACP_CONTRACT_COMMAND or AXOM_REAL_ADAPTER_AGENTS';
|
|
80
|
+
}
|
|
81
|
+
if (!config.prompts)
|
|
82
|
+
return 'sends prompts (model usage): set AXOM_REAL_ADAPTER_PROMPTS=1';
|
|
83
|
+
if (!config.authHome)
|
|
84
|
+
return 'needs a logged-in adapter: set AXOM_REAL_ADAPTER_AUTH_HOME';
|
|
85
|
+
if (tier === 'qoder')
|
|
86
|
+
return config.command ? undefined : 'set AXOM_ACP_CONTRACT_COMMAND to qodercli';
|
|
87
|
+
return config.sdkQodercli ? undefined : 'set AXOM_QODER_SDK_QODERCLI to qodercli';
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A provider history failure fails certification but never prevents local teardown; each local step is guarded on its own, so one
|
|
91
|
+
* failure cannot strand the resources after it. Every failure is reported together.
|
|
92
|
+
*/
|
|
93
|
+
export async function cleanupOwnedFixture(actions) {
|
|
94
|
+
const failures = [];
|
|
95
|
+
try {
|
|
96
|
+
await actions.deleteSession();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
failures.push(error);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
for (const release of [actions.closeTransport, actions.killOwnedGroup, actions.removeWorkspace]) {
|
|
103
|
+
try {
|
|
104
|
+
await release();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
failures.push(error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (failures.length)
|
|
112
|
+
throw new AggregateError(failures, `Qoder cleanup incomplete: ${failures.map(String).join('; ')}`);
|
|
113
|
+
}
|
|
114
|
+
export async function bounded(promise, label, ms) {
|
|
115
|
+
let timer;
|
|
116
|
+
try {
|
|
117
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
118
|
+
timer = setTimeout(() => reject(new Error(`UNCERTIFIED: ${label} exceeded ${ms}ms`)), ms);
|
|
119
|
+
})]);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ---- launcher and environment -------------------------------------------------------------------------------------------------
|
|
126
|
+
/**
|
|
127
|
+
* `launch.cjs <envFile> <pidFile> <command> ...args`: spawns the adapter detached (its own process group), with exactly the
|
|
128
|
+
* environment in `envFile`, stdio inherited (the pipes keeper-acp or the local transport gave the launcher), and records its pid.
|
|
129
|
+
* It forwards SIGTERM/SIGINT to the group and exits with the adapter.
|
|
130
|
+
*/
|
|
131
|
+
const LAUNCHER = `const {spawn}=require('node:child_process');const fs=require('node:fs');
|
|
132
|
+
const [envFile,pidFile,command,...args]=process.argv.slice(2);
|
|
133
|
+
const env=JSON.parse(fs.readFileSync(envFile,'utf8'));
|
|
134
|
+
const child=spawn(command,args,{detached:true,stdio:'inherit',env});
|
|
135
|
+
fs.writeFileSync(pidFile,String(child.pid));
|
|
136
|
+
for(const signal of ['SIGTERM','SIGINT'])process.on(signal,()=>{try{process.kill(-child.pid,signal)}catch{}});
|
|
137
|
+
child.on('exit',(code,signal)=>process.exit(code??(signal?1:0)));
|
|
138
|
+
child.on('error',(error)=>{process.stderr.write(String(error)+'\\n');process.exit(127)});`;
|
|
139
|
+
export async function writeLauncher(dir) {
|
|
140
|
+
const file = join(dir, 'launch.cjs');
|
|
141
|
+
await writeFile(file, LAUNCHER);
|
|
142
|
+
return file;
|
|
143
|
+
}
|
|
144
|
+
/** The adapter's whole environment (see the file comment). `home` is created when it is a temp one. */
|
|
145
|
+
export async function isolatedEnv(home, config, launchEnv = {}) {
|
|
146
|
+
const env = {
|
|
147
|
+
PATH: launchEnv.PATH ?? process.env.PATH ?? '/usr/bin:/bin', HOME: home,
|
|
148
|
+
CLAUDE_CONFIG_DIR: join(home, '.claude'), CODEX_HOME: join(home, '.codex'), XDG_CONFIG_HOME: join(home, '.config'),
|
|
149
|
+
};
|
|
150
|
+
for (const name of config.envPass) {
|
|
151
|
+
const value = process.env[name];
|
|
152
|
+
if (value !== undefined)
|
|
153
|
+
env[name] = value;
|
|
154
|
+
}
|
|
155
|
+
Object.assign(env, launchEnv);
|
|
156
|
+
for (const dir of [env.CLAUDE_CONFIG_DIR, env.CODEX_HOME, env.XDG_CONFIG_HOME])
|
|
157
|
+
await mkdir(dir, { recursive: true });
|
|
158
|
+
return env;
|
|
159
|
+
}
|
|
160
|
+
export async function startRun(transport, prefix) {
|
|
161
|
+
const configDir = await mkdtemp(join(tmpdir(), `${prefix}-${transport}-cfg-`));
|
|
162
|
+
const runDir = await mkdtemp(join(tmpdir(), `${prefix}-${transport}-run-`));
|
|
163
|
+
if (transport === 'local') {
|
|
164
|
+
const local = new LocalTransport({ configDir });
|
|
165
|
+
const acp = new AcpAgents({ cacheDir: configDir, transports: { local }, sessionStartTimeoutMs: 60_000 });
|
|
166
|
+
return { transport, configDir, runDir, acp, reader: local, async dispose() {
|
|
167
|
+
await local.dispose(0);
|
|
168
|
+
for (const dir of [configDir, runDir])
|
|
169
|
+
await rm(dir, { recursive: true, force: true });
|
|
170
|
+
} };
|
|
171
|
+
}
|
|
172
|
+
const launcher = new KeeperLauncher({ configDir, workstationId: 'ws-t30a', server: 'http://test.invalid', distDir: REAL_KEEPER_DIST,
|
|
173
|
+
useSystemd: false, startTimeoutMs: 20_000, umask: 0o022 });
|
|
174
|
+
const keeper = new KeeperTransport(new AcpKeeperClient({ launcher }));
|
|
175
|
+
const acp = new AcpAgents({ cacheDir: configDir, transports: { keeper }, sessionStartTimeoutMs: 60_000 });
|
|
176
|
+
return { transport, configDir, runDir, acp, reader: keeper, async dispose() {
|
|
177
|
+
keeper.close();
|
|
178
|
+
const gen = keeperGen((await readKeeperManifest(join(REAL_KEEPER_DIST, 'acp'))).buildVersion);
|
|
179
|
+
await launcher.stop('acp', gen, 'force').catch(() => { });
|
|
180
|
+
killKeepersUnder(configDir);
|
|
181
|
+
for (const dir of [configDir, runDir])
|
|
182
|
+
await rm(dir, { recursive: true, force: true });
|
|
183
|
+
} };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Every retained raw record of `sessionKey`. A turn's end cuts a checkpoint and the transport may drop the records before it, so a
|
|
187
|
+
* case that needs the open's lines reads them before its first turn.
|
|
188
|
+
*/
|
|
189
|
+
export async function readAllRaw(run, sessionKey) {
|
|
190
|
+
const records = [];
|
|
191
|
+
for (let at = 0;;) {
|
|
192
|
+
let page;
|
|
193
|
+
try {
|
|
194
|
+
page = await run.reader.readRaw(sessionKey, at);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (!(error instanceof RawTruncatedError) || at !== 0)
|
|
198
|
+
throw error;
|
|
199
|
+
at = error.first;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (page.length === 0)
|
|
203
|
+
break;
|
|
204
|
+
records.push(...page);
|
|
205
|
+
at = page[page.length - 1].o + 1;
|
|
206
|
+
}
|
|
207
|
+
return records;
|
|
208
|
+
}
|
|
209
|
+
export const downMethodsOf = (records) => records.filter((r) => r.kind === 'down')
|
|
210
|
+
.map((r) => JSON.parse(r.line).method).filter((m) => m !== undefined);
|
|
211
|
+
let refCounter = 0;
|
|
212
|
+
/** The launcher argv (and its 0600 env file) for one adapter process of `ref`. */
|
|
213
|
+
async function launcherArgs(run, ref, launch, options) {
|
|
214
|
+
const envFile = join(run.runDir, `${ref}.env.json`);
|
|
215
|
+
const pidFile = join(run.runDir, `${ref}.pid`);
|
|
216
|
+
await writeFile(envFile, JSON.stringify(await isolatedEnv(options.home, options.config, launch.launchEnv)), { mode: 0o600 });
|
|
217
|
+
await chmod(envFile, 0o600);
|
|
218
|
+
const launcher = await writeLauncher(run.runDir);
|
|
219
|
+
return { args: [launcher, envFile, pidFile, launch.command, ...launch.args], pidFile };
|
|
220
|
+
}
|
|
221
|
+
export async function openReal(run, launch, options) {
|
|
222
|
+
refCounter += 1;
|
|
223
|
+
const ref = `real-${refCounter}`;
|
|
224
|
+
const { args, pidFile } = await launcherArgs(run, ref, launch, options);
|
|
225
|
+
let opened;
|
|
226
|
+
try {
|
|
227
|
+
opened = await run.acp.open({ agentId: launch.agentId, cwd: options.cwd, env: { HOME: options.home }, agentEnv: {},
|
|
228
|
+
command: process.execPath, args, sessionRef: ref, agentKey: 'real-harness', ...options.over });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
// The ref, so a case can read what the refused open wrote (and kill what it started).
|
|
232
|
+
throw Object.assign(error instanceof Error ? error : new Error(String(error)), { harnessRef: ref, pidFile });
|
|
233
|
+
}
|
|
234
|
+
const onEvent = options.onEvent;
|
|
235
|
+
const watched = watch(opened.session, onEvent ? (event) => onEvent(event, opened.session) : undefined);
|
|
236
|
+
return { ref, session: opened.session, nativeSessionId: opened.nativeSessionId, watched, pidFile,
|
|
237
|
+
keeperHosted: run.acp.placementOf(opened.session)?.keeper === true };
|
|
238
|
+
}
|
|
239
|
+
/** A JSON-RPC error answer as an outcome: `-32601` (method not found) is the adapter rejecting the method itself. */
|
|
240
|
+
const errorOutcome = (error) => error.code === -32601 ? 'unsupported' : `failed: ${error.code ?? '?'} ${error.message ?? ''}`.trimEnd();
|
|
241
|
+
/**
|
|
242
|
+
* `session/delete` on a still-open session's own connection (what the deleted harness did, before closing its transport): an
|
|
243
|
+
* `rpc.request` note, the processor writes the request, and the answer is read back from the raw log. `not-sent` when the session is
|
|
244
|
+
* no longer connected (the processor drops the note) or nothing came back in time.
|
|
245
|
+
*/
|
|
246
|
+
export async function deleteOnLive(run, opened, id, timeoutMs = 15_000) {
|
|
247
|
+
if (!opened.session.liveness.agentAlive)
|
|
248
|
+
return 'not-sent';
|
|
249
|
+
await opened.session.note({ type: 'rpc.request', method: 'session/delete', params: { sessionId: id } });
|
|
250
|
+
const deadline = Date.now() + timeoutMs;
|
|
251
|
+
while (Date.now() < deadline) {
|
|
252
|
+
const lines = (await readAllRaw(run, opened.ref)).filter((r) => r.kind === 'up' || r.kind === 'down')
|
|
253
|
+
.map((r) => ({ kind: r.kind, message: JSON.parse(r.line) }));
|
|
254
|
+
const request = lines.filter((l) => l.kind === 'down' && l.message.method === 'session/delete' && l.message.params?.sessionId === id).at(-1);
|
|
255
|
+
const answer = request && lines.find((l) => l.kind === 'up' && l.message.method === undefined && l.message.id === request.message.id);
|
|
256
|
+
if (answer)
|
|
257
|
+
return answer.message.error ? errorOutcome(answer.message.error) : 'deleted';
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
259
|
+
}
|
|
260
|
+
return 'not-sent';
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* `session/delete` straight over a fresh adapter process (raw ndjson, our own `initialize`), whatever the adapter advertised — the
|
|
264
|
+
* deleted harness sent it unconditionally. Same launcher and environment as the sessions.
|
|
265
|
+
*/
|
|
266
|
+
async function deleteDirect(run, launch, options, ids) {
|
|
267
|
+
refCounter += 1;
|
|
268
|
+
const { args, pidFile } = await launcherArgs(run, `delete-direct-${refCounter}`, launch, options);
|
|
269
|
+
const agent = startRawAgent({ command: process.execPath, args });
|
|
270
|
+
const outcomes = {};
|
|
271
|
+
try {
|
|
272
|
+
agent.send(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: 1, clientCapabilities: {} } }));
|
|
273
|
+
await agent.nextLine((m) => m.id === 1 && m.method === undefined, 30_000);
|
|
274
|
+
for (const [index, id] of ids.entries()) {
|
|
275
|
+
const rpcId = 2 + index;
|
|
276
|
+
agent.send(JSON.stringify({ jsonrpc: '2.0', id: rpcId, method: 'session/delete', params: { sessionId: id } }));
|
|
277
|
+
const answer = JSON.parse(await agent.nextLine((m) => m.id === rpcId && m.method === undefined, 30_000));
|
|
278
|
+
outcomes[id] = answer.error ? errorOutcome(answer.error) : 'deleted';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
await agent.close();
|
|
283
|
+
await killOwnedGroup(pidFile);
|
|
284
|
+
}
|
|
285
|
+
return outcomes;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Deletes the test-owned native sessions; throws unless every one was deleted. Order (the ruling of T30a fix round 1):
|
|
289
|
+
* 1. on the session's own live connection, before its transport closes (the deleted harness's order — an adapter may refuse a
|
|
290
|
+
* delete from another process while the session is open);
|
|
291
|
+
* 2. what is left, over a throwaway connection through the production `deleteNativeSessions`;
|
|
292
|
+
* 3. if that answered `unsupported` only because the adapter does not advertise `sessionCapabilities.delete`, the request is sent
|
|
293
|
+
* directly anyway (logged `advertised:false`). `unsupported` is reported only when the adapter itself rejects the method.
|
|
294
|
+
* Which order a real Qoder needs is not known without one; both run, and the log line says which one deleted each id (T30b reads it).
|
|
295
|
+
*/
|
|
296
|
+
export async function deleteOwnedSessions(run, launch, options, sessions) {
|
|
297
|
+
const ids = [...new Set(sessions.map((s) => s.nativeSessionId).filter((id) => !!id))];
|
|
298
|
+
const outcomes = {};
|
|
299
|
+
for (const id of ids) {
|
|
300
|
+
const live = [...sessions].reverse().find((s) => s.nativeSessionId === id && s.session.liveness.agentAlive);
|
|
301
|
+
if (live)
|
|
302
|
+
outcomes[id] = { via: 'live', outcome: await deleteOnLive(run, live, id) };
|
|
303
|
+
}
|
|
304
|
+
const rest = ids.filter((id) => outcomes[id]?.outcome !== 'deleted');
|
|
305
|
+
if (rest.length > 0) {
|
|
306
|
+
refCounter += 1;
|
|
307
|
+
const { args, pidFile } = await launcherArgs(run, `delete-${refCounter}`, launch, options);
|
|
308
|
+
let results;
|
|
309
|
+
try {
|
|
310
|
+
results = await run.acp.deleteNativeSessions({ agentId: launch.agentId, cwd: options.cwd, env: { HOME: options.home }, agentEnv: {},
|
|
311
|
+
command: process.execPath, args }, rest);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await killOwnedGroup(pidFile);
|
|
315
|
+
}
|
|
316
|
+
for (const result of results)
|
|
317
|
+
outcomes[result.nativeSessionId] = { via: 'deleteNativeSessions', outcome: result.outcome === 'failed'
|
|
318
|
+
? `failed: ${result.error ?? ''}` : result.outcome };
|
|
319
|
+
const unadvertised = rest.filter((id) => outcomes[id]?.outcome === 'unsupported');
|
|
320
|
+
if (unadvertised.length > 0) {
|
|
321
|
+
console.info('Qoder owned-session delete', JSON.stringify({ advertised: false, ids: unadvertised }));
|
|
322
|
+
for (const [id, outcome] of Object.entries(await deleteDirect(run, launch, options, unadvertised)))
|
|
323
|
+
outcomes[id] = { via: 'direct', outcome };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
console.info('Qoder owned-session delete', JSON.stringify(outcomes));
|
|
327
|
+
const failed = ids.filter((id) => outcomes[id]?.outcome !== 'deleted');
|
|
328
|
+
if (failed.length > 0)
|
|
329
|
+
throw new Error(`owned session/delete failed: ${JSON.stringify(Object.fromEntries(failed.map((id) => [id, outcomes[id]])))}`);
|
|
330
|
+
}
|
|
331
|
+
/** The ref and pid file of an open that failed ({@link openReal} attaches them to the error). */
|
|
332
|
+
export const failedOpen = (error) => (error ?? {});
|
|
333
|
+
/** An adapter refused `session/new` / `session/load` for want of credentials (ACP `auth_required`, -32000). */
|
|
334
|
+
export const isAuthRequired = (error) => error.errorCode === -32000 || /authentication required|auth_required/i.test(String(error));
|
|
335
|
+
/** Kills the adapter's own process group (from its pid file); an exited group or a launch that never wrote one is fine. */
|
|
336
|
+
export async function killOwnedGroup(pidFile) {
|
|
337
|
+
try {
|
|
338
|
+
const pid = Number(await readFile(pidFile, 'utf8'));
|
|
339
|
+
if (!Number.isInteger(pid) || pid <= 1)
|
|
340
|
+
throw new Error('Invalid owned process group ID');
|
|
341
|
+
process.kill(-pid, 'SIGKILL');
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
const code = error.code;
|
|
345
|
+
if (code !== 'ENOENT' && code !== 'ESRCH')
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** Sends one prompt and waits for its `agent_turn_done`; returns the turn and the events it produced. */
|
|
350
|
+
export async function turn(opened, text, timeoutMs) {
|
|
351
|
+
const start = opened.watched.events.length;
|
|
352
|
+
const n = opened.watched.turns.length + 1;
|
|
353
|
+
const done = opened.watched.turn(n);
|
|
354
|
+
opened.session.prompt([{ type: 'text', text }], `turn-${refCounter}-${n}`);
|
|
355
|
+
const result = await bounded(done, 'prompt', timeoutMs);
|
|
356
|
+
return { done: result, events: opened.watched.events.slice(start) };
|
|
357
|
+
}
|
|
358
|
+
/** Drains a load's backfill (the history the processor folded inside the load's window). */
|
|
359
|
+
export async function historyOf(opened) {
|
|
360
|
+
const history = [];
|
|
361
|
+
const backfill = opened.session.backfill;
|
|
362
|
+
if (backfill)
|
|
363
|
+
for await (const event of backfill())
|
|
364
|
+
history.push(event);
|
|
365
|
+
return history;
|
|
366
|
+
}
|
|
367
|
+
/** `node dist/cli.js` (the built CLI), for the real SDK adapter (`qoder-acp --qodercli`). */
|
|
368
|
+
export const BUILT_CLI = join(PACKAGE_ROOT, 'dist', 'cli.js');
|
|
369
|
+
export const builtCliExists = () => existsSync(BUILT_CLI);
|
|
370
|
+
/** Runs `node dist/cli.js qoder-acp --prepare-sdk` once (network: the SDK runtime) under `env`. */
|
|
371
|
+
export async function prepareSdkRuntime(env) {
|
|
372
|
+
await new Promise((resolve, reject) => {
|
|
373
|
+
const child = spawn(process.execPath, [BUILT_CLI, 'qoder-acp', '--prepare-sdk'], { env, stdio: ['ignore', 'inherit', 'inherit'] });
|
|
374
|
+
child.once('error', reject);
|
|
375
|
+
child.once('exit', (code) => (code === 0 ? resolve() : reject(new Error(`qoder-acp --prepare-sdk exited ${code}`))));
|
|
376
|
+
});
|
|
377
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// A scripted ACP agent in plain JavaScript over raw ndjson (no SDK), for the Task 21 adoption tests: its turns are driven by
|
|
3
|
+
// marker files, so a test can let a turn run on while the daemon is gone, and prove from files what reached the agent. Run like
|
|
4
|
+
// `fakeAgent.ts`: `node -e "eval(readFileSync(argv[1]))" <this file> <config JSON>` (see `scriptAgentLaunch` in the test helpers).
|
|
5
|
+
//
|
|
6
|
+
// Config (all optional except `dir`):
|
|
7
|
+
// dir: string every marker file lives here
|
|
8
|
+
// sessionId?: string session/new's id (default 'S1'); session/load answers {} for any id
|
|
9
|
+
// chunksBefore?: string[] agent_message_chunk texts sent when a prompt arrives
|
|
10
|
+
// permission?: { id } then: session/request_permission with this JSON-RPC id; waits for its response
|
|
11
|
+
// cancelPermissionOn?: string while waiting: once this file exists, send `$/cancel_request{requestId}` (keep waiting)
|
|
12
|
+
// waitFor?: string then: wait for this file
|
|
13
|
+
// crashOn?: string while a prompt runs: once this file exists, the process exits 3 mid-turn (no end_turn)
|
|
14
|
+
// chunksAfter?: string[] then: more chunks; then the prompt answers {stopReason:'end_turn'}
|
|
15
|
+
// commandsAfterOpen?: object[] an available_commands_update right after session/new or session/load answers
|
|
16
|
+
// chunksAfterOpen?: string[] agent_message_chunk texts right after session/new answers (output outside any turn)
|
|
17
|
+
// mcpHttp?: boolean initialize advertises mcpCapabilities.http (a delegation MCP lease can be opened on it)
|
|
18
|
+
// stderrEnv?: string[] at start: for each env var, its value on stderr (raw, and JSON-escaped) — Task 23's sentinel
|
|
19
|
+
// stderrMcpHeaders?: boolean on session/new and session/load: every MCP header value received, on stderr (raw and JSON)
|
|
20
|
+
// version?: string initialize's agentInfo.version (default '1.0.0') (Task 24)
|
|
21
|
+
// noLoad?: boolean initialize does not advertise loadSession
|
|
22
|
+
// failLoad?: boolean session/load answers an error
|
|
23
|
+
// loadWaitFor?: string session/load waits for this file before it answers
|
|
24
|
+
// historyOnLoad?: string[] agent_message_chunk texts replayed (as history) before session/load answers
|
|
25
|
+
// configOptions?: object[] session/new and session/load answer these configOptions (load: as configured — the agent keeps
|
|
26
|
+
// no choice); session/set_config_option sets currentValue and answers the new list
|
|
27
|
+
// envOut?: string[] at start: these env vars' values into `env.json`
|
|
28
|
+
// initializeWaitFor?: string initialize waits for this file before it answers (Task 24 fix round 1)
|
|
29
|
+
// emitOn?: { file, text } once the session is open: when this file appears, one agent_message_chunk outside any turn
|
|
30
|
+
// chunkOnTerm?: string on SIGTERM: one agent_message_chunk (outside any turn), then exit (fix round 2)
|
|
31
|
+
// stderrOn?: { file, text } once the session is open: when this file appears, one stderr line
|
|
32
|
+
// exitAfterOpenOn?: string once the session is open (session/new or session/load answered): if this file exists, the
|
|
33
|
+
// process exits 3 shortly after — an adapter that dies on every respawn
|
|
34
|
+
//
|
|
35
|
+
// Files it writes: `stdin.jsonl` (every line received), `prompt-<n>` (prompt n arrived), `turn-<n>-ended` (its end_turn was written),
|
|
36
|
+
// `permission-response.json` (the raw
|
|
37
|
+
// response to its permission request), `cancel-received` (a session/cancel notification), `pid`.
|
|
38
|
+
const { appendFileSync, existsSync, writeFileSync } = require('node:fs');
|
|
39
|
+
const path = require('node:path');
|
|
40
|
+
const config = JSON.parse(process.argv[2] || '{}');
|
|
41
|
+
const at = (name) => path.join(config.dir, name);
|
|
42
|
+
writeFileSync(at('pid'), String(process.pid));
|
|
43
|
+
if (config.envOut)
|
|
44
|
+
writeFileSync(at('env.json'), JSON.stringify(Object.fromEntries(config.envOut.map((name) => [name, process.env[name] ?? null]))));
|
|
45
|
+
let configOptions = JSON.parse(JSON.stringify(config.configOptions ?? []));
|
|
46
|
+
let openSessionId;
|
|
47
|
+
if (config.chunkOnTerm) {
|
|
48
|
+
process.on('SIGTERM', () => {
|
|
49
|
+
if (openSessionId)
|
|
50
|
+
notify('session/update', { sessionId: openSessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: config.chunkOnTerm } } });
|
|
51
|
+
setTimeout(() => process.exit(0), 50);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
for (const name of config.stderrEnv ?? []) {
|
|
55
|
+
const value = process.env[name] ?? '';
|
|
56
|
+
process.stderr.write(`env ${name}=${value}\n`);
|
|
57
|
+
process.stderr.write(`env-json ${JSON.stringify({ [name]: value })}\n`);
|
|
58
|
+
}
|
|
59
|
+
const send = (message) => process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
|
|
60
|
+
const notify = (method, params) => send({ method, params });
|
|
61
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
62
|
+
const waitForFile = async (name) => { while (!existsSync(at(name)))
|
|
63
|
+
await sleep(20); };
|
|
64
|
+
const pending = new Map();
|
|
65
|
+
let prompts = 0;
|
|
66
|
+
async function prompt(id, params) {
|
|
67
|
+
prompts += 1;
|
|
68
|
+
writeFileSync(at(`prompt-${prompts}`), JSON.stringify(params));
|
|
69
|
+
const chunk = (text) => notify('session/update', { sessionId: params.sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } } });
|
|
70
|
+
for (const text of config.chunksBefore ?? []) {
|
|
71
|
+
chunk(text);
|
|
72
|
+
await sleep(5);
|
|
73
|
+
}
|
|
74
|
+
if (config.crashOn)
|
|
75
|
+
void waitForFile(config.crashOn).then(() => process.exit(3));
|
|
76
|
+
if (config.permission) {
|
|
77
|
+
const answered = new Promise((resolve) => pending.set(config.permission.id, resolve));
|
|
78
|
+
send({ id: config.permission.id, method: 'session/request_permission', params: {
|
|
79
|
+
sessionId: params.sessionId,
|
|
80
|
+
toolCall: { toolCallId: 'tool-1', title: 'Run the thing', kind: 'execute', status: 'pending' },
|
|
81
|
+
options: [{ optionId: 'allow', name: 'Allow', kind: 'allow_once' }, { optionId: 'deny', name: 'Deny', kind: 'reject_once' }],
|
|
82
|
+
} });
|
|
83
|
+
if (config.cancelPermissionOn) {
|
|
84
|
+
void (async () => {
|
|
85
|
+
await waitForFile(config.cancelPermissionOn);
|
|
86
|
+
if (pending.has(config.permission.id))
|
|
87
|
+
notify('$/cancel_request', { requestId: config.permission.id });
|
|
88
|
+
})();
|
|
89
|
+
}
|
|
90
|
+
const response = await answered;
|
|
91
|
+
writeFileSync(at('permission-response.json'), JSON.stringify(response));
|
|
92
|
+
}
|
|
93
|
+
if (config.waitFor)
|
|
94
|
+
await waitForFile(config.waitFor);
|
|
95
|
+
for (const text of config.chunksAfter ?? []) {
|
|
96
|
+
chunk(text);
|
|
97
|
+
await sleep(5);
|
|
98
|
+
}
|
|
99
|
+
send({ id, result: { stopReason: 'end_turn' } });
|
|
100
|
+
writeFileSync(at(`turn-${prompts}-ended`), '');
|
|
101
|
+
}
|
|
102
|
+
function onLine(line) {
|
|
103
|
+
appendFileSync(at('stdin.jsonl'), `${line}\n`);
|
|
104
|
+
let message;
|
|
105
|
+
try {
|
|
106
|
+
message = JSON.parse(line);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (message.method === undefined && message.id !== undefined) {
|
|
112
|
+
const resolve = pending.get(message.id);
|
|
113
|
+
if (resolve) {
|
|
114
|
+
pending.delete(message.id);
|
|
115
|
+
resolve(message);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
switch (message.method) {
|
|
120
|
+
case 'initialize': {
|
|
121
|
+
const answer = () => send({ id: message.id, result: { protocolVersion: 1, agentCapabilities: { loadSession: !config.noLoad, ...(config.mcpHttp ? { mcpCapabilities: { http: true } } : {}) }, authMethods: [],
|
|
122
|
+
agentInfo: { name: 'script-agent', version: config.version ?? '1.0.0' } } });
|
|
123
|
+
if (config.initializeWaitFor)
|
|
124
|
+
void waitForFile(config.initializeWaitFor).then(answer);
|
|
125
|
+
else
|
|
126
|
+
answer();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
case 'session/set_config_option': {
|
|
130
|
+
configOptions = configOptions.map((option) => (option.id === message.params.configId ? { ...option, currentValue: message.params.value } : option));
|
|
131
|
+
send({ id: message.id, result: { configOptions } });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
case 'session/load':
|
|
135
|
+
void (async () => {
|
|
136
|
+
if (config.loadWaitFor)
|
|
137
|
+
await waitForFile(config.loadWaitFor);
|
|
138
|
+
if (config.failLoad) {
|
|
139
|
+
send({ id: message.id, error: { code: -32002, message: 'Resource not found: session' } });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
for (const text of config.historyOnLoad ?? []) {
|
|
143
|
+
notify('session/update', { sessionId: message.params.sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } } });
|
|
144
|
+
}
|
|
145
|
+
openSession(message);
|
|
146
|
+
})();
|
|
147
|
+
return;
|
|
148
|
+
case 'session/new':
|
|
149
|
+
openSession(message);
|
|
150
|
+
return;
|
|
151
|
+
case 'session/prompt':
|
|
152
|
+
void prompt(message.id, message.params);
|
|
153
|
+
return;
|
|
154
|
+
case 'session/cancel':
|
|
155
|
+
writeFileSync(at('cancel-received'), line);
|
|
156
|
+
return;
|
|
157
|
+
default:
|
|
158
|
+
if (message.id !== undefined)
|
|
159
|
+
send({ id: message.id, error: { code: -32601, message: 'Method not found' } });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function openSession(message) {
|
|
163
|
+
const sessionId = message.method === 'session/new' ? (config.sessionId ?? 'S1') : message.params.sessionId;
|
|
164
|
+
if (config.stderrMcpHeaders) {
|
|
165
|
+
for (const server of message.params.mcpServers ?? []) {
|
|
166
|
+
for (const header of server.headers ?? []) {
|
|
167
|
+
process.stderr.write(`mcp header ${header.name}: ${header.value}\n`);
|
|
168
|
+
process.stderr.write(`mcp header json ${JSON.stringify(header)}\n`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const opened = config.configOptions ? { configOptions: JSON.parse(JSON.stringify(config.configOptions)) } : {};
|
|
173
|
+
if (config.configOptions)
|
|
174
|
+
configOptions = JSON.parse(JSON.stringify(config.configOptions));
|
|
175
|
+
send({ id: message.id, result: message.method === 'session/new' ? { sessionId, ...opened } : opened });
|
|
176
|
+
if (message.method === 'session/new') {
|
|
177
|
+
for (const text of config.chunksAfterOpen ?? []) {
|
|
178
|
+
notify('session/update', { sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } } });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
openSessionId = sessionId;
|
|
182
|
+
if (config.exitAfterOpenOn && existsSync(at(config.exitAfterOpenOn)))
|
|
183
|
+
setTimeout(() => process.exit(3), 50);
|
|
184
|
+
if (config.stderrOn)
|
|
185
|
+
void waitForFile(config.stderrOn.file).then(() => process.stderr.write(`${config.stderrOn.text}\n`));
|
|
186
|
+
if (config.emitOn) {
|
|
187
|
+
void waitForFile(config.emitOn.file).then(() => {
|
|
188
|
+
notify('session/update', { sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: config.emitOn.text } } });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (config.commandsAfterOpen) {
|
|
192
|
+
notify('session/update', { sessionId, update: { sessionUpdate: 'available_commands_update', availableCommands: config.commandsAfterOpen } });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let buffer = '';
|
|
196
|
+
process.stdin.on('data', (chunk) => {
|
|
197
|
+
buffer += chunk.toString('utf8');
|
|
198
|
+
for (let newline = buffer.indexOf('\n'); newline >= 0; newline = buffer.indexOf('\n')) {
|
|
199
|
+
const line = buffer.slice(0, newline);
|
|
200
|
+
buffer = buffer.slice(newline + 1);
|
|
201
|
+
if (line.trim())
|
|
202
|
+
onLine(line);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
process.stdin.on('end', () => process.exit(0));
|
|
206
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const eventOf = (frame) => ({
|
|
2
|
+
eventName: frame.eventName,
|
|
3
|
+
payload: frame.payload,
|
|
4
|
+
...(frame.acpSessionId !== undefined ? { acpSessionId: frame.acpSessionId } : {}),
|
|
5
|
+
});
|
|
6
|
+
/** `onEvent` (optional) sees each `agent_event` as it arrives — how a test answers cards the moment they are raised. */
|
|
7
|
+
export function watch(session, onEvent) {
|
|
8
|
+
const watched = { frames: [], events: [], turns: [],
|
|
9
|
+
activity: [], diagnostics: [] };
|
|
10
|
+
const waiters = [];
|
|
11
|
+
session.onFrames((frames) => {
|
|
12
|
+
for (const output of frames) {
|
|
13
|
+
watched.frames.push(output);
|
|
14
|
+
const frame = output.frame;
|
|
15
|
+
if (frame.t === 'agent_event') {
|
|
16
|
+
const event = eventOf(frame);
|
|
17
|
+
watched.events.push(event);
|
|
18
|
+
onEvent?.(event);
|
|
19
|
+
}
|
|
20
|
+
else if (frame.t === 'agent_session_state')
|
|
21
|
+
watched.activity.push(frame.raw);
|
|
22
|
+
else if (frame.t === 'agent_runtime_profile')
|
|
23
|
+
watched.diagnostics.push(frame.diagnostics);
|
|
24
|
+
else if (frame.t === 'agent_turn_done') {
|
|
25
|
+
watched.turns.push(frame);
|
|
26
|
+
for (const waiter of waiters.filter((w) => w.n <= watched.turns.length)) {
|
|
27
|
+
waiters.splice(waiters.indexOf(waiter), 1);
|
|
28
|
+
waiter.resolve(watched.turns[waiter.n - 1]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
...watched,
|
|
35
|
+
turn: (n = 1) => new Promise((resolve) => {
|
|
36
|
+
if (watched.turns.length >= n)
|
|
37
|
+
resolve(watched.turns[n - 1]);
|
|
38
|
+
else
|
|
39
|
+
waiters.push({ n, resolve });
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -25,9 +25,11 @@ import { execFile } from 'node:child_process';
|
|
|
25
25
|
* `binaryInstall.ts`'s zip-extraction fallback — keep working unchanged; it is `undefined` on a
|
|
26
26
|
* timeout, which is never `ENOENT`.
|
|
27
27
|
*/
|
|
28
|
-
export function runTimedCommand(command, args, timeoutMs) {
|
|
28
|
+
export function runTimedCommand(command, args, timeoutMs, options = {}) {
|
|
29
29
|
return new Promise((resolve) => {
|
|
30
|
-
execFile(command, args, {
|
|
30
|
+
execFile(command, args, {
|
|
31
|
+
encoding: 'utf8', timeout: timeoutMs, windowsHide: true, maxBuffer: 16 * 1024 * 1024, ...(options.env ? { env: options.env } : {}),
|
|
32
|
+
}, (error, stdout) => {
|
|
31
33
|
if (!error) {
|
|
32
34
|
resolve({ ok: true, stdout: stdout.toString() });
|
|
33
35
|
return;
|