@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
|
@@ -5,6 +5,7 @@ import { SESSION_WINDOW_BYTES, SESSION_WINDOW_EVENTS } from './acp/outbox.js';
|
|
|
5
5
|
import { SocketGate } from './agentFlow.js';
|
|
6
6
|
import { AGENT_PROBE_TIMEOUT_MS, AgentRefusedSessionError, LOCAL_SESSION_UNMOVABLE } from './acp/acpAgents.js';
|
|
7
7
|
import { projectFrame } from './acp/processor/projection.js';
|
|
8
|
+
import { TaskRegistry } from './update/taskRegistry.js';
|
|
8
9
|
import { reportsActivity, supportsAsyncTaskStop } from './acp/activityReporting.js';
|
|
9
10
|
import { activityIsBusy } from './acp/activityBusy.js';
|
|
10
11
|
import { ADAPTER_LOST_REOPEN_ATTEMPTS, plainSwitchBlocker, REOPEN_REQUEST_RETRY_MS, switchBlockers, SWITCH_QUIET_MS } from './acp/switching.js';
|
|
@@ -230,6 +231,8 @@ export class AgentSessionManager {
|
|
|
230
231
|
agents;
|
|
231
232
|
journalDir;
|
|
232
233
|
tasks;
|
|
234
|
+
admission;
|
|
235
|
+
ownership;
|
|
233
236
|
sessions = new Map();
|
|
234
237
|
/** A failed recorded-build respawn keeps this binding even after no LiveSession remains. */
|
|
235
238
|
retainedRespawnBuilds = new Map();
|
|
@@ -338,10 +341,12 @@ export class AgentSessionManager {
|
|
|
338
341
|
/** `<configDir>/agents`: the legacy journals (`sessions/<ref>.jsonl`) an older CLI left are migrated from here, once. */
|
|
339
342
|
journalDir,
|
|
340
343
|
/** Shared update admission and force-cancellation authority, when this daemon supports updates. */
|
|
341
|
-
tasks) {
|
|
344
|
+
tasks, admission = () => true, ownership) {
|
|
342
345
|
this.agents = agents;
|
|
343
346
|
this.journalDir = journalDir;
|
|
344
347
|
this.tasks = tasks;
|
|
348
|
+
this.admission = admission;
|
|
349
|
+
this.ownership = ownership;
|
|
345
350
|
this.send = send;
|
|
346
351
|
}
|
|
347
352
|
/** Where an older CLI's journals live: `<journalDir>/sessions/<sessionRef>.jsonl` (read-only legacy, `journal.ts`). */
|
|
@@ -369,7 +374,7 @@ export class AgentSessionManager {
|
|
|
369
374
|
}
|
|
370
375
|
for (const sessionRef of refs) {
|
|
371
376
|
try {
|
|
372
|
-
const entries = await readLegacyJournal(this.sessionsDir, sessionRef);
|
|
377
|
+
const entries = await readLegacyJournal(this.sessionsDir, sessionRef, this.ownership);
|
|
373
378
|
const imported = await importOutbox(sessionRef, entries.map((entry) => ({
|
|
374
379
|
seq: entry.seq,
|
|
375
380
|
...(entry.src ? { src: entry.src } : {}),
|
|
@@ -560,7 +565,7 @@ export class AgentSessionManager {
|
|
|
560
565
|
this.send = send;
|
|
561
566
|
this.serverAgentWire = serverAgentWire;
|
|
562
567
|
// A new connection: its own window and socket. What was sent on the previous one waits for the server's reopen to say how far
|
|
563
|
-
// it got
|
|
568
|
+
// it got. An already-exited adapter's recovered turn is replayed in full to finish it before reopen.
|
|
564
569
|
this.epoch += 1;
|
|
565
570
|
for (const live of this.sessions.values())
|
|
566
571
|
live.session.outbox.connectionChanged();
|
|
@@ -568,6 +573,9 @@ export class AgentSessionManager {
|
|
|
568
573
|
// Before any inbound frame on this connection can be answered: a sweep notice that arrived here
|
|
569
574
|
// after the server had already re-opened its ref would invalidate a healthy generation.
|
|
570
575
|
this.flushSwept();
|
|
576
|
+
for (const live of this.sessions.values())
|
|
577
|
+
if (live.replayDeadTail)
|
|
578
|
+
live.session.outbox.resendAll();
|
|
571
579
|
this.pump();
|
|
572
580
|
// Account observations are outside the session journal. Replay the cached state on reconnect
|
|
573
581
|
// so a server restart does not depend on the provider sending another quota notification.
|
|
@@ -738,12 +746,17 @@ export class AgentSessionManager {
|
|
|
738
746
|
highestScopedJournalSeq = Math.max(highestScopedJournalSeq, item.seq);
|
|
739
747
|
}
|
|
740
748
|
const live = {
|
|
741
|
-
sessionRef: candidate.sessionRef, adopted: true, agentId: candidate.agentId, agentKey: candidate.agentKey, session,
|
|
749
|
+
sessionRef: candidate.sessionRef, adopted: true, replayDeadTail: candidate.agentAlive === false, agentId: candidate.agentId, agentKey: candidate.agentKey, session,
|
|
742
750
|
nativeSessionId: session.nativeSessionId ?? null,
|
|
743
751
|
scopedReplayBlocked: false, highestScopedJournalSeq, unwatch: () => { }, replays: Promise.resolve(), closed: false,
|
|
744
752
|
lastActivityAt: Date.now(),
|
|
745
753
|
};
|
|
746
|
-
this.register(live);
|
|
754
|
+
this.register(live, this.adoptionOrigin);
|
|
755
|
+
// An exited adapter is adopted only to finish its recovered turn. Release its entire retained
|
|
756
|
+
// prefix in order so that tail can settle the server's turn before a reopen respawns the adapter.
|
|
757
|
+
// Live adapters retain the ordinary reopen/watermark gate.
|
|
758
|
+
if (live.replayDeadTail)
|
|
759
|
+
live.session.outbox.resendAll();
|
|
747
760
|
console.info(`[acp:${live.agentId} agent:${live.agentKey}] adopted session ${live.sessionRef} from keeper-acp`);
|
|
748
761
|
}
|
|
749
762
|
/** Waits (bounded) for the start-time adoption; what is still rebuilding afterwards is reported as such. */
|
|
@@ -778,6 +791,23 @@ export class AgentSessionManager {
|
|
|
778
791
|
clearTimeout(timer);
|
|
779
792
|
}
|
|
780
793
|
async handleFrame(frame) {
|
|
794
|
+
const live = 'sessionRef' in frame && typeof frame.sessionRef === 'string' ? this.liveSession(frame.sessionRef) : undefined;
|
|
795
|
+
const openContinuation = frame.t === 'agent_open' && ((live && !this.needsRespawn(live)) || this.opening.has(frame.sessionRef) || this.adopting.has(frame.sessionRef));
|
|
796
|
+
const newWork = !openContinuation && ['agent_open', 'agent_prompt', 'agent_list_native', 'agent_delete_native', 'agent_account_read', 'agent_verify', 'agent_authenticate', 'agent_install', 'agent_switch_idle'].includes(frame.t);
|
|
797
|
+
const registry = this.tasks ?? this.acceptedTasks;
|
|
798
|
+
const ticket = newWork && this.admission() ? registry.beginAcceptedAgentWork(frame.t, frame, frame.t === 'agent_prompt' ? this.adoptionOrigin : undefined) : null;
|
|
799
|
+
const admitted = (!newWork && (frame.t !== 'agent_open' || this.admission())) || ticket !== null;
|
|
800
|
+
try {
|
|
801
|
+
await this.handleAcceptedFrame(frame, ticket, admitted, registry);
|
|
802
|
+
}
|
|
803
|
+
finally {
|
|
804
|
+
if (ticket)
|
|
805
|
+
registry.finishAcceptedAgentWork(ticket);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
acceptedTasks = new TaskRegistry();
|
|
809
|
+
adoptionOrigin = {};
|
|
810
|
+
async handleAcceptedFrame(frame, ticket, admitted, registry) {
|
|
781
811
|
// A frame for a ref keeper-acp still runs from a previous daemon waits until that session is rebuilt (Task 21): answering
|
|
782
812
|
// it "unknown session" would end a turn that is going on.
|
|
783
813
|
const frameRef = 'sessionRef' in frame && typeof frame.sessionRef === 'string' ? frame.sessionRef : undefined;
|
|
@@ -792,7 +822,7 @@ export class AgentSessionManager {
|
|
|
792
822
|
await parked;
|
|
793
823
|
switch (frame.t) {
|
|
794
824
|
case 'agent_open': {
|
|
795
|
-
const admission = this.reserveOpen(frame
|
|
825
|
+
const admission = this.reserveOpen(frame, ticket, admitted, registry);
|
|
796
826
|
if (admission === 'rejected')
|
|
797
827
|
return;
|
|
798
828
|
const ref = frame.sessionRef;
|
|
@@ -802,7 +832,7 @@ export class AgentSessionManager {
|
|
|
802
832
|
}
|
|
803
833
|
finally {
|
|
804
834
|
if (admission === 'reserved')
|
|
805
|
-
|
|
835
|
+
registry.agentOpenFinished(ref);
|
|
806
836
|
const left = (this.openHandling.get(ref) ?? 1) - 1;
|
|
807
837
|
if (left > 0)
|
|
808
838
|
this.openHandling.set(ref, left);
|
|
@@ -818,7 +848,7 @@ export class AgentSessionManager {
|
|
|
818
848
|
const live = this.routableSession(frame.sessionRef, 'prompt');
|
|
819
849
|
if (!live)
|
|
820
850
|
return;
|
|
821
|
-
if (
|
|
851
|
+
if (!admitted) {
|
|
822
852
|
// The refusal is the daemon's decision, so it is a note (`agent_prompt{admitted:false}`) and the processor ends the
|
|
823
853
|
// turn `failed` with the same words (`ADMISSION_REFUSED`), numbered in the session's own sequence (spec §5.4). No
|
|
824
854
|
// `authMethods`: the processor's short frame carries no `agentRefused`, so the projection adds none — the prompt
|
|
@@ -826,6 +856,14 @@ export class AgentSessionManager {
|
|
|
826
856
|
live.session.refusePrompt(frame.platformTurnId);
|
|
827
857
|
return;
|
|
828
858
|
}
|
|
859
|
+
if (ticket)
|
|
860
|
+
try {
|
|
861
|
+
registry.consumeAcceptedAgentWork(ticket, frame.t, frame);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
live.session.refusePrompt(frame.platformTurnId);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
829
867
|
live.session.prompt(promptBlocks(frame), frame.platformTurnId);
|
|
830
868
|
return;
|
|
831
869
|
}
|
|
@@ -963,6 +1001,10 @@ export class AgentSessionManager {
|
|
|
963
1001
|
* keep holding that row or to deliver it the ordinary way. A silent drop parks it forever.
|
|
964
1002
|
*/
|
|
965
1003
|
case 'agent_steer': {
|
|
1004
|
+
if (!this.admission()) {
|
|
1005
|
+
this.emit({ t: 'agent_steered', id: frame.id, ok: false, error: 'Registration admission is paused' });
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
966
1008
|
const live = this.routableSession(frame.sessionRef, 'steer');
|
|
967
1009
|
if (!live) {
|
|
968
1010
|
this.emit({ t: 'agent_steered', id: frame.id, ok: false, error: `no live session '${frame.sessionRef}'` });
|
|
@@ -1004,6 +1046,10 @@ export class AgentSessionManager {
|
|
|
1004
1046
|
* a request that never resolves.
|
|
1005
1047
|
*/
|
|
1006
1048
|
case 'agent_set_config_option': {
|
|
1049
|
+
if (!this.admission()) {
|
|
1050
|
+
this.emit({ t: 'agent_config_option_set', id: frame.id, ok: false, error: 'Registration admission is paused' });
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1007
1053
|
const live = this.routableSession(frame.sessionRef, 'set_config_option');
|
|
1008
1054
|
if (!live) {
|
|
1009
1055
|
this.emit({ t: 'agent_config_option_set', id: frame.id, ok: false, error: `no live session '${frame.sessionRef}'` });
|
|
@@ -1039,14 +1085,14 @@ export class AgentSessionManager {
|
|
|
1039
1085
|
return;
|
|
1040
1086
|
}
|
|
1041
1087
|
case 'agent_list_native':
|
|
1042
|
-
if (
|
|
1088
|
+
if (!admitted) {
|
|
1043
1089
|
this.emit({ t: 'agent_native_sessions', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
|
|
1044
1090
|
return;
|
|
1045
1091
|
}
|
|
1046
1092
|
await this.onListNative(frame);
|
|
1047
1093
|
return;
|
|
1048
1094
|
case 'agent_delete_native':
|
|
1049
|
-
if (
|
|
1095
|
+
if (!admitted) {
|
|
1050
1096
|
this.emit({ t: 'agent_native_deleted', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
|
|
1051
1097
|
return;
|
|
1052
1098
|
}
|
|
@@ -1055,7 +1101,7 @@ export class AgentSessionManager {
|
|
|
1055
1101
|
// Both of these spawn an agent subprocess, exactly as `agent_list_native` does, so they answer
|
|
1056
1102
|
// a draining workstation the same way rather than starting work it is trying to finish.
|
|
1057
1103
|
case 'agent_account_read': {
|
|
1058
|
-
if (
|
|
1104
|
+
if (!admitted) {
|
|
1059
1105
|
this.emit({ t: 'agent_account_read_result', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
|
|
1060
1106
|
return;
|
|
1061
1107
|
}
|
|
@@ -1077,14 +1123,14 @@ export class AgentSessionManager {
|
|
|
1077
1123
|
return;
|
|
1078
1124
|
}
|
|
1079
1125
|
case 'agent_verify':
|
|
1080
|
-
if (
|
|
1126
|
+
if (!admitted) {
|
|
1081
1127
|
this.emit({ t: 'agent_verified', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
|
|
1082
1128
|
return;
|
|
1083
1129
|
}
|
|
1084
1130
|
await this.onVerify(frame);
|
|
1085
1131
|
return;
|
|
1086
1132
|
case 'agent_authenticate':
|
|
1087
|
-
if (
|
|
1133
|
+
if (!admitted) {
|
|
1088
1134
|
this.emit({ t: 'agent_authenticated', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
|
|
1089
1135
|
return;
|
|
1090
1136
|
}
|
|
@@ -1093,7 +1139,7 @@ export class AgentSessionManager {
|
|
|
1093
1139
|
// Downloads a package; starts nothing. Still refused while draining, because a workstation on
|
|
1094
1140
|
// its way down should not begin a multi-megabyte fetch it will be killed in the middle of.
|
|
1095
1141
|
case 'agent_install':
|
|
1096
|
-
if (
|
|
1142
|
+
if (!admitted) {
|
|
1097
1143
|
this.emit({ t: 'agent_installed', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
|
|
1098
1144
|
return;
|
|
1099
1145
|
}
|
|
@@ -1101,7 +1147,7 @@ export class AgentSessionManager {
|
|
|
1101
1147
|
return;
|
|
1102
1148
|
// "Move idle sessions" (Task 25): a move spawns adapters, so a draining workstation refuses it like an install.
|
|
1103
1149
|
case 'agent_switch_idle':
|
|
1104
|
-
if (
|
|
1150
|
+
if (!admitted) {
|
|
1105
1151
|
this.emit({ t: 'agent_switch_idle_result', id: frame.id, ok: false, error: 'Workstation is not accepting new work' });
|
|
1106
1152
|
return;
|
|
1107
1153
|
}
|
|
@@ -1149,24 +1195,31 @@ export class AgentSessionManager {
|
|
|
1149
1195
|
* stop leaves attached. A ref absent from both maps is genuinely new work and still needs
|
|
1150
1196
|
* the ordinary TaskRegistry admission reservation.
|
|
1151
1197
|
*/
|
|
1152
|
-
reserveOpen(
|
|
1153
|
-
|
|
1154
|
-
|
|
1198
|
+
reserveOpen(frame, ticket, admitted, registry) {
|
|
1199
|
+
const { id: openId, sessionRef } = frame;
|
|
1200
|
+
if (!admitted) {
|
|
1201
|
+
this.emit({ t: 'agent_opened', id: openId, ok: false, error: 'Workstation is not accepting new work' });
|
|
1202
|
+
return 'rejected';
|
|
1203
|
+
}
|
|
1155
1204
|
const live = this.liveSession(sessionRef);
|
|
1156
1205
|
// A keeper-hosted session whose adapter is gone is respawned by this open: new work, not a continuation — it takes a reservation,
|
|
1157
1206
|
// and a draining workstation refuses it like any open (spec §8.2; fix round 1, minor 9).
|
|
1158
1207
|
const respawn = live !== undefined && this.needsRespawn(live);
|
|
1159
|
-
if (
|
|
1208
|
+
if (registry.isForceStopping()) {
|
|
1160
1209
|
// A keeper-hosted session a force update left attached (TaskRegistry.stopAll) is still reopened as a continuation — a reconnect
|
|
1161
1210
|
// during the download must not cut it off; anything else is refused.
|
|
1162
|
-
if (live && !respawn &&
|
|
1211
|
+
if (live && !respawn && registry.isDurableSession(sessionRef))
|
|
1163
1212
|
return 'continuation';
|
|
1164
1213
|
this.emit({ t: 'agent_opened', id: openId, ok: false, error: 'Workstation is not accepting new work' });
|
|
1165
1214
|
return 'rejected';
|
|
1166
1215
|
}
|
|
1167
1216
|
if ((live && !respawn) || this.opening.has(sessionRef) || this.adopting.has(sessionRef))
|
|
1168
1217
|
return 'continuation';
|
|
1169
|
-
if (
|
|
1218
|
+
if (ticket) {
|
|
1219
|
+
registry.reserveAcceptedAgentOpen(ticket, frame);
|
|
1220
|
+
return 'reserved';
|
|
1221
|
+
}
|
|
1222
|
+
if (registry.tryReserveAgentOpen(sessionRef))
|
|
1170
1223
|
return 'reserved';
|
|
1171
1224
|
this.emit({ t: 'agent_opened', id: openId, ok: false, error: 'Workstation is not accepting new work' });
|
|
1172
1225
|
return 'rejected';
|
|
@@ -2241,7 +2294,7 @@ export class AgentSessionManager {
|
|
|
2241
2294
|
return { live };
|
|
2242
2295
|
}
|
|
2243
2296
|
/** A session joins: held, registered with the TaskRegistry, its frames and its outbox wired to the sender. */
|
|
2244
|
-
register(live) {
|
|
2297
|
+
register(live, adoption) {
|
|
2245
2298
|
const session = live.session;
|
|
2246
2299
|
this.detached.delete(live.sessionRef);
|
|
2247
2300
|
this.sessions.set(live.sessionRef, live);
|
|
@@ -2251,7 +2304,7 @@ export class AgentSessionManager {
|
|
|
2251
2304
|
() => this.retire(live, durable ? 'detach' : 'destroy'),
|
|
2252
2305
|
// 两个层级量取或:本进程发出的提示词是否在途(agent 自己也许在续跑一个我们没发起的回合,
|
|
2253
2306
|
// 那一种由第二项回答),以及 agent 自报的状态。两者都不累积,所以没有可泄漏的状态。
|
|
2254
|
-
() => live.session.liveness.turnRunning || activityIsBusy(live.session.activityRaw), { durable });
|
|
2307
|
+
() => live.session.liveness.turnRunning || activityIsBusy(live.session.activityRaw), { durable, adoption });
|
|
2255
2308
|
// Everything the session produces arrives as processor frames (Task 19): numbered by the processor, projected for the
|
|
2256
2309
|
// connected server here, recorded in the outbox (the numbered ones) and sent from it (Task 20).
|
|
2257
2310
|
live.unwatch = session.outbox.onChange(() => this.pump());
|
|
@@ -3,6 +3,7 @@ import { connectEncryptedChannel } from '../connection/encryptedChannel.js';
|
|
|
3
3
|
import { AGENT_WIRE_MIN_VERSION, AGENT_WIRE_RESUME_OFFER_VERSION, AGENT_WIRE_VERSION, decodeAgentFrame, encodeAgentFrame, outboundAgentFrameRejection, } from '../protocol/agent.js';
|
|
4
4
|
import { bridgesForWire, helloRoster } from '../protocol/common.js';
|
|
5
5
|
import { workstationChannelUrl } from './url.js';
|
|
6
|
+
import { DeviceEventRouter } from '../runtime/events.js';
|
|
6
7
|
/** Binds daemon-scoped sessions only to the encrypted agent channel's sender. */
|
|
7
8
|
export class AgentChannel {
|
|
8
9
|
options;
|
|
@@ -24,7 +25,14 @@ export class AgentChannel {
|
|
|
24
25
|
});
|
|
25
26
|
// Hoisted rather than inlined twice: `closeAll` compares it by identity to decide whether the
|
|
26
27
|
// binding it is being asked to drop is still this connection's. See AgentSessionManager.closeAll.
|
|
27
|
-
const
|
|
28
|
+
const registrationId = this.options.registrationId;
|
|
29
|
+
const events = registrationId ? new DeviceEventRouter((_recipient, event) => channel.send(event.payload), new Set([registrationId])) : undefined;
|
|
30
|
+
const send = (frame) => {
|
|
31
|
+
if (events && registrationId)
|
|
32
|
+
events.publish({ scope: 'registration', registrationId, payload: frame });
|
|
33
|
+
else
|
|
34
|
+
channel.send(frame);
|
|
35
|
+
};
|
|
28
36
|
// The socket's unsent bytes: the manager's socket gate holds session frames back above 1 MiB so the heartbeat (written by the
|
|
29
37
|
// channel itself) never queues behind a backlog (spec §5.6).
|
|
30
38
|
void dispatchAgentChannelFrames(channel.messages, send, this.options.agents, () => channel.close(), () => channel.bufferedAmount);
|
|
@@ -11,7 +11,8 @@ import { TaskRegistry } from '../update/taskRegistry.js';
|
|
|
11
11
|
import { toFileError } from '../files/errors.js';
|
|
12
12
|
import { object } from '../tools/validation.js';
|
|
13
13
|
import { isUnaccountedTool } from '../tools/router.js';
|
|
14
|
-
import { requestRootFileExecutors } from '../files/adapters/computerFileFrames.js';
|
|
14
|
+
import { requestRootFileExecutors, fileErrorFrame } from '../files/adapters/computerFileFrames.js';
|
|
15
|
+
import { DeviceEventRouter } from '../runtime/events.js';
|
|
15
16
|
/**
|
|
16
17
|
* Consumes computer frames without serializing long-running commands behind one another.
|
|
17
18
|
* All command failures and post-close sends are contained here because this is called detached
|
|
@@ -211,6 +212,20 @@ export async function dispatchComputerFrames(messages, send, execute, tasks = ne
|
|
|
211
212
|
};
|
|
212
213
|
try {
|
|
213
214
|
for await (const frame of messages) {
|
|
215
|
+
if (durable.admission?.() === false) {
|
|
216
|
+
if (frame.t === 'exec_request') {
|
|
217
|
+
safeSend({ t: 'exec_exit', id: frame.id, exitCode: null, stdout: '', stderr: 'Registration admission is paused', timedOut: false, truncated: false });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (frame.t === 'tool_request' && frame.method !== 'tools.info') {
|
|
221
|
+
safeSend({ t: 'tool_result', id: frame.id, ok: false, result: {}, error: 'Registration admission is paused' });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (frame.t === 'file_write_request' || frame.t === 'file_mkdir_request') {
|
|
225
|
+
safeSend(fileErrorFrame(frame, { code: 'workstation_offline', message: 'Registration admission is paused' }));
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
214
229
|
if (frame.t.startsWith('desktop_'))
|
|
215
230
|
desktop?.onFrame(frame);
|
|
216
231
|
else if (frame.t === 'tool_request')
|
|
@@ -279,9 +294,17 @@ export class ComputerChannel {
|
|
|
279
294
|
return { closed: channel.closed, close: () => channel.close() };
|
|
280
295
|
}
|
|
281
296
|
async dispatch(channel) {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
297
|
+
const registrationId = this.options.registrationId;
|
|
298
|
+
const events = registrationId ? new DeviceEventRouter((_recipient, event) => channel.send(event.payload), new Set([registrationId])) : undefined;
|
|
299
|
+
const send = (frame) => {
|
|
300
|
+
if (events && registrationId)
|
|
301
|
+
events.publish({ scope: 'registration', registrationId, payload: frame });
|
|
302
|
+
else
|
|
303
|
+
channel.send(frame);
|
|
304
|
+
};
|
|
305
|
+
const desktop = new DesktopBridge(this.options.desktopEnabled === false ? undefined : resolveBrowserBinding(this.options.browserBinding), frame => channel.send(frame), () => channel.bufferedAmount);
|
|
306
|
+
await dispatchComputerFrames(channel.messages, send, this.options.runExec ?? ((request, onOutput, signal) => runExec(request, onOutput, signal)), this.tasks, this.options, this.tools, desktop, {
|
|
307
|
+
host: this.execs, bufferedAmount: () => channel.bufferedAmount, log: this.options.log, admission: this.options.admission,
|
|
285
308
|
});
|
|
286
309
|
}
|
|
287
310
|
}
|
|
@@ -97,7 +97,16 @@ export function metricsForManagementWire(metrics, serverManagementWire) {
|
|
|
97
97
|
*
|
|
98
98
|
* Exported for tests; production's only caller is `routeMessages`.
|
|
99
99
|
*/
|
|
100
|
-
export function dispatchUpdateCommand(channel, command, onUpdate, serverManagementWire) {
|
|
100
|
+
export function dispatchUpdateCommand(channel, command, onUpdate, serverManagementWire, requiredRemoteUpdatePolicy) {
|
|
101
|
+
if (requiredRemoteUpdatePolicy && command.remoteForcePolicy !== requiredRemoteUpdatePolicy) {
|
|
102
|
+
if (channel.state === 'OPEN') {
|
|
103
|
+
try {
|
|
104
|
+
channel.send(toClientUpdateStatusFrame(command, { phase: 'FAILED', remoteForcePolicy: 'drain-only', error: 'upgrade_required: the server must support authenticated device update readiness' }, serverManagementWire));
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
}
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
}
|
|
101
110
|
return onUpdate(command)
|
|
102
111
|
.then((status) => {
|
|
103
112
|
if (channel.state !== 'OPEN')
|
|
@@ -189,12 +198,27 @@ export class ManagementChannel {
|
|
|
189
198
|
constructor(options) {
|
|
190
199
|
this.options = options;
|
|
191
200
|
}
|
|
201
|
+
async receiveDeviceFact(event) {
|
|
202
|
+
const generation = this.generation;
|
|
203
|
+
const channel = this.channel;
|
|
204
|
+
if (event.scope !== 'device' || !generation || channel?.state !== 'OPEN')
|
|
205
|
+
return;
|
|
206
|
+
if (event.kind === 'tool-state' && this.options.onExternalAgentsScan) {
|
|
207
|
+
const bridges = await this.options.onExternalAgentsScan();
|
|
208
|
+
if (this.generation === generation && this.channel === channel)
|
|
209
|
+
this.pushRoster(bridges);
|
|
210
|
+
}
|
|
211
|
+
else if (event.kind === 'desktop-capability')
|
|
212
|
+
this.refreshDesktopCapabilities(generation);
|
|
213
|
+
}
|
|
192
214
|
refreshDesktopCapabilities(generation) {
|
|
193
215
|
if (generation === undefined || generation === this.generation)
|
|
194
216
|
this.desktopReporter?.refresh();
|
|
195
217
|
}
|
|
196
218
|
/** T10 reports only on its authenticated current management socket. */
|
|
197
219
|
sendDesktopState(generation, frame) {
|
|
220
|
+
if (frame.t === 'desktop_ownership_result' && (this.serverManagementWire ?? 0) < 10)
|
|
221
|
+
return false;
|
|
198
222
|
if (generation !== this.generation || (this.serverManagementWire ?? 0) < 9 || this.channel?.state !== 'OPEN')
|
|
199
223
|
return false;
|
|
200
224
|
try {
|
|
@@ -293,7 +317,7 @@ export class ManagementChannel {
|
|
|
293
317
|
// is never driven by the local browser/device-authorization flow.
|
|
294
318
|
await this.options.onReady?.({ generation: ready.generation, selectedWire: ready.protocols.MANAGEMENT, release: ready.release });
|
|
295
319
|
const pending = new Map();
|
|
296
|
-
void this.routeMessages(channel, pending);
|
|
320
|
+
void this.routeMessages(channel, pending, ready.generation);
|
|
297
321
|
return {
|
|
298
322
|
generation: ready.generation,
|
|
299
323
|
closed: channel.closed,
|
|
@@ -344,17 +368,24 @@ export class ManagementChannel {
|
|
|
344
368
|
return frame;
|
|
345
369
|
throw new Error('Management channel closed before ready');
|
|
346
370
|
}
|
|
347
|
-
async routeMessages(channel, pending) {
|
|
371
|
+
async routeMessages(channel, pending, generation) {
|
|
348
372
|
try {
|
|
349
373
|
for await (const frame of channel.messages) {
|
|
350
374
|
if (channel !== this.channel)
|
|
351
375
|
break;
|
|
376
|
+
if (frame.t === 'desktop_automation_prepare') {
|
|
377
|
+
if (frame.deviceGeneration === this.generation && this.generation && this.options.onAutomationPrepare)
|
|
378
|
+
void this.options.onAutomationPrepare(frame, this.generation).catch(() => { });
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
352
381
|
if (frame.t === 'workstation_capabilities_request') {
|
|
353
382
|
if ((this.serverManagementWire ?? 0) >= 9 && frame.generation === this.generation)
|
|
354
383
|
this.desktopReporter?.refresh();
|
|
355
384
|
continue;
|
|
356
385
|
}
|
|
357
|
-
if ((frame.t === 'desktop_background' || frame.t === 'desktop_lobby' || frame.t === 'desktop_prepare' || frame.t === 'desktop_cancel') && (this.serverManagementWire ?? 0) >= 9) {
|
|
386
|
+
if ((frame.t === 'desktop_background' || frame.t === 'desktop_lobby' || frame.t === 'desktop_prepare' || frame.t === 'desktop_cancel' || frame.t === 'desktop_ownership') && (this.serverManagementWire ?? 0) >= 9) {
|
|
387
|
+
if (frame.t === 'desktop_ownership' && (this.serverManagementWire ?? 0) < 10)
|
|
388
|
+
continue;
|
|
358
389
|
if (frame.t !== 'desktop_cancel' && frame.deviceGeneration !== this.generation)
|
|
359
390
|
continue;
|
|
360
391
|
if (this.options.onDesktopCommand && this.generation) {
|
|
@@ -374,7 +405,7 @@ export class ManagementChannel {
|
|
|
374
405
|
if (frame.t === 'client_update' && this.options.onUpdate) {
|
|
375
406
|
// Deliberately not awaited — see dispatchUpdateCommand for why inline handling both
|
|
376
407
|
// stalled this loop for the whole update and threw the final status away.
|
|
377
|
-
void dispatchUpdateCommand(channel, frame, this.options.onUpdate, this.serverManagementWire);
|
|
408
|
+
void dispatchUpdateCommand(channel, frame, command => this.options.onUpdate(command, generation), this.serverManagementWire, this.options.requiredRemoteUpdatePolicy);
|
|
378
409
|
continue;
|
|
379
410
|
}
|
|
380
411
|
if (frame.t === 'keeper_control' && this.options.onKeeperControl) {
|
|
@@ -6,6 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import yaml from 'js-yaml';
|
|
8
8
|
import { z } from 'zod';
|
|
9
|
+
import { withWorkstationLock } from './control/locks.js';
|
|
10
|
+
import { assertLegacyRoot } from './control/rootFormat.js';
|
|
9
11
|
const localPath = z.string().refine(p => path.isAbsolute(p) && !p.includes('\0'));
|
|
10
12
|
const nativeConfig = z.strictObject({ kind: z.literal('linux-native'), helperPath: localPath, runtimeDir: localPath });
|
|
11
13
|
export function updateServiceContext(configDir = clientDir(), overrides = {}) {
|
|
@@ -14,10 +16,14 @@ export function updateServiceContext(configDir = clientDir(), overrides = {}) {
|
|
|
14
16
|
const managed = loadManagedCliInstallSync(packageRoot);
|
|
15
17
|
const nodePath = managed?.nodePath ?? overrides.nodePath ?? process.execPath;
|
|
16
18
|
const npmPrefix = managed?.prefix ?? overrides.npmPrefix ?? process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(packageRoot, '../../../..');
|
|
19
|
+
// A previous sudo npm command can leave the interactive user's cache root-owned. Updates and
|
|
20
|
+
// their detached rollback watchdog share a cache owned by this workstation service instead.
|
|
21
|
+
const env = { ...npmExecutionContext({ prefix: npmPrefix, nodePath }, process.env), NPM_CONFIG_CACHE: path.join(configDir, 'update/npm-cache') };
|
|
22
|
+
delete env.npm_config_cache;
|
|
17
23
|
return {
|
|
18
24
|
configDir,
|
|
19
25
|
...(managed ? { managed } : {}),
|
|
20
|
-
env
|
|
26
|
+
env,
|
|
21
27
|
platform: overrides.platform ?? (process.platform === 'darwin' ? 'darwin' : 'linux'),
|
|
22
28
|
nodePath,
|
|
23
29
|
npmPath: managed?.npmPath ?? overrides.npmPath ?? process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(path.dirname(nodePath), 'npm'),
|
|
@@ -33,8 +39,11 @@ export function clientDir() {
|
|
|
33
39
|
return process.env.AXOM_WORKSTATION_CONFIG_DIR ?? path.join(os.homedir(), '.axom', 'workstation');
|
|
34
40
|
}
|
|
35
41
|
export async function saveClientConfig(dir, cfg) {
|
|
36
|
-
await
|
|
37
|
-
|
|
42
|
+
await withWorkstationLock(dir, 'operation', async () => {
|
|
43
|
+
await assertLegacyRoot(dir);
|
|
44
|
+
await fs.mkdir(dir, { recursive: true });
|
|
45
|
+
await fs.writeFile(path.join(dir, 'config.yml'), yaml.dump(cfg), { mode: 0o600 });
|
|
46
|
+
});
|
|
38
47
|
}
|
|
39
48
|
export async function loadClientConfig(dir) {
|
|
40
49
|
const raw = await fs.readFile(path.join(dir, 'config.yml'), 'utf8');
|
|
@@ -50,9 +59,12 @@ export async function loadClientConfig(dir) {
|
|
|
50
59
|
return { ...(browserBinding ? { browserBinding } : {}), ...(desktopProvider ? { desktopProvider } : {}), server: parsed.server, workstationId: parsed.workstationId, ...(typeof parsed.ownerUserId === 'string' && parsed.ownerUserId ? { ownerUserId: parsed.ownerUserId } : {}), ...(Array.isArray(parsed.workspaces) ? { workspaces: parsed.workspaces } : {}) };
|
|
51
60
|
}
|
|
52
61
|
export async function writeKeys(dir, clientKeyB64, serverPubB64) {
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
await withWorkstationLock(dir, 'operation', async () => {
|
|
63
|
+
await assertLegacyRoot(dir);
|
|
64
|
+
await fs.mkdir(dir, { recursive: true });
|
|
65
|
+
await fs.writeFile(path.join(dir, 'client.key'), clientKeyB64, { mode: 0o600 });
|
|
66
|
+
await fs.writeFile(path.join(dir, 'server.pub'), serverPubB64);
|
|
67
|
+
});
|
|
56
68
|
}
|
|
57
69
|
export async function readKeys(dir) {
|
|
58
70
|
const clientKey = Buffer.from((await fs.readFile(path.join(dir, 'client.key'), 'utf8')).trim(), 'base64');
|
|
@@ -7,6 +7,7 @@ import { loadClientConfig } from '../config.js';
|
|
|
7
7
|
import { privateDirectory } from '../keepers/kit/paths.js';
|
|
8
8
|
import { bindingSchema, operationRecordSchema, LocalControlError } from './protocol.js';
|
|
9
9
|
import { normalizeWorkstationServer } from './serverIdentity.js';
|
|
10
|
+
import { assertLegacyRoot } from './rootFormat.js';
|
|
10
11
|
const IDENTITY_FILES = ['client.key', 'server.pub', 'config.yml'];
|
|
11
12
|
const missing = (error) => error.code === 'ENOENT';
|
|
12
13
|
async function readPrivateFile(file) {
|
|
@@ -106,6 +107,7 @@ export class BindingStore {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
async recover() {
|
|
110
|
+
await assertLegacyRoot(this.directory);
|
|
109
111
|
if (!await exists(this.pending))
|
|
110
112
|
return;
|
|
111
113
|
const stat = await fs.lstat(this.pending);
|
|
@@ -148,6 +150,7 @@ export class BindingStore {
|
|
|
148
150
|
await syncDirectory(this.directory);
|
|
149
151
|
}
|
|
150
152
|
async commit(config, keys, operation, repair = false) {
|
|
153
|
+
await assertLegacyRoot(this.directory);
|
|
151
154
|
const binding = bindingOf(config);
|
|
152
155
|
validateKeys(keys);
|
|
153
156
|
const safeOperation = operationRecordSchema.parse(operation);
|
|
@@ -176,6 +179,7 @@ export class BindingStore {
|
|
|
176
179
|
await this.recover();
|
|
177
180
|
}
|
|
178
181
|
async clear(expectedRevision) {
|
|
182
|
+
await assertLegacyRoot(this.directory);
|
|
179
183
|
await this.recover();
|
|
180
184
|
const current = await this.inspect();
|
|
181
185
|
if (current.revision !== expectedRevision || current.identityState !== 'ready')
|
|
@@ -10,6 +10,7 @@ import { inspectWorkstation } from './inspect.js';
|
|
|
10
10
|
import { enrollWorkstation, enrollmentFailure } from './enrollment.js';
|
|
11
11
|
import { withWorkstationLock } from './locks.js';
|
|
12
12
|
import { bindingGuardSchema, LocalControlError } from './protocol.js';
|
|
13
|
+
export { executeDeviceCommand } from './deviceCommands.js';
|
|
13
14
|
const lifecycleRequest = z.object({ operationId: z.string().min(1).max(128), guard: bindingGuardSchema }).strict();
|
|
14
15
|
export async function readLocalRequest(input) {
|
|
15
16
|
const chunks = [];
|