@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
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { loadClientConfig, readKeys } from '../config.js';
|
|
7
|
+
import { BindingStore } from '../control/bindingStore.js';
|
|
8
|
+
import { assertLockOwnership, assertWorkstationOperationLock, withWorkstationLock } from '../control/locks.js';
|
|
9
|
+
import { LocalControlError, operationRecordSchema } from '../control/protocol.js';
|
|
10
|
+
import { inspectRuntime } from '../control/runtimeClient.js';
|
|
11
|
+
import { normalizeWorkstationServer } from '../control/serverIdentity.js';
|
|
12
|
+
import { registrationIdSchema } from './model.js';
|
|
13
|
+
import { RegistrationStore } from './store.js';
|
|
14
|
+
import { assertDeviceUpdateTransitionSafe } from '../control/updateTransition.js';
|
|
15
|
+
import { withLegacyBridgeMigration, assertLegacyBridgeMutationAllowed } from '../update/legacyBridge.js';
|
|
16
|
+
const files = ['config.yml', 'client.key', 'server.pub'];
|
|
17
|
+
const hashSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
18
|
+
const receiptSchema = z.object({
|
|
19
|
+
schemaVersion: z.literal(1), registrationId: registrationIdSchema, legacyRevision: hashSchema,
|
|
20
|
+
keeperServer: z.string().min(1), workstationId: z.string().min(1), ownerUserId: z.string().min(1),
|
|
21
|
+
files: z.object({ 'config.yml': hashSchema, 'client.key': hashSchema, 'server.pub': hashSchema }).strict(),
|
|
22
|
+
sources: z.object({ journalDirectory: z.string(), localOutboxDirectory: z.string(), adapterCacheDirectory: z.string() }).strict(),
|
|
23
|
+
state: z.enum(['staged', 'committed', 'complete']), registrationRevision: hashSchema.optional(),
|
|
24
|
+
}).strict().refine(value => value.state === 'staged' || value.registrationRevision !== undefined);
|
|
25
|
+
const removalNamespaces = new WeakMap();
|
|
26
|
+
export function assertRemovalNamespace(namespace) {
|
|
27
|
+
const lease = removalNamespaces.get(namespace);
|
|
28
|
+
if (!lease || assertWorkstationOperationLock(namespace.root) !== lease)
|
|
29
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Removal namespace has not been verified in this operation');
|
|
30
|
+
}
|
|
31
|
+
export async function resolveRemovalNamespaceLocked(root, id, expectedRevision) {
|
|
32
|
+
root = path.resolve(root);
|
|
33
|
+
assertWorkstationOperationLock(root);
|
|
34
|
+
const registration = await new RegistrationStore(root).readMetadata(id, expectedRevision);
|
|
35
|
+
let server = registration.server;
|
|
36
|
+
const legacy = registration.keeperConfigDirectory === root;
|
|
37
|
+
if (legacy) {
|
|
38
|
+
await directory(root);
|
|
39
|
+
const receipt = await readReceipt(root);
|
|
40
|
+
if (!receipt || receipt.state !== 'complete' || !(await checkDevice(root))?.initialized || receipt.registrationId !== id || normalizeWorkstationServer(receipt.keeperServer) !== registration.server || receipt.ownerUserId !== registration.ownerUserId || receipt.workstationId !== registration.workstationId)
|
|
41
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Legacy removal lineage needs verification');
|
|
42
|
+
for (const name of [...files, 'identity.pending'])
|
|
43
|
+
if (await present(path.join(root, name)))
|
|
44
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Legacy identity retirement needs verification');
|
|
45
|
+
await checkSources(root);
|
|
46
|
+
server = receipt.keeperServer;
|
|
47
|
+
}
|
|
48
|
+
assertWorkstationOperationLock(root);
|
|
49
|
+
const namespace = Object.freeze({ root, registrationId: id, revision: registration.revision, configDir: registration.keeperConfigDirectory, dataDirectory: registration.dataDirectory, workstationId: registration.workstationId, server, legacy });
|
|
50
|
+
removalNamespaces.set(namespace, assertWorkstationOperationLock(root));
|
|
51
|
+
return namespace;
|
|
52
|
+
}
|
|
53
|
+
const missing = (error) => error.code === 'ENOENT';
|
|
54
|
+
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
55
|
+
const receiptFile = (root) => path.join(root, 'legacy-migration.json');
|
|
56
|
+
function sourcePaths(root) {
|
|
57
|
+
return { journalDirectory: path.join(root, 'agents/sessions'), localOutboxDirectory: path.join(root, 'agents/local'), adapterCacheDirectory: root };
|
|
58
|
+
}
|
|
59
|
+
async function directory(directory) {
|
|
60
|
+
const stat = await fs.lstat(directory);
|
|
61
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
62
|
+
throw Error('Unsafe migration directory');
|
|
63
|
+
}
|
|
64
|
+
async function normalizeCanonicalLegacyParent(root) {
|
|
65
|
+
const parent = path.join(os.homedir(), '.axom');
|
|
66
|
+
if (root !== path.join(parent, 'workstation'))
|
|
67
|
+
return;
|
|
68
|
+
const handle = await fs.open(parent, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
69
|
+
try {
|
|
70
|
+
const stat = await handle.stat();
|
|
71
|
+
if (!stat.isDirectory() || (process.getuid && stat.uid !== process.getuid()))
|
|
72
|
+
throw Error('Unsafe canonical CLI metadata parent');
|
|
73
|
+
if ((stat.mode & 0o7777) !== 0o700) {
|
|
74
|
+
await handle.chmod(0o700);
|
|
75
|
+
await handle.sync();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await handle.close();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function file(file, publicKey = false) {
|
|
83
|
+
const handle = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
84
|
+
try {
|
|
85
|
+
const stat = await handle.stat();
|
|
86
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1_048_576 || (stat.mode & (publicKey ? 0o022 : 0o077)) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
87
|
+
throw Error('Unsafe migration file');
|
|
88
|
+
const buffer = Buffer.alloc(stat.size + 1);
|
|
89
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
90
|
+
if (bytesRead !== stat.size)
|
|
91
|
+
throw Error('Migration file changed');
|
|
92
|
+
return buffer.subarray(0, bytesRead);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await handle.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function present(file) {
|
|
99
|
+
try {
|
|
100
|
+
await fs.lstat(file);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (missing(error))
|
|
105
|
+
return false;
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function sync(root) {
|
|
110
|
+
const handle = await fs.open(root, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
111
|
+
try {
|
|
112
|
+
await handle.sync();
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
await handle.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function writeReceipt(root, receipt) {
|
|
119
|
+
assertLockOwnership();
|
|
120
|
+
await directory(root);
|
|
121
|
+
if (await present(receiptFile(root)))
|
|
122
|
+
await file(receiptFile(root));
|
|
123
|
+
const temporary = path.join(root, `.migration-${randomUUID()}`);
|
|
124
|
+
const handle = await fs.open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
125
|
+
try {
|
|
126
|
+
await handle.writeFile(JSON.stringify(receiptSchema.parse(receipt)));
|
|
127
|
+
await handle.sync();
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await handle.close();
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
assertLockOwnership();
|
|
134
|
+
await fs.rename(temporary, receiptFile(root));
|
|
135
|
+
await sync(root);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await fs.rm(temporary, { force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function readReceipt(root) {
|
|
142
|
+
if (!await present(receiptFile(root)))
|
|
143
|
+
return null;
|
|
144
|
+
const receipt = receiptSchema.parse(JSON.parse((await file(receiptFile(root))).toString()));
|
|
145
|
+
if (JSON.stringify(receipt.sources) !== JSON.stringify(sourcePaths(root)))
|
|
146
|
+
throw Error('Unrecognized legacy source paths');
|
|
147
|
+
normalizeWorkstationServer(receipt.keeperServer);
|
|
148
|
+
return receipt;
|
|
149
|
+
}
|
|
150
|
+
async function checkSources(root, normalizeLegacyModes = false) {
|
|
151
|
+
await directory(root);
|
|
152
|
+
for (const relative of ['agents', 'agents/sessions', 'agents/local', 'acp-adapters']) {
|
|
153
|
+
const current = path.join(root, relative);
|
|
154
|
+
if (!await present(current))
|
|
155
|
+
continue;
|
|
156
|
+
if (!normalizeLegacyModes) {
|
|
157
|
+
await directory(current);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Older CLIs created these owned journals/caches with the login umask (0755/0775).
|
|
161
|
+
// Tighten the opened directory itself, preserving every entry and the keeper namespace.
|
|
162
|
+
const handle = await fs.open(current, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
163
|
+
try {
|
|
164
|
+
const stat = await handle.stat();
|
|
165
|
+
if (!stat.isDirectory() || (process.getuid && stat.uid !== process.getuid()))
|
|
166
|
+
throw Error('Unsafe migration source directory');
|
|
167
|
+
if ((stat.mode & 0o7777) !== 0o700) {
|
|
168
|
+
await handle.chmod(0o700);
|
|
169
|
+
await handle.sync();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await handle.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function checkDevice(root) {
|
|
178
|
+
if (!await present(path.join(root, 'device.json')))
|
|
179
|
+
return null;
|
|
180
|
+
return z.object({ schemaVersion: z.literal(1), deviceId: z.string().uuid(), initialized: z.boolean() }).strict().parse(JSON.parse((await file(path.join(root, 'device.json'))).toString()));
|
|
181
|
+
}
|
|
182
|
+
async function checkLegacyEvidence(root) {
|
|
183
|
+
if (await present(path.join(root, 'identity.pending')))
|
|
184
|
+
throw Error('Incomplete legacy identity');
|
|
185
|
+
if (await present(path.join(root, 'operation.json'))) {
|
|
186
|
+
const operation = operationRecordSchema.parse(JSON.parse((await file(path.join(root, 'operation.json'))).toString()));
|
|
187
|
+
if (operation.outcome === 'pending' || operation.outcome === 'unknown')
|
|
188
|
+
throw Error('Unresolved legacy operation');
|
|
189
|
+
}
|
|
190
|
+
if (await present(path.join(root, 'binding-history.json')))
|
|
191
|
+
await file(path.join(root, 'binding-history.json'));
|
|
192
|
+
await new BindingStore(root).history();
|
|
193
|
+
}
|
|
194
|
+
async function verifyRemainingFiles(root, receipt, all) {
|
|
195
|
+
for (const name of files) {
|
|
196
|
+
const current = path.join(root, name);
|
|
197
|
+
if (!all && !await present(current))
|
|
198
|
+
continue;
|
|
199
|
+
if (hash(await file(current, name === 'server.pub')) !== receipt.files[name])
|
|
200
|
+
throw Error('Legacy identity changed since migration began');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function legacyRevision(identity) {
|
|
204
|
+
const registration = identity.registration;
|
|
205
|
+
const binding = { server: registration.server, workstationId: registration.workstationId, ownerUserId: registration.ownerUserId };
|
|
206
|
+
return createHash('sha256').update(JSON.stringify(binding)).update('\0').update(identity.clientKey.toString('base64')).update('\0').update(identity.serverPub.toString('base64')).digest('hex');
|
|
207
|
+
}
|
|
208
|
+
async function verifyImported(store, receipt) {
|
|
209
|
+
const identity = await store.read(receipt.registrationId);
|
|
210
|
+
const registration = identity.registration;
|
|
211
|
+
if (registration.server !== normalizeWorkstationServer(receipt.keeperServer) || registration.ownerUserId !== receipt.ownerUserId || registration.workstationId !== receipt.workstationId || registration.keeperConfigDirectory !== store.root || legacyRevision(identity) !== receipt.legacyRevision || (receipt.registrationRevision && registration.revision !== receipt.registrationRevision))
|
|
212
|
+
throw Error('Migration registration changed');
|
|
213
|
+
}
|
|
214
|
+
export async function migrateLegacyRegistration(root, options = {}) {
|
|
215
|
+
root = path.resolve(root);
|
|
216
|
+
try {
|
|
217
|
+
await directory(root);
|
|
218
|
+
await normalizeCanonicalLegacyParent(root);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return { kind: missing(error) ? 'absent' : 'needs-verification' };
|
|
222
|
+
}
|
|
223
|
+
let injected;
|
|
224
|
+
const checkpoint = async (stage) => { try {
|
|
225
|
+
await options.afterStage?.(stage);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
injected = error;
|
|
229
|
+
throw error;
|
|
230
|
+
} };
|
|
231
|
+
try {
|
|
232
|
+
return await withLegacyBridgeMigration(root, () => withWorkstationLock(root, 'operation', async () => {
|
|
233
|
+
await assertDeviceUpdateTransitionSafe(root, options.startupPackage);
|
|
234
|
+
await directory(root);
|
|
235
|
+
const device = await checkDevice(root);
|
|
236
|
+
let receipt = await readReceipt(root);
|
|
237
|
+
if (receipt?.state === 'complete') {
|
|
238
|
+
if (!device?.initialized)
|
|
239
|
+
throw Error('Missing migration device marker');
|
|
240
|
+
for (const name of files)
|
|
241
|
+
if (await present(path.join(root, name)))
|
|
242
|
+
throw Error('Legacy identity reappeared after migration');
|
|
243
|
+
return { kind: 'already-migrated', registrationId: receipt.registrationId };
|
|
244
|
+
}
|
|
245
|
+
if ((await inspectRuntime(root)).state !== 'stopped')
|
|
246
|
+
return { kind: 'needs-verification' };
|
|
247
|
+
await checkLegacyEvidence(root);
|
|
248
|
+
await checkSources(root, true);
|
|
249
|
+
if (!receipt) {
|
|
250
|
+
const legacy = await new BindingStore(root).inspect();
|
|
251
|
+
if (legacy.identityState === 'absent')
|
|
252
|
+
return { kind: 'absent' };
|
|
253
|
+
if (legacy.identityState !== 'ready' || !legacy.binding?.ownerUserId)
|
|
254
|
+
return { kind: 'needs-verification' };
|
|
255
|
+
const config = await loadClientConfig(root);
|
|
256
|
+
receipt = { schemaVersion: 1, registrationId: randomUUID(), legacyRevision: legacy.revision, keeperServer: config.server, workstationId: legacy.binding.workstationId, ownerUserId: legacy.binding.ownerUserId, files: { 'config.yml': hash(await file(path.join(root, 'config.yml'))), 'client.key': hash(await file(path.join(root, 'client.key'))), 'server.pub': hash(await file(path.join(root, 'server.pub'), true)) }, sources: sourcePaths(root), state: 'staged' };
|
|
257
|
+
await writeReceipt(root, receipt);
|
|
258
|
+
await checkpoint('receipt-persisted');
|
|
259
|
+
}
|
|
260
|
+
await verifyRemainingFiles(root, receipt, false);
|
|
261
|
+
const store = new RegistrationStore(root, { afterStage: stage => checkpoint(`store-${stage}`) });
|
|
262
|
+
const registrations = await store.list();
|
|
263
|
+
const registrationId = receipt.registrationId;
|
|
264
|
+
let registration = registrations.find(entry => entry.id === registrationId);
|
|
265
|
+
if (!registration) {
|
|
266
|
+
if (receipt.state !== 'staged')
|
|
267
|
+
throw Error('Committed migration identity is missing');
|
|
268
|
+
await verifyRemainingFiles(root, receipt, true);
|
|
269
|
+
const legacy = await new BindingStore(root).inspect();
|
|
270
|
+
if (legacy.identityState !== 'ready' || legacy.revision !== receipt.legacyRevision)
|
|
271
|
+
throw Error('Legacy identity changed');
|
|
272
|
+
const configuration = await loadClientConfig(root);
|
|
273
|
+
const keys = await readKeys(root);
|
|
274
|
+
registration = await store.commit({ registration: { id: receipt.registrationId, server: receipt.keeperServer, ownerUserId: receipt.ownerUserId, workstationId: receipt.workstationId, dataDirectory: path.join(root, 'registrations', receipt.registrationId, 'data'), keeperConfigDirectory: root }, ...keys, expectedRevision: null, configuration });
|
|
275
|
+
}
|
|
276
|
+
await verifyImported(store, receipt);
|
|
277
|
+
if (receipt.state === 'staged') {
|
|
278
|
+
receipt = { ...receipt, state: 'committed', registrationRevision: registration.revision };
|
|
279
|
+
await writeReceipt(root, receipt);
|
|
280
|
+
}
|
|
281
|
+
await checkpoint('registration-committed');
|
|
282
|
+
for (const name of files) {
|
|
283
|
+
const current = path.join(root, name);
|
|
284
|
+
if (await present(current)) {
|
|
285
|
+
if (hash(await file(current, name === 'server.pub')) !== receipt.files[name])
|
|
286
|
+
throw Error('Legacy retirement source changed');
|
|
287
|
+
assertLockOwnership();
|
|
288
|
+
await fs.unlink(current);
|
|
289
|
+
await sync(root);
|
|
290
|
+
}
|
|
291
|
+
await checkpoint(`retired-${name}`);
|
|
292
|
+
}
|
|
293
|
+
await writeReceipt(root, { ...receipt, state: 'complete' });
|
|
294
|
+
return { kind: 'migrated', registrationId: receipt.registrationId };
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (error === injected)
|
|
299
|
+
throw error;
|
|
300
|
+
return { kind: 'needs-verification' };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export async function readLegacyRegistrationSources(root, id) {
|
|
304
|
+
root = path.resolve(root);
|
|
305
|
+
registrationIdSchema.parse(id);
|
|
306
|
+
await directory(root);
|
|
307
|
+
return withWorkstationLock(root, 'operation', async () => {
|
|
308
|
+
const device = await checkDevice(root);
|
|
309
|
+
const receipt = await readReceipt(root);
|
|
310
|
+
if (!receipt || receipt.registrationId !== id)
|
|
311
|
+
return null;
|
|
312
|
+
if (receipt.state !== 'complete' || !device?.initialized)
|
|
313
|
+
throw Error('Legacy migration is not complete');
|
|
314
|
+
const store = new RegistrationStore(root);
|
|
315
|
+
const inspection = await store.inspect();
|
|
316
|
+
if (!inspection.known)
|
|
317
|
+
throw Error('Registration storage needs verification');
|
|
318
|
+
const entry = inspection.entries.find(entry => entry.id === id);
|
|
319
|
+
if (!entry)
|
|
320
|
+
return null;
|
|
321
|
+
if (entry.state !== 'ready')
|
|
322
|
+
throw Error('Migrated registration needs verification');
|
|
323
|
+
const registration = entry.registration;
|
|
324
|
+
if (registration.server !== normalizeWorkstationServer(receipt.keeperServer) || registration.ownerUserId !== receipt.ownerUserId || registration.workstationId !== receipt.workstationId || registration.keeperConfigDirectory !== root)
|
|
325
|
+
throw Error('Legacy ownership changed');
|
|
326
|
+
await checkSources(root);
|
|
327
|
+
return { ...receipt.sources, keeperServer: receipt.keeperServer };
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
export async function assertRegistrationEnrollmentAdmission(root, options = {}) {
|
|
331
|
+
await assertLegacyBridgeMutationAllowed(root, options.startupPackage);
|
|
332
|
+
try {
|
|
333
|
+
await directory(root);
|
|
334
|
+
const receipt = await readReceipt(root);
|
|
335
|
+
if (receipt) {
|
|
336
|
+
if (receipt.state !== 'complete' || !(await checkDevice(root))?.initialized)
|
|
337
|
+
throw Error('Incomplete legacy migration');
|
|
338
|
+
for (const name of [...files, 'identity.pending'])
|
|
339
|
+
if (await present(path.join(root, name)))
|
|
340
|
+
throw Error('Legacy identity remains');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
for (const name of [...files, 'identity.pending']) {
|
|
344
|
+
if (await present(path.join(root, name)))
|
|
345
|
+
throw Error('Legacy registration requires handoff');
|
|
346
|
+
}
|
|
347
|
+
const historicalEvidence = await present(path.join(root, 'operation.json')) || await present(path.join(root, 'binding-history.json'));
|
|
348
|
+
if (historicalEvidence)
|
|
349
|
+
await checkLegacyEvidence(root);
|
|
350
|
+
if ((historicalEvidence || !await checkDevice(root)) && (await inspectRuntime(root)).state !== 'stopped')
|
|
351
|
+
throw Error('Legacy runtime requires verification');
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
throw new LocalControlError('CORRUPT_IDENTITY', 'Legacy workstation registration requires migration or verification before enrollment');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { normalizeWorkstationServer } from '../control/serverIdentity.js';
|
|
3
|
+
export const registrationIdSchema = z.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/);
|
|
4
|
+
export const registrationServerSchema = z.string().transform((server, context) => {
|
|
5
|
+
try {
|
|
6
|
+
return normalizeWorkstationServer(server);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
context.addIssue({ code: 'custom', message: 'Invalid workstation server URL' });
|
|
10
|
+
return z.NEVER;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
export const registrationIdentitySchema = z.object({
|
|
14
|
+
id: registrationIdSchema,
|
|
15
|
+
server: registrationServerSchema,
|
|
16
|
+
ownerUserId: z.string().min(1),
|
|
17
|
+
workstationId: z.string().min(1),
|
|
18
|
+
revision: z.string().min(1),
|
|
19
|
+
}).strict();
|
|
20
|
+
export const registrationSchema = registrationIdentitySchema.extend({
|
|
21
|
+
dataDirectory: z.string().min(1),
|
|
22
|
+
keeperConfigDirectory: z.string().min(1),
|
|
23
|
+
}).strict();
|
|
24
|
+
export const registrationActivitySchema = z.object({
|
|
25
|
+
known: z.boolean(),
|
|
26
|
+
agents: z.number().int().nonnegative(),
|
|
27
|
+
terminals: z.number().int().nonnegative(),
|
|
28
|
+
commands: z.number().int().nonnegative(),
|
|
29
|
+
transfers: z.number().int().nonnegative(),
|
|
30
|
+
desktopConsumers: z.number().int().nonnegative(),
|
|
31
|
+
}).strict();
|
|
32
|
+
export function registrationKey(server, ownerUserId) {
|
|
33
|
+
return JSON.stringify([normalizeWorkstationServer(server), ownerUserId]);
|
|
34
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { withWorkstationLock } from '../control/locks.js';
|
|
3
|
+
import { LocalControlError } from '../control/protocol.js';
|
|
4
|
+
import { TargetAdmissionPendingError } from '../runtime/deviceRuntime.js';
|
|
5
|
+
import { retireRegistrationEnrollment } from './enrollment.js';
|
|
6
|
+
import { operationIdSchema } from './enrollmentPending.js';
|
|
7
|
+
import { resolveRemovalNamespaceLocked } from './migration.js';
|
|
8
|
+
import { registrationIdSchema } from './model.js';
|
|
9
|
+
const inputSchema = z.object({ id: registrationIdSchema, expectedRevision: z.string().regex(/^[a-f0-9]{64}$/), operationId: operationIdSchema, runtimeGeneration: z.string().min(1).max(128), force: z.boolean() }).strict();
|
|
10
|
+
export class RegistrationRemovalController {
|
|
11
|
+
options;
|
|
12
|
+
originals = new Map();
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
async removeRegistration(value) {
|
|
17
|
+
this.options.device.assertMutationAllowed();
|
|
18
|
+
const parsed = inputSchema.safeParse(value);
|
|
19
|
+
if (!parsed.success)
|
|
20
|
+
throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Invalid local removal request');
|
|
21
|
+
const input = parsed.data;
|
|
22
|
+
if (input.runtimeGeneration !== this.options.generation)
|
|
23
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Original runtime generation changed');
|
|
24
|
+
let original = this.originals.get(input.id);
|
|
25
|
+
if (original && JSON.stringify(original.input) !== JSON.stringify(input))
|
|
26
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Original removal operation changed');
|
|
27
|
+
if (!original) {
|
|
28
|
+
if ([...this.originals.values()].some(entry => entry.input.operationId === input.operationId))
|
|
29
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Removal operation target changed');
|
|
30
|
+
original = { input, activity: null };
|
|
31
|
+
this.originals.set(input.id, original);
|
|
32
|
+
}
|
|
33
|
+
if (original.operation)
|
|
34
|
+
return original.operation;
|
|
35
|
+
const finishMutation = this.options.device.beginMembershipMutation();
|
|
36
|
+
const operation = this.run(original).finally(finishMutation);
|
|
37
|
+
original.operation = operation;
|
|
38
|
+
try {
|
|
39
|
+
return await operation;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!original.hold)
|
|
43
|
+
this.originals.delete(input.id);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
original.operation = undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async current(original) {
|
|
51
|
+
const input = original.input;
|
|
52
|
+
const intent = (await this.options.store.removalIntents()).find(entry => entry.operationId === input.operationId || entry.registration.id === input.id && entry.phase !== 'cancelled');
|
|
53
|
+
if (intent && (intent.operationId !== input.operationId || intent.registration.id !== input.id || intent.registration.revision !== input.expectedRevision || intent.force !== input.force))
|
|
54
|
+
throw new LocalControlError('BINDING_CONFLICT', 'Durable removal operation changed');
|
|
55
|
+
return intent;
|
|
56
|
+
}
|
|
57
|
+
async restore(original) {
|
|
58
|
+
const { device, store } = this.options;
|
|
59
|
+
if (original.preparation)
|
|
60
|
+
await device.releaseTargetResources(original.hold, original.preparation);
|
|
61
|
+
else if (original.hold)
|
|
62
|
+
await device.releaseTargetAdmission(original.hold);
|
|
63
|
+
const intent = await this.current(original);
|
|
64
|
+
if (intent)
|
|
65
|
+
await withWorkstationLock(store.root, 'operation', () => store.finishRemovalRestoration(intent));
|
|
66
|
+
await original.resumeRecovered?.();
|
|
67
|
+
this.originals.delete(original.input.id);
|
|
68
|
+
}
|
|
69
|
+
async run(original) {
|
|
70
|
+
const { store, device } = this.options;
|
|
71
|
+
const input = original.input;
|
|
72
|
+
let intent = await this.current(original);
|
|
73
|
+
try {
|
|
74
|
+
if (intent?.phase === 'cancelled') {
|
|
75
|
+
this.originals.delete(input.id);
|
|
76
|
+
return { status: 'busy', activity: original.activity };
|
|
77
|
+
}
|
|
78
|
+
if (intent?.phase === 'completed') {
|
|
79
|
+
if (!await store.verifyRemovalCompletion(intent.registration))
|
|
80
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Completed removal needs verification');
|
|
81
|
+
if (original.hold && !original.finalized) {
|
|
82
|
+
await device.finalizeTargetRemoval(original.hold);
|
|
83
|
+
original.finalized = true;
|
|
84
|
+
}
|
|
85
|
+
else if (!original.hold)
|
|
86
|
+
await device.reconcile();
|
|
87
|
+
this.originals.delete(input.id);
|
|
88
|
+
return { status: 'removed', activity: original.activity };
|
|
89
|
+
}
|
|
90
|
+
if (intent && !original.hold && intent.phase === 'restoring') {
|
|
91
|
+
await withWorkstationLock(store.root, 'operation', () => store.finishRemovalRestoration(intent));
|
|
92
|
+
this.originals.delete(input.id);
|
|
93
|
+
await device.reconcile();
|
|
94
|
+
return { status: 'busy', activity: null };
|
|
95
|
+
}
|
|
96
|
+
if (intent && !original.hold && ['disposed', 'credentials-retired'].includes(intent.phase)) {
|
|
97
|
+
// Resource shutdown was already persisted; only credential retirement and membership publication remain.
|
|
98
|
+
original.registration = intent.registration;
|
|
99
|
+
original.sealed = true;
|
|
100
|
+
original.disposed = true;
|
|
101
|
+
}
|
|
102
|
+
else if (intent && !original.hold) {
|
|
103
|
+
original.resumeRecovered ??= await device.recoverRemovalRuntime(input);
|
|
104
|
+
original.registration = intent.registration;
|
|
105
|
+
}
|
|
106
|
+
if (original.restoring || intent?.phase === 'restoring') {
|
|
107
|
+
await this.restore(original);
|
|
108
|
+
return { status: 'busy', activity: original.activity };
|
|
109
|
+
}
|
|
110
|
+
if (!original.hold && !original.disposed) {
|
|
111
|
+
original.registration = await store.readMetadata(input.id, input.expectedRevision);
|
|
112
|
+
original.activity = (await device.activities()).get(input.id) ?? null;
|
|
113
|
+
original.hold = await device.prepareTargetAdmission(input, async () => {
|
|
114
|
+
const cancelling = await this.current(original);
|
|
115
|
+
if (cancelling)
|
|
116
|
+
await withWorkstationLock(store.root, 'operation', () => store.cancelRemoval(cancelling));
|
|
117
|
+
original.restoring = true;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (!intent)
|
|
121
|
+
intent = await withWorkstationLock(store.root, 'operation', () => store.beginRemoval({ schemaVersion: 1, registration: original.registration, operationId: input.operationId, runtimeGeneration: input.runtimeGeneration, force: input.force, phase: 'intent' }));
|
|
122
|
+
if (!original.disposed && !original.preparation)
|
|
123
|
+
original.preparation = await device.prepareTargetResources(original.hold);
|
|
124
|
+
if (!original.disposed && !original.sealAttempted) {
|
|
125
|
+
const observation = await device.observeTargetResources(original.hold, original.preparation);
|
|
126
|
+
original.activity = (await device.activities()).get(input.id) ?? original.activity;
|
|
127
|
+
if (observation.status !== 'candidate' && !input.force) {
|
|
128
|
+
await this.restore(original);
|
|
129
|
+
return { status: observation.status === 'busy' ? 'busy' : 'pending', activity: original.activity };
|
|
130
|
+
}
|
|
131
|
+
original.sealAttempted = true;
|
|
132
|
+
}
|
|
133
|
+
if (!original.sealed) {
|
|
134
|
+
await device.sealTargetResources(original.hold, original.preparation, input.force ? 'force' : undefined);
|
|
135
|
+
original.sealed = true;
|
|
136
|
+
}
|
|
137
|
+
intent = await this.current(original);
|
|
138
|
+
if (!intent)
|
|
139
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Original removal intent is unavailable');
|
|
140
|
+
if (original.sealed && intent.phase === 'intent')
|
|
141
|
+
intent = await withWorkstationLock(store.root, 'operation', () => store.advanceRemoval(intent, 'sealed'));
|
|
142
|
+
if (!original.disposed) {
|
|
143
|
+
await withWorkstationLock(store.root, 'operation', async () => {
|
|
144
|
+
const namespace = await resolveRemovalNamespaceLocked(store.root, input.id, input.expectedRevision);
|
|
145
|
+
original.settlement = await device.prepareTargetDisposition(original.hold, original.preparation, namespace);
|
|
146
|
+
if (!original.sealed) {
|
|
147
|
+
original.sealed = true;
|
|
148
|
+
intent = await store.advanceRemoval(intent, 'sealed');
|
|
149
|
+
}
|
|
150
|
+
await device.disposeTargetResources(original.hold, original.preparation, original.settlement, namespace);
|
|
151
|
+
original.disposed = true;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (intent.phase === 'sealed')
|
|
155
|
+
intent = await withWorkstationLock(store.root, 'operation', () => store.advanceRemoval(intent, 'disposed'));
|
|
156
|
+
if (intent.phase === 'disposed') {
|
|
157
|
+
await withWorkstationLock(store.root, 'operation', () => retireRegistrationEnrollment(store.root, intent.registration));
|
|
158
|
+
intent = await withWorkstationLock(store.root, 'operation', () => store.advanceRemoval(intent, 'credentials-retired'));
|
|
159
|
+
}
|
|
160
|
+
if (!await store.verifyRemovalCompletion(intent.registration))
|
|
161
|
+
await withWorkstationLock(store.root, 'operation', () => store.remove(input.id, input.expectedRevision));
|
|
162
|
+
if (!await store.verifyRemovalCompletion(intent.registration))
|
|
163
|
+
throw new LocalControlError('OUTCOME_UNKNOWN', 'Original removal finalization needs verification');
|
|
164
|
+
if (!original.finalized) {
|
|
165
|
+
if (original.hold)
|
|
166
|
+
await device.finalizeTargetRemoval(original.hold);
|
|
167
|
+
original.finalized = true;
|
|
168
|
+
}
|
|
169
|
+
await withWorkstationLock(store.root, 'operation', () => store.advanceRemoval(intent, 'completed'));
|
|
170
|
+
if (!original.hold)
|
|
171
|
+
await device.reconcile();
|
|
172
|
+
this.originals.delete(input.id);
|
|
173
|
+
return { status: 'removed', activity: original.activity };
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error instanceof TargetAdmissionPendingError) {
|
|
177
|
+
original.hold ??= error.hold;
|
|
178
|
+
original.restoring = true;
|
|
179
|
+
}
|
|
180
|
+
let durable;
|
|
181
|
+
try {
|
|
182
|
+
durable = await this.current(original);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return { status: 'pending', activity: original.activity };
|
|
186
|
+
}
|
|
187
|
+
if (original.restoring || durable?.phase === 'restoring') {
|
|
188
|
+
try {
|
|
189
|
+
await this.restore(original);
|
|
190
|
+
return { status: error instanceof LocalControlError && error.code === 'BUSY' ? 'busy' : 'pending', activity: original.activity };
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return { status: 'pending', activity: original.activity };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (durable?.phase === 'intent' && !original.sealed && original.hold && !(input.force && original.sealAttempted)) {
|
|
197
|
+
try {
|
|
198
|
+
await this.restore(original);
|
|
199
|
+
const activity = original.activity;
|
|
200
|
+
if (!input.force && activity?.known && activity.agents + activity.terminals + activity.commands + activity.transfers + activity.desktopConsumers > 0)
|
|
201
|
+
return { status: 'busy', activity };
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return { status: 'pending', activity: original.activity };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!durable && original.hold && !original.sealed) {
|
|
208
|
+
try {
|
|
209
|
+
await this.restore(original);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return { status: 'pending', activity: original.activity };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (!durable && !original.hold)
|
|
216
|
+
this.originals.delete(input.id);
|
|
217
|
+
if (error instanceof LocalControlError && ['BINDING_CONFLICT', 'UNSUPPORTED_PROTOCOL'].includes(error.code))
|
|
218
|
+
throw error;
|
|
219
|
+
return { status: 'pending', activity: original.activity };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|