@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
package/dist/commands/update.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { inspectWorkstation } from '../workstation/control/inspect.js';
|
|
3
|
+
import { requestDeviceRuntimeUpdate, requestRuntimeUpdate } from '../workstation/control/runtimeClient.js';
|
|
4
|
+
import { inspectLocalDevice } from '../workstation/control/deviceCommands.js';
|
|
5
|
+
import { assertDeviceGuard, deviceRuntimeUpdateRequestSchema } from '../workstation/control/deviceProtocol.js';
|
|
6
|
+
import { withWorkstationLock } from '../workstation/control/locks.js';
|
|
7
|
+
import { bindingGuardSchema, LocalControlError } from '../workstation/control/protocol.js';
|
|
8
|
+
import { enrollmentFailure } from '../workstation/control/enrollment.js';
|
|
9
|
+
import { readLocalRequest } from '../workstation/control/commands.js';
|
|
10
|
+
import { deploymentDownloadURL, normalizeWorkstationServer, workstationEndpoint } from '../workstation/control/serverIdentity.js';
|
|
1
11
|
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
2
14
|
import { UsageError, loadConfig, resolveServer } from '../config.js';
|
|
3
15
|
import { runCommand } from '../output.js';
|
|
4
16
|
import { clientDir } from '../workstation/config.js';
|
|
@@ -13,8 +25,8 @@ function isManifest(value) {
|
|
|
13
25
|
}
|
|
14
26
|
/** Fetches the release the server currently publishes for this exact deployment. */
|
|
15
27
|
async function fetchManifest(server, fetchImpl) {
|
|
16
|
-
const url =
|
|
17
|
-
const response = await fetchImpl(url);
|
|
28
|
+
const url = workstationEndpoint(server, '/download/client/manifest.json').toString();
|
|
29
|
+
const response = await fetchImpl(url, { redirect: 'error', signal: AbortSignal.timeout(30_000) });
|
|
18
30
|
if (!response.ok) {
|
|
19
31
|
throw new Error(`Fetching the release manifest failed with HTTP ${response.status}`);
|
|
20
32
|
}
|
|
@@ -55,6 +67,35 @@ export async function updateAction(options, deps = {}) {
|
|
|
55
67
|
version: manifest.version, url: manifest.url, size: manifest.size, sha512: manifest.sha512,
|
|
56
68
|
};
|
|
57
69
|
const dir = deps.dir ?? clientDir();
|
|
70
|
+
if (!deps.createCoordinator) {
|
|
71
|
+
let deviceRoot = false;
|
|
72
|
+
for (const name of ['device.json', 'legacy-migration.json']) {
|
|
73
|
+
try {
|
|
74
|
+
await fs.lstat(path.join(dir, name));
|
|
75
|
+
deviceRoot = true;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error.code !== 'ENOENT')
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (deviceRoot) {
|
|
83
|
+
const current = await inspectLocalDevice(dir);
|
|
84
|
+
const result = await updateLocalCli({ schemaVersion: 2, action: 'update', operationId: `manual-${randomUUID()}`, server, guard: { deviceRevision: current.deviceRevision, runtimeGeneration: current.runtime.generation }, expectedRelease: { version: release.version, sha512: release.sha512 } }, dir, fetchImpl);
|
|
85
|
+
if (result.status === 'succeeded' || result.status === 'pending' && !result.error) {
|
|
86
|
+
(deps.log ?? console.log)(result.status === 'pending' ? 'Update requested. Check workstation status for progress.' : `Installed ${release.version}.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(result.error?.message ?? 'The workstation is busy; try again when its work finishes.');
|
|
90
|
+
}
|
|
91
|
+
const current = await inspectWorkstation(dir);
|
|
92
|
+
const result = await updateLocalCli({ operationId: `manual-${randomUUID()}`, server, guard: { revision: current.revision, runtimeGeneration: current.runtime.generation }, expectedRelease: { version: release.version, sha512: release.sha512 } }, dir, fetchImpl);
|
|
93
|
+
if (result.status === 'succeeded' || result.status === 'pending') {
|
|
94
|
+
(deps.log ?? console.log)(result.status === 'pending' ? 'Update requested. Check workstation status for progress.' : `Installed ${release.version}.`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
throw new Error(result.error?.message ?? 'The workstation is busy; try again when its work finishes.');
|
|
98
|
+
}
|
|
58
99
|
const coordinator = deps.createCoordinator ? deps.createCoordinator({ dir, server }) : defaultCoordinator(dir, server);
|
|
59
100
|
const command = { commandId: `manual-${randomUUID()}`, release, force: false };
|
|
60
101
|
const status = await coordinator.handle(command);
|
|
@@ -78,11 +119,93 @@ export async function updateAction(options, deps = {}) {
|
|
|
78
119
|
throw new Error(status.error ?? `Update did not complete (phase: ${status.phase}).`);
|
|
79
120
|
}
|
|
80
121
|
}
|
|
122
|
+
const updateRequestSchema = z.object({ operationId: z.string().min(1).max(128), server: z.string(), guard: bindingGuardSchema, expectedRelease: z.object({ version: z.string(), sha512: z.string().regex(/^[a-f0-9]{128}$/i) }).strict() }).strict();
|
|
123
|
+
const deviceUpdateCliSchema = deviceRuntimeUpdateRequestSchema.omit({ release: true }).extend({ expectedRelease: updateRequestSchema.shape.expectedRelease }).strict();
|
|
124
|
+
async function updateDeviceCli(raw, directory, fetchImpl) {
|
|
125
|
+
let operationId = randomUUID();
|
|
126
|
+
try {
|
|
127
|
+
const request = deviceUpdateCliSchema.parse(raw);
|
|
128
|
+
operationId = request.operationId;
|
|
129
|
+
const server = normalizeWorkstationServer(request.server);
|
|
130
|
+
const current = await inspectLocalDevice(directory);
|
|
131
|
+
if (current.runtime.state !== 'running')
|
|
132
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Start and verify the original device owner before updating');
|
|
133
|
+
assertDeviceGuard(request.guard, current);
|
|
134
|
+
const release = await fetchManifest(server, fetchImpl);
|
|
135
|
+
if (release.version !== request.expectedRelease.version || release.sha512 !== request.expectedRelease.sha512)
|
|
136
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The published release changed; review it again');
|
|
137
|
+
deploymentDownloadURL(server, release.url);
|
|
138
|
+
const status = await requestDeviceRuntimeUpdate(directory, { schemaVersion: 2, action: 'update', operationId, server, guard: request.guard, release });
|
|
139
|
+
const result = ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : ['FAILED', 'ROLLED_BACK'].includes(status.phase) ? 'failed' : 'pending';
|
|
140
|
+
const inspection = await inspectLocalDevice(directory).catch(() => null);
|
|
141
|
+
return { operationId, registrationId: null, status: result, inspection, ...(status.error ? { error: { code: 'SERVICE_FAILED', message: status.error } } : {}) };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const code = error instanceof LocalControlError ? error.code : 'UNSUPPORTED_PROTOCOL';
|
|
145
|
+
return { operationId, registrationId: null, status: code === 'BUSY' ? 'busy' : code === 'OUTCOME_UNKNOWN' ? 'pending' : 'failed', inspection: null, error: { code, message: 'Device update requires the original verified owner and unchanged source/release' } };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function updateLocalCli(raw, directory, fetchImpl = globalThis.fetch) {
|
|
149
|
+
if (raw && typeof raw === 'object' && 'schemaVersion' in raw)
|
|
150
|
+
return updateDeviceCli(raw, directory, fetchImpl);
|
|
151
|
+
try {
|
|
152
|
+
const request = updateRequestSchema.parse(raw);
|
|
153
|
+
const server = normalizeWorkstationServer(request.server);
|
|
154
|
+
const manifest = await fetchManifest(server, fetchImpl);
|
|
155
|
+
if (manifest.version !== request.expectedRelease.version || manifest.sha512 !== request.expectedRelease.sha512)
|
|
156
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The published release changed; review it again');
|
|
157
|
+
manifest.url = deploymentDownloadURL(server, manifest.url);
|
|
158
|
+
const before = await inspectWorkstation(directory);
|
|
159
|
+
if (before.revision !== request.guard.revision || before.runtime.generation !== request.guard.runtimeGeneration)
|
|
160
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
161
|
+
if (before.runtime.state === 'unknown')
|
|
162
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the existing daemon before updating');
|
|
163
|
+
if (before.runtime.state === 'running') {
|
|
164
|
+
if (before.runtime.binding?.server !== server)
|
|
165
|
+
throw new LocalControlError('INSTANCE_OCCUPIED', 'Another server owns the running daemon');
|
|
166
|
+
const status = await requestRuntimeUpdate(directory, manifest, request.operationId);
|
|
167
|
+
return { status: ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' ? 'failed' : 'pending', inspection: await inspectWorkstation(directory) };
|
|
168
|
+
}
|
|
169
|
+
return await withWorkstationLock(directory, 'installation', () => withWorkstationLock(directory, 'operation', async () => {
|
|
170
|
+
const current = await inspectWorkstation(directory);
|
|
171
|
+
if (current.revision !== request.guard.revision || current.runtime.generation !== request.guard.runtimeGeneration)
|
|
172
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
|
|
173
|
+
if (current.runtime.state === 'unknown')
|
|
174
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Stop and verify the existing daemon before updating');
|
|
175
|
+
if (current.runtime.state !== 'stopped')
|
|
176
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'The original stopped owner changed before offline update admission');
|
|
177
|
+
const status = await createUpdateCoordinator({ dir: directory, server, cliPath: CLI_PATH, serviceLifecycle: 'none', installationLockHeld: true }).handle({ commandId: request.operationId, release: manifest, force: false });
|
|
178
|
+
return { status: ['BUSY', 'UPDATE_IN_PROGRESS'].includes(status.phase) ? 'busy' : status.phase === 'SUCCEEDED' ? 'succeeded' : status.phase === 'FAILED' || status.phase === 'ROLLED_BACK' ? 'failed' : 'pending', inspection: await inspectWorkstation(directory) };
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return enrollmentFailure(error, directory);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
81
185
|
export function registerUpdateCommand(program) {
|
|
82
186
|
const serverOption = () => program.opts().server;
|
|
83
187
|
program
|
|
84
188
|
.command('update')
|
|
85
189
|
.description('Update axom-cli to the release the configured server currently publishes')
|
|
190
|
+
.option('--json', 'Emit local management protocol JSON')
|
|
191
|
+
.option('--input-stdin', 'Read the guarded update request from stdin')
|
|
86
192
|
.option('--version <version>', 'Fail unless the server-published release matches this exact version')
|
|
87
|
-
.action((options) => runCommand(() =>
|
|
193
|
+
.action((options) => runCommand(async () => {
|
|
194
|
+
if (!options.json)
|
|
195
|
+
return updateAction({ server: serverOption(), version: options.version });
|
|
196
|
+
if (!options.inputStdin)
|
|
197
|
+
throw new UsageError('--json requires --input-stdin');
|
|
198
|
+
let request;
|
|
199
|
+
let result;
|
|
200
|
+
try {
|
|
201
|
+
request = await readLocalRequest(process.stdin);
|
|
202
|
+
result = await updateLocalCli(request, clientDir());
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
result = await enrollmentFailure(error, clientDir());
|
|
206
|
+
}
|
|
207
|
+
const device = deviceUpdateCliSchema.safeParse(request);
|
|
208
|
+
const id = updateRequestSchema.safeParse(request);
|
|
209
|
+
process.stdout.write(JSON.stringify({ schemaVersion: device.success ? 2 : 1, kind: 'result', operationId: device.success ? device.data.operationId : id.success ? id.data.operationId : randomUUID(), result }) + '\n');
|
|
210
|
+
}));
|
|
88
211
|
}
|
|
@@ -1,171 +1,108 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { loadConfig, resolveServer, UsageError } from '../config.js';
|
|
4
|
-
import { accessTokenFor } from '../credentials.js';
|
|
5
|
-
import { ensureFreshToken } from '../mcp.js';
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { UsageError } from '../config.js';
|
|
6
3
|
import { runCommand } from '../output.js';
|
|
7
|
-
import { clientDir
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* An existing workstation credential boots directly — the common case, and the only sane one for a
|
|
19
|
-
* container that gets recreated on every restart: re-registering each boot would rotate keys for no
|
|
20
|
-
* reason. Otherwise a usable access token registers this machine — `AXOM_ACCESS_TOKEN` (set by a
|
|
21
|
-
* container with no human and no browser) or a stored user token (from `axom auth`), refreshing it
|
|
22
|
-
* first if it's stale. With neither available, an interactive terminal falls back to the device
|
|
23
|
-
* flow; a headless one gets a hard stop naming the fix.
|
|
24
|
-
*/
|
|
25
|
-
async function ensureRegistered(deps, serverFlag, nameFlag) {
|
|
26
|
-
const registered = await loadClientConfig(clientDir()).then(() => true, () => false);
|
|
27
|
-
if (registered)
|
|
28
|
-
return false;
|
|
29
|
-
const config = await loadConfig();
|
|
30
|
-
const server = resolveServer(config, serverFlag);
|
|
31
|
-
let token = await accessTokenFor(server);
|
|
32
|
-
if (!token) {
|
|
33
|
-
if (!deps.isInteractive()) {
|
|
34
|
-
throw new UsageError('No credential for this workstation. Set AXOM_ACCESS_TOKEN (create one under Settings → Security → ' +
|
|
35
|
-
'Access Tokens), or run `axom auth` on a machine with a browser.');
|
|
36
|
-
}
|
|
37
|
-
await deps.authenticate(server);
|
|
38
|
-
token = await ensureFreshToken(server);
|
|
39
|
-
}
|
|
40
|
-
const cwd = process.cwd();
|
|
41
|
-
await deps.registerWorkstation(server, resolveWorkstationName(nameFlag, process.env, hostname()), token, {
|
|
42
|
-
name: sanitizeName(basename(cwd), 'default'),
|
|
43
|
-
cwd,
|
|
44
|
-
shell: process.env.SHELL ?? '/bin/bash',
|
|
45
|
-
});
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Removes `AXOM_ACCESS_TOKEN` from *this process's* copy of the environment, right before the bare
|
|
50
|
-
* `axom workstation` command becomes a long-lived daemon.
|
|
51
|
-
*
|
|
52
|
-
* The daemon spawns agents and shell commands with `{ ...process.env, ... }` (exec.ts, acpClient.ts),
|
|
53
|
-
* so anything still in here reaches every model-authored command and every dependency postinstall
|
|
54
|
-
* that runs in `/workspace` — a container is precisely where this product runs untrusted code. What
|
|
55
|
-
* would leak is not the workstation's own key pair (scoped to one workstation, on disk) but a
|
|
56
|
-
* credential that authenticates *as the user*: it can register further workstations under their
|
|
57
|
-
* account and, with business scopes, call granted MCP tools as them, until somebody notices and
|
|
58
|
-
* revokes it. Fixed once here rather than filtered at each of the two spawn sites, so the next spawn site
|
|
59
|
-
* inherits the fix instead of having to remember it.
|
|
60
|
-
*
|
|
61
|
-
* Nothing after this point needs the value. The daemon authenticates with the file credential
|
|
62
|
-
* written at registration (`clientKey`/`serverPub`), and the `container` update lifecycle downloads
|
|
63
|
-
* its release over that same authenticated management channel.
|
|
64
|
-
*
|
|
65
|
-
* Unconditional, not gated on "we just registered": a container that still has its `/data/ws` volume
|
|
66
|
-
* boots straight past `ensureRegistered`, and that is exactly the long-lived container this protects.
|
|
67
|
-
*
|
|
68
|
-
* The token stays in the *container's* environment, so a restarted container still registers — only
|
|
69
|
-
* this process's copy is dropped. Every reader of it lives before this line: `accessTokenFor`
|
|
70
|
-
* (credentials.ts) reads `process.env.AXOM_ACCESS_TOKEN` on its first line, and its only caller here
|
|
71
|
-
* is `ensureRegistered` above; `insufficientScopeError`/`envAccessTokenRejectedError` (mcp.ts) read
|
|
72
|
-
* it too, but only from the one-shot `axom execute`/`axom struct` CLI processes, which are separate
|
|
73
|
-
* processes with their own untouched copy.
|
|
74
|
-
*
|
|
75
|
-
* `workstation start` deliberately does not call this: it registers and hands off to launchd/systemd
|
|
76
|
-
* rather than becoming the daemon, so dropping the variable would affect only a CLI process that is
|
|
77
|
-
* about to exit.
|
|
78
|
-
*/
|
|
79
|
-
function dropAccessTokenFromEnvironment() {
|
|
80
|
-
delete process.env.AXOM_ACCESS_TOKEN;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Registers `axom workstation start|stop|restart|status|install|uninstall`, the bare
|
|
84
|
-
* `axom workstation` (Docker ENTRYPOINT / debugging foreground run), and the hidden top-level
|
|
85
|
-
* `axom __daemon`.
|
|
86
|
-
*
|
|
87
|
-
* `__daemon` is registered on `program` itself, not on the `workstation` subcommand group: the
|
|
88
|
-
* generated launchd/systemd service definitions invoke it as `<cliPath> __daemon`, a bare
|
|
89
|
-
* top-level argument (see service/launchd.ts and service/systemd.ts).
|
|
90
|
-
*/
|
|
4
|
+
import { clientDir } from '../workstation/config.js';
|
|
5
|
+
import { configureRd, rdStatus } from '../workstation/rd/control.js';
|
|
6
|
+
import { runManagedDesktop } from '../workstation/rd/sessions.js';
|
|
7
|
+
import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
|
|
8
|
+
import { readLocalRequest } from '../workstation/control/commands.js';
|
|
9
|
+
import { LocalControlError } from '../workstation/control/protocol.js';
|
|
10
|
+
import { deviceEventSchema } from '../workstation/control/deviceProtocol.js';
|
|
11
|
+
import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
|
|
12
|
+
import { registrationIdSchema } from '../workstation/registrations/model.js';
|
|
13
|
+
import { addWorkstationRegistration, listWorkstationRegistrations, removeWorkstationRegistration } from './workstationRegistrations.js';
|
|
91
14
|
export function registerWorkstationCommands(program, deps) {
|
|
92
|
-
const workstation = program.command('workstation').description('Manage
|
|
93
|
-
|
|
94
|
-
// registration time, so `axom --server <url> workstation start` works. `axom auth` only writes
|
|
95
|
-
// `defaultServer` when it is unset, so a user who authenticated against localhost first and
|
|
96
|
-
// production second would otherwise have no way to keep `start` off localhost.
|
|
15
|
+
const workstation = program.command('workstation').description('Manage this device and its workstation registration collection');
|
|
16
|
+
const registrations = deps.registrations ?? { add: addWorkstationRegistration, list: listWorkstationRegistrations, remove: removeWorkstationRegistration };
|
|
97
17
|
const serverOption = () => program.opts().server;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
workstation
|
|
151
|
-
.
|
|
152
|
-
|
|
153
|
-
.
|
|
18
|
+
const applyRd = deps.configureRd ?? configureRd;
|
|
19
|
+
const rejectSelectors = (options = {}) => {
|
|
20
|
+
const group = workstation.opts();
|
|
21
|
+
if (options.server !== undefined || options.name !== undefined || group.server !== undefined || group.name !== undefined || serverOption() !== undefined)
|
|
22
|
+
throw new UsageError('Workstation lifecycle commands apply to the whole device. Use `axom workstation add [address]` to add a server/account registration, or `axom workstation remove <registrationId>` to remove one.');
|
|
23
|
+
};
|
|
24
|
+
const removed = () => runCommand(async () => { throw new UsageError('This workstation command was removed. Use `axom workstation add [address]`, `list`, or `remove <registrationId>`; start/stop operate on the whole device.'); });
|
|
25
|
+
const dispatch = async (action, options, task, registrationId) => {
|
|
26
|
+
const target = registrationIdSchema.safeParse(registrationId);
|
|
27
|
+
const operation = { operationId: randomUUID(), registrationId: target.success ? target.data : null };
|
|
28
|
+
const captureOperationId = (value) => {
|
|
29
|
+
const parsed = operationIdSchema.safeParse(value);
|
|
30
|
+
if (!parsed.success)
|
|
31
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid workstation operation correlation ID');
|
|
32
|
+
operation.operationId = parsed.data;
|
|
33
|
+
};
|
|
34
|
+
if (!options.json) {
|
|
35
|
+
if (options.inputStdin)
|
|
36
|
+
throw new UsageError('--input-stdin requires --json');
|
|
37
|
+
if (options.operationId !== undefined)
|
|
38
|
+
captureOperationId(options.operationId);
|
|
39
|
+
await task(operation);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
if (options.operationId !== undefined)
|
|
44
|
+
captureOperationId(options.operationId);
|
|
45
|
+
if (!options.inputStdin) {
|
|
46
|
+
await task(operation);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const request = await readLocalRequest(process.stdin);
|
|
50
|
+
if (request && typeof request === 'object' && 'operationId' in request)
|
|
51
|
+
captureOperationId(request.operationId);
|
|
52
|
+
if (!request || typeof request !== 'object' || !('action' in request) || request.action !== action)
|
|
53
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Use a matching v2 device command request');
|
|
54
|
+
if (registrationId !== undefined && (!('guard' in request) || !request.guard || typeof request.guard !== 'object' || !('registrationId' in request.guard) || request.guard.registrationId !== registrationId))
|
|
55
|
+
throw new LocalControlError('BINDING_CONFLICT', 'The guarded removal must match the exact registration ID');
|
|
56
|
+
const result = await executeDeviceCommand(request, clientDir());
|
|
57
|
+
if (result.operationId !== operation.operationId)
|
|
58
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Original workstation operation reply does not match');
|
|
59
|
+
deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
|
|
60
|
+
if (result.status !== 'succeeded')
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const code = error instanceof LocalControlError ? error.code : 'SERVICE_FAILED';
|
|
65
|
+
deps.log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: operation.operationId, result: { operationId: operation.operationId, registrationId: operation.registrationId, status: code === 'BUSY' ? 'busy' : code === 'OUTCOME_UNKNOWN' ? 'pending' : ['BINDING_CONFLICT', 'OWNER_MISMATCH'].includes(code) ? 'conflict' : 'failed', inspection: null, error: { code, message: error instanceof LocalControlError ? error.message : 'The workstation operation could not be confirmed' } } })));
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
workstation.command('list').description('List local workstation registrations and device state').option('--json', 'Emit the v2 device collection').action(options => runCommand(() => dispatch('inspect', options, () => registrations.list(options))));
|
|
70
|
+
workstation.command('add [address]').description('Add an exact server/account registration without changing ordinary CLI login').option('--name <name>', 'Workstation name').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a protected v2 enrollment request from stdin').option('--operation-id <id>', 'Operation correlation ID').option('--owner <userId>', 'Require this account identity')
|
|
71
|
+
.action((address, options) => runCommand(() => dispatch('add-registration', options, operation => registrations.add({ ...options, operation, name: options.name ?? workstation.opts().name, server: address ?? serverOption(), expectedOwnerUserId: options.owner }))));
|
|
72
|
+
workstation.command('remove <registrationId>').description('Remove this exact local registration; busy resources require explicit --force').option('--force', 'Retire this registration’s resources').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read the original guarded v2 removal request from stdin').option('--operation-id <id>', 'Retry the original operation ID')
|
|
73
|
+
.action((id, options) => runCommand(() => dispatch('remove-registration', options, operation => registrations.remove(id, { ...options, operation }), id)));
|
|
74
|
+
for (const verb of ['start', 'stop', 'restart', 'install', 'uninstall']) {
|
|
75
|
+
const command = workstation.command(verb).description(`${verb} the whole-device service`).option('--name <name>', 'Removed selector; use workstation add').option('--server <url>', 'Removed selector; use workstation add').option('--json', 'Emit a v2 correlated result').option('--input-stdin', 'Read a guarded v2 whole-device request from stdin');
|
|
76
|
+
if (verb === 'start')
|
|
77
|
+
command.option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display');
|
|
78
|
+
command.action(options => runCommand(async () => {
|
|
79
|
+
rejectSelectors(options);
|
|
80
|
+
const rdOptions = { ...workstation.opts(), ...options };
|
|
81
|
+
const hasRd = rdOptions.enableRd || rdOptions.disableRd || rdOptions.rdSession || rdOptions.rdDisplay;
|
|
82
|
+
if (hasRd && (verb !== 'start' || options.json))
|
|
83
|
+
throw new UsageError('RD flags require a plain `axom workstation start` or foreground command');
|
|
84
|
+
if (options.json && !options.inputStdin)
|
|
85
|
+
throw new UsageError('--json requires --input-stdin for device lifecycle commands');
|
|
86
|
+
if (hasRd)
|
|
87
|
+
await applyRd(clientDir(), rdOptions);
|
|
88
|
+
await dispatch(verb, options, () => deps[verb]());
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
workstation.command('status').description('Report the whole-device service status').action(() => runCommand(async () => { rejectSelectors(); deps.log(await deps.status()); }));
|
|
92
|
+
workstation.command('inspect').requiredOption('--json', 'Emit the v2 device collection').action(() => runCommand(async () => { deps.log(JSON.stringify(await inspectLocalDevice(clientDir()))); }));
|
|
93
|
+
for (const verb of ['auth', 'register', 'forget', 'begin-remove', 'adopt-owner', 'recover', 'set-enabled'])
|
|
94
|
+
workstation.command(verb, { hidden: true }).allowUnknownOption().allowExcessArguments().action(removed);
|
|
95
|
+
const rd = workstation.command('rd').description('Manage Linux remote desktop');
|
|
96
|
+
rd.command('status').action(() => runCommand(async () => { deps.log(JSON.stringify(await rdStatus(clientDir()), null, 2)); }));
|
|
97
|
+
rd.command('disable').action(() => runCommand(() => applyRd(clientDir(), { disableRd: true })));
|
|
98
|
+
workstation.option('--server <url>', 'Removed registration shortcut; use workstation add').option('--name <name>', 'Removed registration shortcut; use workstation add')
|
|
99
|
+
.option('--enable-rd', 'Install and enable Linux remote desktop').option('--disable-rd', 'Disable remote desktop').option('--rd-session <mode>', 'Desktop selection: auto, existing, or virtual').option('--rd-display <display>', 'Choose a local X11 display')
|
|
154
100
|
.action((options) => runCommand(async () => {
|
|
155
|
-
|
|
156
|
-
|
|
101
|
+
rejectSelectors(options);
|
|
102
|
+
await applyRd(clientDir(), options);
|
|
103
|
+
delete process.env.AXOM_ACCESS_TOKEN;
|
|
157
104
|
await deps.runDaemon({ supervision: deps.detectSupervision() });
|
|
158
105
|
}));
|
|
159
|
-
program.command('
|
|
160
|
-
|
|
161
|
-
// OS-service treatment. See RunDaemonOptions in daemon.ts for why this must be an explicit
|
|
162
|
-
// flag rather than "no `supervision` was passed".
|
|
163
|
-
//
|
|
164
|
-
// Same reasoning as the bare command's call: this process is about to become the daemon that
|
|
165
|
-
// spawns agents and shell commands with its own environment. A service definition is not
|
|
166
|
-
// supposed to carry AXOM_ACCESS_TOKEN at all, which makes this usually a no-op — and exactly
|
|
167
|
-
// why it belongs here rather than being left to whoever writes the next plist.
|
|
168
|
-
dropAccessTokenFromEnvironment();
|
|
169
|
-
await deps.runDaemon({ supervisedByServiceManager: true });
|
|
170
|
-
}));
|
|
106
|
+
program.command('__rd-desktop', { hidden: true }).requiredOption('--directory <path>').action((options) => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await runManagedDesktop(options.directory); }));
|
|
107
|
+
program.command('__daemon', { hidden: true }).action(() => runCommand(async () => { delete process.env.AXOM_ACCESS_TOKEN; await deps.runDaemon({ supervisedByServiceManager: true }); }));
|
|
171
108
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { basename } from 'node:path';
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { peekAccessToken, revokeRefreshToken } from '../credentials.js';
|
|
6
|
+
import { resolveAuthServer } from './auth.js';
|
|
7
|
+
import { clientDir } from '../workstation/config.js';
|
|
8
|
+
import { executeDeviceCommand, inspectLocalDevice } from '../workstation/control/deviceCommands.js';
|
|
9
|
+
import { deviceEventSchema, deviceResultSchema, parseDeviceInspection } from '../workstation/control/deviceProtocol.js';
|
|
10
|
+
import { LocalControlError } from '../workstation/control/protocol.js';
|
|
11
|
+
import { normalizeWorkstationServer } from '../workstation/control/serverIdentity.js';
|
|
12
|
+
import { verifyRegistrationAuthority } from '../workstation/registrations/enrollment.js';
|
|
13
|
+
import { authorizeDevice } from '../workstation/oauth/deviceAuthorization.js';
|
|
14
|
+
import { resolveWorkstationName, sanitizeName } from '../workstation/names.js';
|
|
15
|
+
import { operationIdSchema } from '../workstation/registrations/enrollmentPending.js';
|
|
16
|
+
import { registrationIdSchema } from '../workstation/registrations/model.js';
|
|
17
|
+
const defaults = {
|
|
18
|
+
inspect: () => inspectLocalDevice(clientDir()), execute: request => executeDeviceCommand(request, clientDir()),
|
|
19
|
+
authority: verifyRegistrationAuthority, authorize: authorizeDevice, revoke: revokeRefreshToken, log: console.log,
|
|
20
|
+
};
|
|
21
|
+
export async function resolveWorkstationAddServer(explicit) {
|
|
22
|
+
return resolveAuthServer(await loadConfig(), explicit);
|
|
23
|
+
}
|
|
24
|
+
function printResult(result, options, log) {
|
|
25
|
+
result = deviceResultSchema.parse(result);
|
|
26
|
+
if (options.json)
|
|
27
|
+
log(JSON.stringify(deviceEventSchema.parse({ schemaVersion: 2, kind: 'result', operationId: result.operationId, result })));
|
|
28
|
+
else
|
|
29
|
+
log(`${result.status}: ${result.registrationId ?? 'device'} (operation ${result.operationId})${result.error ? ` — ${result.error.code}: ${result.error.message}` : ''}`);
|
|
30
|
+
if (result.status !== 'succeeded')
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
export async function listWorkstationRegistrations(options = {}, overrides = {}) {
|
|
34
|
+
const deps = { ...defaults, ...overrides };
|
|
35
|
+
const inspection = parseDeviceInspection(await deps.inspect());
|
|
36
|
+
if (options.json) {
|
|
37
|
+
deps.log(JSON.stringify(inspection));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
deps.log(`Workstation device: ${inspection.runtime.state}`);
|
|
41
|
+
if (!inspection.registrations.length)
|
|
42
|
+
deps.log('No workstation registrations. Use `axom workstation add [address]`.');
|
|
43
|
+
for (const registration of inspection.registrations)
|
|
44
|
+
deps.log(`${registration.id}\t${registration.server}\taccount=${registration.ownerUserId}\tworkstation=${registration.workstationId}\t${registration.connection}\t${registration.identityState}\t${registration.removalState}`);
|
|
45
|
+
for (const pending of inspection.pending)
|
|
46
|
+
deps.log(`pending: ${pending.id}\t${pending.kind}\t${pending.registrationId ?? 'device'}\t${pending.phase}\t${pending.outcome}`);
|
|
47
|
+
}
|
|
48
|
+
export async function addWorkstationRegistration(options = {}, overrides = {}) {
|
|
49
|
+
const deps = { ...defaults, ...overrides };
|
|
50
|
+
const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
|
|
51
|
+
const server = normalizeWorkstationServer(await resolveWorkstationAddServer(options.server));
|
|
52
|
+
const before = parseDeviceInspection(await deps.inspect());
|
|
53
|
+
if (before.runtime.state === 'unknown')
|
|
54
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Verify the workstation owner before adding a registration');
|
|
55
|
+
const operationId = operationIdSchema.parse(operation.operationId);
|
|
56
|
+
const hasExplicitAccessToken = Boolean(process.env.AXOM_ACCESS_TOKEN?.trim());
|
|
57
|
+
let token = await peekAccessToken(server);
|
|
58
|
+
let ownerUserId;
|
|
59
|
+
if (token) {
|
|
60
|
+
try {
|
|
61
|
+
ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (!(error instanceof LocalControlError))
|
|
65
|
+
throw error;
|
|
66
|
+
if (error.code !== 'AUTHORIZATION_REQUIRED' && (error.code !== 'OWNER_MISMATCH' || hasExplicitAccessToken))
|
|
67
|
+
throw error;
|
|
68
|
+
token = undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let issued;
|
|
72
|
+
try {
|
|
73
|
+
if (!token) {
|
|
74
|
+
if (!options.json)
|
|
75
|
+
deps.log('Workstation permission needs a separate approval; unavailable or expired ordinary access credentials are left unchanged.');
|
|
76
|
+
const host = hostname();
|
|
77
|
+
issued = await deps.authorize(server, `Axomwork workstation (${host})`, host, { print: message => { process.stderr.write(message + '\n'); } }, ['workstation'], Object.freeze({ deviceId: `workstation-${randomUUID()}` }));
|
|
78
|
+
token = issued.accessToken;
|
|
79
|
+
ownerUserId = await deps.authority({ server, accessToken: token, expectedOwnerUserId: options.expectedOwnerUserId });
|
|
80
|
+
}
|
|
81
|
+
const cwd = process.cwd();
|
|
82
|
+
const result = await deps.execute({ schemaVersion: 2, action: 'add-registration', operationId,
|
|
83
|
+
guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation },
|
|
84
|
+
input: { operationId, server, accessToken: token, expectedOwnerUserId: ownerUserId, defaults: { name: resolveWorkstationName(options.name, process.env, hostname()), shell: process.env.SHELL ?? '/bin/sh', workspace: { name: sanitizeName(basename(cwd), 'default'), cwd, shell: process.env.SHELL ?? '/bin/sh' } } },
|
|
85
|
+
});
|
|
86
|
+
printResult(result, options, deps.log);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
if (issued)
|
|
90
|
+
await deps.revoke(server, issued.refreshToken);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function removeWorkstationRegistration(id, options = {}, overrides = {}) {
|
|
94
|
+
const deps = { ...defaults, ...overrides };
|
|
95
|
+
const operation = options.operation ?? { operationId: operationIdSchema.parse(options.operationId ?? randomUUID()), registrationId: null };
|
|
96
|
+
const target = registrationIdSchema.safeParse(id);
|
|
97
|
+
if (!target.success) {
|
|
98
|
+
operation.registrationId = null;
|
|
99
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Invalid registration ID; use `axom workstation list`');
|
|
100
|
+
}
|
|
101
|
+
operation.registrationId = target.data;
|
|
102
|
+
const before = parseDeviceInspection(await deps.inspect());
|
|
103
|
+
const registration = before.registrations.find(candidate => candidate.id === id);
|
|
104
|
+
if (!registration)
|
|
105
|
+
throw new LocalControlError('BINDING_CONFLICT', 'No registration matches that exact ID; use `axom workstation list`');
|
|
106
|
+
const pending = before.pending.find(operation => operation.kind === 'remove' && operation.registrationId === id);
|
|
107
|
+
const operationId = operationIdSchema.parse(options.operationId ?? pending?.id ?? operation.operationId);
|
|
108
|
+
operation.operationId = operationId;
|
|
109
|
+
if (before.runtime.state !== 'running' || before.runtime.generation === null) {
|
|
110
|
+
printResult({ operationId, registrationId: id, status: 'pending', inspection: before, error: { code: 'OUTCOME_UNKNOWN', message: pending ? 'Original removal authority is unavailable; preserve its pending operation and data for verification' : 'Removal needs the authenticated running device owner; start the device, inspect, and retry' } }, options, deps.log);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
printResult(await deps.execute({ schemaVersion: 2, action: 'remove-registration', operationId, force: options.force === true,
|
|
114
|
+
guard: { deviceRevision: before.deviceRevision, runtimeGeneration: before.runtime.generation, registrationId: id, registrationRevision: registration.revision },
|
|
115
|
+
}), options, deps.log);
|
|
116
|
+
}
|
package/dist/credentialBroker.js
CHANGED
|
@@ -30,9 +30,17 @@ export async function requestCredentialBroker(request) {
|
|
|
30
30
|
return await sendRequest(brokerRequest, controller.signal);
|
|
31
31
|
}
|
|
32
32
|
catch (error) {
|
|
33
|
-
if (error instanceof BrokerOperationError)
|
|
33
|
+
if (error instanceof BrokerOperationError) {
|
|
34
|
+
if (request.action === 'peek' && error.message === 'Unsupported credential broker operation.')
|
|
35
|
+
return { ok: true, version: PROTOCOL_VERSION };
|
|
34
36
|
throw error;
|
|
37
|
+
}
|
|
35
38
|
throwIfAborted(controller.signal);
|
|
39
|
+
if (request.action === 'peek') {
|
|
40
|
+
if (['ENOENT', 'ECONNREFUSED'].includes(error.code ?? ''))
|
|
41
|
+
return { ok: true, version: PROTOCOL_VERSION };
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
36
44
|
await installAndStartBroker(controller.signal);
|
|
37
45
|
let lastError;
|
|
38
46
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
@@ -129,6 +137,9 @@ async function handleRequest(raw, connection, credentials) {
|
|
|
129
137
|
const accessToken = await credentials.accessTokenFromNativeStore(request.server, request.forceRefresh);
|
|
130
138
|
response = { ok: true, accessToken, version: PROTOCOL_VERSION };
|
|
131
139
|
}
|
|
140
|
+
else if (request.action === 'peek') {
|
|
141
|
+
response = { ok: true, accessToken: credentials.peekNativeAccessToken(request.server), version: PROTOCOL_VERSION };
|
|
142
|
+
}
|
|
132
143
|
else if (request.action === 'revoke') {
|
|
133
144
|
const persisted = await credentials.loadNativeTokens(request.server);
|
|
134
145
|
if (persisted?.refreshToken)
|
|
@@ -319,7 +330,7 @@ function parseRequest(raw) {
|
|
|
319
330
|
const request = JSON.parse(raw);
|
|
320
331
|
if (request.version !== PROTOCOL_VERSION)
|
|
321
332
|
throw new Error('Unsupported credential broker protocol version.');
|
|
322
|
-
if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke') {
|
|
333
|
+
if (request.action !== 'save' && request.action !== 'access' && request.action !== 'clear' && request.action !== 'revoke' && request.action !== 'peek') {
|
|
323
334
|
throw new Error('Unsupported credential broker operation.');
|
|
324
335
|
}
|
|
325
336
|
if (typeof request.server !== 'string' || !request.server)
|