@platless/axom-cli 0.3.13 → 0.6.36
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 +18 -5
- package/dist/axom-patch/cli/README.md +119 -9
- 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 +123 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -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/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +194 -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/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +39 -15
- package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/zcode-models.json +37 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
- package/dist/cli.js +4 -9
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- 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 +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2192 -636
- package/dist/workstation/acp/acpClient.js +84 -495
- 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 +324 -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 +290 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +239 -159
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +60 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +78 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +282 -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 +105 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +496 -0
- package/dist/workstation/acp/processor/state.js +67 -0
- package/dist/workstation/acp/processor/step.js +442 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +58 -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 +390 -0
- package/dist/workstation/acp/processorSession.js +1348 -0
- package/dist/workstation/acp/registry/catalog.js +189 -41
- package/dist/workstation/acp/registry/launch.js +76 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +111 -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/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +295 -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/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +135 -0
- package/dist/workstation/acp/test/realAdapter.js +331 -0
- package/dist/workstation/acp/test/scriptAgent.js +214 -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 +1289 -544
- 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 +172 -8
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +393 -12
- package/dist/workstation/daemonctl.js +103 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -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/credentials.js +142 -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} +152 -39
- 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 +124 -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 +289 -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 +76 -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/managedInstall.js +57 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +50 -19
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +141 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +154 -93
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- 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/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +11 -8
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/qoderStartup.js +0 -45
- 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/backgroundProcess.js +0 -106
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
- package/dist/workstation/qoderAdapter/history.js +0 -87
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -93
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -77
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
|
@@ -1,499 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { AgentSpawnError } from './acpClient.js';
|
|
3
|
-
import { AGENT_FRAME_SHAPES, decodeAgentFrame, encodeAgentFrame } from '../protocol/agent.js';
|
|
4
|
-
import { validateWireEvent } from './validate.js';
|
|
5
|
-
/**
|
|
6
|
-
* Default wait for output, deliberately under vitest's 5s default test timeout: whichever fires
|
|
7
|
-
* first writes the failure message, and "timed out waiting for <what>" is far more useful than
|
|
8
|
-
* vitest's generic test timeout.
|
|
9
|
-
*/
|
|
10
|
-
export const DEFAULT_CONTRACT_TIMEOUT_MS = 4_000;
|
|
11
|
-
/** Multiplier on the measured turn duration used as the quiet window in the buffering check. */
|
|
12
|
-
const BUFFERING_QUIET_FACTOR = 3;
|
|
13
|
-
/** Floor for that window, so a synchronous sample (turn duration ≈ 0) still gets a real one. */
|
|
14
|
-
const BUFFERING_QUIET_FLOOR_MS = 25;
|
|
15
|
-
const TURN_STOP_REASONS = new Set([
|
|
16
|
-
'end_turn', 'max_tokens', 'max_turns', 'refusal', 'interrupted', 'superseded', 'expired', 'orphaned',
|
|
17
|
-
]);
|
|
18
|
-
function turnResultViolation(result) {
|
|
19
|
-
if (!['completed', 'interrupted', 'failed'].includes(result.status)) {
|
|
20
|
-
return `turn result has invalid status '${String(result.status)}'`;
|
|
21
|
-
}
|
|
22
|
-
if (result.stopReason !== undefined && !TURN_STOP_REASONS.has(result.stopReason)) {
|
|
23
|
-
return `turn result has invalid stopReason '${String(result.stopReason)}'`;
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
/** Registers both sinks the way `agentSessions.ts` does: `onEvent` first, then `onTurnDone`. */
|
|
28
|
-
function record(session) {
|
|
29
|
-
const events = [];
|
|
30
|
-
const turns = [];
|
|
31
|
-
session.onEvent((e) => events.push(e));
|
|
32
|
-
session.onTurnDone((r) => turns.push(r));
|
|
33
|
-
return { events, turns };
|
|
34
|
-
}
|
|
35
|
-
function timeoutOf(hooks) {
|
|
36
|
-
return hooks.timeoutMs ?? DEFAULT_CONTRACT_TIMEOUT_MS;
|
|
37
|
-
}
|
|
38
|
-
function sleep(ms) {
|
|
39
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
-
}
|
|
41
|
-
async function waitFor(hooks, what, predicate) {
|
|
42
|
-
const deadline = Date.now() + timeoutOf(hooks);
|
|
43
|
-
while (!predicate()) {
|
|
44
|
-
if (Date.now() >= deadline)
|
|
45
|
-
throw new Error(`timed out waiting for ${what}`);
|
|
46
|
-
await sleep(5);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Opens a session, runs the body, and always disposes.
|
|
51
|
-
*
|
|
52
|
-
* `session.close()` is not called here: it is clause 6's own subject, and a session that cannot
|
|
53
|
-
* close should fail that check alone rather than smear an error across every other one. `dispose()`
|
|
54
|
-
* is what actually reclaims the subprocess, and its failures are swallowed for the same reason.
|
|
55
|
-
*/
|
|
56
|
-
async function withSession(hooks, open, body) {
|
|
57
|
-
const target = await open();
|
|
58
|
-
try {
|
|
59
|
-
await body(target.session);
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
try {
|
|
63
|
-
await target.dispose();
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// teardown is not any clause's subject
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function requireHook(hooks, name) {
|
|
71
|
-
const hook = hooks[name];
|
|
72
|
-
// Only reachable if a caller invoked a gated check directly, or the skip branch in
|
|
73
|
-
// runAcpContract was removed. Throwing keeps that from reading as a pass.
|
|
74
|
-
if (!hook)
|
|
75
|
-
throw new Error(`the '${name}' hook is required for this check`);
|
|
76
|
-
return hook;
|
|
77
|
-
}
|
|
78
|
-
function isPermissionCard(e) {
|
|
79
|
-
return e.eventName === 'permission_request';
|
|
80
|
-
}
|
|
81
|
-
/** Names an event for a failure message without trusting it to be well-formed. */
|
|
82
|
-
function nameOf(e) {
|
|
83
|
-
try {
|
|
84
|
-
return String(e?.eventName);
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return '<uninspectable>';
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function describeRejection(what, e) {
|
|
91
|
-
const rejection = validateWireEvent(e);
|
|
92
|
-
if (!rejection)
|
|
93
|
-
return null;
|
|
94
|
-
return `${what} '${nameOf(e)}' violates the wire contract: ${rejection.reason}${rejection.detail ? ` (${rejection.detail})` : ''}`;
|
|
95
|
-
}
|
|
96
|
-
// --- the checks ---------------------------------------------------------------------------------
|
|
97
|
-
/**
|
|
98
|
-
* Every clause, in the deleted kit's own order so the two can be read side by side.
|
|
99
|
-
*
|
|
100
|
-
* Two of that kit's ten checks are gone, and one is here with a different assertion:
|
|
101
|
-
*
|
|
102
|
-
* - `probe-total` (clause 1, "`probe()` reports availability instead of throwing", plus
|
|
103
|
-
* `AgentBridge.type` being a non-empty string) is **dropped**. Neither subject exists: ACP has no
|
|
104
|
-
* `probe`, and an agent's identity is now a registry id the client is handed rather than a string
|
|
105
|
-
* a bridge declares about itself. More than that, `AcpAgents.verify` deliberately *does* throw
|
|
106
|
-
* when the agent cannot be launched — the clause is not merely unimplemented, it is inverted on
|
|
107
|
-
* purpose (see `acpAgents.ts`'s "Two outcomes, and neither of them is 'not authenticated'").
|
|
108
|
-
* - `probe-reports-unavailable` (clause 1) survives as {@link unavailableAgentIsDiagnosable}, with a
|
|
109
|
-
* **new assertion**. The clause it encoded — an absent binary must stay distinguishable from a
|
|
110
|
-
* transient failure, because nothing above this layer can tell them apart — still holds exactly.
|
|
111
|
-
* The mechanism inverted: `{available: false}` became a typed `AgentSpawnError`, and it is that
|
|
112
|
-
* typing, not a boolean, that keeps "not installed" from collapsing into a generic failure.
|
|
113
|
-
*/
|
|
114
|
-
export const acpContractChecks = [
|
|
115
|
-
{
|
|
116
|
-
id: 'unavailable-agent-is-diagnosable',
|
|
117
|
-
clause: 1,
|
|
118
|
-
title: 'an agent that is not installed fails with a typed, actionable error',
|
|
119
|
-
requires: 'connectUnavailableAgent',
|
|
120
|
-
run: async (hooks) => {
|
|
121
|
-
const connect = requireHook(hooks, 'connectUnavailableAgent');
|
|
122
|
-
let outcome;
|
|
123
|
-
try {
|
|
124
|
-
outcome = { ok: true, value: await connect() };
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
outcome = { ok: false, error };
|
|
128
|
-
}
|
|
129
|
-
// Resolving is the failure mode this catches, and it is the subtle one: a client that reports
|
|
130
|
-
// a missing agent as connected sends every later failure to the wrong layer, and the user gets
|
|
131
|
-
// "did not open a session" for an agent that was never on the machine.
|
|
132
|
-
expect(outcome.ok, 'connecting to an agent that is not installed must fail, not resolve').toBe(false);
|
|
133
|
-
const error = outcome.error;
|
|
134
|
-
// Typed, not merely thrown. `AcpAgents.verify` catches around `AcpSession.open` and turns
|
|
135
|
-
// failures into an inconclusive `sessionError`; only the launch failure being its own class
|
|
136
|
-
// keeps "not installed" from being answered as "installed, but something went wrong".
|
|
137
|
-
expect(error instanceof AgentSpawnError, `a not-installed agent must reject with AgentSpawnError, got ${error instanceof Error ? error.constructor.name : typeof error}: ${String(error)}`).toBe(true);
|
|
138
|
-
expect(String(error.message).length, 'the rejection must carry a message a user can act on').toBeGreaterThan(0);
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
id: 'pre-registration-buffering',
|
|
143
|
-
clause: 2,
|
|
144
|
-
title: 'events produced before onEvent/onTurnDone are buffered and flushed on registration',
|
|
145
|
-
requires: null,
|
|
146
|
-
run: async (hooks) => {
|
|
147
|
-
// `agentSessions.ts` registers its sinks immediately *after* the session resolves, so anything
|
|
148
|
-
// produced during or right after open lands with no callback attached.
|
|
149
|
-
//
|
|
150
|
-
// Driving-then-registering is not on its own enough to prove buffering: `driveOneTurn`
|
|
151
|
-
// resolves long before the agent says anything, so registration wins the race and the events
|
|
152
|
-
// arrive live. The check would then pass for a client that buffers nothing at all.
|
|
153
|
-
//
|
|
154
|
-
// So: measure how long a turn takes when observed live, then drive a second turn with no sink
|
|
155
|
-
// attached and wait past that duration before registering. By then the turn is over. Anything
|
|
156
|
-
// that still arrives was held; an implementation that drops has nothing left to deliver and
|
|
157
|
-
// times out.
|
|
158
|
-
let turnMs = 0;
|
|
159
|
-
await withSession(hooks, hooks.openSession, async (session) => {
|
|
160
|
-
const seen = record(session);
|
|
161
|
-
const started = Date.now();
|
|
162
|
-
await hooks.driveOneTurn(session);
|
|
163
|
-
await waitFor(hooks, 'a baseline turn to produce an event (with the sinks already registered)', () => seen.events.length > 0);
|
|
164
|
-
await waitFor(hooks, 'a baseline turn to finish (with the sinks already registered)', () => seen.turns.length > 0);
|
|
165
|
-
turnMs = Date.now() - started;
|
|
166
|
-
});
|
|
167
|
-
const quietMs = Math.max(BUFFERING_QUIET_FLOOR_MS, turnMs * BUFFERING_QUIET_FACTOR);
|
|
168
|
-
if (quietMs > timeoutOf(hooks)) {
|
|
169
|
-
// Deliberately loud rather than clamped to the timeout. Clamping still "works" — the
|
|
170
|
-
// baseline finished inside timeoutMs, so the window would stay longer than a turn — but it
|
|
171
|
-
// shrinks the margin towards 1x precisely when turns are slow, and says nothing while doing
|
|
172
|
-
// it. That is a silent degradation of the check itself, which is what this refuses.
|
|
173
|
-
throw new Error(`a baseline turn took ${turnMs}ms, so this check needs a ${quietMs}ms quiet window, but ` +
|
|
174
|
-
`timeoutMs is ${timeoutOf(hooks)}ms. Raise timeoutMs above ${quietMs} (and vitest's own ` +
|
|
175
|
-
`testTimeout past it) so the buffering check has a real window rather than one barely ` +
|
|
176
|
-
`longer than a turn.`);
|
|
177
|
-
}
|
|
178
|
-
const dropped = (what) => `${what} to be flushed on registration — the turn was driven and left to run for ${quietMs}ms ` +
|
|
179
|
-
`(a live turn took ${turnMs}ms) with no sink attached, so an implementation that drops instead ` +
|
|
180
|
-
`of buffering has already discarded it`;
|
|
181
|
-
await withSession(hooks, hooks.openSession, async (session) => {
|
|
182
|
-
await hooks.driveOneTurn(session);
|
|
183
|
-
await sleep(quietMs);
|
|
184
|
-
const seen = record(session);
|
|
185
|
-
await waitFor(hooks, dropped('events produced before registration'), () => seen.events.length > 0);
|
|
186
|
-
await waitFor(hooks, dropped('the turn result produced before registration'), () => seen.turns.length > 0);
|
|
187
|
-
});
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
id: 'events-valid',
|
|
192
|
-
clause: 3,
|
|
193
|
-
title: 'every emitted event passes validateWireEvent',
|
|
194
|
-
requires: null,
|
|
195
|
-
run: async (hooks) => {
|
|
196
|
-
await withSession(hooks, hooks.openSession, async (session) => {
|
|
197
|
-
const seen = record(session);
|
|
198
|
-
await hooks.driveOneTurn(session);
|
|
199
|
-
await waitFor(hooks, 'the driven turn to produce at least one event', () => seen.events.length > 0);
|
|
200
|
-
await waitFor(hooks, 'the driven turn to finish', () => seen.turns.length > 0);
|
|
201
|
-
for (const e of seen.events) {
|
|
202
|
-
// Encoded as a string so the assertion message names the offending event and reason;
|
|
203
|
-
// a bare `toBeNull()` on the rejection object reads as "expected {…} to be null".
|
|
204
|
-
expect(describeRejection('event', e)).toBeNull();
|
|
205
|
-
}
|
|
206
|
-
for (const turn of seen.turns) {
|
|
207
|
-
expect(turnResultViolation(turn)).toBeNull();
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
id: 'no-unknown-request-events',
|
|
214
|
-
clause: 3,
|
|
215
|
-
title: 'no request-shaped event other than permission_request is emitted',
|
|
216
|
-
requires: null,
|
|
217
|
-
run: async (hooks) => {
|
|
218
|
-
// Split out of `events-valid` on purpose, even though validateWireEvent rejects these too:
|
|
219
|
-
// this is the clause an implementer is most likely to trip while doing something reasonable.
|
|
220
|
-
// Under ACP the temptation is concrete and sitting in the code already — `permission.ts`
|
|
221
|
-
// declines every `elicitation/create`, and "just emit an `elicitation_request`" is the
|
|
222
|
-
// obvious-looking next step. Since Plan B it is *more* tempting, not less: the platform now
|
|
223
|
-
// owns that event name for its own agents' questions. It is still unanswerable from a bridge —
|
|
224
|
-
// `TimelineEventWire.decode` has no branch for it, so the server drops the event and advances
|
|
225
|
-
// its watermark past it, it can never even be replayed, and the agent waits forever. A named
|
|
226
|
-
// check says that in the failure message instead of leaving it inside a generic one.
|
|
227
|
-
await withSession(hooks, hooks.openSession, async (session) => {
|
|
228
|
-
const seen = record(session);
|
|
229
|
-
await hooks.driveOneTurn(session);
|
|
230
|
-
await waitFor(hooks, 'the driven turn to produce at least one event', () => seen.events.length > 0);
|
|
231
|
-
await waitFor(hooks, 'the driven turn to finish', () => seen.turns.length > 0);
|
|
232
|
-
const offenders = seen.events
|
|
233
|
-
.map(nameOf)
|
|
234
|
-
.filter((name) => (name.endsWith('_request') || name.endsWith('_requested')) && name !== 'permission_request');
|
|
235
|
-
expect(offenders, 'only permission_request may carry a request suffix — the platform has no way to answer ' +
|
|
236
|
-
'any other request-shaped event, and the server drops it. `_requested` is the retired ' +
|
|
237
|
-
'spelling and is caught here too, so an unupgraded bridge fails loudly rather than ' +
|
|
238
|
-
'having its approval cards silently land as generic items').toEqual([]);
|
|
239
|
-
});
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
id: 'permission-card-answerable',
|
|
244
|
-
clause: 3,
|
|
245
|
-
title: 'permission_request cards are well-formed and answerable',
|
|
246
|
-
requires: 'drivePermission',
|
|
247
|
-
run: async (hooks) => {
|
|
248
|
-
const drivePermission = requireHook(hooks, 'drivePermission');
|
|
249
|
-
await withSession(hooks, hooks.openPermissionSession ?? hooks.openSession, async (session) => {
|
|
250
|
-
const seen = record(session);
|
|
251
|
-
await drivePermission(session);
|
|
252
|
-
await waitFor(hooks, 'a permission_request event', () => seen.events.some(isPermissionCard));
|
|
253
|
-
// An option-less card is the one that hurts: the session parks on a prompt the user has no
|
|
254
|
-
// button to answer, and Stop becomes the only way out. Under ACP the options are the
|
|
255
|
-
// *agent's* own (`permission.ts` passes them through rather than inventing a vocabulary), so
|
|
256
|
-
// this clause is now a statement about what this client refuses to relay.
|
|
257
|
-
for (const card of seen.events.filter(isPermissionCard)) {
|
|
258
|
-
expect(describeRejection('permission card', card)).toBeNull();
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
id: 'stale-answer-tolerated',
|
|
265
|
-
clause: 4,
|
|
266
|
-
title: 'answer() tolerates an unknown requestId',
|
|
267
|
-
requires: null,
|
|
268
|
-
run: async (hooks) => {
|
|
269
|
-
await withSession(hooks, hooks.openSession, async (session) => {
|
|
270
|
-
const seen = record(session);
|
|
271
|
-
// The server synthesizes a deny for the outstanding card when the user sends a plain
|
|
272
|
-
// message instead of answering it — by then the client may have forgotten the id entirely.
|
|
273
|
-
expect(() => session.answer('contract-never-issued-request-id', 'deny'), 'answer() with an unknown requestId must not throw').not.toThrow();
|
|
274
|
-
// …and must not have wedged the session either.
|
|
275
|
-
await hooks.driveOneTurn(session);
|
|
276
|
-
await waitFor(hooks, 'the session to keep working after a stale answer', () => seen.turns.length > 0);
|
|
277
|
-
});
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
{
|
|
281
|
-
id: 'superseded-answer-tolerated',
|
|
282
|
-
clause: 4,
|
|
283
|
-
title: 'answer() tolerates a requestId that was already answered',
|
|
284
|
-
requires: 'drivePermission',
|
|
285
|
-
run: async (hooks) => {
|
|
286
|
-
const drivePermission = requireHook(hooks, 'drivePermission');
|
|
287
|
-
await withSession(hooks, hooks.openPermissionSession ?? hooks.openSession, async (session) => {
|
|
288
|
-
const seen = record(session);
|
|
289
|
-
await drivePermission(session);
|
|
290
|
-
await waitFor(hooks, 'a permission_request event', () => seen.events.some(isPermissionCard));
|
|
291
|
-
const card = seen.events.find(isPermissionCard);
|
|
292
|
-
const requestId = String(card?.payload?.requestId ?? '');
|
|
293
|
-
expect(requestId, 'permission_request must carry a requestId').not.toBe('');
|
|
294
|
-
const options = Array.isArray(card?.payload?.options) ? card.payload.options : [];
|
|
295
|
-
const outcome = typeof options[0]?.optionId === 'string' ? String(options[0].optionId) : 'deny';
|
|
296
|
-
expect(() => session.answer(requestId, outcome), 'answering an outstanding card must not throw').not.toThrow();
|
|
297
|
-
// The same id a second time: the card is settled and the client has dropped it, but the
|
|
298
|
-
// server can still send a synthesized deny for it.
|
|
299
|
-
expect(() => session.answer(requestId, 'deny'), 'answer() for an already-settled requestId must not throw').not.toThrow();
|
|
300
|
-
});
|
|
301
|
-
},
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
id: 'interrupt-ends-turn',
|
|
305
|
-
clause: 5,
|
|
306
|
-
title: "interrupt() ends the turn with status 'interrupted'",
|
|
307
|
-
// The deleted per-bridge suite's equivalent clause ran unconditionally, against every bridge. It
|
|
308
|
-
// now runs only when a hook can force a real in-flight turn, and `realAdapterHooks()` (this
|
|
309
|
-
// file's real-adapter describe block, below) deliberately doesn't provide `openHangingSession` —
|
|
310
|
-
// see its comment — so against a real ACP adapter this clause's coverage is not reduced, it is
|
|
311
|
-
// zero (review finding, task-11 fix round 1, finding 3).
|
|
312
|
-
requires: 'openHangingSession',
|
|
313
|
-
run: async (hooks) => {
|
|
314
|
-
const openHanging = requireHook(hooks, 'openHangingSession');
|
|
315
|
-
await withSession(hooks, openHanging, async (session) => {
|
|
316
|
-
const seen = record(session);
|
|
317
|
-
// Driven with prompt() against a session whose turn will not finish on its own: a turn that
|
|
318
|
-
// already completed cannot be interrupted, so against an ordinary session the check would
|
|
319
|
-
// prove nothing. The assertion is on the *last* result, so an early 'completed' is a failure
|
|
320
|
-
// rather than something the check quietly accepts.
|
|
321
|
-
session.prompt([{ type: 'text', text: 'contract: start a turn that will be interrupted' }]);
|
|
322
|
-
await session.interrupt();
|
|
323
|
-
await waitFor(hooks, 'a turn result after interrupt()', () => seen.turns.length > 0);
|
|
324
|
-
const last = seen.turns[seen.turns.length - 1];
|
|
325
|
-
expect(last?.status, 'the turn after interrupt() must end as interrupted').toBe('interrupted');
|
|
326
|
-
});
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
id: 'close-idempotent',
|
|
331
|
-
clause: 6,
|
|
332
|
-
title: 'close() is idempotent',
|
|
333
|
-
requires: null,
|
|
334
|
-
run: async (hooks) => {
|
|
335
|
-
const target = await hooks.openSession();
|
|
336
|
-
try {
|
|
337
|
-
await target.session.close();
|
|
338
|
-
// `agentSessions.ts` can close a session a transport drop already tore down; a second close
|
|
339
|
-
// must be a no-op, not a rejection that surfaces as a spurious daemon error.
|
|
340
|
-
await target.session.close();
|
|
341
|
-
}
|
|
342
|
-
finally {
|
|
343
|
-
await target.dispose().catch(() => undefined);
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
id: 'every-prompt-gets-a-turn-result',
|
|
349
|
-
clause: 7,
|
|
350
|
-
title: 'two overlapping prompts each report their own terminal turn result',
|
|
351
|
-
requires: null,
|
|
352
|
-
run: async (hooks) => {
|
|
353
|
-
// New in ACP, and pinning a real defect rather than a hypothetical one.
|
|
354
|
-
//
|
|
355
|
-
// `AcpSession.prompt`'s own doc comment lists an overlapping call as an expected case, and
|
|
356
|
-
// ACP itself allows it — the SDK's `ActiveSession.prompt` enqueues a `stop` message per call,
|
|
357
|
-
// so two prompts really do produce two completions on the read queue. Everything downstream
|
|
358
|
-
// is built on one result per prompt: each `prompt()` call pushes its own entry onto
|
|
359
|
-
// `AcpSession.inFlightTurns`, `finishTurn` pops exactly one entry per completion it sees (not
|
|
360
|
-
// "the" entry, "an" entry — see its own doc), and `agentSessions.ts`'s `onAgentTurnDone`
|
|
361
|
-
// fires once per `finishTurn` call and emits one `agent_turn_done` frame each time (2026-09-21
|
|
362
|
-
// rewrite; there is no longer a `tasks.agentTurnStarted`/`agentTurnFinished` bracket in the
|
|
363
|
-
// middle of that path, but the one-result-per-prompt pairing itself is unchanged — it was
|
|
364
|
-
// always `inFlightTurns`' push/pop symmetry, not the bracket, that guaranteed it). The
|
|
365
|
-
// server's run stays parked until a `turn_done` arrives, and `agent_message` — the event that
|
|
366
|
-
// flushes the server's delta buffer and attaches the turn's usage figures — is synthesized
|
|
367
|
-
// only as a turn ends. A dropped second result therefore does not merely lose a status line:
|
|
368
|
-
// it strands the second turn's text and parks the run indefinitely.
|
|
369
|
-
//
|
|
370
|
-
// Driven through `driveOneTurn` twice concurrently rather than two bare `prompt()` calls so a
|
|
371
|
-
// real adapter's hook (which may need to say something specific to get output) is what decides
|
|
372
|
-
// how a turn is started.
|
|
373
|
-
await withSession(hooks, hooks.openSession, async (session) => {
|
|
374
|
-
const seen = record(session);
|
|
375
|
-
await Promise.all([hooks.driveOneTurn(session), hooks.driveOneTurn(session)]);
|
|
376
|
-
await waitFor(hooks, 'both overlapping turns to report a terminal result — a second turn whose result never ' +
|
|
377
|
-
'arrives leaves the platform parked on a run that has already finished', () => seen.turns.length >= 2);
|
|
378
|
-
for (const turn of seen.turns) {
|
|
379
|
-
expect(turnResultViolation(turn)).toBeNull();
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
|
-
];
|
|
385
|
-
/**
|
|
386
|
-
* Which checks {@link runAcpContract} will run and which it will skip, for these hooks.
|
|
387
|
-
*
|
|
388
|
-
* Extracted so "skipped, never silently passed" is assertable. Left inline in the `describe` body,
|
|
389
|
-
* the decision lives only in a branch whose failure mode — a case that runs an empty body and
|
|
390
|
-
* reports green — no assertion can see.
|
|
391
|
-
*/
|
|
392
|
-
export function acpContractRunPlan(hooks) {
|
|
393
|
-
return acpContractChecks.map((check) => ({
|
|
394
|
-
id: check.id,
|
|
395
|
-
skipped: check.requires !== null && hooks[check.requires] === undefined,
|
|
396
|
-
}));
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Clause 8 — every field the agent channel treats as required must actually be enforced by the
|
|
400
|
-
* decoder, for every frame type.
|
|
401
|
-
*
|
|
402
|
-
* ### Why this is one general clause rather than an assertion per field
|
|
403
|
-
*
|
|
404
|
-
* Task 10a shipped `agent_authenticate` with no guard on `methodId`. The frame decoded cleanly, and
|
|
405
|
-
* `AcpAuth.authenticate(undefined)` ran all the way to the agent. The follow-up review then
|
|
406
|
-
* enumerated every field of the two new frames by hand and found nothing else — a real answer with
|
|
407
|
-
* a one-release shelf life, because the next frame added gets no such treatment.
|
|
408
|
-
*
|
|
409
|
-
* ### Where "required" comes from, and why it must not come from the guard table
|
|
410
|
-
*
|
|
411
|
-
* The obvious implementation — read the field names off `AGENT_FRAME_SHAPES` and check each one is
|
|
412
|
-
* enforced — is circular, and measurably so: deleting `methodId` from that table leaves such a check
|
|
413
|
-
* green, because the check stops looking for exactly the field the guard stopped guarding. That is
|
|
414
|
-
* the original bug reproduced, not caught.
|
|
415
|
-
*
|
|
416
|
-
* So required-ness is taken from a **second, independent declaration**: the `AgentFrame` union.
|
|
417
|
-
* `samples` is typed as `AgentFrame`, so TypeScript will not accept an entry missing a non-optional
|
|
418
|
-
* field, and the samples are required to carry *only* those fields. A sample's own keys are therefore
|
|
419
|
-
* the union's mandatory field list, arrived at without consulting the guard table at all. This
|
|
420
|
-
* function then asserts the two agree — set equality, so a guard the table forgot and a field a
|
|
421
|
-
* sample invented are both failures — before asserting the decoder actually rejects each one.
|
|
422
|
-
*
|
|
423
|
-
* Hook-free, unlike every clause above, which is why it is its own exported function rather than an
|
|
424
|
-
* entry in {@link acpContractChecks}: it has no session to drive. The samples come from the caller
|
|
425
|
-
* because a valid instance of each frame is test data, not something a type can synthesize.
|
|
426
|
-
*/
|
|
427
|
-
export function assertRequiredFieldsEnforced(samples,
|
|
428
|
-
/**
|
|
429
|
-
* The decoder under test. Defaults to the real one; parameterized only so `contract.test.ts` can
|
|
430
|
-
* hand in a deliberately lenient decoder and assert this function rejects it. Without that, "the
|
|
431
|
-
* clause holds" and "the clause is unfalsifiable" look identical from the outside.
|
|
432
|
-
*/
|
|
433
|
-
decode = decodeAgentFrame) {
|
|
434
|
-
// Coverage first: without this, a frame type with no sample contributes no assertions at all and
|
|
435
|
-
// the whole clause passes vacuously for it.
|
|
436
|
-
expect(Object.keys(samples).sort(), 'every agent frame type needs a valid sample here, or its required fields go unchecked').toEqual(Object.keys(AGENT_FRAME_SHAPES).sort());
|
|
437
|
-
// Every sample is vetted before any of them is mutilated, rather than vetting and mutilating one
|
|
438
|
-
// frame at a time. Interleaved, a rotten sample late in the map would only be reported *after*
|
|
439
|
-
// some earlier frame's deletion had already failed for an unrelated reason, and the message a
|
|
440
|
-
// reader gets would name the wrong frame.
|
|
441
|
-
const requiredByType = new Map();
|
|
442
|
-
for (const [type, sample] of Object.entries(samples)) {
|
|
443
|
-
expect(sample.t, `the sample filed under '${type}' must be a ${type} frame`).toBe(type);
|
|
444
|
-
// The sample itself has to decode, or every deletion below "fails" for the wrong reason and the
|
|
445
|
-
// clause would pass while proving nothing.
|
|
446
|
-
expect(() => decode(encodeAgentFrame(sample)), `the sample ${type} frame must itself decode`).not.toThrow();
|
|
447
|
-
// The union's mandatory fields, read off an instance the type checker vouched for. `t` is the
|
|
448
|
-
// discriminator rather than a payload field, and `decodeChannelFrame` rejects an unknown one
|
|
449
|
-
// before any guard runs, so it is not this clause's subject.
|
|
450
|
-
const fromUnion = Object.keys(sample).filter((key) => key !== 't').sort();
|
|
451
|
-
expect(fromUnion.length, `the sample ${type} frame carries no fields, so it checks nothing`).toBeGreaterThan(0);
|
|
452
|
-
expect(
|
|
453
|
-
// Cast, not a narrowed lookup: `type` is `samples`'s own `string` key, but the coverage
|
|
454
|
-
// assertion just above (line 608-610) already proved it is one of `AGENT_FRAME_SHAPES`'s keys
|
|
455
|
-
// before this loop ever runs.
|
|
456
|
-
Object.keys(AGENT_FRAME_SHAPES[type].required).sort(), `${type}'s guard table and its AgentFrame union member disagree about which fields are ` +
|
|
457
|
-
'required. Either the table is missing a guard for a field the type says is mandatory ' +
|
|
458
|
-
'(the shape `agent_authenticate.methodId` shipped in), or this sample carries an optional ' +
|
|
459
|
-
'field it should not — samples here must carry required fields and nothing else').toEqual(fromUnion);
|
|
460
|
-
requiredByType.set(type, fromUnion);
|
|
461
|
-
}
|
|
462
|
-
for (const [type, sample] of Object.entries(samples)) {
|
|
463
|
-
for (const field of requiredByType.get(type)) {
|
|
464
|
-
const mutilated = { ...sample };
|
|
465
|
-
delete mutilated[field];
|
|
466
|
-
expect(() => decode(JSON.stringify(mutilated)), `${type} must be rejected when required field '${field}' is missing — the Kotlin twin cannot ` +
|
|
467
|
-
'decode it, and a frame that passes here and fails there is a silent drop on the far side').toThrow();
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
/**
|
|
472
|
-
* Run the whole contract suite. Call at the top level of a test file:
|
|
473
|
-
*
|
|
474
|
-
* ```ts
|
|
475
|
-
* runAcpContract('ACP client over the in-repo fake agent', { openSession: …, driveOneTurn: … });
|
|
476
|
-
* ```
|
|
477
|
-
*
|
|
478
|
-
* Cases needing an optional hook are skipped — visibly, as skipped — when it is absent.
|
|
479
|
-
*/
|
|
480
|
-
export function runAcpContract(name, hooks) {
|
|
481
|
-
const plan = new Map(acpContractRunPlan(hooks).map((entry) => [entry.id, entry.skipped]));
|
|
482
|
-
describe(name, () => {
|
|
483
|
-
for (const check of acpContractChecks) {
|
|
484
|
-
const label = `clause ${check.clause}: ${check.title}`;
|
|
485
|
-
if (plan.get(check.id)) {
|
|
486
|
-
// The real body, not an empty one. Never executed while skipped — but the instant `.skip`
|
|
487
|
-
// comes off, the check runs, hits its missing hook and goes red. An empty body would go
|
|
488
|
-
// green and leave the run plan still reporting it as skipped, so nothing would notice.
|
|
489
|
-
it.skip(label, async () => {
|
|
490
|
-
await check.run(hooks);
|
|
491
|
-
}, hooks.vitestTimeoutMs);
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
it(label, async () => {
|
|
495
|
-
await check.run(hooks);
|
|
496
|
-
}, hooks.vitestTimeoutMs);
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export function buildSessionMcpServers(userServers = [], lease) {
|
|
2
|
-
if (!lease)
|
|
3
|
-
return [...userServers];
|
|
4
|
-
const fail = () => { throw new Error('Delegation MCP configuration is unavailable'); };
|
|
5
|
-
let url;
|
|
6
|
-
try {
|
|
7
|
-
url = new URL(lease.url);
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return fail();
|
|
11
|
-
}
|
|
12
|
-
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash ||
|
|
13
|
-
url.pathname !== '/mcp/delegation' || !Number.isSafeInteger(lease.expiresAtEpochMs) ||
|
|
14
|
-
lease.expiresAtEpochMs <= Date.now() || userServers.some((server) => server.name === 'axom-delegation'))
|
|
15
|
-
return fail();
|
|
16
|
-
const headers = Object.entries(lease.headers);
|
|
17
|
-
if (headers.length !== 1 || headers[0][0].toLowerCase() !== 'authorization' ||
|
|
18
|
-
!/^Bearer [A-Za-z0-9_-]+$/.test(headers[0][1]))
|
|
19
|
-
return fail();
|
|
20
|
-
return [...userServers, {
|
|
21
|
-
type: 'http', name: 'axom-delegation', url: url.toString(),
|
|
22
|
-
headers: headers.map(([name, value]) => ({ name, value })),
|
|
23
|
-
}];
|
|
24
|
-
}
|