@platless/axom-cli 0.6.36 → 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 +7 -1
- package/dist/axom-patch/cli/README.md +31 -2
- package/dist/axom-patch/cli/account-limits/generate.py +2 -2
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
- 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 +137 -41
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
- package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
- package/dist/axom-patch/cli/runtime.json +3 -1
- 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 +7 -7
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -9
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
- package/dist/cli.js +9 -11
- package/dist/commands/auth.js +4 -1
- package/dist/commands/update.js +68 -7
- package/dist/commands/workstation.js +94 -223
- 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 +2 -2
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +11 -4
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +4 -3
- package/dist/workstation/acp/processor/control.js +2 -2
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/session.js +3 -2
- package/dist/workstation/acp/processor/state.js +1 -0
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +25 -0
- package/dist/workstation/acp/processorSession.js +12 -1
- package/dist/workstation/acp/registry/catalog.js +6 -6
- package/dist/workstation/acp/test/keeperWorld.js +4 -3
- package/dist/workstation/agentSessions.js +76 -23
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +36 -5
- package/dist/workstation/config.js +18 -6
- package/dist/workstation/control/bindingStore.js +4 -0
- package/dist/workstation/control/commands.js +1 -0
- package/dist/workstation/control/daemonIdentity.js +90 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/locks.js +51 -4
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +60 -5
- package/dist/workstation/control/runtimeHost.js +96 -9
- package/dist/workstation/control/serverIdentity.js +2 -2
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +262 -360
- package/dist/workstation/daemonctl.js +150 -12
- 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/automationPreparation.js +1 -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 +2 -1
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +2 -2
- package/dist/workstation/desktopControl/helperProcess.js +250 -1
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +4 -2
- package/dist/workstation/desktopControl/macProvider.js +41 -40
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +31 -5
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +15 -6
- package/dist/workstation/desktopControl/supervisor.js +110 -17
- package/dist/workstation/desktopControl/wallpaper.js +26 -6
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/host.js +9 -2
- 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/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +80 -4
- 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/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/management.js +23 -4
- package/dist/workstation/rd/artifact.js +4 -3
- package/dist/workstation/rd/control.js +3 -5
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +44 -10
- package/dist/workstation/rd/state.js +14 -2
- package/dist/workstation/rd/update.js +5 -8
- package/dist/workstation/rd/upkeep.js +44 -2
- 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 +93 -1
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/systemMetrics.js +3 -0
- 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/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 +80 -10
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +2 -2
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +7 -1
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +27427 -14410
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +7 -3
- package/dist/native/darwin-arm64/axom-quic +0 -0
|
@@ -7,6 +7,8 @@ import { pidAlive, processCommand } from '../keepers/kit/lock.js';
|
|
|
7
7
|
import { processStartTime } from '../keepers/kit/orphanSweep.js';
|
|
8
8
|
import { LocalControlError } from './protocol.js';
|
|
9
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';
|
|
10
12
|
export function runtimePaths(directory) {
|
|
11
13
|
const root = path.join(path.resolve(directory), 'control');
|
|
12
14
|
const key = createHash('sha256').update(`${process.getuid?.() ?? 'user'}:${path.resolve(directory)}`).digest('hex').slice(0, 24);
|
|
@@ -35,6 +37,13 @@ export async function runtimeOwnerLive(owner) {
|
|
|
35
37
|
return true;
|
|
36
38
|
}
|
|
37
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) {
|
|
38
47
|
const paths = runtimePaths(options.directory);
|
|
39
48
|
await privateDirectory(path.dirname(paths.ownerFile));
|
|
40
49
|
await privateDirectory(path.dirname(paths.socket));
|
|
@@ -65,7 +74,7 @@ export async function openRuntimeHost(options) {
|
|
|
65
74
|
await fs.rm(paths.socket, { force: true });
|
|
66
75
|
const generation = randomUUID();
|
|
67
76
|
const token = randomBytes(32).toString('hex');
|
|
68
|
-
const
|
|
77
|
+
const preparations = new Map();
|
|
69
78
|
let closed = false;
|
|
70
79
|
const clients = new Set();
|
|
71
80
|
const server = net.createServer(socket => {
|
|
@@ -93,20 +102,98 @@ export async function openRuntimeHost(options) {
|
|
|
93
102
|
throw new LocalControlError('BINDING_CONFLICT', 'Runtime generation changed');
|
|
94
103
|
let value;
|
|
95
104
|
if (request.action === 'inspect')
|
|
96
|
-
value =
|
|
97
|
-
else if (request.action === '
|
|
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');
|
|
98
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
|
+
}
|
|
99
177
|
else if (request.action === 'update') {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
+
}
|
|
104
191
|
}
|
|
105
192
|
else
|
|
106
193
|
throw Error('Invalid action');
|
|
107
194
|
socket.end(JSON.stringify({ ok: true, value }) + '\n');
|
|
108
195
|
if (request.action === 'stop')
|
|
109
|
-
setImmediate(() => { void options.shutdown().catch(() => { }); });
|
|
196
|
+
setImmediate(() => { void options.shutdown(request.stopMode === 'terminate' ? 'terminate' : 'detach').catch(() => { }); });
|
|
110
197
|
}
|
|
111
198
|
catch (error) {
|
|
112
199
|
socket.end(JSON.stringify({ ok: false, code: error instanceof LocalControlError ? error.code : 'SERVICE_FAILED' }) + '\n');
|
|
@@ -136,7 +223,7 @@ export async function openRuntimeHost(options) {
|
|
|
136
223
|
await fs.chmod(paths.socket, 0o600);
|
|
137
224
|
await fs.writeFile(paths.tokenFile, token, { mode: 0o600 });
|
|
138
225
|
await fs.chmod(paths.tokenFile, 0o600);
|
|
139
|
-
await fs.writeFile(paths.ownerFile, JSON.stringify({ schemaVersion: 1, pid: process.pid, command: await processCommand(process.pid), startTime: await processStartTime(process.pid), generation }), { mode: 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 });
|
|
140
227
|
await fs.chmod(paths.ownerFile, 0o600);
|
|
141
228
|
return { generation, close };
|
|
142
229
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const ENDPOINTS = new Set(['/workstation/register', '/download/client/manifest.json', '/rest/auth/me', '/rest/auth/scopes/check']);
|
|
1
|
+
const ENDPOINTS = new Set(['/workstation/register', '/workstation/enroll', '/workstation/enroll/identity', '/download/client/manifest.json', '/rest/auth/me', '/rest/auth/scopes/check']);
|
|
2
2
|
export function normalizeWorkstationServer(value) {
|
|
3
3
|
if (/[\u0000-\u0020\\]/.test(value))
|
|
4
4
|
throw new Error('Invalid workstation server URL');
|
|
@@ -16,7 +16,7 @@ export function normalizeWorkstationServer(value) {
|
|
|
16
16
|
return url.origin + url.pathname.replace(/\/+$/, '');
|
|
17
17
|
}
|
|
18
18
|
export function workstationEndpoint(server, apiPath) {
|
|
19
|
-
if (!ENDPOINTS.has(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
20
|
throw new Error('Unrecognized workstation endpoint');
|
|
21
21
|
return new URL(normalizeWorkstationServer(server) + apiPath);
|
|
22
22
|
}
|
|
@@ -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
|
+
}
|