@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.
Files changed (159) hide show
  1. package/README.md +7 -1
  2. package/dist/axom-patch/cli/README.md +31 -2
  3. package/dist/axom-patch/cli/account-limits/generate.py +2 -2
  4. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  5. package/dist/axom-patch/cli/codex-terminal-output.json +2 -2
  6. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  7. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +1 -1
  8. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  9. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  10. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +137 -41
  11. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +5 -88
  12. package/dist/axom-patch/cli/qoder-native-acp.json +23 -23
  13. package/dist/axom-patch/cli/runtime.json +3 -1
  14. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  15. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  16. package/dist/axom-patch/cli/zcode-account-limits.json +7 -7
  17. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  18. package/dist/axom-patch/cli/zcode-models.json +9 -9
  19. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -10
  20. package/dist/cli.js +9 -11
  21. package/dist/commands/auth.js +4 -1
  22. package/dist/commands/update.js +68 -7
  23. package/dist/commands/workstation.js +94 -223
  24. package/dist/commands/workstationRegistrations.js +116 -0
  25. package/dist/credentialBroker.js +13 -2
  26. package/dist/credentials.js +16 -0
  27. package/dist/install/claude/commands.js +2 -2
  28. package/dist/keepers/terminal/keeper.mjs +1089 -673
  29. package/dist/keepers/terminal/manifest.json +4 -4
  30. package/dist/native/darwin-x64/axom-quic +0 -0
  31. package/dist/workstation/acp/acpAgents.js +11 -4
  32. package/dist/workstation/acp/outbox.js +12 -5
  33. package/dist/workstation/acp/permission.js +2 -2
  34. package/dist/workstation/acp/processor/checkpoint.js +4 -3
  35. package/dist/workstation/acp/processor/control.js +2 -2
  36. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  37. package/dist/workstation/acp/processor/session.js +3 -2
  38. package/dist/workstation/acp/processor/state.js +1 -0
  39. package/dist/workstation/acp/processor/transport/local.js +19 -3
  40. package/dist/workstation/acp/processor/updates.js +25 -0
  41. package/dist/workstation/acp/processorSession.js +12 -1
  42. package/dist/workstation/acp/registry/catalog.js +6 -6
  43. package/dist/workstation/acp/test/keeperWorld.js +4 -3
  44. package/dist/workstation/agentSessions.js +76 -23
  45. package/dist/workstation/channels/agent.js +9 -1
  46. package/dist/workstation/channels/computer.js +27 -4
  47. package/dist/workstation/channels/management.js +36 -5
  48. package/dist/workstation/config.js +18 -6
  49. package/dist/workstation/control/bindingStore.js +4 -0
  50. package/dist/workstation/control/commands.js +1 -0
  51. package/dist/workstation/control/daemonIdentity.js +90 -0
  52. package/dist/workstation/control/deviceCommands.js +191 -0
  53. package/dist/workstation/control/deviceProtocol.js +68 -0
  54. package/dist/workstation/control/locks.js +51 -4
  55. package/dist/workstation/control/rootFormat.js +16 -0
  56. package/dist/workstation/control/runtimeClient.js +60 -5
  57. package/dist/workstation/control/runtimeHost.js +96 -9
  58. package/dist/workstation/control/serverIdentity.js +2 -2
  59. package/dist/workstation/control/updateTransition.js +91 -0
  60. package/dist/workstation/daemon.js +262 -360
  61. package/dist/workstation/daemonctl.js +150 -12
  62. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  63. package/dist/workstation/desktopControl/automationJson.js +74 -0
  64. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  65. package/dist/workstation/desktopControl/automationPreparation.js +1 -0
  66. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  67. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  68. package/dist/workstation/desktopControl/automationSession.js +81 -0
  69. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  70. package/dist/workstation/desktopControl/capabilities.js +2 -1
  71. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  72. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  73. package/dist/workstation/desktopControl/deviceSockets.js +2 -2
  74. package/dist/workstation/desktopControl/helperProcess.js +250 -1
  75. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  76. package/dist/workstation/desktopControl/lobby.js +4 -2
  77. package/dist/workstation/desktopControl/macProvider.js +41 -40
  78. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  79. package/dist/workstation/desktopControl/nativeProvider.js +31 -5
  80. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  81. package/dist/workstation/desktopControl/protocol.js +15 -6
  82. package/dist/workstation/desktopControl/supervisor.js +110 -17
  83. package/dist/workstation/desktopControl/wallpaper.js +26 -6
  84. package/dist/workstation/exec/policy.js +5 -0
  85. package/dist/workstation/files/client.js +10 -1
  86. package/dist/workstation/files/host.js +9 -2
  87. package/dist/workstation/journal.js +5 -2
  88. package/dist/workstation/keepers/acp/client.js +38 -2
  89. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  90. package/dist/workstation/keepers/kit/client.js +2 -2
  91. package/dist/workstation/keepers/kit/launcher.js +80 -4
  92. package/dist/workstation/keepers/legacy.js +16 -2
  93. package/dist/workstation/keepers/policy.js +5 -34
  94. package/dist/workstation/keepers/terminal/client.js +115 -23
  95. package/dist/workstation/keepers/terminal/exec.js +6 -3
  96. package/dist/workstation/keepers/terminal/frames.js +73 -0
  97. package/dist/workstation/keepers/terminal/generations.js +5 -8
  98. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  99. package/dist/workstation/keepers/terminal/input.js +146 -0
  100. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  101. package/dist/workstation/keepers/terminal/main.js +1 -1
  102. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  103. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  104. package/dist/workstation/keepers/terminal/pty.js +1 -330
  105. package/dist/workstation/keepers/terminal/screen.js +246 -0
  106. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  107. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  108. package/dist/workstation/protocol/management.js +23 -4
  109. package/dist/workstation/rd/artifact.js +4 -3
  110. package/dist/workstation/rd/control.js +3 -5
  111. package/dist/workstation/rd/host.js +213 -0
  112. package/dist/workstation/rd/migrationReadiness.js +46 -0
  113. package/dist/workstation/rd/provider.js +44 -10
  114. package/dist/workstation/rd/state.js +14 -2
  115. package/dist/workstation/rd/update.js +5 -8
  116. package/dist/workstation/rd/upkeep.js +44 -2
  117. package/dist/workstation/registrations/enrollment.js +250 -0
  118. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  119. package/dist/workstation/registrations/migration.js +356 -0
  120. package/dist/workstation/registrations/model.js +34 -0
  121. package/dist/workstation/registrations/removal.js +222 -0
  122. package/dist/workstation/registrations/store.js +737 -0
  123. package/dist/workstation/runtime/desktopBinding.js +20 -0
  124. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  125. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  126. package/dist/workstation/runtime/deviceServices.js +117 -0
  127. package/dist/workstation/runtime/events.js +20 -0
  128. package/dist/workstation/runtime/ownership.js +100 -0
  129. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  130. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  131. package/dist/workstation/runtime/removalActivity.js +184 -0
  132. package/dist/workstation/service/installer.js +93 -1
  133. package/dist/workstation/service/launchd.js +1 -0
  134. package/dist/workstation/service/systemd.js +1 -0
  135. package/dist/workstation/systemMetrics.js +3 -0
  136. package/dist/workstation/tools/client.js +8 -27
  137. package/dist/workstation/tools/info.js +6 -13
  138. package/dist/workstation/tools/terminalRouter.js +17 -172
  139. package/dist/workstation/tools/validation.js +55 -1
  140. package/dist/workstation/update/archiveEvidence.js +206 -0
  141. package/dist/workstation/update/coordinator.js +34 -13
  142. package/dist/workstation/update/deviceCompatibility.js +119 -0
  143. package/dist/workstation/update/deviceCoordinator.js +369 -0
  144. package/dist/workstation/update/deviceHealth.js +104 -0
  145. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  146. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  147. package/dist/workstation/update/deviceRollback.js +78 -0
  148. package/dist/workstation/update/globalInstall.js +80 -10
  149. package/dist/workstation/update/legacyBridge.js +414 -0
  150. package/dist/workstation/update/legacyOwner.js +39 -0
  151. package/dist/workstation/update/npmInstaller.js +2 -2
  152. package/dist/workstation/update/privateDurability.js +61 -0
  153. package/dist/workstation/update/recoveryPlan.js +124 -0
  154. package/dist/workstation/update/runUpdate.js +7 -1
  155. package/dist/workstation/update/taskRegistry.js +161 -3
  156. package/dist/workstation/update/watchdog.js +27427 -14410
  157. package/dist/workstation/update/watchdogProcess.js +72 -0
  158. package/package.json +7 -3
  159. 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 binding = { ...options.binding };
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 = { ...await options.snapshot(), state: 'running', binding, generation, mode: options.mode };
97
- else if (request.action === 'stop')
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
- const r = request.release;
101
- 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))
102
- throw Error('Invalid update request');
103
- value = await options.update(r, request.operationId);
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
+ }