@platless/axom-cli 0.5.9 → 0.6.52
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 +25 -6
- package/dist/axom-patch/cli/README.md +129 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- 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 +55 -13
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- 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/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- 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 +19 -19
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +32 -3
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- 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 -61
- 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/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- 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 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { privateDirectory, socketLimit } from '../keepers/kit/paths.js';
|
|
6
|
+
import { pidAlive, processCommand } from '../keepers/kit/lock.js';
|
|
7
|
+
import { processStartTime } from '../keepers/kit/orphanSweep.js';
|
|
8
|
+
import { LocalControlError } from './protocol.js';
|
|
9
|
+
import { withPrivateLock } from './locks.js';
|
|
10
|
+
import { addRegistrationRequestSchema, assertDeviceGuard, deviceInspectionSchema, deviceRuntimeUpdateRequestSchema, removeRegistrationRequestSchema, deviceResultSchema } from './deviceProtocol.js';
|
|
11
|
+
import { deviceHealthExpectationSchema, deviceHealthRequestSchema, deviceHealthWitnessSchema, deviceRollbackReceiptSchema } from '../update/deviceHealthProtocol.js';
|
|
12
|
+
export function runtimePaths(directory) {
|
|
13
|
+
const root = path.join(path.resolve(directory), 'control');
|
|
14
|
+
const key = createHash('sha256').update(`${process.getuid?.() ?? 'user'}:${path.resolve(directory)}`).digest('hex').slice(0, 24);
|
|
15
|
+
const local = path.join(root, 'runtime.sock');
|
|
16
|
+
return { socket: Buffer.byteLength(local) < socketLimit() ? local : `/tmp/axom-control-${process.getuid?.() ?? 'user'}/${key}.sock`, tokenFile: path.join(root, 'runtime.token'), ownerFile: path.join(root, 'runtime.owner.json') };
|
|
17
|
+
}
|
|
18
|
+
export async function readControlFile(file) {
|
|
19
|
+
const stat = await fs.lstat(file);
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 65_536 || (process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o077))
|
|
21
|
+
throw Error('Unsafe control file');
|
|
22
|
+
return fs.readFile(file, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
/** A persisted PID can belong to another process after a container restart or reboot. */
|
|
25
|
+
export async function runtimeOwnerLive(owner) {
|
|
26
|
+
if (!pidAlive(owner.pid))
|
|
27
|
+
return false;
|
|
28
|
+
const command = await processCommand(owner.pid);
|
|
29
|
+
if (command !== null && command !== owner.command)
|
|
30
|
+
return false;
|
|
31
|
+
if (typeof owner.startTime === 'string') {
|
|
32
|
+
const startTime = await processStartTime(owner.pid);
|
|
33
|
+
if (startTime !== null && startTime !== owner.startTime)
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
// Inability to read the process identity is not evidence that it stopped.
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
export async function openRuntimeHost(options) {
|
|
40
|
+
const binding = { ...options.binding };
|
|
41
|
+
return openControlHost({ ...options, inspect: async (generation) => ({ ...await options.snapshot(), state: 'running', binding, generation, mode: options.mode }), shutdown: () => options.shutdown() });
|
|
42
|
+
}
|
|
43
|
+
export async function openDeviceRuntimeHost(options) {
|
|
44
|
+
return openControlHost({ ...options, inspect: async (generation) => deviceInspectionSchema.parse({ ...await options.snapshot(), runtime: { state: 'running', generation, mode: options.mode } }), device: true });
|
|
45
|
+
}
|
|
46
|
+
async function openControlHost(options) {
|
|
47
|
+
const paths = runtimePaths(options.directory);
|
|
48
|
+
await privateDirectory(path.dirname(paths.ownerFile));
|
|
49
|
+
await privateDirectory(path.dirname(paths.socket));
|
|
50
|
+
return withPrivateLock(path.join(options.directory, 'control/runtime-start'), async () => {
|
|
51
|
+
try {
|
|
52
|
+
const owner = JSON.parse(await readControlFile(paths.ownerFile));
|
|
53
|
+
if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0 || typeof owner.command !== 'string')
|
|
54
|
+
throw Error('Unknown runtime owner');
|
|
55
|
+
if (await runtimeOwnerLive(owner))
|
|
56
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'A workstation daemon already owns this computer');
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code !== 'ENOENT') {
|
|
60
|
+
if (error instanceof LocalControlError)
|
|
61
|
+
throw error;
|
|
62
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Cannot verify the existing workstation daemon');
|
|
63
|
+
}
|
|
64
|
+
// A socket with no owner cannot safely be reclaimed.
|
|
65
|
+
try {
|
|
66
|
+
await fs.lstat(paths.socket);
|
|
67
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Unverified workstation socket');
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (e.code !== 'ENOENT')
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
await fs.rm(paths.socket, { force: true });
|
|
75
|
+
const generation = randomUUID();
|
|
76
|
+
const token = randomBytes(32).toString('hex');
|
|
77
|
+
const preparations = new Map();
|
|
78
|
+
let closed = false;
|
|
79
|
+
const clients = new Set();
|
|
80
|
+
const server = net.createServer(socket => {
|
|
81
|
+
clients.add(socket);
|
|
82
|
+
socket.on('close', () => clients.delete(socket));
|
|
83
|
+
socket.on('error', () => { });
|
|
84
|
+
socket.setTimeout(3000, () => socket.destroy());
|
|
85
|
+
let body = '';
|
|
86
|
+
let handled = false;
|
|
87
|
+
socket.on('data', chunk => {
|
|
88
|
+
body += chunk.toString();
|
|
89
|
+
if (Buffer.byteLength(body) > 65_536) {
|
|
90
|
+
socket.destroy();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (handled || !body.includes('\n'))
|
|
94
|
+
return;
|
|
95
|
+
handled = true;
|
|
96
|
+
void (async () => {
|
|
97
|
+
try {
|
|
98
|
+
const request = JSON.parse(body.split('\n')[0]);
|
|
99
|
+
if (typeof request.token !== 'string' || request.token.length !== token.length || !timingSafeEqual(Buffer.from(token), Buffer.from(request.token)))
|
|
100
|
+
throw Error('Unauthorized');
|
|
101
|
+
if (request.generation !== generation)
|
|
102
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Runtime generation changed');
|
|
103
|
+
let value;
|
|
104
|
+
if (request.action === 'inspect')
|
|
105
|
+
value = await options.inspect(generation);
|
|
106
|
+
else if (request.action === 'prepare-update-rollback' && options.device && options.prepareUpdateRollback) {
|
|
107
|
+
if (Object.keys(request).some(key => !['token', 'generation', 'action', 'request'].includes(key)))
|
|
108
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid private rollback preparation envelope');
|
|
109
|
+
const expected = deviceHealthExpectationSchema.parse(request.request);
|
|
110
|
+
socket.setTimeout(20_000, () => socket.destroy());
|
|
111
|
+
const receipt = deviceRollbackReceiptSchema.parse(await options.prepareUpdateRollback(expected, generation));
|
|
112
|
+
if (receipt.daemonGeneration !== generation)
|
|
113
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Original rollback cleanup receipt belongs to another daemon');
|
|
114
|
+
value = receipt;
|
|
115
|
+
}
|
|
116
|
+
else if (request.action === 'update-health' && options.device && options.updateHealth) {
|
|
117
|
+
if (Object.keys(request).some(key => !['token', 'generation', 'action', 'request'].includes(key)))
|
|
118
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid private device health envelope');
|
|
119
|
+
const parsed = deviceHealthRequestSchema.safeParse(request.request);
|
|
120
|
+
if (!parsed.success)
|
|
121
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid private device health request');
|
|
122
|
+
const witness = deviceHealthWitnessSchema.parse(await options.updateHealth(parsed.data, generation));
|
|
123
|
+
if (witness.daemonGeneration !== generation)
|
|
124
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Private health witness is not owned by this daemon');
|
|
125
|
+
value = witness;
|
|
126
|
+
}
|
|
127
|
+
else if (request.action === 'remove-registration' && options.device && options.removeRegistration) {
|
|
128
|
+
if (Object.keys(request).some(key => !['token', 'generation', 'action', 'request'].includes(key)))
|
|
129
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid local removal envelope');
|
|
130
|
+
const parsed = removeRegistrationRequestSchema.safeParse(request.request);
|
|
131
|
+
if (!parsed.success)
|
|
132
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid local device removal request');
|
|
133
|
+
if (parsed.data.guard.runtimeGeneration !== generation)
|
|
134
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Original removal runtime generation changed');
|
|
135
|
+
socket.setTimeout(20_000, () => socket.destroy());
|
|
136
|
+
value = deviceResultSchema.parse(await options.removeRegistration(parsed.data));
|
|
137
|
+
}
|
|
138
|
+
else if (request.action === 'add-registration' && options.device && options.addRegistration) {
|
|
139
|
+
if (Object.keys(request).some(key => !['token', 'generation', 'action', 'request'].includes(key)))
|
|
140
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid local enrollment envelope');
|
|
141
|
+
const parsed = addRegistrationRequestSchema.safeParse(request.request);
|
|
142
|
+
if (!parsed.success)
|
|
143
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid local device enrollment request');
|
|
144
|
+
if (parsed.data.guard.runtimeGeneration !== generation)
|
|
145
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Original enrollment runtime generation changed');
|
|
146
|
+
socket.setTimeout(35_000, () => socket.destroy());
|
|
147
|
+
value = deviceResultSchema.parse(await options.addRegistration(parsed.data));
|
|
148
|
+
}
|
|
149
|
+
else if (request.action === 'stop') {
|
|
150
|
+
if (options.device && request.stopMode !== 'detach' && request.stopMode !== 'terminate')
|
|
151
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Device shutdown requires an explicit stop mode');
|
|
152
|
+
value = { accepted: true };
|
|
153
|
+
}
|
|
154
|
+
else if (request.action === 'prepare-stop' && options.device && options.prepareShutdown) {
|
|
155
|
+
if (request.stopMode !== 'detach' && request.stopMode !== 'terminate')
|
|
156
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Device preparation requires an explicit stop mode');
|
|
157
|
+
if (typeof request.operationId !== 'string' || request.operationId.length < 1 || request.operationId.length > 128 || /[\u0000-\u001f\u007f]/.test(request.operationId))
|
|
158
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid device preparation operation ID');
|
|
159
|
+
let prepared = preparations.get(request.operationId);
|
|
160
|
+
if (prepared && prepared.mode !== request.stopMode)
|
|
161
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Device preparation mode changed');
|
|
162
|
+
if (!prepared) {
|
|
163
|
+
if (preparations.size >= 128)
|
|
164
|
+
throw new LocalControlError('BUSY', 'Too many device preparation operations');
|
|
165
|
+
const result = options.prepareShutdown(request.stopMode, request.operationId).catch(error => { preparations.delete(request.operationId); throw error; });
|
|
166
|
+
prepared = { mode: request.stopMode, result };
|
|
167
|
+
preparations.set(request.operationId, prepared);
|
|
168
|
+
}
|
|
169
|
+
socket.setTimeout(20_000, () => socket.destroy());
|
|
170
|
+
await prepared.result;
|
|
171
|
+
value = { prepared: true, generation, stopMode: prepared.mode };
|
|
172
|
+
}
|
|
173
|
+
else if (request.action === 'reconcile' && options.reconcile) {
|
|
174
|
+
await options.reconcile();
|
|
175
|
+
value = { accepted: true };
|
|
176
|
+
}
|
|
177
|
+
else if (request.action === 'update') {
|
|
178
|
+
if (options.device) {
|
|
179
|
+
if (Object.keys(request).some(key => !['token', 'generation', 'action', 'request'].includes(key)))
|
|
180
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid device update envelope');
|
|
181
|
+
const parsed = deviceRuntimeUpdateRequestSchema.parse(request.request);
|
|
182
|
+
assertDeviceGuard(parsed.guard, deviceInspectionSchema.parse(await options.inspect(generation)));
|
|
183
|
+
value = await options.update(parsed.release, parsed.operationId, parsed.server);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const r = request.release;
|
|
187
|
+
if (typeof request.operationId !== 'string' || request.operationId.length > 128 || !r || typeof r.version !== 'string' || typeof r.url !== 'string' || typeof r.sha512 !== 'string' || !Number.isSafeInteger(r.size))
|
|
188
|
+
throw Error('Invalid update request');
|
|
189
|
+
value = await options.update(r, request.operationId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
throw Error('Invalid action');
|
|
194
|
+
socket.end(JSON.stringify({ ok: true, value }) + '\n');
|
|
195
|
+
if (request.action === 'stop')
|
|
196
|
+
setImmediate(() => { void options.shutdown(request.stopMode === 'terminate' ? 'terminate' : 'detach').catch(() => { }); });
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
socket.end(JSON.stringify({ ok: false, code: error instanceof LocalControlError ? error.code : 'SERVICE_FAILED' }) + '\n');
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
const close = async () => {
|
|
205
|
+
if (closed)
|
|
206
|
+
return;
|
|
207
|
+
closed = true;
|
|
208
|
+
for (const socket of clients)
|
|
209
|
+
socket.destroy();
|
|
210
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
211
|
+
// Only remove our own generation.
|
|
212
|
+
try {
|
|
213
|
+
if (JSON.parse(await readControlFile(paths.ownerFile)).generation === generation) {
|
|
214
|
+
await fs.rm(paths.ownerFile, { force: true });
|
|
215
|
+
await fs.rm(paths.tokenFile, { force: true });
|
|
216
|
+
await fs.rm(paths.socket, { force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch { /* never remove another owner */ }
|
|
220
|
+
};
|
|
221
|
+
try {
|
|
222
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(paths.socket, () => { server.removeListener('error', reject); resolve(); }); });
|
|
223
|
+
await fs.chmod(paths.socket, 0o600);
|
|
224
|
+
await fs.writeFile(paths.tokenFile, token, { mode: 0o600 });
|
|
225
|
+
await fs.chmod(paths.tokenFile, 0o600);
|
|
226
|
+
await fs.writeFile(paths.ownerFile, JSON.stringify({ schemaVersion: options.device ? 2 : 1, pid: process.pid, command: await processCommand(process.pid), startTime: await processStartTime(process.pid), generation }), { mode: 0o600 });
|
|
227
|
+
await fs.chmod(paths.ownerFile, 0o600);
|
|
228
|
+
return { generation, close };
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
await close();
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const ENDPOINTS = new Set(['/workstation/register', '/workstation/enroll', '/workstation/enroll/identity', '/download/client/manifest.json', '/rest/auth/me', '/rest/auth/scopes/check']);
|
|
2
|
+
export function normalizeWorkstationServer(value) {
|
|
3
|
+
if (/[\u0000-\u0020\\]/.test(value))
|
|
4
|
+
throw new Error('Invalid workstation server URL');
|
|
5
|
+
// Inspect the original path: URL construction silently removes encoded dot segments.
|
|
6
|
+
const rawPath = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*(\/[^?#]*)?/.exec(value)?.[1] ?? '';
|
|
7
|
+
for (const part of rawPath.split('/')) {
|
|
8
|
+
const decoded = decodeURIComponent(part);
|
|
9
|
+
if (decoded === '.' || decoded === '..' || /[\/\\\u0000-\u001f\u007f]/.test(decoded))
|
|
10
|
+
throw new Error('Invalid workstation server path');
|
|
11
|
+
}
|
|
12
|
+
const url = new URL(value);
|
|
13
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
14
|
+
throw new Error('Workstation server must be an HTTP(S) URL without credentials, query or fragment');
|
|
15
|
+
}
|
|
16
|
+
return url.origin + url.pathname.replace(/\/+$/, '');
|
|
17
|
+
}
|
|
18
|
+
export function workstationEndpoint(server, apiPath) {
|
|
19
|
+
if (!ENDPOINTS.has(apiPath) && !/^\/workstation\/enroll\/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/.test(apiPath))
|
|
20
|
+
throw new Error('Unrecognized workstation endpoint');
|
|
21
|
+
return new URL(normalizeWorkstationServer(server) + apiPath);
|
|
22
|
+
}
|
|
23
|
+
/** Catalog URLs are deployment-relative even when a reverse proxy adds a path prefix. */
|
|
24
|
+
export function deploymentDownloadURL(server, value) {
|
|
25
|
+
const base = normalizeWorkstationServer(server);
|
|
26
|
+
const configured = new URL(base);
|
|
27
|
+
const candidate = value.startsWith('/download/client/') ? base + value : new URL(value, base + '/').toString();
|
|
28
|
+
const normalized = normalizeWorkstationServer(candidate);
|
|
29
|
+
const url = new URL(normalized);
|
|
30
|
+
if (url.origin !== configured.origin)
|
|
31
|
+
throw new Error('Artifact URL must use the configured server origin');
|
|
32
|
+
if (!url.pathname.startsWith(configured.pathname.replace(/\/$/, '') + '/download/client/'))
|
|
33
|
+
throw new Error('Artifact URL must stay in the configured deployment');
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { LocalControlError } from './protocol.js';
|
|
4
|
+
import { readControlFile } from './runtimeHost.js';
|
|
5
|
+
import { readDeviceBootstrapRecovery, readPrivateUpdateBytes, updateTransactionKey } from '../update/recoveryPlan.js';
|
|
6
|
+
import { readLegacyBridge, assertLegacyBridgeTransferred, assertLegacyBridgeMutationAllowed } from '../update/legacyBridge.js';
|
|
7
|
+
export async function assertDurableDeviceMutationAllowed(directory) {
|
|
8
|
+
await assertLegacyBridgeMutationAllowed(directory);
|
|
9
|
+
let bytes;
|
|
10
|
+
try {
|
|
11
|
+
bytes = await readPrivateUpdateBytes(path.join(directory, 'update/device-transaction.json'), 33_554_432);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === 'ENOENT')
|
|
15
|
+
return;
|
|
16
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Private device update ownership requires compatible recovery');
|
|
17
|
+
}
|
|
18
|
+
let transaction;
|
|
19
|
+
try {
|
|
20
|
+
const { deviceTransactionSchema } = await import('../update/deviceCoordinator.js');
|
|
21
|
+
transaction = deviceTransactionSchema.parse(JSON.parse(bytes.toString('utf8')));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Preserve corrupt private device update ownership before changing registrations');
|
|
25
|
+
}
|
|
26
|
+
if (transaction.recoveryRequired || !['SUCCEEDED', 'FAILED', 'ROLLED_BACK', 'BUSY'].includes(transaction.phase))
|
|
27
|
+
throw new LocalControlError('BUSY', 'The original durable device update still owns membership admission');
|
|
28
|
+
if (['FAILED', 'BUSY'].includes(transaction.phase) && transaction.installation !== 'not-started')
|
|
29
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Original update mutation cannot be excluded; preserve compatible recovery evidence');
|
|
30
|
+
}
|
|
31
|
+
export async function assertDeviceUpdateTransitionSafe(directory, startup) {
|
|
32
|
+
const bridge = await readLegacyBridge(directory, startup);
|
|
33
|
+
if (bridge) {
|
|
34
|
+
await assertLegacyBridgeTransferred(bridge);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const remediation = 'Unresolved legacy update/watchdog evidence: verify the predecessor updater and watchdog have exited, install a compatible CLI, then explicitly retire the verified old update evidence before starting the device daemon';
|
|
38
|
+
if (startup) {
|
|
39
|
+
try {
|
|
40
|
+
const update = path.join(directory, 'update');
|
|
41
|
+
const stat = await fs.lstat(update);
|
|
42
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.mode & 0o077 || process.getuid && stat.uid !== process.getuid())
|
|
43
|
+
throw Error(remediation);
|
|
44
|
+
const plans = (await fs.readdir(update)).filter(name => /^watchdog-.*\.json(?:\..*)?$/.test(name));
|
|
45
|
+
if (plans.length) {
|
|
46
|
+
const recovery = await readDeviceBootstrapRecovery(directory, startup);
|
|
47
|
+
if (!recovery)
|
|
48
|
+
throw Error(remediation);
|
|
49
|
+
if (plans.length !== 1 || plans[0] !== `watchdog-${updateTransactionKey(recovery.transaction.transactionId)}.json`)
|
|
50
|
+
throw Error(remediation);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const transaction = JSON.parse((await readPrivateUpdateBytes(path.join(update, 'device-transaction.json'), 33_554_432)).toString('utf8'));
|
|
55
|
+
if (transaction.lifecycle === 'container' || transaction.lifecycle === 'none') {
|
|
56
|
+
await readDeviceBootstrapRecovery(directory, startup);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code !== 'ENOENT')
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code !== 'ENOENT')
|
|
67
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', remediation);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const state = JSON.parse(await readControlFile(path.join(directory, 'update-state.json')));
|
|
72
|
+
if (!state || !['SUCCEEDED', 'FAILED', 'ROLLED_BACK', 'BUSY', 'UPDATE_IN_PROGRESS'].includes(state.phase))
|
|
73
|
+
throw Error(remediation);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error.code !== 'ENOENT')
|
|
77
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', remediation);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const update = path.join(directory, 'update');
|
|
81
|
+
const stat = await fs.lstat(update);
|
|
82
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) || (process.getuid && stat.uid !== process.getuid()))
|
|
83
|
+
throw Error(remediation);
|
|
84
|
+
if ((await fs.readdir(update)).some(name => /^watchdog-.*\.json(?:\..*)?$/.test(name)))
|
|
85
|
+
throw Error(remediation);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== 'ENOENT')
|
|
89
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', remediation);
|
|
90
|
+
}
|
|
91
|
+
}
|