@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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/** Bundled release certification is the trust root; writable receipts never authorize a runtime. */
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, mkdtemp, open, readFile, readdir, realpath, rm } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import semver from 'semver';
|
|
9
|
+
import { inside, assertSafeAgentId, assertSafePackageName } from './builds/model.js';
|
|
10
|
+
import { nodeVersionOf } from './builds/nodeGate.js';
|
|
11
|
+
import { expectedPatchedFiles, patchDigest, verifyPatchedFiles } from './managedPatches.js';
|
|
12
|
+
import { managedRuntimePolicies, packagePatches, patchAssetRoot, validatePackagePatch, validatePatchPath } from './patchAssets.js';
|
|
13
|
+
export const REQUIRED_RUNTIME_CHECKS = ['initialize', 'ordinary-input', 'permissions', 'images-settings', 'parallel-subagents', 'background-reconnect', 'next-append', 'next-button', 'stop', 'cached-resume', 'legacy-migration', 'explicit-load', 'replay', 'install-gates', 'platform'];
|
|
14
|
+
const hashPattern = /^[a-f0-9]{64}$/;
|
|
15
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
16
|
+
function requireThat(condition, message) {
|
|
17
|
+
if (!condition)
|
|
18
|
+
throw new Error(`Managed runtime release: ${message}`);
|
|
19
|
+
}
|
|
20
|
+
function exactVersion(value) { return typeof value === 'string' && semver.valid(value) === value && semver.prerelease(value) === null; }
|
|
21
|
+
export function managedRuntimeFor(agent, policies = managedRuntimePolicies) {
|
|
22
|
+
const found = policies.filter(policy => policy.agent === agent);
|
|
23
|
+
requireThat(found.length <= 1, 'duplicate agent policy');
|
|
24
|
+
return found[0];
|
|
25
|
+
}
|
|
26
|
+
export function assertRuntimeRequest(policy, name, version) {
|
|
27
|
+
assertSafeAgentId(policy.agent);
|
|
28
|
+
assertSafePackageName(policy.package);
|
|
29
|
+
validatePatchPath(policy.release);
|
|
30
|
+
requireThat(exactVersion(policy.version) && policy.ignore_scripts === true, 'invalid exact version or script policy');
|
|
31
|
+
requireThat(Array.isArray(policy.required_patches) && policy.required_patches.length > 0 && policy.required_patches.every(id => typeof id === 'string' && id.length > 0) && new Set(policy.required_patches).size === policy.required_patches.length, 'invalid required patches');
|
|
32
|
+
requireThat(name === policy.package && version === policy.version, `only certified ${policy.package}@${policy.version} may be installed`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reject aliases in every component; use nonblocking nofollow opens so a FIFO cannot hang
|
|
36
|
+
* verification. Root-owned system symlinks (`/var -> /private/var` on macOS, in front of every
|
|
37
|
+
* per-user tmpdir) are crossed, not refused: a root-owned link to a root-owned, non-group-writable
|
|
38
|
+
* directory is not plantable by the running user. Every user-owned symlink is still refused.
|
|
39
|
+
*/
|
|
40
|
+
async function ownedFile(filename) {
|
|
41
|
+
const full = path.resolve(filename);
|
|
42
|
+
let current = path.parse(full).root;
|
|
43
|
+
const systemSymlink = async (uid) => {
|
|
44
|
+
if (uid !== 0 || process.getuid?.() === 0)
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
const target = await realpath(current);
|
|
48
|
+
const stat = await lstat(target);
|
|
49
|
+
return stat.isDirectory() && stat.uid === 0 && (stat.mode & 0o022) === 0;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
for (const part of full.slice(current.length).split(path.sep).filter(Boolean)) {
|
|
56
|
+
current = path.join(current, part);
|
|
57
|
+
const info = await lstat(current);
|
|
58
|
+
requireThat(!info.isSymbolicLink() || (await systemSymlink(info.uid)), 'symlink in runtime path');
|
|
59
|
+
}
|
|
60
|
+
const file = await open(full, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
61
|
+
if (!(await file.stat()).isFile()) {
|
|
62
|
+
await file.close();
|
|
63
|
+
throw new Error('Managed runtime requires a regular file');
|
|
64
|
+
}
|
|
65
|
+
return file;
|
|
66
|
+
}
|
|
67
|
+
async function readReleaseAsset(relative, assets) {
|
|
68
|
+
validatePatchPath(relative);
|
|
69
|
+
const base = assets.assetRoot ?? patchAssetRoot;
|
|
70
|
+
const handle = await ownedFile(inside(base instanceof URL ? fileURLToPath(base) : base, relative));
|
|
71
|
+
try {
|
|
72
|
+
requireThat((await handle.stat()).size <= 8 * 1024 * 1024, 'manifest too large');
|
|
73
|
+
return JSON.parse(await handle.readFile('utf8'));
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await handle.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function loadRuntimeRelease(policy, assets = {}) {
|
|
80
|
+
assertRuntimeRequest(policy, policy.package, policy.version);
|
|
81
|
+
return validateRuntimeRelease(await readReleaseAsset(policy.release, assets), policy, assets);
|
|
82
|
+
}
|
|
83
|
+
function validateRuntimeRelease(r, policy, assets, retained = false) {
|
|
84
|
+
assertRuntimeRequest(policy, policy.package, policy.version);
|
|
85
|
+
requireThat(r?.schema === 1 && typeof r.id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(r.id), 'invalid schema/id');
|
|
86
|
+
requireThat(r.package === policy.package && r.version === policy.version, 'policy/package/version mismatch');
|
|
87
|
+
requireThat(typeof r.integrity === 'string' && /^sha512-[A-Za-z0-9+/]{86}==$/.test(r.integrity), 'invalid archive integrity');
|
|
88
|
+
requireThat(r.bin && typeof r.bin.name === 'string' && r.bin.name.length > 0, 'invalid bin');
|
|
89
|
+
validatePatchPath(r.bin.path);
|
|
90
|
+
requireThat(r.bin.args === undefined || (Array.isArray(r.bin.args) && r.bin.args.every(arg => typeof arg === 'string' && !arg.includes('\0'))), 'invalid static argv');
|
|
91
|
+
requireThat(r.agentInfo && typeof r.agentInfo.name === 'string' && r.agentInfo.name.length > 0 && exactVersion(r.agentInfo.version), 'invalid agent identity');
|
|
92
|
+
requireThat(r.node && typeof r.node.range === 'string' && semver.validRange(r.node.range), 'invalid Node range');
|
|
93
|
+
requireThat(Array.isArray(r.historyVersions) && r.historyVersions.includes(r.version) && r.historyVersions.every(exactVersion) && new Set(r.historyVersions).size === r.historyVersions.length, 'invalid history versions');
|
|
94
|
+
requireThat(same(r.patches, policy.required_patches), 'ordered required patches mismatch');
|
|
95
|
+
requireThat(r.files && typeof r.files === 'object' && !Array.isArray(r.files) && Object.keys(r.files).length > 0, 'missing trusted files');
|
|
96
|
+
for (const [file, hash] of Object.entries(r.files)) {
|
|
97
|
+
validatePatchPath(file);
|
|
98
|
+
requireThat(typeof hash === 'string' && hashPattern.test(hash), 'invalid trusted file digest');
|
|
99
|
+
}
|
|
100
|
+
requireThat(r.files['package.json'] && r.files[r.bin.path], 'manifest and executable must be trusted files');
|
|
101
|
+
requireThat(r.capabilities?.cachedReconnect === 'resume' && same(r.capabilities.steering, ['next']) && r.capabilities.subagents === true, 'invalid capabilities');
|
|
102
|
+
const c = r.certification;
|
|
103
|
+
requireThat(c && typeof c.suite === 'string' && c.suite.length > 0 && Array.isArray(c.checks) && Array.isArray(c.platforms), 'missing certification');
|
|
104
|
+
requireThat(c.checks.length === REQUIRED_RUNTIME_CHECKS.length && new Set(c.checks.map(v => v.name)).size === c.checks.length && REQUIRED_RUNTIME_CHECKS.every(name => c.checks.some(v => v.name === name)), 'incomplete or duplicate required checks');
|
|
105
|
+
requireThat(c.checks.every(v => ['PASS', 'NOT_PASS'].includes(v.status) && hashPattern.test(v.evidenceSha256)), 'invalid certification check status');
|
|
106
|
+
requireThat(c.platforms.every(v => typeof v.platform === 'string' && typeof v.arch === 'string' && ['PASS', 'NOT_PASS'].includes(v.status) && hashPattern.test(v.evidenceSha256)) && new Set(c.platforms.map(v => `${v.platform}/${v.arch}`)).size === c.platforms.length, 'invalid platform records');
|
|
107
|
+
requireThat(c.platforms.some(v => v.platform === process.platform && v.arch === process.arch), 'platform record missing');
|
|
108
|
+
requireThat(r.runtimeTools === undefined || Array.isArray(r.runtimeTools), 'invalid runtime tools');
|
|
109
|
+
const tools = r.runtimeTools ?? [];
|
|
110
|
+
requireThat(new Set(tools.map(v => `${v.package?.name ?? r.package}/${v.platform}/${v.arch}/${v.path}`)).size === tools.length, 'duplicate runtime tools');
|
|
111
|
+
for (const tool of tools) {
|
|
112
|
+
validatePatchPath(tool.path);
|
|
113
|
+
if (tool.package !== undefined) {
|
|
114
|
+
requireThat(tool.package && typeof tool.package.name === 'string' && typeof tool.package.version === 'string' && semver.valid(tool.package.version) === tool.package.version, 'invalid exact tool package identity');
|
|
115
|
+
assertSafePackageName(tool.package.name);
|
|
116
|
+
}
|
|
117
|
+
const primary = tool.package === undefined || tool.package.name === r.package;
|
|
118
|
+
requireThat(typeof tool.platform === 'string' && typeof tool.arch === 'string' && hashPattern.test(tool.sha256) && (!primary || r.files[tool.path] === tool.sha256) && c.platforms.some(v => v.platform === tool.platform && v.arch === tool.arch), 'tool must match a trusted digest and declared platform');
|
|
119
|
+
}
|
|
120
|
+
if (retained)
|
|
121
|
+
requireThat(typeof r.patchDigest === 'string' && hashPattern.test(r.patchDigest), 'retained release requires trusted patch digest');
|
|
122
|
+
else {
|
|
123
|
+
const digest = patchDigest(runtimeRecipes(policy, r, assets.recipes));
|
|
124
|
+
requireThat(r.patchDigest === undefined || r.patchDigest === digest, 'release patch digest disagrees with bundled recipes');
|
|
125
|
+
}
|
|
126
|
+
return r;
|
|
127
|
+
}
|
|
128
|
+
export function runtimeRecipes(policy, release, recipes = packagePatches) {
|
|
129
|
+
const selected = recipes.filter(recipe => recipe.package === policy.package && recipe.versions?.includes(policy.version));
|
|
130
|
+
requireThat(same(selected.map(v => v.id), policy.required_patches), 'missing or unordered exact patch recipe');
|
|
131
|
+
for (const recipe of selected) {
|
|
132
|
+
validatePackagePatch(recipe);
|
|
133
|
+
requireThat(recipe.bin === undefined || recipe.bin === release.bin.name, 'patch bin disagrees with release');
|
|
134
|
+
}
|
|
135
|
+
for (const [qualified, hash] of Object.entries(expectedPatchedFiles(selected))) {
|
|
136
|
+
requireThat(release.files[qualified.slice(policy.package.length + 1)] === hash, 'patch digest disagrees with trusted release files');
|
|
137
|
+
}
|
|
138
|
+
return selected;
|
|
139
|
+
}
|
|
140
|
+
/** Reverse the declared edits to derive trusted pristine hashes, without trusting package metadata. */
|
|
141
|
+
function pristineFiles(release, recipes) {
|
|
142
|
+
const files = { ...release.files };
|
|
143
|
+
for (const recipe of [...recipes].reverse()) {
|
|
144
|
+
for (const asset of recipe.assets ?? [])
|
|
145
|
+
delete files[asset.target];
|
|
146
|
+
for (const edit of [...recipe.edits].reverse()) {
|
|
147
|
+
requireThat(files[edit.file] === edit.after_sha256 && hashPattern.test(edit.before_sha256), 'inconsistent patch hash chain');
|
|
148
|
+
files[edit.file] = edit.before_sha256;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return files;
|
|
152
|
+
}
|
|
153
|
+
export async function verifyRuntimeTree(options) {
|
|
154
|
+
const r = await loadRuntimeRelease(options.policy, options);
|
|
155
|
+
return verifyReleaseTree(options, r);
|
|
156
|
+
}
|
|
157
|
+
async function verifyReleaseTree(options, r) {
|
|
158
|
+
const pkg = inside(path.join(options.treeRoot, 'node_modules'), r.package);
|
|
159
|
+
const files = options.pristine ? pristineFiles(r, runtimeRecipes(options.policy, r, options.recipes)) : r.files;
|
|
160
|
+
await verifyPatchedFiles(pkg, files);
|
|
161
|
+
// Extra executable code or links must not survive a cache adoption, even with matching package.json.
|
|
162
|
+
const actual = [];
|
|
163
|
+
const walk = async (dir, prefix = '') => {
|
|
164
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
165
|
+
const rel = prefix + entry.name;
|
|
166
|
+
if (entry.isDirectory())
|
|
167
|
+
await walk(path.join(dir, entry.name), rel + '/');
|
|
168
|
+
else {
|
|
169
|
+
requireThat(entry.isFile(), 'non-regular runtime asset');
|
|
170
|
+
actual.push(rel);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
await walk(pkg);
|
|
175
|
+
requireThat(same(actual.sort(), Object.keys(files).sort()), 'unexpected or missing runtime files');
|
|
176
|
+
const metadata = JSON.parse(await readFile(path.join(pkg, 'package.json'), 'utf8'));
|
|
177
|
+
requireThat(metadata.name === r.package && metadata.version === r.version && metadata.bin?.[r.bin.name] === r.bin.path, 'package identity/bin mismatch');
|
|
178
|
+
for (const tool of r.runtimeTools ?? []) {
|
|
179
|
+
if (tool.platform !== process.platform || tool.arch !== process.arch)
|
|
180
|
+
continue;
|
|
181
|
+
const toolPackage = tool.package ?? { name: r.package, version: r.version };
|
|
182
|
+
const toolDir = inside(path.join(options.treeRoot, 'node_modules'), toolPackage.name);
|
|
183
|
+
const manifest = await ownedFile(path.join(toolDir, 'package.json'));
|
|
184
|
+
try {
|
|
185
|
+
const identity = JSON.parse(await manifest.readFile('utf8'));
|
|
186
|
+
requireThat(identity.name === toolPackage.name && identity.version === toolPackage.version, 'runtime tool package identity mismatch');
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await manifest.close();
|
|
190
|
+
}
|
|
191
|
+
// External tools are authenticated by this explicit record, not by a claim about their whole package.
|
|
192
|
+
await verifyPatchedFiles(toolDir, { [tool.path]: tool.sha256 });
|
|
193
|
+
const handle = await ownedFile(inside(toolDir, tool.path));
|
|
194
|
+
try {
|
|
195
|
+
if (options.prepareTools)
|
|
196
|
+
await handle.chmod(0o755);
|
|
197
|
+
requireThat(process.platform === 'win32' || ((await handle.stat()).mode & 0o111) !== 0, `runtime tool not executable: ${tool.path}`);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
await handle.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return r;
|
|
204
|
+
}
|
|
205
|
+
export async function verifyManagedRuntime(options) {
|
|
206
|
+
const release = await loadRuntimeRelease(options.policy, options);
|
|
207
|
+
await verifyReleaseBuild(options, release, patchDigest(runtimeRecipes(options.policy, release, options.recipes)));
|
|
208
|
+
}
|
|
209
|
+
/** Explicit recorded bindings/rollback only. Every historical authority is bundled and listed, never a writable receipt. */
|
|
210
|
+
export async function verifyRecordedManagedRuntime(options) {
|
|
211
|
+
const active = await loadRuntimeRelease(options.policy, options);
|
|
212
|
+
if (options.build.runtimeReleaseId === active.id) {
|
|
213
|
+
await verifyReleaseBuild(options, active, patchDigest(runtimeRecipes(options.policy, active, options.recipes)));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const paths = options.policy.retained_releases ?? [];
|
|
217
|
+
requireThat(Array.isArray(paths) && new Set(paths).size === paths.length && !paths.includes(options.policy.release), 'duplicate or active retained release path');
|
|
218
|
+
const ids = new Set([active.id]);
|
|
219
|
+
let match;
|
|
220
|
+
for (const relative of paths) {
|
|
221
|
+
const release = await readReleaseAsset(relative, options);
|
|
222
|
+
requireThat(release?.schema === 1 && typeof release.id === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(release.id) && !ids.has(release.id), 'invalid, duplicate or active retained release identity');
|
|
223
|
+
ids.add(release.id);
|
|
224
|
+
if (release.id === options.build.runtimeReleaseId)
|
|
225
|
+
match = { release, path: relative };
|
|
226
|
+
}
|
|
227
|
+
requireThat(match, 'recorded release is not explicitly retained');
|
|
228
|
+
const policy = { agent: options.policy.agent, package: match.release.package, version: match.release.version, release: match.path, required_patches: match.release.patches, ignore_scripts: true };
|
|
229
|
+
const release = validateRuntimeRelease(match.release, policy, options, true);
|
|
230
|
+
requireThat(release.package !== options.policy.package || semver.lte(release.version, options.policy.version), 'retained release is a future version');
|
|
231
|
+
await verifyReleaseBuild({ ...options, policy }, release, release.patchDigest);
|
|
232
|
+
}
|
|
233
|
+
async function verifyReleaseBuild(options, release, digest) {
|
|
234
|
+
const { policy, build } = options;
|
|
235
|
+
requireThat(release.certification.checks.length === REQUIRED_RUNTIME_CHECKS.length && release.certification.checks.every(v => v.status === 'PASS'), 'release certification is NOT_PASS');
|
|
236
|
+
requireThat(release.certification.platforms.some(v => v.platform === process.platform && v.arch === process.arch && v.status === 'PASS'), 'platform is NOT_PASS');
|
|
237
|
+
assertRuntimeRequest(policy, build.package ?? '', build.version);
|
|
238
|
+
await verifyReleaseTree({ ...options, treeRoot: inside(options.storeRoot, build.dir), pristine: false }, release);
|
|
239
|
+
requireThat(build.agentId === policy.agent && build.runtimeReleaseId === release.id, 'build release identity mismatch');
|
|
240
|
+
requireThat(build.kind === 'npm-patched' && build.patchDigest === digest && same(build.patches, release.patches), 'build patch identity mismatch');
|
|
241
|
+
requireThat(build.launch.entry === `node_modules/${release.package}/${release.bin.path}` && build.launch.runtime === 'node', 'build executable/dispatcher mismatch');
|
|
242
|
+
requireThat(same(build.launch.args, release.bin.args ?? []), 'build static argv mismatch');
|
|
243
|
+
const nodeVersion = await nodeVersionOf(options.nodePath);
|
|
244
|
+
requireThat(semver.satisfies(nodeVersion, release.node.range, { includePrerelease: false }), `Node ${nodeVersion} outside ${release.node.range}`);
|
|
245
|
+
if (options.initialize)
|
|
246
|
+
await initializeProbe(build, options.storeRoot, options.nodePath, release);
|
|
247
|
+
}
|
|
248
|
+
/** One initialize only, no credentials or user HOME, bounded streams/time, awaited process cleanup. */
|
|
249
|
+
async function initializeProbe(build, storeRoot, nodePath, release) {
|
|
250
|
+
const home = await mkdtemp(path.join(tmpdir(), 'runtime-probe-'));
|
|
251
|
+
try {
|
|
252
|
+
await new Promise((resolve, reject) => {
|
|
253
|
+
const dir = inside(storeRoot, build.dir);
|
|
254
|
+
const child = spawn(nodePath, [inside(dir, build.launch.entry), ...build.launch.args], {
|
|
255
|
+
cwd: home, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
256
|
+
env: { PATH: [path.dirname(nodePath), ...(build.launch.pathDirs ?? []).map(p => inside(dir, p)), process.env.PATH ?? ''].join(path.delimiter), HOME: home, USERPROFILE: home, APPDATA: home, LOCALAPPDATA: home, XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home, TMPDIR: home, TEMP: home, TMP: home, ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}) },
|
|
257
|
+
});
|
|
258
|
+
let output = '';
|
|
259
|
+
let bytes = 0;
|
|
260
|
+
let stderrBytes = 0;
|
|
261
|
+
let response = false;
|
|
262
|
+
let failure;
|
|
263
|
+
const fail = (message) => { failure ??= new Error(`Managed runtime initialize: ${message}`); child.kill('SIGKILL'); };
|
|
264
|
+
const timeout = setTimeout(() => fail('timed out'), 30_000);
|
|
265
|
+
child.on('error', error => { failure ??= error; });
|
|
266
|
+
child.stdin.on('error', () => { });
|
|
267
|
+
child.stderr.on('data', (data) => { stderrBytes += data.length; if (stderrBytes > 1024 * 1024)
|
|
268
|
+
fail('stderr limit exceeded'); });
|
|
269
|
+
child.stdout.on('data', (data) => {
|
|
270
|
+
bytes += data.length;
|
|
271
|
+
if (bytes > 1024 * 1024) {
|
|
272
|
+
fail('stdout limit exceeded');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
output += data.toString('utf8');
|
|
276
|
+
let end;
|
|
277
|
+
while ((end = output.indexOf('\n')) >= 0) {
|
|
278
|
+
const line = output.slice(0, end);
|
|
279
|
+
output = output.slice(end + 1);
|
|
280
|
+
try {
|
|
281
|
+
requireThat(!response, 'extra initialize response');
|
|
282
|
+
const msg = JSON.parse(line);
|
|
283
|
+
const result = msg.result;
|
|
284
|
+
requireThat(msg.jsonrpc === '2.0' && msg.id === 'axom-runtime-initialize' && !Object.hasOwn(msg, 'error') && !Object.hasOwn(msg, 'method') && result?.protocolVersion === 1, 'invalid correlated initialize result');
|
|
285
|
+
requireThat(result.agentInfo?.name === release.agentInfo.name && result.agentInfo?.version === release.agentInfo.version, 'initialize agent identity mismatch');
|
|
286
|
+
requireThat(result._meta?.axom?.releaseId === release.id && result._meta?.axom?.cachedReconnect === 'resume', 'initialize release/resume mismatch');
|
|
287
|
+
requireThat(result.agentCapabilities?.loadSession === true && result.agentCapabilities?.sessionCapabilities?.resume && typeof result.agentCapabilities.sessionCapabilities.resume === 'object' && !Array.isArray(result.agentCapabilities.sessionCapabilities.resume), 'missing resume capability');
|
|
288
|
+
requireThat(result._meta?.steering?.supported === true && same(result._meta.steering.timings, ['next']), 'missing next capability');
|
|
289
|
+
response = true;
|
|
290
|
+
child.stdin.end();
|
|
291
|
+
// Await graceful EOF cleanup within the same overall deadline. Native agents may
|
|
292
|
+
// flush telemetry before exiting; a second, shorter deadline can kill a healthy one.
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
fail(error.message);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
child.once('close', (code) => {
|
|
300
|
+
clearTimeout(timeout);
|
|
301
|
+
if (!failure && (!response || output.trim() || code !== 0))
|
|
302
|
+
failure = new Error('Managed runtime initialize: incomplete response or startup failure');
|
|
303
|
+
if (failure)
|
|
304
|
+
reject(failure);
|
|
305
|
+
else
|
|
306
|
+
resolve();
|
|
307
|
+
});
|
|
308
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 'axom-runtime-initialize', method: 'initialize', params: { protocolVersion: 1, clientInfo: { name: 'axom-runtime-check', version: '1.0.0' }, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true } } }) + '\n');
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await rm(home, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ## Subagent relations bypass the window (fix round 1, spec §5.6)
|
|
3
|
+
*
|
|
4
|
+
* The server holds back its applied watermark — so its acks — at the first child frame whose subagent relation (`subagent_spawned`,
|
|
5
|
+
* the server's `TimelineEvent.SubagentSpawned`) it has not received yet (`noteApplied` capped at `nativePendingBefore - 1`,
|
|
6
|
+
* `ExternalAgentEventApplier.kt`). Every later frame then counts against the window; if the relation comes 256 or more frames after
|
|
7
|
+
* that first orphan child frame, a window that held it back would stall the session for good (the server waits 30 s, poisons, the
|
|
8
|
+
* reopen repeats it). So:
|
|
9
|
+
*
|
|
10
|
+
* - **producing**: while a child frame whose relation is not recorded yet is unacknowledged ({@link orphanOutstanding}), the record
|
|
11
|
+
* loop may run past the window, so the relation can be folded at all. Whatever the branch, this life never holds more than
|
|
12
|
+
* {@link RELATION_WAIT_EVENTS} / {@link RELATION_WAIT_BYTES} (1024 frames / 32 MiB) unacknowledged — pinned entries included — plus
|
|
13
|
+
* the frames of the one record being folded: that is the daemon's memory bound per session;
|
|
14
|
+
* - **sending**: once a relation that resolves such a child (a *resolving* relation) is recorded, it and every entry before it stop
|
|
15
|
+
* counting against either window ({@link pinnedThrough}): the sender takes them past the window, in seq order (sending the
|
|
16
|
+
* relation first would make the server drop the lower seqs as duplicates); the socket gate still applies. They must keep not
|
|
17
|
+
* counting after they are sent: the server applied them but wasted its acks while capped (each ack attempt resets its 32-event
|
|
18
|
+
* count), so its next ack needs up to 31 more applied frames — a window still full of them would deadlock there. They leave the
|
|
19
|
+
* outbox with that ack. A relation that resolves nothing recorded is not what the server waits for and keeps to the window
|
|
20
|
+
* (sending past it could overflow a parked server's 512-frame hold-off).
|
|
21
|
+
*
|
|
22
|
+
* ## Residual (documented, not fixed)
|
|
23
|
+
*
|
|
24
|
+
* While the producing window is full — a server parked on an approval with ≥ 256 root frames held, a connection that is down, an
|
|
25
|
+
* applier that never acknowledges — the record loop folds nothing: a steer or config RPC waits for the processor's reply and can
|
|
26
|
+
* time out on the server; an `agent_interrupt` note is folded (and the `session/cancel` written) only once the server unparks.
|
|
27
|
+
*/
|
|
28
|
+
/** Unacknowledged events per session (spec §5.6, `W_EVT`). `ACK_EVERY_EVENTS(32) < W_EVT ≤ HOLD_OFF_CAPACITY(512)`. */
|
|
29
|
+
export const SESSION_WINDOW_EVENTS = 256;
|
|
30
|
+
/** Unacknowledged bytes per session (spec §5.6): a few huge frames cannot hog the window. */
|
|
31
|
+
export const SESSION_WINDOW_BYTES = 8 * 1024 * 1024;
|
|
32
|
+
/** How far production may run past the window while a recorded child frame still waits for its relation (bounds memory). */
|
|
33
|
+
export const RELATION_WAIT_EVENTS = 4 * SESSION_WINDOW_EVENTS;
|
|
34
|
+
export const RELATION_WAIT_BYTES = 4 * SESSION_WINDOW_BYTES;
|
|
35
|
+
/** A frame carrying a subagent relation (the server's `SubagentSpawned`): it bypasses the session window. */
|
|
36
|
+
export function isRelationFrame(frame) {
|
|
37
|
+
return frame.t === 'agent_event' && frame.eventName === 'subagent_spawned';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `src.i` of an outbox entry no record produced: a frame this daemon numbered itself (a degraded session's failed `turn_done`,
|
|
41
|
+
* `src.o` = the last record folded before it) or one imported from a legacy journal (`src.o` = 0). A replay never reproduces it
|
|
42
|
+
* (T21 skips it when comparing). Far above any per-record frame index.
|
|
43
|
+
*/
|
|
44
|
+
export const DAEMON_FRAME_INDEX = 1 << 24;
|
|
45
|
+
/**
|
|
46
|
+
* Connection epoch of an entry that may have reached the server on an earlier connection (or in an earlier daemon life): it is not
|
|
47
|
+
* sent again until the server reopens the ref and says how far it got ({@link SessionOutbox.resendAll} after an `ack` of the open's
|
|
48
|
+
* `resumeFromSeq`) — resending a frame the server already applied could, for a `turn_done`, end a newer turn (analysis B §3.1).
|
|
49
|
+
*/
|
|
50
|
+
export const SENT_EARLIER = -1;
|
|
51
|
+
const frameBytes = (frame) => Buffer.byteLength(JSON.stringify(frame));
|
|
52
|
+
export function isOutboxTail(value) {
|
|
53
|
+
if (typeof value !== 'object' || value === null)
|
|
54
|
+
return false;
|
|
55
|
+
const { seq, src } = value;
|
|
56
|
+
return Number.isSafeInteger(seq) && typeof src === 'object' && src !== null &&
|
|
57
|
+
Number.isSafeInteger(src.o) && Number.isSafeInteger(src.i);
|
|
58
|
+
}
|
|
59
|
+
export class SessionOutbox {
|
|
60
|
+
store;
|
|
61
|
+
items = [];
|
|
62
|
+
committed;
|
|
63
|
+
highestSeq = 0;
|
|
64
|
+
ownEvents = 0;
|
|
65
|
+
ownBytes = 0;
|
|
66
|
+
chain = Promise.resolve();
|
|
67
|
+
roomWaiters = new Set();
|
|
68
|
+
listeners = new Set();
|
|
69
|
+
stopped = false;
|
|
70
|
+
/** Children whose relation frame this outbox recorded. */
|
|
71
|
+
related = new Set();
|
|
72
|
+
/** Seq of the latest resolving relation: entries at or below it count against neither window (file comment). */
|
|
73
|
+
pinned = 0;
|
|
74
|
+
isRelated;
|
|
75
|
+
/**
|
|
76
|
+
* `isRelated`: whether the session already knows a child's relation from elsewhere (the processor's relation graph — a relation
|
|
77
|
+
* recorded by an earlier life, or replayed by a load). Without it only relations this outbox recorded count.
|
|
78
|
+
*/
|
|
79
|
+
constructor(store, init = {}) {
|
|
80
|
+
this.store = store;
|
|
81
|
+
this.isRelated = init.isRelated ?? (() => false);
|
|
82
|
+
this.committed = isOutboxTail(init.committedTail) ? { seq: init.committedTail.seq, src: { ...init.committedTail.src } } : null;
|
|
83
|
+
this.highestSeq = this.committed?.seq ?? 0;
|
|
84
|
+
for (const record of init.leftovers ?? []) {
|
|
85
|
+
// Read back past the watermark only; a record at or below it is acknowledged (truncation is by whole segments).
|
|
86
|
+
if (this.committed && record.seq <= this.committed.seq)
|
|
87
|
+
continue;
|
|
88
|
+
this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes: frameBytes(record.frame), leftover: true,
|
|
89
|
+
...this.classify(record.seq, record.frame), sentOn: SENT_EARLIER });
|
|
90
|
+
this.highestSeq = Math.max(this.highestSeq, record.seq);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolving relation / orphan frame, in record order (a relation relates its child for every later frame). An orphan is a frame
|
|
95
|
+
* the server retains until a relation it has not received: a child frame (`acpSessionId`) whose child is unrelated, and — Task
|
|
96
|
+
* 22 — a `subagent_state_update` (any origin) naming an unrelated child (`payload.subagentSessionId`): the server's
|
|
97
|
+
* `receiveNativeEventLocked` holds it, and with it its acks, until that child's `subagent_spawned` exactly as it holds a child
|
|
98
|
+
* frame. The state update's own child is the key: a root-origin one would otherwise not be an orphan at all, its relation would
|
|
99
|
+
* keep to the window, and the session would stall where Task 20 fixed it for child frames.
|
|
100
|
+
*/
|
|
101
|
+
classify(seq, frame) {
|
|
102
|
+
const unrelated = (child) => child !== undefined && !this.related.has(child) && !this.isRelated(child) ? child : undefined;
|
|
103
|
+
const origin = frame.t === 'agent_event' && typeof frame.acpSessionId === 'string' ? frame.acpSessionId : undefined;
|
|
104
|
+
const stateChild = frame.t === 'agent_event' && frame.eventName === 'subagent_state_update'
|
|
105
|
+
? frame.payload?.subagentSessionId
|
|
106
|
+
: undefined;
|
|
107
|
+
const orphanOf = unrelated(typeof stateChild === 'string' ? stateChild : undefined) ?? unrelated(origin);
|
|
108
|
+
let resolving = false;
|
|
109
|
+
if (isRelationFrame(frame)) {
|
|
110
|
+
const child = frame.payload?.subagentSessionId;
|
|
111
|
+
if (typeof child === 'string') {
|
|
112
|
+
resolving = this.items.some((item) => item.orphanOf === child);
|
|
113
|
+
this.related.add(child);
|
|
114
|
+
if (resolving)
|
|
115
|
+
this.pinned = Math.max(this.pinned, seq);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { resolving, ...(orphanOf !== undefined ? { orphanOf } : {}) };
|
|
119
|
+
}
|
|
120
|
+
/** Entries at or below this seq count against neither window (the latest resolving relation; 0: none). */
|
|
121
|
+
get pinnedThrough() { return this.pinned; }
|
|
122
|
+
/** An unacknowledged child frame whose relation is still not recorded: the server is (likely) holding its acks for it. */
|
|
123
|
+
orphanOutstanding() {
|
|
124
|
+
return this.items.some((item) => item.orphanOf !== undefined && !this.related.has(item.orphanOf) && !this.isRelated(item.orphanOf));
|
|
125
|
+
}
|
|
126
|
+
/** Highest seq this outbox ever held (entries, or the acknowledged watermark): a new life numbers after it (§5.5). */
|
|
127
|
+
get lastSeq() { return this.highestSeq; }
|
|
128
|
+
/** The acknowledged watermark (`committedTail`), `null` before the first truncating ack. */
|
|
129
|
+
get committedTail() { return this.committed; }
|
|
130
|
+
/** Unacknowledged entries, in seq order. */
|
|
131
|
+
get pending() { return this.items; }
|
|
132
|
+
/** Unacknowledged entries appended by this life (the producing window), past {@link pinnedThrough}. */
|
|
133
|
+
get backlog() {
|
|
134
|
+
if (this.pinned === 0)
|
|
135
|
+
return { events: this.ownEvents, bytes: this.ownBytes };
|
|
136
|
+
let events = 0;
|
|
137
|
+
let bytes = 0;
|
|
138
|
+
for (const item of this.items)
|
|
139
|
+
if (!item.leftover && item.seq > this.pinned) {
|
|
140
|
+
events += 1;
|
|
141
|
+
bytes += item.bytes;
|
|
142
|
+
}
|
|
143
|
+
return { events, bytes };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Entries sent on connection `epoch` and not acknowledged (the sending window). Entries at or below {@link pinnedThrough} are
|
|
147
|
+
* sent past the window either way (the sender bypasses it for them); once sent they count only when `pinnedCountsWhenSent` —
|
|
148
|
+
* a server that acknowledges as soon as the relation releases its cap (`AGENT_WIRE_RELATION_ACK_VERSION`, Task 22). An older
|
|
149
|
+
* server wastes its acks while capped, so its first ack after the relation can need up to 31 more applied frames: counting the
|
|
150
|
+
* pinned ones there would deadlock right after the relation (fix round 1 of Task 20).
|
|
151
|
+
*/
|
|
152
|
+
inflight(epoch, pinnedCountsWhenSent = false) {
|
|
153
|
+
let events = 0;
|
|
154
|
+
let bytes = 0;
|
|
155
|
+
for (const item of this.items) {
|
|
156
|
+
if (item.sentOn === epoch && (pinnedCountsWhenSent || item.seq > this.pinned)) {
|
|
157
|
+
events += 1;
|
|
158
|
+
bytes += item.bytes;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { events, bytes };
|
|
162
|
+
}
|
|
163
|
+
/** Called whenever there may be something new to send (an append, an ack opening the window, a resend). */
|
|
164
|
+
onChange(listener) {
|
|
165
|
+
this.listeners.add(listener);
|
|
166
|
+
return () => { this.listeners.delete(listener); };
|
|
167
|
+
}
|
|
168
|
+
changed() { for (const listener of [...this.listeners])
|
|
169
|
+
listener(); }
|
|
170
|
+
run(op) {
|
|
171
|
+
const result = this.chain.then(op);
|
|
172
|
+
this.chain = result.then(() => undefined, () => undefined);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
/** Waits for every stream operation queued so far. */
|
|
176
|
+
flush() { return this.chain; }
|
|
177
|
+
/**
|
|
178
|
+
* Record frames before they are sent. Resolves once they are durable; rejects when the stream refused them (the caller degrades
|
|
179
|
+
* the session — a frame that could not be recorded is not sent either).
|
|
180
|
+
*/
|
|
181
|
+
append(frames) {
|
|
182
|
+
if (frames.length === 0)
|
|
183
|
+
return this.chain;
|
|
184
|
+
return this.run(async () => {
|
|
185
|
+
const entries = frames.map((f) => ({ seq: f.seq, src: { o: f.src.o, i: f.src.i }, frame: { ...f.frame } }));
|
|
186
|
+
const { first } = await this.store.append(entries);
|
|
187
|
+
entries.forEach((entry, index) => {
|
|
188
|
+
const bytes = frameBytes(entry.frame);
|
|
189
|
+
this.items.push({ o: first + index, seq: entry.seq, src: entry.src, frame: entry.frame, bytes, leftover: false, ...this.classify(entry.seq, entry.frame), sentOn: null });
|
|
190
|
+
this.highestSeq = Math.max(this.highestSeq, entry.seq);
|
|
191
|
+
this.ownEvents += 1;
|
|
192
|
+
this.ownBytes += bytes;
|
|
193
|
+
});
|
|
194
|
+
this.wakeRoom(); // a resolving relation unpins the producing window
|
|
195
|
+
this.changed();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The server applied everything through `throughSeq` (`agent_event_ack`, or an open's `resumeFromSeq`): forget those entries and
|
|
200
|
+
* truncate the stream through the last of them, persisting `{seq, src}` of that entry as the watermark. Nothing held at or below
|
|
201
|
+
* it: nothing changes. A failed commit is reported (`onError`) — the entries are acknowledged either way; the stream keeps them
|
|
202
|
+
* until a later commit passes them.
|
|
203
|
+
*/
|
|
204
|
+
ack(throughSeq, onError) {
|
|
205
|
+
return this.run(async () => {
|
|
206
|
+
let k = -1;
|
|
207
|
+
for (let i = 0; i < this.items.length && this.items[i].seq <= throughSeq; i++)
|
|
208
|
+
k = i;
|
|
209
|
+
if (k < 0)
|
|
210
|
+
return;
|
|
211
|
+
const last = this.items[k];
|
|
212
|
+
const removed = this.items.splice(0, k + 1);
|
|
213
|
+
for (const item of removed)
|
|
214
|
+
if (!item.leftover) {
|
|
215
|
+
this.ownEvents -= 1;
|
|
216
|
+
this.ownBytes -= item.bytes;
|
|
217
|
+
}
|
|
218
|
+
this.committed = { seq: last.seq, src: { ...last.src } };
|
|
219
|
+
this.wakeRoom();
|
|
220
|
+
this.changed();
|
|
221
|
+
try {
|
|
222
|
+
await this.store.commit({ outbox: { through: last.o, tail: { seq: last.seq, src: { ...last.src } } } });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
onError?.(error);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* A processor checkpoint at raw `offset`, and raw truncation through it (the keeper refuses anything past it) unless `truncate` is
|
|
231
|
+
* false (a marker checkpoint, Task 21).
|
|
232
|
+
*/
|
|
233
|
+
commitCheckpoint(offset, blob, truncate = true) {
|
|
234
|
+
return this.run(async () => {
|
|
235
|
+
await this.store.commit({ checkpoint: { offset, blob }, ...(truncate ? { rawThrough: offset } : {}) });
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The session re-attached to keeper-acp after its connection was lost (Task 21): the stream is the truth again. An append whose
|
|
240
|
+
* reply was lost may have landed (it is in `leftovers` now) or not (it is gone, and the record loop folds its record again). The
|
|
241
|
+
* entries are replaced by what the stream holds past its watermark; an entry this object already had keeps whether (and on which
|
|
242
|
+
* connection) it was sent and whether an earlier life left it; one it never saw was never sent. Call it with no stream operation
|
|
243
|
+
* in flight ({@link flush} first). Listeners and room waiters are kept.
|
|
244
|
+
*/
|
|
245
|
+
rebase(init) {
|
|
246
|
+
const previous = new Map(this.items.map((item) => [item.seq, item]));
|
|
247
|
+
this.items.length = 0;
|
|
248
|
+
this.related.clear();
|
|
249
|
+
this.pinned = 0;
|
|
250
|
+
this.ownEvents = 0;
|
|
251
|
+
this.ownBytes = 0;
|
|
252
|
+
if (isOutboxTail(init.committedTail) && (this.committed === null || init.committedTail.seq >= this.committed.seq)) {
|
|
253
|
+
this.committed = { seq: init.committedTail.seq, src: { ...init.committedTail.src } };
|
|
254
|
+
}
|
|
255
|
+
this.highestSeq = Math.max(this.highestSeq, this.committed?.seq ?? 0);
|
|
256
|
+
for (const record of init.leftovers) {
|
|
257
|
+
if (this.committed && record.seq <= this.committed.seq)
|
|
258
|
+
continue;
|
|
259
|
+
const before = previous.get(record.seq);
|
|
260
|
+
const leftover = before?.leftover ?? false;
|
|
261
|
+
const bytes = frameBytes(record.frame);
|
|
262
|
+
this.items.push({ o: record.o, seq: record.seq, src: { ...record.src }, frame: record.frame, bytes, leftover,
|
|
263
|
+
...this.classify(record.seq, record.frame), sentOn: before ? before.sentOn : null });
|
|
264
|
+
if (!leftover) {
|
|
265
|
+
this.ownEvents += 1;
|
|
266
|
+
this.ownBytes += bytes;
|
|
267
|
+
}
|
|
268
|
+
this.highestSeq = Math.max(this.highestSeq, record.seq);
|
|
269
|
+
}
|
|
270
|
+
this.wakeRoom();
|
|
271
|
+
this.changed();
|
|
272
|
+
}
|
|
273
|
+
/** Mark every unacknowledged entry unsent: the sender resends them in order (a reopen's replay, after the ack of its watermark). */
|
|
274
|
+
resendAll() {
|
|
275
|
+
for (const item of this.items)
|
|
276
|
+
item.sentOn = null;
|
|
277
|
+
this.changed();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A new connection: everything recorded so far — sent on an earlier connection or produced while none was bound — is held until
|
|
281
|
+
* the server reopens the ref and says how far it got ({@link SENT_EARLIER}); what is recorded from now on is sent at once. (The
|
|
282
|
+
* journal's semantics: frames produced while unbound were replayed by the reopen, never sent on their own.)
|
|
283
|
+
*/
|
|
284
|
+
connectionChanged() {
|
|
285
|
+
for (const item of this.items)
|
|
286
|
+
item.sentOn = SENT_EARLIER;
|
|
287
|
+
}
|
|
288
|
+
/** The first entry never sent (the sender's next candidate), in seq order. */
|
|
289
|
+
nextUnsent() {
|
|
290
|
+
return this.items.find((item) => item.sentOn === null);
|
|
291
|
+
}
|
|
292
|
+
/** Resolves when this life's unacknowledged backlog is below the window (immediately once {@link stopWaiting} ran). */
|
|
293
|
+
room() {
|
|
294
|
+
if (this.hasRoom())
|
|
295
|
+
return undefined;
|
|
296
|
+
return new Promise((resolve) => { this.roomWaiters.add(resolve); });
|
|
297
|
+
}
|
|
298
|
+
hasRoom() {
|
|
299
|
+
if (this.stopped)
|
|
300
|
+
return true;
|
|
301
|
+
// A hard bound on everything this life holds unacknowledged, pinned or not: chained resolving relations each reset `backlog`,
|
|
302
|
+
// but pinned entries leave memory only with an ack.
|
|
303
|
+
if (this.ownEvents >= RELATION_WAIT_EVENTS || this.ownBytes >= RELATION_WAIT_BYTES)
|
|
304
|
+
return false;
|
|
305
|
+
const { events, bytes } = this.backlog;
|
|
306
|
+
if (events < SESSION_WINDOW_EVENTS && bytes < SESSION_WINDOW_BYTES)
|
|
307
|
+
return true;
|
|
308
|
+
// Past the window only while a relation is awaited: the server cannot acknowledge before it, so it must be folded.
|
|
309
|
+
return this.orphanOutstanding();
|
|
310
|
+
}
|
|
311
|
+
wakeRoom() {
|
|
312
|
+
if (!this.hasRoom())
|
|
313
|
+
return;
|
|
314
|
+
for (const resolve of [...this.roomWaiters])
|
|
315
|
+
resolve();
|
|
316
|
+
this.roomWaiters.clear();
|
|
317
|
+
}
|
|
318
|
+
/** The session is closing or degraded: nobody waits for room any more. */
|
|
319
|
+
stopWaiting() {
|
|
320
|
+
this.stopped = true;
|
|
321
|
+
this.wakeRoom();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** An outbox store over a transport session (`acp.outbox.append` / `acp.commit`). */
|
|
325
|
+
export function transportOutboxStore(transport, sessionKey) {
|
|
326
|
+
return {
|
|
327
|
+
append: (entries) => transport.appendOutbox(sessionKey, entries),
|
|
328
|
+
commit: (params) => transport.commit(sessionKey, params),
|
|
329
|
+
};
|
|
330
|
+
}
|