@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,124 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { assertIdentityCanStart } from './daemonIdentity.js';
|
|
4
|
+
import { normalizeWorkstationServer } from './serverIdentity.js';
|
|
5
|
+
import { saveOperation } from './enrollment.js';
|
|
6
|
+
import { clientDir, loadClientConfig, readKeys } from '../config.js';
|
|
7
|
+
import { createDaemonController } from '../daemonctl.js';
|
|
8
|
+
import { BindingStore } from './bindingStore.js';
|
|
9
|
+
import { inspectWorkstation } from './inspect.js';
|
|
10
|
+
import { enrollWorkstation, enrollmentFailure } from './enrollment.js';
|
|
11
|
+
import { withWorkstationLock } from './locks.js';
|
|
12
|
+
import { bindingGuardSchema, LocalControlError } from './protocol.js';
|
|
13
|
+
const lifecycleRequest = z.object({ operationId: z.string().min(1).max(128), guard: bindingGuardSchema }).strict();
|
|
14
|
+
export async function readLocalRequest(input) {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
let size = 0;
|
|
17
|
+
for await (const part of input) {
|
|
18
|
+
const chunk = Buffer.from(part);
|
|
19
|
+
size += chunk.length;
|
|
20
|
+
if (size > 65_536)
|
|
21
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid local command input');
|
|
22
|
+
chunks.push(chunk);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid local command input');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function executeLocalCommand(action, input, directory, controllerFactory, inspect = inspectWorkstation) {
|
|
32
|
+
if (action === 'register')
|
|
33
|
+
return enrollWorkstation(input, directory);
|
|
34
|
+
try {
|
|
35
|
+
const request = action === 'adopt-owner' ? lifecycleRequest.extend({ server: z.string(), expectedOwnerUserId: z.string().min(1), workstationId: z.string().min(1) }).parse(input) : action === 'set-enabled' ? lifecycleRequest.extend({ enabled: z.boolean() }).parse(input) : lifecycleRequest.parse(input);
|
|
36
|
+
return await withWorkstationLock(directory, 'operation', async () => {
|
|
37
|
+
const before = await inspect(directory);
|
|
38
|
+
if (before.revision !== request.guard.revision || before.runtime.generation !== request.guard.runtimeGeneration)
|
|
39
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
40
|
+
if (action === 'recover') {
|
|
41
|
+
if (before.runtime.state !== 'stopped' || before.keepers !== 'stopped')
|
|
42
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the workstation before recovering its identity');
|
|
43
|
+
await new BindingStore(directory).recover();
|
|
44
|
+
return { status: 'succeeded', inspection: await inspect(directory) };
|
|
45
|
+
}
|
|
46
|
+
if (before.pending?.kind === 'remove' && !['begin-remove', 'forget', 'stop'].includes(action))
|
|
47
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Finish removing this workstation before continuing');
|
|
48
|
+
if (before.runtime.state === 'unknown')
|
|
49
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify the running workstation');
|
|
50
|
+
if (action === 'adopt-owner') {
|
|
51
|
+
const ownerRequest = lifecycleRequest.extend({ server: z.string(), expectedOwnerUserId: z.string().min(1), workstationId: z.string().min(1) }).parse(input);
|
|
52
|
+
if (before.identityState !== 'ready' || !before.binding || before.binding.ownerUserId || before.binding.server !== normalizeWorkstationServer(ownerRequest.server) || before.binding.workstationId !== ownerRequest.workstationId)
|
|
53
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The local connection changed');
|
|
54
|
+
const config = await loadClientConfig(directory);
|
|
55
|
+
const keys = await readKeys(directory);
|
|
56
|
+
await new BindingStore(directory).commit({ ...config, ownerUserId: ownerRequest.expectedOwnerUserId }, { clientKey: keys.clientKey.toString('base64'), serverPub: keys.serverPub.toString('base64') }, { id: request.operationId, kind: 'register', phase: 'owner-confirmed', target: { server: before.binding.server, ownerUserId: ownerRequest.expectedOwnerUserId }, workstationId: before.binding.workstationId, outcome: 'succeeded' });
|
|
57
|
+
}
|
|
58
|
+
else if (action === 'forget') {
|
|
59
|
+
if (before.runtime.state !== 'stopped' || before.keepers !== 'stopped')
|
|
60
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Stop the workstation and its sessions before removing the connection');
|
|
61
|
+
await new BindingStore(directory).clear(request.guard.revision);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
if (before.identityState !== 'ready')
|
|
65
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'The workstation needs registration or repair');
|
|
66
|
+
const controller = controllerFactory?.() ?? createDaemonController({ identity: () => new BindingStore(directory).inspect(), runtime: async () => (await inspect(directory)).runtime });
|
|
67
|
+
if (action === 'set-enabled') {
|
|
68
|
+
const enabled = lifecycleRequest.extend({ enabled: z.boolean() }).parse(input).enabled;
|
|
69
|
+
if (enabled)
|
|
70
|
+
await assertIdentityCanStart(directory);
|
|
71
|
+
if (!controller.setEnabled)
|
|
72
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Update the CLI to manage service enablement');
|
|
73
|
+
await controller.setEnabled(enabled, request);
|
|
74
|
+
}
|
|
75
|
+
else if (action === 'begin-remove') {
|
|
76
|
+
if (before.runtime.state !== 'stopped' || before.keepers !== 'stopped')
|
|
77
|
+
await controller.stop(request);
|
|
78
|
+
const stopped = await inspect(directory);
|
|
79
|
+
if (stopped.runtime.state !== 'stopped' || stopped.keepers !== 'stopped')
|
|
80
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Workstation shutdown could not be confirmed');
|
|
81
|
+
await saveOperation(directory, { id: request.operationId, kind: 'remove', phase: 'removing', target: { server: before.binding.server, ownerUserId: before.binding.ownerUserId ?? null }, workstationId: before.binding.workstationId, outcome: 'pending' });
|
|
82
|
+
}
|
|
83
|
+
else if (action === 'install') {
|
|
84
|
+
if (before.runtime.state !== 'stopped')
|
|
85
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Stop the workstation before changing its service');
|
|
86
|
+
await controller.install();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (action === 'start' || action === 'restart')
|
|
90
|
+
await assertIdentityCanStart(directory);
|
|
91
|
+
await controller[action](request);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const after = await inspect(directory);
|
|
95
|
+
if (action === 'set-enabled') {
|
|
96
|
+
const expected = lifecycleRequest.extend({ enabled: z.boolean() }).parse(input).enabled;
|
|
97
|
+
if (!after.service || after.service.enabled !== expected || after.service.reason || (!expected && (after.runtime.state !== 'stopped' || after.keepers !== 'stopped'))) {
|
|
98
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Service enablement could not be confirmed');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (action === 'stop' && (after.runtime.state !== 'stopped' || after.keepers !== 'stopped'))
|
|
102
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Workstation shutdown could not be confirmed');
|
|
103
|
+
return { status: 'succeeded', inspection: after };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return enrollmentFailure(error, directory);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function runLocalJsonCommand(action, input = process.stdin, write = value => { process.stdout.write(value); }, directory = clientDir(), controllerFactory, inspect = inspectWorkstation) {
|
|
111
|
+
let operationId = randomUUID();
|
|
112
|
+
let result;
|
|
113
|
+
try {
|
|
114
|
+
const request = await readLocalRequest(input);
|
|
115
|
+
if (typeof request === 'object' && request && 'operationId' in request && typeof request.operationId === 'string' && /^[\w-]{1,128}$/.test(request.operationId))
|
|
116
|
+
operationId = request.operationId;
|
|
117
|
+
result = await executeLocalCommand(action, request, directory, controllerFactory, inspect);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
result = await enrollmentFailure(error, directory);
|
|
121
|
+
}
|
|
122
|
+
const event = { schemaVersion: 1, kind: 'result', operationId, result };
|
|
123
|
+
write(JSON.stringify(event) + '\n');
|
|
124
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadClientConfig, readKeys } from '../config.js';
|
|
3
|
+
import { BindingStore } from './bindingStore.js';
|
|
4
|
+
import { withWorkstationLock } from './locks.js';
|
|
5
|
+
import { inspectRuntime } from './runtimeClient.js';
|
|
6
|
+
import { readControlFile } from './runtimeHost.js';
|
|
7
|
+
import { LocalControlError, operationRecordSchema } from './protocol.js';
|
|
8
|
+
export async function assertIdentityCanStart(directory) {
|
|
9
|
+
try {
|
|
10
|
+
const operation = operationRecordSchema.parse(JSON.parse(await readControlFile(path.join(directory, 'operation.json'))));
|
|
11
|
+
if (operation.outcome === 'pending' || operation.outcome === 'unknown')
|
|
12
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', operation.kind === 'remove' ? 'Finish removing this workstation before starting it' : 'The workstation operation needs verification');
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code !== 'ENOENT')
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function recoverStoppedIdentity(directory) {
|
|
20
|
+
const store = new BindingStore(directory);
|
|
21
|
+
if ((await store.inspect()).identityState === 'incomplete') {
|
|
22
|
+
const runtime = await inspectRuntime(directory);
|
|
23
|
+
const { inspectKeepers } = await import('./inspect.js');
|
|
24
|
+
if (runtime.state !== 'stopped' || await inspectKeepers(directory) !== 'stopped')
|
|
25
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the existing workstation before recovering its identity');
|
|
26
|
+
}
|
|
27
|
+
await store.recover();
|
|
28
|
+
}
|
|
29
|
+
/** Publish runtime ownership under the same lock as identity writes; initialization uses this snapshot only. */
|
|
30
|
+
export async function claimDaemonIdentity(directory, claim) {
|
|
31
|
+
const deadline = Date.now() + 10_000;
|
|
32
|
+
for (;;)
|
|
33
|
+
try {
|
|
34
|
+
return await withWorkstationLock(directory, 'operation', async () => {
|
|
35
|
+
const runtime = await inspectRuntime(directory);
|
|
36
|
+
if (runtime.state !== 'stopped')
|
|
37
|
+
throw new LocalControlError(runtime.state === 'running' ? 'INSTANCE_OCCUPIED' : 'OUTCOME_UNKNOWN', 'The existing workstation must be verified before startup');
|
|
38
|
+
const store = new BindingStore(directory);
|
|
39
|
+
await recoverStoppedIdentity(directory);
|
|
40
|
+
await assertIdentityCanStart(directory);
|
|
41
|
+
if ((await store.inspect()).identityState !== 'ready')
|
|
42
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'The workstation identity needs registration or repair');
|
|
43
|
+
const config = await loadClientConfig(directory);
|
|
44
|
+
const keys = await readKeys(directory);
|
|
45
|
+
return claim({ config, ...keys });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (!(error instanceof LocalControlError) || error.code !== 'BUSY' || Date.now() >= deadline)
|
|
50
|
+
throw error;
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { sanitizeName } from '../names.js';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { requestWorkstationRegistration } from '../register.js';
|
|
8
|
+
import { BindingStore } from './bindingStore.js';
|
|
9
|
+
import { inspectRuntime } from './runtimeClient.js';
|
|
10
|
+
import { inspectWorkstation } from './inspect.js';
|
|
11
|
+
import { bindingGuardSchema, LocalControlError, operationRecordSchema } from './protocol.js';
|
|
12
|
+
import { assertLockOwnership, withWorkstationLock } from './locks.js';
|
|
13
|
+
import { normalizeWorkstationServer } from './serverIdentity.js';
|
|
14
|
+
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
15
|
+
import { KeeperLauncher } from '../keepers/kit/launcher.js';
|
|
16
|
+
import { stopKeepers } from '../keepers/stop.js';
|
|
17
|
+
export const registrationInputSchema = z.object({ operationId: z.string().min(1).max(128), server: z.string(), expectedOwnerUserId: z.string().min(1), name: z.string().min(1).max(200).optional(), workstationId: z.string().min(1).optional(), workspace: z.object({ name: z.string(), cwd: z.string(), shell: z.string() }).optional(), guard: bindingGuardSchema, switchConfirmed: z.boolean(), accessToken: z.string().min(1).max(32_768) }).strict();
|
|
18
|
+
export async function saveOperation(directory, operation) {
|
|
19
|
+
assertLockOwnership();
|
|
20
|
+
await privateDirectory(directory);
|
|
21
|
+
const file = path.join(directory, 'operation.json');
|
|
22
|
+
const temporary = file + '.' + randomUUID();
|
|
23
|
+
const handle = await fs.open(temporary, 'wx', 0o600);
|
|
24
|
+
try {
|
|
25
|
+
await handle.writeFile(JSON.stringify(operationRecordSchema.parse(operation)));
|
|
26
|
+
await handle.sync();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await handle.close();
|
|
30
|
+
}
|
|
31
|
+
assertLockOwnership();
|
|
32
|
+
await fs.rename(temporary, file);
|
|
33
|
+
}
|
|
34
|
+
export async function enrollmentFailure(error, directory) {
|
|
35
|
+
const e = error instanceof LocalControlError ? error : new LocalControlError('SERVICE_FAILED', 'The local workstation operation failed. Check its status and try again.');
|
|
36
|
+
return { status: e.code === 'BUSY' ? 'busy' : e.code === 'OUTCOME_UNKNOWN' ? 'pending' : ['BINDING_CONFLICT', 'INSTANCE_OCCUPIED'].includes(e.code) ? 'conflict' : 'failed', inspection: await inspectWorkstation(directory).catch(() => null), error: { code: e.code, message: e.message } };
|
|
37
|
+
}
|
|
38
|
+
async function defaultShell() {
|
|
39
|
+
const candidates = [process.env.SHELL, os.userInfo().shell, process.platform === 'darwin' ? '/bin/zsh' : '/bin/sh'];
|
|
40
|
+
for (const shell of candidates)
|
|
41
|
+
if (shell && path.isAbsolute(shell) && !/[\x00-\x1f]/.test(shell)) {
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(shell, fs.constants.X_OK);
|
|
44
|
+
if ((await fs.stat(shell)).isFile())
|
|
45
|
+
return shell;
|
|
46
|
+
}
|
|
47
|
+
catch { /* Try the user's login shell, then the platform fallback. */ }
|
|
48
|
+
}
|
|
49
|
+
throw new LocalControlError('UNSUPPORTED_INSTALLATION', 'No executable login shell is available');
|
|
50
|
+
}
|
|
51
|
+
async function enroll(input, directory, deps) {
|
|
52
|
+
const server = normalizeWorkstationServer(input.server);
|
|
53
|
+
const store = new BindingStore(directory);
|
|
54
|
+
await store.recover();
|
|
55
|
+
const identity = await store.inspect();
|
|
56
|
+
const runtime = await deps.runtime();
|
|
57
|
+
if (runtime.state !== 'stopped')
|
|
58
|
+
throw new LocalControlError(runtime.state === 'running' ? 'INSTANCE_OCCUPIED' : 'OUTCOME_UNKNOWN', 'Stop the existing workstation before registering this computer');
|
|
59
|
+
if (input.guard && (identity.revision !== input.guard.revision || runtime.generation !== input.guard.runtimeGeneration))
|
|
60
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
61
|
+
if (identity.identityState === 'corrupt' && (!identity.binding || !input.expectedOwnerUserId || (identity.binding.ownerUserId && identity.binding.ownerUserId !== input.expectedOwnerUserId) || identity.binding.server !== server))
|
|
62
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'The saved workstation identity needs verification');
|
|
63
|
+
const switching = identity.binding && (identity.binding.server !== server || (input.expectedOwnerUserId && identity.binding.ownerUserId !== input.expectedOwnerUserId));
|
|
64
|
+
if (switching && !input.switchConfirmed)
|
|
65
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Confirm switching this computer to the selected server and account');
|
|
66
|
+
const inspection = await inspectWorkstation(directory);
|
|
67
|
+
if (inspection.pending && ['pending', 'unknown'].includes(inspection.pending.outcome))
|
|
68
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'An earlier registration needs verification. Refresh the workstation list before retrying.');
|
|
69
|
+
if (identity.binding) {
|
|
70
|
+
const reports = await stopKeepers(new KeeperLauncher({ configDir: directory, ...identity.binding }));
|
|
71
|
+
if (reports.some(report => !report.result.stopped))
|
|
72
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Existing workstation sessions could not be stopped');
|
|
73
|
+
}
|
|
74
|
+
const history = await store.history();
|
|
75
|
+
const known = input.expectedOwnerUserId ? history.find(item => item.server === server && item.ownerUserId === input.expectedOwnerUserId) : undefined;
|
|
76
|
+
const same = identity.binding?.server === server && (!input.expectedOwnerUserId || identity.binding.ownerUserId === input.expectedOwnerUserId);
|
|
77
|
+
const workstationId = input.workstationId ?? known?.workstationId ?? (same ? identity.binding?.workstationId : undefined);
|
|
78
|
+
const operation = { id: input.operationId, kind: switching ? 'switch' : 'register', phase: 'registering', target: { server, ownerUserId: input.expectedOwnerUserId ?? null }, outcome: 'pending' };
|
|
79
|
+
await saveOperation(directory, operation);
|
|
80
|
+
try {
|
|
81
|
+
const workspace = input.workspace;
|
|
82
|
+
const shell = workspace?.shell ?? await defaultShell();
|
|
83
|
+
const response = await requestWorkstationRegistration(server, input.name ?? sanitizeName(os.hostname(), 'computer'), input.accessToken, workspace, workstationId, shell);
|
|
84
|
+
if (input.expectedOwnerUserId && response.ownerUserId !== input.expectedOwnerUserId)
|
|
85
|
+
throw new LocalControlError('OWNER_MISMATCH', 'The server returned a different workstation owner');
|
|
86
|
+
assertLockOwnership();
|
|
87
|
+
await store.commit({ server, workstationId: response.workstationId, ...(response.ownerUserId ? { ownerUserId: response.ownerUserId } : {}), workspaces: workspace ? [{ name: workspace.name, cwd: workspace.cwd }] : [] }, response, { ...operation, workstationId: response.workstationId, phase: 'registered', outcome: 'succeeded' }, identity.identityState === 'corrupt');
|
|
88
|
+
return { status: 'succeeded', inspection: await inspectWorkstation(directory) };
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
await saveOperation(directory, { ...operation, outcome: error instanceof LocalControlError && error.code !== 'OUTCOME_UNKNOWN' ? 'failed' : 'unknown' });
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function enrollWorkstation(input, directory, deps = { runtime: () => inspectRuntime(directory) }) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = registrationInputSchema.parse(input);
|
|
98
|
+
return await withWorkstationLock(directory, 'operation', () => enroll(parsed, directory, deps));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return enrollmentFailure(error, directory);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export async function enrollLegacyWorkstation(input, directory) {
|
|
105
|
+
await withWorkstationLock(directory, 'operation', () => enroll(input, directory, { runtime: () => inspectRuntime(directory) }));
|
|
106
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import { createServiceManager } from '../service/installer.js';
|
|
3
|
+
import { serviceContext } from '../daemonctl.js';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { BindingStore } from './bindingStore.js';
|
|
8
|
+
import { inspectRuntime } from './runtimeClient.js';
|
|
9
|
+
import { localInspectionSchema, operationRecordSchema } from './protocol.js';
|
|
10
|
+
import { readControlFile } from './runtimeHost.js';
|
|
11
|
+
import { updateServiceContext } from '../config.js';
|
|
12
|
+
import { CLIENT_VERSION } from '../version.js';
|
|
13
|
+
import { readPidManifest, manifestProcessLive } from '../keepers/kit/lock.js';
|
|
14
|
+
export async function inspectKeepers(directory) {
|
|
15
|
+
try {
|
|
16
|
+
let running = false;
|
|
17
|
+
for (const kind of ['acp', 'terminal']) {
|
|
18
|
+
const root = path.join(directory, 'keepers', kind, 'run');
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = await fs.readdir(root);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
if (e.code === 'ENOENT')
|
|
25
|
+
continue;
|
|
26
|
+
throw e;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const manifest = await readPidManifest(path.join(root, entry, 'keeper.pid.json'));
|
|
30
|
+
if (manifest && await manifestProcessLive(manifest))
|
|
31
|
+
running = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return running ? 'running' : 'stopped';
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function inspectWorkstation(directory, deps = {}) {
|
|
41
|
+
const identity = await new BindingStore(directory).inspect();
|
|
42
|
+
let pending = null;
|
|
43
|
+
try {
|
|
44
|
+
const operation = operationRecordSchema.parse(JSON.parse(await readControlFile(path.join(directory, 'operation.json'))));
|
|
45
|
+
if (operation.outcome !== 'succeeded')
|
|
46
|
+
pending = operation;
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (e.code !== 'ENOENT')
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../cli.js');
|
|
53
|
+
const context = updateServiceContext(directory);
|
|
54
|
+
let runtime = await inspectRuntime(directory);
|
|
55
|
+
const standardHome = os.userInfo().homedir;
|
|
56
|
+
const standardDirectory = path.join(standardHome, '.axom/workstation');
|
|
57
|
+
const probeLegacy = deps.legacyServiceRunning ?? (process.platform === 'darwin' && path.resolve(directory) === standardDirectory && os.homedir() === standardHome
|
|
58
|
+
? async () => (await createServiceManager(serviceContext()).status()).running : undefined);
|
|
59
|
+
if (runtime.state === 'stopped' && probeLegacy) {
|
|
60
|
+
try {
|
|
61
|
+
if (await probeLegacy())
|
|
62
|
+
runtime = { ...runtime, state: 'unknown', connection: 'unknown' };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
runtime = { ...runtime, state: 'unknown', connection: 'unknown' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const service = (process.platform === 'darwin' || process.platform === 'linux') && path.resolve(directory) === standardDirectory && os.homedir() === standardHome
|
|
69
|
+
? await createServiceManager(serviceContext()).enablement() : undefined;
|
|
70
|
+
return localInspectionSchema.parse({ schemaVersion: 1, ...identity, installation: { source: context.managed ? 'managed' : 'existing', version: CLIENT_VERSION, nodeVersion: process.versions.node, nodePath: context.nodePath, cliPath, npmPath: context.npmPath, npmPrefix: context.npmPrefix }, runtime, ...(service ? { service } : {}), keepers: await inspectKeepers(directory), pending });
|
|
71
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
6
|
+
import lockfile from 'proper-lockfile';
|
|
7
|
+
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
8
|
+
import { pidAlive, processCommand } from '../keepers/kit/lock.js';
|
|
9
|
+
import { LocalControlError } from './protocol.js';
|
|
10
|
+
const ownership = new AsyncLocalStorage();
|
|
11
|
+
export function assertLockOwnership() {
|
|
12
|
+
if (ownership.getStore()?.valid === false)
|
|
13
|
+
throw new LocalControlError('BUSY', 'Workstation lock ownership was lost');
|
|
14
|
+
}
|
|
15
|
+
export async function withPrivateLock(stem, work) {
|
|
16
|
+
await privateDirectory(path.dirname(stem));
|
|
17
|
+
const ownerFile = stem + '.owner.json';
|
|
18
|
+
// proper-lockfile's time lease alone is insufficient: a suspended live app still owns its operation.
|
|
19
|
+
let lockPresent = false;
|
|
20
|
+
try {
|
|
21
|
+
const stat = await fs.lstat(stem + '.lock');
|
|
22
|
+
lockPresent = true;
|
|
23
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
24
|
+
throw Error('Unsafe lock');
|
|
25
|
+
if (Date.now() - stat.mtimeMs > 30_000) {
|
|
26
|
+
const owner = JSON.parse(await fs.readFile(ownerFile, 'utf8'));
|
|
27
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.command !== 'string')
|
|
28
|
+
throw Error('Unknown owner');
|
|
29
|
+
if (pidAlive(owner.pid))
|
|
30
|
+
throw Error('Live or unverifiable owner');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (lockPresent || error.code !== 'ENOENT')
|
|
35
|
+
throw new LocalControlError('BUSY', 'Another workstation operation is active');
|
|
36
|
+
}
|
|
37
|
+
const lease = { valid: true };
|
|
38
|
+
const nonce = randomUUID();
|
|
39
|
+
let release;
|
|
40
|
+
try {
|
|
41
|
+
release = await lockfile.lock(stem, { realpath: false, stale: 30_000, update: 2_000, retries: 0, onCompromised: () => { lease.valid = false; } });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new LocalControlError('BUSY', 'Another workstation operation is active');
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await fs.writeFile(ownerFile, JSON.stringify({ schemaVersion: 1, pid: process.pid, command: await processCommand(process.pid), nonce }), { mode: 0o600 });
|
|
48
|
+
return await ownership.run(lease, async () => { const result = await work(); assertLockOwnership(); return result; });
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
if (lease.valid) {
|
|
52
|
+
try {
|
|
53
|
+
if (JSON.parse(await fs.readFile(ownerFile, 'utf8')).nonce === nonce)
|
|
54
|
+
await fs.rm(ownerFile);
|
|
55
|
+
}
|
|
56
|
+
catch { /* release still required */ }
|
|
57
|
+
await release();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function withWorkstationLock(directory, name, work) {
|
|
62
|
+
return withPrivateLock(name === 'operation' ? path.join(directory, 'control/operation') : path.join(os.homedir(), '.axom/locks/cli-install'), work);
|
|
63
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const bindingSchema = z.object({ server: z.string().min(1), workstationId: z.string().min(1), ownerUserId: z.string().min(1).optional() });
|
|
3
|
+
export const bindingGuardSchema = z.object({ revision: z.string().min(1), runtimeGeneration: z.string().nullable() });
|
|
4
|
+
const activitySchema = z.object({ commands: z.number().int().nonnegative(), terminals: z.number().int().nonnegative(), agents: z.number().int().nonnegative(), transfers: z.number().int().nonnegative() });
|
|
5
|
+
export const runtimeSnapshotSchema = z.object({
|
|
6
|
+
state: z.enum(['stopped', 'running', 'unknown']), binding: bindingSchema.nullable(), generation: z.string().nullable(),
|
|
7
|
+
mode: z.enum(['launchd', 'foreground', 'unknown']), connection: z.enum(['offline', 'connecting', 'online', 'reconnecting', 'rejected', 'unknown']), activity: activitySchema.nullable(),
|
|
8
|
+
});
|
|
9
|
+
export const cliInstallationSchema = z.object({
|
|
10
|
+
source: z.enum(['managed', 'existing']), version: z.string(), nodeVersion: z.string(),
|
|
11
|
+
nodePath: z.string(), cliPath: z.string(), npmPath: z.string(), npmPrefix: z.string(),
|
|
12
|
+
});
|
|
13
|
+
export const operationRecordSchema = z.object({
|
|
14
|
+
id: z.string().min(1), kind: z.enum(['register', 'switch', 'start', 'stop', 'remove', 'update']), phase: z.string().min(1).max(80),
|
|
15
|
+
target: z.object({ server: z.string().min(1), ownerUserId: z.string().min(1).nullable() }).strict(),
|
|
16
|
+
workstationId: z.string().min(1).optional(), outcome: z.enum(['pending', 'succeeded', 'failed', 'unknown']),
|
|
17
|
+
}).strict();
|
|
18
|
+
export const serviceSnapshotSchema = z.object({ installed: z.boolean(), enabled: z.boolean().nullable(), loaded: z.boolean(), running: z.boolean(), reason: z.string().optional() });
|
|
19
|
+
export const localInspectionSchema = z.object({
|
|
20
|
+
schemaVersion: z.literal(1), installation: cliInstallationSchema, binding: bindingSchema.nullable(), revision: z.string().min(1),
|
|
21
|
+
identityState: z.enum(['absent', 'ready', 'incomplete', 'corrupt']), runtime: runtimeSnapshotSchema,
|
|
22
|
+
service: serviceSnapshotSchema.optional(),
|
|
23
|
+
keepers: z.enum(['stopped', 'running', 'unknown']), pending: operationRecordSchema.nullable(),
|
|
24
|
+
});
|
|
25
|
+
export const localErrorCodeSchema = z.enum(['UNSUPPORTED_INSTALLATION', 'UNSUPPORTED_PROTOCOL', 'BINDING_CONFLICT', 'INSTANCE_OCCUPIED', 'AUTHORIZATION_REQUIRED', 'OWNER_MISMATCH', 'NETWORK_FAILED', 'CORRUPT_IDENTITY', 'SERVICE_FAILED', 'BUSY', 'OUTCOME_UNKNOWN']);
|
|
26
|
+
export const cliResultSchema = z.object({
|
|
27
|
+
status: z.enum(['succeeded', 'busy', 'pending', 'failed', 'conflict']), inspection: localInspectionSchema.nullable(),
|
|
28
|
+
error: z.object({ code: localErrorCodeSchema, message: z.string().max(1024) }).optional(),
|
|
29
|
+
}).refine(value => value.status !== 'succeeded' || value.inspection !== null, 'Successful operations require inspection');
|
|
30
|
+
export class LocalControlError extends Error {
|
|
31
|
+
code;
|
|
32
|
+
constructor(code, message) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.code = code;
|
|
35
|
+
this.name = 'LocalControlError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function stoppedRuntime() {
|
|
39
|
+
return { state: 'stopped', binding: null, generation: null, mode: 'unknown', connection: 'offline', activity: null };
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { LocalControlError } from './protocol.js';
|
|
5
|
+
import { readControlFile, runtimeOwnerLive, runtimePaths } from './runtimeHost.js';
|
|
6
|
+
const run = promisify(execFile);
|
|
7
|
+
async function parentPid(pid) {
|
|
8
|
+
let value;
|
|
9
|
+
if (process.platform === 'linux') {
|
|
10
|
+
const stat = await fs.readFile(`/proc/${pid}/stat`, 'utf8');
|
|
11
|
+
value = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/)[1];
|
|
12
|
+
}
|
|
13
|
+
else
|
|
14
|
+
value = (await run('ps', ['-o', 'ppid=', '-p', String(pid)], { timeout: 1000 })).stdout.trim();
|
|
15
|
+
const parent = Number(value);
|
|
16
|
+
if (!value || !Number.isSafeInteger(parent) || parent < 0)
|
|
17
|
+
throw new Error('Unknown process parent');
|
|
18
|
+
return parent;
|
|
19
|
+
}
|
|
20
|
+
export async function assertRuntimeRestartSafe(directory, generation, overrides = {}) {
|
|
21
|
+
const deps = { owner: async () => JSON.parse(await readControlFile(runtimePaths(directory).ownerFile)),
|
|
22
|
+
live: runtimeOwnerLive, parent: parentPid, pid: process.pid, ...overrides };
|
|
23
|
+
try {
|
|
24
|
+
const owner = await deps.owner();
|
|
25
|
+
if (owner.generation !== generation)
|
|
26
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed before restart');
|
|
27
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid <= 1 || !await deps.live(owner))
|
|
28
|
+
throw new Error('Unknown runtime process');
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const deadline = Date.now() + 3000;
|
|
31
|
+
let current = deps.pid;
|
|
32
|
+
while (current > 1 && !seen.has(current) && seen.size < 128 && Date.now() < deadline) {
|
|
33
|
+
if (current === owner.pid)
|
|
34
|
+
throw new LocalControlError('BUSY', 'This command is running inside the workstation daemon. Restarting it would terminate the current agent or command. Finish the session or move it to an independent keeper before restarting.');
|
|
35
|
+
seen.add(current);
|
|
36
|
+
current = await deps.parent(current);
|
|
37
|
+
}
|
|
38
|
+
if (current > 1)
|
|
39
|
+
throw new Error('Unknown process ancestry');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof LocalControlError)
|
|
43
|
+
throw error;
|
|
44
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify that workstation restart will preserve the calling session');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { runtimePaths, readControlFile, runtimeOwnerLive } from './runtimeHost.js';
|
|
3
|
+
import { runtimeSnapshotSchema, stoppedRuntime, LocalControlError } from './protocol.js';
|
|
4
|
+
import { pidAlive } from '../keepers/kit/lock.js';
|
|
5
|
+
export async function runtimeRequest(directory, action, expectedGeneration, extra = {}) {
|
|
6
|
+
const paths = runtimePaths(directory);
|
|
7
|
+
const owner = JSON.parse(await readControlFile(paths.ownerFile));
|
|
8
|
+
if (typeof owner.generation !== 'string' || !pidAlive(owner.pid))
|
|
9
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Runtime owner cannot be verified');
|
|
10
|
+
if (expectedGeneration && owner.generation !== expectedGeneration)
|
|
11
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Runtime changed');
|
|
12
|
+
const token = await readControlFile(paths.tokenFile);
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const socket = net.createConnection(paths.socket);
|
|
15
|
+
let data = '';
|
|
16
|
+
let done = false;
|
|
17
|
+
const fail = (error) => { if (!done) {
|
|
18
|
+
done = true;
|
|
19
|
+
socket.destroy();
|
|
20
|
+
reject(error);
|
|
21
|
+
} };
|
|
22
|
+
socket.setTimeout(3000, () => fail(new LocalControlError('OUTCOME_UNKNOWN', 'Runtime did not respond')));
|
|
23
|
+
socket.on('error', () => fail(new LocalControlError('OUTCOME_UNKNOWN', 'Cannot contact runtime')));
|
|
24
|
+
socket.on('end', () => { if (!done)
|
|
25
|
+
fail(new LocalControlError('OUTCOME_UNKNOWN', 'Incomplete runtime response')); });
|
|
26
|
+
socket.on('connect', () => socket.write(JSON.stringify({ ...extra, action, generation: owner.generation, token }) + '\n'));
|
|
27
|
+
socket.on('data', chunk => {
|
|
28
|
+
data += chunk.toString();
|
|
29
|
+
if (Buffer.byteLength(data) > 65_536) {
|
|
30
|
+
fail(Error('Oversized response'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!data.includes('\n') || done)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
const result = JSON.parse(data.split('\n')[0]);
|
|
37
|
+
if (!result.ok)
|
|
38
|
+
throw new LocalControlError(result.code === 'BINDING_CONFLICT' ? result.code : 'SERVICE_FAILED', 'Runtime rejected the request');
|
|
39
|
+
done = true;
|
|
40
|
+
socket.destroy();
|
|
41
|
+
resolve(result.value);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
fail(error);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export async function inspectRuntime(directory) {
|
|
50
|
+
try {
|
|
51
|
+
return runtimeSnapshotSchema.parse(await runtimeRequest(directory, 'inspect'));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
try {
|
|
55
|
+
const owner = JSON.parse(await readControlFile(runtimePaths(directory).ownerFile));
|
|
56
|
+
if (Number.isSafeInteger(owner.pid) && owner.pid > 0 && typeof owner.command === 'string' && typeof owner.generation === 'string' && !await runtimeOwnerLive(owner))
|
|
57
|
+
return stoppedRuntime();
|
|
58
|
+
}
|
|
59
|
+
catch { /* Partial ownership evidence remains unknown. */ }
|
|
60
|
+
if (error.code === 'ENOENT') {
|
|
61
|
+
// No control files at all is stopped; partial state must remain uncertain.
|
|
62
|
+
const p = runtimePaths(directory);
|
|
63
|
+
const { promises: fs } = await import('node:fs');
|
|
64
|
+
try {
|
|
65
|
+
await fs.lstat(p.socket);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e.code === 'ENOENT') {
|
|
69
|
+
try {
|
|
70
|
+
await fs.lstat(p.ownerFile);
|
|
71
|
+
}
|
|
72
|
+
catch (e2) {
|
|
73
|
+
if (e2.code === 'ENOENT')
|
|
74
|
+
return stoppedRuntime();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { ...stoppedRuntime(), state: 'unknown', connection: 'unknown' };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function stopRuntime(directory, expectedGeneration) {
|
|
83
|
+
await runtimeRequest(directory, 'stop', expectedGeneration);
|
|
84
|
+
const deadline = Date.now() + 10_000;
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
const state = await inspectRuntime(directory);
|
|
87
|
+
if (state.state === 'stopped')
|
|
88
|
+
return;
|
|
89
|
+
if (state.generation && state.generation !== expectedGeneration)
|
|
90
|
+
throw new LocalControlError('BINDING_CONFLICT', 'A new daemon started');
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
92
|
+
}
|
|
93
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Workstation shutdown has not been confirmed');
|
|
94
|
+
}
|
|
95
|
+
export async function requestRuntimeUpdate(directory, release, operationId) {
|
|
96
|
+
return await runtimeRequest(directory, 'update', undefined, { release, operationId });
|
|
97
|
+
}
|