@platless/axom-cli 0.5.9 → 0.6.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/dist/axom-patch/cli/README.md +129 -12
- package/dist/axom-patch/cli/account-limits/generate.py +15 -12
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
- package/dist/axom-patch/cli/claude-account-limits.json +9 -9
- package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
- package/dist/axom-patch/cli/codex-account-limits.json +11 -11
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
- package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
- package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
- package/dist/axom-patch/cli/nightly-task.md +55 -13
- package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
- package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
- package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
- package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
- package/dist/axom-patch/cli/qoder-steering.md +20 -25
- package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
- package/dist/axom-patch/cli/runtime.json +19 -19
- package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
- package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-env-account.json +58 -0
- package/dist/axom-patch/cli/zcode-models.json +9 -10
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
- package/dist/cli.js +11 -45
- package/dist/commands/auth.js +4 -1
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +126 -3
- package/dist/commands/workstation.js +101 -164
- package/dist/commands/workstationRegistrations.js +116 -0
- package/dist/credentialBroker.js +13 -2
- package/dist/credentials.js +16 -0
- package/dist/install/claude/commands.js +15 -15
- package/dist/keepers/terminal/keeper.mjs +1089 -673
- package/dist/keepers/terminal/manifest.json +4 -4
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-x64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +113 -102
- package/dist/workstation/acp/acpClient.js +26 -0
- package/dist/workstation/acp/builds/install/npm.js +99 -10
- package/dist/workstation/acp/initializeResponse.js +6 -1
- package/dist/workstation/acp/managedPatches.js +189 -25
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +12 -5
- package/dist/workstation/acp/patchAssets.js +40 -0
- package/dist/workstation/acp/permission.js +2 -2
- package/dist/workstation/acp/processor/checkpoint.js +26 -34
- package/dist/workstation/acp/processor/control.js +7 -80
- package/dist/workstation/acp/processor/jsonrpc.js +8 -0
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +54 -82
- package/dist/workstation/acp/processor/state.js +7 -13
- package/dist/workstation/acp/processor/step.js +3 -20
- package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
- package/dist/workstation/acp/processor/transport/local.js +19 -3
- package/dist/workstation/acp/processor/updates.js +156 -105
- package/dist/workstation/acp/processorSession.js +20 -173
- package/dist/workstation/acp/registry/catalog.js +65 -39
- package/dist/workstation/acp/registry/launch.js +16 -1
- package/dist/workstation/acp/switching.js +0 -3
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +32 -3
- package/dist/workstation/acp/test/raw/sources.js +16 -130
- package/dist/workstation/acp/test/realAdapter.js +1 -47
- package/dist/workstation/acp/test/scriptAgent.js +11 -3
- package/dist/workstation/agentFlow.js +1 -1
- package/dist/workstation/agentSessions.js +114 -166
- package/dist/workstation/channels/agent.js +9 -1
- package/dist/workstation/channels/computer.js +27 -4
- package/dist/workstation/channels/management.js +130 -15
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +33 -9
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +204 -0
- package/dist/workstation/control/commands.js +125 -0
- package/dist/workstation/control/daemonIdentity.js +143 -0
- package/dist/workstation/control/deviceCommands.js +191 -0
- package/dist/workstation/control/deviceProtocol.js +68 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +110 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/rootFormat.js +16 -0
- package/dist/workstation/control/runtimeClient.js +152 -0
- package/dist/workstation/control/runtimeHost.js +235 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/control/updateTransition.js +91 -0
- package/dist/workstation/daemon.js +266 -264
- package/dist/workstation/daemonctl.js +226 -25
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/automationDevice.js +219 -0
- package/dist/workstation/desktopControl/automationJson.js +74 -0
- package/dist/workstation/desktopControl/automationLocalState.js +23 -0
- package/dist/workstation/desktopControl/automationProtocol.js +253 -0
- package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
- package/dist/workstation/desktopControl/automationSession.js +81 -0
- package/dist/workstation/desktopControl/automationTransport.js +138 -0
- package/dist/workstation/desktopControl/capabilities.js +93 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
- package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +602 -0
- package/dist/workstation/desktopControl/hostGrant.js +65 -0
- package/dist/workstation/desktopControl/lobby.js +131 -0
- package/dist/workstation/desktopControl/macProvider.js +156 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
- package/dist/workstation/desktopControl/protocol.js +311 -0
- package/dist/workstation/desktopControl/provider.js +1 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1774 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +98 -0
- package/dist/workstation/exec/policy.js +5 -0
- package/dist/workstation/files/client.js +10 -1
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/host.js +9 -2
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/journal.js +5 -2
- package/dist/workstation/keepers/acp/client.js +38 -2
- package/dist/workstation/keepers/kit/buildVersion.js +35 -11
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/client.js +2 -2
- package/dist/workstation/keepers/kit/launcher.js +123 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/keepers/legacy.js +16 -2
- package/dist/workstation/keepers/policy.js +5 -34
- package/dist/workstation/keepers/terminal/client.js +115 -23
- package/dist/workstation/keepers/terminal/exec.js +6 -3
- package/dist/workstation/keepers/terminal/frames.js +73 -0
- package/dist/workstation/keepers/terminal/generations.js +5 -8
- package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
- package/dist/workstation/keepers/terminal/input.js +146 -0
- package/dist/workstation/keepers/terminal/keeper.js +22 -8
- package/dist/workstation/keepers/terminal/main.js +1 -1
- package/dist/workstation/keepers/terminal/metadata.js +138 -0
- package/dist/workstation/keepers/terminal/protocol.js +4 -28
- package/dist/workstation/keepers/terminal/pty.js +1 -330
- package/dist/workstation/keepers/terminal/screen.js +246 -0
- package/dist/workstation/keepers/terminal/sessions.js +526 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/oauth/deviceAuthorization.js +31 -17
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +113 -5
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +120 -0
- package/dist/workstation/rd/control.js +117 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/host.js +213 -0
- package/dist/workstation/rd/migrationReadiness.js +46 -0
- package/dist/workstation/rd/provider.js +256 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +57 -0
- package/dist/workstation/rd/update.js +168 -0
- package/dist/workstation/rd/upkeep.js +80 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/registrations/enrollment.js +250 -0
- package/dist/workstation/registrations/enrollmentPending.js +361 -0
- package/dist/workstation/registrations/migration.js +356 -0
- package/dist/workstation/registrations/model.js +34 -0
- package/dist/workstation/registrations/removal.js +222 -0
- package/dist/workstation/registrations/store.js +737 -0
- package/dist/workstation/runtime/desktopBinding.js +20 -0
- package/dist/workstation/runtime/deviceDesktop.js +193 -0
- package/dist/workstation/runtime/deviceRuntime.js +568 -0
- package/dist/workstation/runtime/deviceServices.js +117 -0
- package/dist/workstation/runtime/events.js +20 -0
- package/dist/workstation/runtime/ownership.js +100 -0
- package/dist/workstation/runtime/registrationRuntime.js +575 -0
- package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
- package/dist/workstation/runtime/removalActivity.js +184 -0
- package/dist/workstation/service/installer.js +197 -14
- package/dist/workstation/service/launchd.js +2 -0
- package/dist/workstation/service/systemd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +47 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +8 -27
- package/dist/workstation/tools/info.js +6 -13
- package/dist/workstation/tools/terminalRouter.js +17 -172
- package/dist/workstation/tools/validation.js +55 -1
- package/dist/workstation/update/archiveEvidence.js +206 -0
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +34 -13
- package/dist/workstation/update/deviceCompatibility.js +119 -0
- package/dist/workstation/update/deviceCoordinator.js +369 -0
- package/dist/workstation/update/deviceHealth.js +104 -0
- package/dist/workstation/update/deviceHealthProtocol.js +26 -0
- package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
- package/dist/workstation/update/deviceRollback.js +78 -0
- package/dist/workstation/update/globalInstall.js +186 -0
- package/dist/workstation/update/legacyBridge.js +414 -0
- package/dist/workstation/update/legacyOwner.js +39 -0
- package/dist/workstation/update/npmInstaller.js +51 -25
- package/dist/workstation/update/privateDurability.js +61 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/recoveryPlan.js +124 -0
- package/dist/workstation/update/runUpdate.js +10 -3
- package/dist/workstation/update/taskRegistry.js +161 -3
- package/dist/workstation/update/watchdog.js +29806 -333
- package/dist/workstation/update/watchdogProcess.js +72 -0
- package/package.json +11 -7
- package/skills/axom/SKILL.md +11 -11
- package/skills/axom-definition/SKILL.md +3 -3
- package/skills/axom-platform/SKILL.md +3 -3
- package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
- package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
- package/dist/axom-patch/cli/qoder-steering.json +0 -52
- package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
- package/dist/commands/guide.js +0 -38
- package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
- package/dist/runtime-deps/qoder-sdk/package.json +0 -8
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/processor/sideChannel.js +0 -80
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
- package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
- package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
- package/dist/workstation/qoderAdapter/history.js +0 -85
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -95
- package/dist/workstation/qoderAdapter/mapper.js +0 -117
- package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
- package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
- package/dist/workstation/qoderAdapter/permissions.js +0 -61
- package/dist/workstation/qoderAdapter/profile.js +0 -93
- package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
- package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
- package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -85
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
|
@@ -97,23 +97,27 @@ export async function gcBundles(options) {
|
|
|
97
97
|
const keep = new Set(options.keep ?? []);
|
|
98
98
|
const removed = [];
|
|
99
99
|
for (const kind of KEEPER_KINDS) {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
100
|
+
const kindRoot = path.join(path.resolve(options.configDir), 'keepers', kind);
|
|
101
|
+
const entries = await fs.readdir(kindRoot, { withFileTypes: true }).catch(() => []);
|
|
102
|
+
const roots = [kindRoot, ...entries.filter(entry => entry.isDirectory() && /^[0-9a-f]{32}$/.test(entry.name)).map(entry => path.join(kindRoot, entry.name))];
|
|
103
|
+
for (const root of roots) {
|
|
104
|
+
const referenced = new Set(keep);
|
|
105
|
+
for (const gen of await fs.readdir(path.join(root, 'run')).catch(() => [])) {
|
|
106
|
+
const manifest = await readPidManifest(path.join(root, 'run', gen, 'keeper.pid.json')).catch(() => null);
|
|
107
|
+
if (manifest && await manifestProcessLive(manifest))
|
|
108
|
+
referenced.add(manifest.buildVersion);
|
|
109
|
+
}
|
|
110
|
+
const bundles = path.join(root, 'bundles');
|
|
111
|
+
for (const name of await fs.readdir(bundles).catch(() => [])) {
|
|
112
|
+
if (referenced.has(name))
|
|
113
|
+
continue;
|
|
114
|
+
const dir = path.join(bundles, name);
|
|
115
|
+
const stat = await fs.lstat(dir).catch(() => null);
|
|
116
|
+
if (!stat || (minAge > 0 && now - Math.max(stat.mtimeMs, stat.ctimeMs) < minAge))
|
|
117
|
+
continue;
|
|
118
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
119
|
+
removed.push(dir);
|
|
120
|
+
}
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
return removed;
|
|
@@ -24,7 +24,7 @@ export class KeeperRequestTimeoutError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
export class KeeperNotConnectedError extends Error {
|
|
26
26
|
code = 'not_connected';
|
|
27
|
-
constructor(message = 'Keeper is not connected') { super(message); this.name = 'KeeperNotConnectedError'; }
|
|
27
|
+
constructor(message = 'Keeper is not connected', options) { super(message, options); this.name = 'KeeperNotConnectedError'; }
|
|
28
28
|
}
|
|
29
29
|
export class KeeperClosedError extends Error {
|
|
30
30
|
code = 'closed';
|
|
@@ -154,7 +154,7 @@ function connectLink(socketPath, limits, timeoutMs, onEvt, onClose) {
|
|
|
154
154
|
const timer = setTimeout(() => fail(new KeeperNotConnectedError('Timed out connecting to the keeper')), timeoutMs);
|
|
155
155
|
const fail = (error) => { if (settled)
|
|
156
156
|
return; settled = true; clearTimeout(timer); socket.destroy(); reject(error); };
|
|
157
|
-
socket.once('error', (error) => fail(new KeeperNotConnectedError(error.message)));
|
|
157
|
+
socket.once('error', (error) => fail(new KeeperNotConnectedError(error.message, { cause: error })));
|
|
158
158
|
socket.once('connect', () => {
|
|
159
159
|
if (settled)
|
|
160
160
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Daemon-side: starts, probes and stops keepers. Exempt from the keeper-side import guard.
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
|
+
import { constants, openSync, fstatSync, readFileSync, closeSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { keeperOneShot, readKeeperToken } from './client.js';
|
|
@@ -9,6 +10,7 @@ import { keeperGen } from './identity.js';
|
|
|
9
10
|
import { encodeLaunchConfig, launchConfigOf } from './launchConfig.js';
|
|
10
11
|
import { acquireStartupLock, liveManifest, manifestProcessLive, pidAlive, processCommand, readPidManifest, releaseStartupLock } from './lock.js';
|
|
11
12
|
import { bundleDirFor, ensureKeeperDirectories, keeperPaths, touchSocket } from './paths.js';
|
|
13
|
+
import { assertOwnedDirectory, assertRegistrationOwner, legacyOwnershipAllowed, legacySourcesOf, readOwnerFile, RegistrationOwnershipError } from '../../runtime/ownership.js';
|
|
12
14
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
15
|
/** `keeper.ping` on a fresh connection. Null unless the answer is from this exact keeper generation. */
|
|
14
16
|
export async function pingKeeper(paths, timeoutMs = 250) {
|
|
@@ -46,20 +48,122 @@ export class KeeperLauncher {
|
|
|
46
48
|
constructor(options) {
|
|
47
49
|
this.options = options;
|
|
48
50
|
}
|
|
51
|
+
get ownership() { return this.options.ownership; }
|
|
52
|
+
assertOwnership(kind, gen = 'ownership') { this.pathsFor(kind, gen); }
|
|
53
|
+
allowsLegacyOwnership(kind, gen) {
|
|
54
|
+
const sources = legacySourcesOf(this.ownership);
|
|
55
|
+
return !!sources && this.options.server === sources.keeperServer && path.resolve(this.options.configDir) === path.resolve(sources.adapterCacheDirectory) && legacyOwnershipAllowed(this.ownership, this.pathsFor(kind, gen).root, kind);
|
|
56
|
+
}
|
|
57
|
+
assertSessionOwnership(label, gen) {
|
|
58
|
+
assertRegistrationOwner(label, this.ownership, this.allowsLegacyOwnership('acp', gen) ? { directory: this.pathsFor('acp', gen).root, kind: 'acp' } : undefined);
|
|
59
|
+
}
|
|
60
|
+
assertResourceOwnership(kind, gen, resource, id, create = false) {
|
|
61
|
+
if (!this.ownership)
|
|
62
|
+
return;
|
|
63
|
+
const paths = this.pathsFor(kind, gen);
|
|
64
|
+
const directory = path.join(paths.dataDir, 'registration-owners', resource);
|
|
65
|
+
const file = path.join(directory, `${encodeURIComponent(id)}.json`);
|
|
66
|
+
try {
|
|
67
|
+
assertRegistrationOwner(readOwnerFile(file), this.ownership);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code !== 'ENOENT')
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
if (!create && !this.allowsLegacyOwnership(kind, gen))
|
|
75
|
+
throw new RegistrationOwnershipError('Registration resource owner is unknown; state quarantined');
|
|
76
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
77
|
+
try {
|
|
78
|
+
writeFileSync(file, JSON.stringify({ schemaVersion: 1, registrationId: this.ownership.registrationId }), { mode: 0o600, flag: 'wx' });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code !== 'EEXIST')
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
assertRegistrationOwner(readOwnerFile(file), this.ownership);
|
|
85
|
+
}
|
|
86
|
+
async assertKeeperResources(kind, gen, timeoutMs = 1000) {
|
|
87
|
+
if (!this.ownership)
|
|
88
|
+
return;
|
|
89
|
+
const paths = this.pathsFor(kind, gen);
|
|
90
|
+
const token = await readKeeperToken(paths.tokenFile);
|
|
91
|
+
if (kind === 'acp') {
|
|
92
|
+
const listed = await keeperOneShot(paths.socket, 'acp.list', {}, timeoutMs, { kind, token });
|
|
93
|
+
if (!Array.isArray(listed.sessions))
|
|
94
|
+
throw new RegistrationOwnershipError('Registration session ownership needs verification');
|
|
95
|
+
for (const session of listed.sessions)
|
|
96
|
+
this.assertSessionOwnership(session.label, gen);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const ptys = await keeperOneShot(paths.socket, 'pty.list', {}, timeoutMs, { kind, token });
|
|
100
|
+
const execs = await keeperOneShot(paths.socket, 'exec.list', {}, timeoutMs, { kind, token });
|
|
101
|
+
if (!Array.isArray(ptys.sessions) || !Array.isArray(execs.execs))
|
|
102
|
+
throw new RegistrationOwnershipError('Registration shell ownership needs verification');
|
|
103
|
+
for (const session of ptys.sessions)
|
|
104
|
+
this.assertResourceOwnership(kind, gen, 'pty', session.id);
|
|
105
|
+
for (const exec of execs.execs)
|
|
106
|
+
this.assertResourceOwnership(kind, gen, 'exec', exec.execId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
49
109
|
get distDir() { return this.options.distDir ?? defaultDistDir(); }
|
|
50
110
|
probe(paths, timeoutMs) { return this.options.probe ? this.options.probe(paths) : pingKeeper(paths, timeoutMs); }
|
|
51
111
|
/** Paths of generation `gen` of `kind` (no build needed). */
|
|
52
|
-
pathsFor(kind, gen) {
|
|
112
|
+
pathsFor(kind, gen) {
|
|
113
|
+
const scoped = keeperPaths(this.options, kind, gen, { fallbackRoot: this.options.fallbackRoot });
|
|
114
|
+
assertOwnedDirectory(scoped.root, this.ownership, kind);
|
|
115
|
+
try {
|
|
116
|
+
lstatSync(scoped.pidFile);
|
|
117
|
+
return scoped;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.code !== 'ENOENT')
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const legacy = keeperPaths(this.options, kind, gen, { legacyLayout: true });
|
|
124
|
+
let descriptor;
|
|
125
|
+
try {
|
|
126
|
+
descriptor = openSync(legacy.pidFile, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
127
|
+
const stat = fstatSync(descriptor);
|
|
128
|
+
if (!stat.isFile() || stat.size > 4096 || process.getuid && stat.uid !== process.getuid())
|
|
129
|
+
throw new Error('Unsafe keeper PID manifest');
|
|
130
|
+
const manifest = JSON.parse(readFileSync(descriptor, 'utf8'));
|
|
131
|
+
if (manifest.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen) {
|
|
132
|
+
assertOwnedDirectory(legacy.root, this.ownership, kind);
|
|
133
|
+
return legacy;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error.code !== 'ENOENT') {
|
|
138
|
+
if (this.ownership && !(error instanceof RegistrationOwnershipError))
|
|
139
|
+
throw new RegistrationOwnershipError('Keeper registration ownership could not be verified; state quarantined', error);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (descriptor !== undefined)
|
|
145
|
+
closeSync(descriptor);
|
|
146
|
+
}
|
|
147
|
+
return scoped;
|
|
148
|
+
}
|
|
53
149
|
/** The generation the current build of `kind` runs as (`dist/keepers/<kind>/manifest.json`); starts nothing. */
|
|
54
150
|
async currentGen(kind) {
|
|
55
151
|
return keeperGen((await readKeeperManifest(path.join(this.distDir, kind))).buildVersion);
|
|
56
152
|
}
|
|
57
153
|
/** Generations of `kind` whose pid manifest names a live keeper of that generation (`<root>/run/<gen>`); starts nothing. */
|
|
58
154
|
async liveGenerations(kind) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
155
|
+
const scoped = keeperPaths(this.options, kind, 'probe');
|
|
156
|
+
const legacy = keeperPaths(this.options, kind, 'probe', { legacyLayout: true });
|
|
157
|
+
const roots = [scoped.root, legacy.root];
|
|
158
|
+
const gens = new Set();
|
|
159
|
+
for (const root of roots)
|
|
160
|
+
for (const gen of await fs.readdir(path.join(root, 'run')).catch(() => [])) {
|
|
161
|
+
const manifest = await readPidManifest(path.join(root, 'run', gen, 'keeper.pid.json')).catch(() => null);
|
|
162
|
+
if (manifest?.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen)
|
|
163
|
+
gens.add(gen);
|
|
164
|
+
}
|
|
61
165
|
const live = [];
|
|
62
|
-
for (const gen of gens.sort())
|
|
166
|
+
for (const gen of [...gens].sort())
|
|
63
167
|
if (await liveManifest(this.pathsFor(kind, gen)).catch(() => false))
|
|
64
168
|
live.push(gen);
|
|
65
169
|
return live;
|
|
@@ -76,7 +180,7 @@ export class KeeperLauncher {
|
|
|
76
180
|
async start(kind) {
|
|
77
181
|
const deadline = Date.now() + (this.options.startTimeoutMs ?? 7500);
|
|
78
182
|
const manifest = await readKeeperManifest(path.join(this.distDir, kind));
|
|
79
|
-
const paths =
|
|
183
|
+
const paths = this.pathsFor(kind, keeperGen(manifest.buildVersion));
|
|
80
184
|
await ensureKeeperDirectories(paths);
|
|
81
185
|
// A keeper that already answers needs no snapshot check: it runs from the snapshot it was started with.
|
|
82
186
|
const alive = await this.probe(paths);
|
|
@@ -157,7 +261,10 @@ export class KeeperLauncher {
|
|
|
157
261
|
// `detached` is what makes it survive; do not add launchd keys or drop it.
|
|
158
262
|
const child = spawn(nodePath, [script, config], { detached: true, stdio: 'ignore' });
|
|
159
263
|
child.on('error', () => { });
|
|
264
|
+
this.options.onNativeAcquired?.(child, paths);
|
|
160
265
|
child.unref();
|
|
266
|
+
if (child.pid)
|
|
267
|
+
await this.options.onNativeSpawn?.(child.pid, paths, child);
|
|
161
268
|
}
|
|
162
269
|
/**
|
|
163
270
|
* Spec §3.1: a socket that fell back to `/tmp` (its configDir path is too long for sockaddr_un) is touched for every generation
|
|
@@ -165,9 +272,10 @@ export class KeeperLauncher {
|
|
|
165
272
|
* socket. Sockets under configDir are left alone. Returns how many were touched; never throws.
|
|
166
273
|
*/
|
|
167
274
|
async touchSockets(kind) {
|
|
168
|
-
const
|
|
275
|
+
const root = keeperPaths(this.options, kind, 'probe').root;
|
|
276
|
+
const gens = new Set([...await fs.readdir(path.join(root, 'run')).catch(() => []), ...await this.liveGenerations(kind)]);
|
|
169
277
|
let touched = 0;
|
|
170
|
-
for (const gen of
|
|
278
|
+
for (const gen of gens) {
|
|
171
279
|
const paths = this.pathsFor(kind, gen);
|
|
172
280
|
if (paths.socketFallback && await touchSocket(paths).catch(() => false))
|
|
173
281
|
touched += 1;
|
|
@@ -204,6 +312,14 @@ export class KeeperLauncher {
|
|
|
204
312
|
}
|
|
205
313
|
return true;
|
|
206
314
|
};
|
|
315
|
+
if (this.ownership) {
|
|
316
|
+
try {
|
|
317
|
+
await this.assertKeeperResources(kind, gen);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return { stopped: false, pid, reason: 'Registration resource ownership needs verification' };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
207
323
|
// keeper.stop needs the connection token (the daemon owns the run directory it lives in). Without a usable token
|
|
208
324
|
// the keeper cannot be asked, so after the same identity check the force path uses, SIGTERM it (its handler is the graceful stop).
|
|
209
325
|
let reason;
|
|
@@ -16,7 +16,8 @@ export function keeperLockPort(identity, gen) {
|
|
|
16
16
|
}
|
|
17
17
|
export function keeperPaths(input, kind, gen, options = {}) {
|
|
18
18
|
const identity = keeperIdentity(input, kind);
|
|
19
|
-
const
|
|
19
|
+
const kindRoot = path.join(path.resolve(input.configDir), 'keepers', kind);
|
|
20
|
+
const root = options.legacyLayout ? kindRoot : path.join(kindRoot, identity);
|
|
20
21
|
const runDir = path.join(root, 'run', gen);
|
|
21
22
|
let socket = path.join(runDir, 'keeper.sock');
|
|
22
23
|
let socketDir = runDir;
|
|
@@ -9,8 +9,19 @@
|
|
|
9
9
|
import { execFile, spawn } from 'node:child_process';
|
|
10
10
|
import { promises as fs } from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
14
|
+
import { keeperRpc } from '../tools/client.js';
|
|
13
15
|
import { pidAlive } from './kit/lock.js';
|
|
16
|
+
import { legacySourcesOf } from '../runtime/ownership.js';
|
|
17
|
+
export function legacyKeeperPaths(options) {
|
|
18
|
+
const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server])).digest('hex').slice(0, 32);
|
|
19
|
+
const namespace = `axom-tools-${process.getuid?.() ?? 'user'}`;
|
|
20
|
+
let dir = path.join(process.platform === 'darwin' ? '/tmp' : os.tmpdir(), namespace, identity);
|
|
21
|
+
if (Buffer.byteLength(path.join(dir, 'keeper.sock')) > 100)
|
|
22
|
+
dir = path.join('/tmp', namespace, identity);
|
|
23
|
+
return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: 20000 + Number.parseInt(identity.slice(0, 8), 16) % 40000, unit: `axom-tools-keeper-${identity}` };
|
|
24
|
+
}
|
|
14
25
|
/** A legacy keeper with no open PTY is stopped once nothing was routed to it for this long (the user may still read an exited one). */
|
|
15
26
|
export const LEGACY_IDLE_MS = 10 * 60_000;
|
|
16
27
|
/**
|
|
@@ -57,9 +68,12 @@ export class LegacyToolsKeeper {
|
|
|
57
68
|
rpc;
|
|
58
69
|
constructor(options) {
|
|
59
70
|
this.options = options;
|
|
60
|
-
this.paths = options.paths ??
|
|
71
|
+
this.paths = options.paths ?? legacyKeeperPaths(options.tools);
|
|
61
72
|
this.now = options.now ?? Date.now;
|
|
62
73
|
this.rpc = options.rpc ?? keeperRpc;
|
|
74
|
+
const sources = legacySourcesOf(options.ownership);
|
|
75
|
+
if (options.ownership && (!sources || path.resolve(options.tools.configDir) !== path.resolve(sources.adapterCacheDirectory) || options.tools.server !== sources.keeperServer))
|
|
76
|
+
this.current = 'absent';
|
|
63
77
|
}
|
|
64
78
|
get state() { return this.current; }
|
|
65
79
|
/** Present or busy: it may hold sessions, so the router asks it. */
|
|
@@ -19,8 +19,6 @@
|
|
|
19
19
|
// older one gets a persistent intent to move there when idle (`moveOffOlderKeeperGenerations`, Task 24); an older
|
|
20
20
|
// generation that hosts no adapter any more is stopped (`retireIdleGenerations`). Never for `available` unless asked.
|
|
21
21
|
// replace-now stops the older generations at once (their in-flight turns end; the sessions reopen through session/load).
|
|
22
|
-
// legacy the legacy tools keeper (spec §9) retires by itself (keepers/legacy.ts); it is reported `required`/`when-idle`, and
|
|
23
|
-
// replace-now stops it at once.
|
|
24
22
|
import { promises as fs } from 'node:fs';
|
|
25
23
|
import path from 'node:path';
|
|
26
24
|
import { keeperOneShot, readKeeperToken } from './kit/client.js';
|
|
@@ -268,24 +266,8 @@ export class KeeperPolicy {
|
|
|
268
266
|
this.log(`[keepers] keeper-terminal could not be described: ${error instanceof Error ? error.message : String(error)}`);
|
|
269
267
|
}
|
|
270
268
|
}
|
|
271
|
-
const legacy = this.options.legacy;
|
|
272
|
-
if (legacy?.live)
|
|
273
|
-
out.push({ live: true, current: false, status: this.legacyStatus(legacy) });
|
|
274
269
|
return out;
|
|
275
270
|
}
|
|
276
|
-
legacyStatus(legacy) {
|
|
277
|
-
const { state, open } = legacy.status();
|
|
278
|
-
const shell = nameLabel(this.options.legacyShell?.(), 'shell');
|
|
279
|
-
const sessions = legacy.lastSessions();
|
|
280
|
-
return {
|
|
281
|
-
kind: 'terminal', gen: 'legacy', legacy: true, state: state === 'present' || state === 'stopping' ? 'running' : 'unavailable', // busy = not answering
|
|
282
|
-
protocolVersion: 0, buildVersion: 'legacy', replacement: 'required', pending: 'when-idle',
|
|
283
|
-
metrics: { ptyOpen: open ?? sessions.filter((session) => !session.exited).length, ptyActive: 0, execRunning: 0 },
|
|
284
|
-
items: sessions.slice(0, KEEPER_STATUS_MAX_ITEMS).map((session) => ({
|
|
285
|
-
type: 'pty', id: clip(session.id, 256), label: shell, state: session.exited ? 'exited' : 'idle', sinceEpochMs: 0,
|
|
286
|
-
})),
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
271
|
async collectAcp() {
|
|
290
272
|
const acp = this.options.acp;
|
|
291
273
|
if (!acp)
|
|
@@ -351,7 +333,7 @@ export class KeeperPolicy {
|
|
|
351
333
|
/** required/mandatory, never decided before (a cancel or a restart never brings an automatic decision back): queue it. */
|
|
352
334
|
async automatic(sampled) {
|
|
353
335
|
for (const { status, live, current } of sampled) {
|
|
354
|
-
if (
|
|
336
|
+
if (current || !live || !replacesByItself(status.replacement))
|
|
355
337
|
continue;
|
|
356
338
|
if (this.decisions.has(decisionKey(status.kind, status.gen)))
|
|
357
339
|
continue;
|
|
@@ -375,7 +357,7 @@ export class KeeperPolicy {
|
|
|
375
357
|
async runQueued(sampled) {
|
|
376
358
|
const acpPending = [];
|
|
377
359
|
for (const { status, live, current } of sampled) {
|
|
378
|
-
if (
|
|
360
|
+
if (current || this.pendingOf(status.kind, status.gen) !== 'when-idle')
|
|
379
361
|
continue;
|
|
380
362
|
if (status.kind === 'acp') {
|
|
381
363
|
if (live)
|
|
@@ -465,8 +447,7 @@ export class KeeperPolicy {
|
|
|
465
447
|
}
|
|
466
448
|
async apply(frame) {
|
|
467
449
|
const sampled = await this.sampled(true);
|
|
468
|
-
const older = sampled.filter((entry) => entry.status.kind === frame.kind && !entry.current && entry.live
|
|
469
|
-
const legacy = frame.kind === 'terminal' ? sampled.find((entry) => entry.status.legacy) : undefined;
|
|
450
|
+
const older = sampled.filter((entry) => entry.status.kind === frame.kind && !entry.current && entry.live);
|
|
470
451
|
const affected = [];
|
|
471
452
|
let ok = true;
|
|
472
453
|
let incomplete = false;
|
|
@@ -502,15 +483,6 @@ export class KeeperPolicy {
|
|
|
502
483
|
error = `generation ${status.gen} could not be stopped`;
|
|
503
484
|
}
|
|
504
485
|
}
|
|
505
|
-
if (legacy && this.options.legacy) {
|
|
506
|
-
const shell = nameLabel(this.options.legacyShell?.(), 'shell');
|
|
507
|
-
for (const session of await this.options.legacy.replaceNow())
|
|
508
|
-
affected.push({ type: 'pty', id: session.id, label: shell, gen: 'legacy' });
|
|
509
|
-
if (this.options.legacy.live) {
|
|
510
|
-
ok = false;
|
|
511
|
-
error = 'the legacy tools keeper could not be stopped';
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
486
|
break;
|
|
515
487
|
}
|
|
516
488
|
case 'cancel': {
|
|
@@ -525,9 +497,8 @@ export class KeeperPolicy {
|
|
|
525
497
|
}
|
|
526
498
|
}
|
|
527
499
|
this.sample = undefined;
|
|
528
|
-
//
|
|
529
|
-
const queued = [...this.decisions].some(([key, decision]) => key.startsWith(`${frame.kind}:`) && decision.mode === 'when-idle')
|
|
530
|
-
|| (frame.kind === 'terminal' && this.options.legacy?.live === true);
|
|
500
|
+
// Read decisions directly: an in-flight sample may predate them.
|
|
501
|
+
const queued = [...this.decisions].some(([key, decision]) => key.startsWith(`${frame.kind}:`) && decision.mode === 'when-idle');
|
|
531
502
|
if (affected.length > KEEPER_AFFECTED_MAX_ITEMS)
|
|
532
503
|
incomplete = true;
|
|
533
504
|
return {
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { readKeeperManifest } from '../kit/bundle.js';
|
|
6
|
-
import { KeeperClient } from '../kit/client.js';
|
|
6
|
+
import { KeeperClient, KeeperNotConnectedError, KeeperRequestError } from '../kit/client.js';
|
|
7
|
+
import { liveManifest } from '../kit/lock.js';
|
|
7
8
|
import { keeperGen } from '../kit/identity.js';
|
|
8
9
|
import { defaultDistDir } from '../kit/launcher.js';
|
|
9
10
|
import { protocolWindow } from '../kit/protocol.js';
|
|
11
|
+
import { RegistrationOwnershipError } from '../../runtime/ownership.js';
|
|
10
12
|
import { PTY_CAPABILITY, PTY_UNAVAILABLE_ERROR, TERMINAL_PROTOCOL_VERSION, execStream, } from './protocol.js';
|
|
13
|
+
import { TERMINAL_GRID_LIMITS } from './gridProtocol.js';
|
|
11
14
|
/** keeper-terminal could not be reached (launch, connect or hello failed): the request was never sent. */
|
|
12
15
|
export class KeeperLaunchError extends Error {
|
|
13
16
|
cause;
|
|
@@ -27,7 +30,12 @@ export function terminalStatusOf(hello) {
|
|
|
27
30
|
const NOT_RUNNING_TTL_MS = 5000;
|
|
28
31
|
/** The default bound of {@link TerminalKeeperClient.probe}: a real connect and hello, not a quick peek. */
|
|
29
32
|
export const KEEPER_PROBE_TIMEOUT_MS = 3000;
|
|
30
|
-
const errnoOf = (error) =>
|
|
33
|
+
const errnoOf = (error) => {
|
|
34
|
+
// The kit keeps its public not_connected code; classify only the underlying socket errno.
|
|
35
|
+
const source = error instanceof KeeperNotConnectedError ? error.cause : error;
|
|
36
|
+
return typeof source === 'object' && source !== null && typeof source.code === 'string'
|
|
37
|
+
? source.code : undefined;
|
|
38
|
+
};
|
|
31
39
|
/** Connect errors that prove nothing listens: the socket file is gone, or a stale one with no listener behind it. */
|
|
32
40
|
const ABSENT_ERRNOS = new Set(['ENOENT', 'ECONNREFUSED']);
|
|
33
41
|
const messageOf = (error) => (error instanceof Error ? error.message : String(error));
|
|
@@ -48,7 +56,7 @@ export class TerminalKeeperClient {
|
|
|
48
56
|
}
|
|
49
57
|
get daemonProtocol() { return this.options.daemonProtocol ?? protocolWindow(TERMINAL_PROTOCOL_VERSION); }
|
|
50
58
|
clientFor(paths) {
|
|
51
|
-
if (this.client && this.clientGen === paths.gen)
|
|
59
|
+
if (this.client && this.client.state !== 'closed' && this.clientGen === paths.gen)
|
|
52
60
|
return this.client;
|
|
53
61
|
this.client?.close();
|
|
54
62
|
this.client = new KeeperClient({
|
|
@@ -70,6 +78,7 @@ export class TerminalKeeperClient {
|
|
|
70
78
|
* most one keeper per kind at a time. A `gen`-bound client only connects to its generation.
|
|
71
79
|
*/
|
|
72
80
|
async ensure() {
|
|
81
|
+
this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
|
|
73
82
|
const current = this.client;
|
|
74
83
|
if (this.options.gen !== undefined) {
|
|
75
84
|
if (current && current.state === 'ready')
|
|
@@ -123,24 +132,41 @@ export class TerminalKeeperClient {
|
|
|
123
132
|
return probing;
|
|
124
133
|
}
|
|
125
134
|
async probeOnce(timeoutMs) {
|
|
135
|
+
try {
|
|
136
|
+
this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { state: 'unknown', reason: 'Registration owner needs verification' };
|
|
140
|
+
}
|
|
126
141
|
const absent = () => { this.notRunningUntil = Date.now() + NOT_RUNNING_TTL_MS; return { state: 'absent' }; };
|
|
127
142
|
let gen;
|
|
128
143
|
try {
|
|
129
|
-
gen = this.options.gen
|
|
144
|
+
gen = this.options.gen ?? this.currentGen
|
|
130
145
|
?? keeperGen((await readKeeperManifest(path.join(this.options.distDir ?? defaultDistDir(), 'terminal'))).buildVersion);
|
|
131
146
|
}
|
|
132
|
-
catch {
|
|
133
|
-
return absent();
|
|
134
|
-
} //
|
|
147
|
+
catch (error) {
|
|
148
|
+
return errnoOf(error) === 'ENOENT' ? absent() : { state: 'unknown', reason: messageOf(error) };
|
|
149
|
+
} // A missing build is absent; unreadable or invalid metadata is inconclusive.
|
|
135
150
|
const paths = this.options.launcher.pathsFor('terminal', gen);
|
|
136
151
|
if (this.options.gen === undefined)
|
|
137
152
|
this.currentGen = paths.gen;
|
|
153
|
+
// A live owner may be initializing, busy, or have lost its socket. None proves its sessions are gone.
|
|
154
|
+
const unreachable = async () => {
|
|
155
|
+
try {
|
|
156
|
+
if (await liveManifest(paths))
|
|
157
|
+
return { state: 'unknown', reason: 'live keeper owner is unreachable' };
|
|
158
|
+
return absent();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
return { state: 'unknown', reason: messageOf(error) };
|
|
162
|
+
}
|
|
163
|
+
};
|
|
138
164
|
try {
|
|
139
165
|
await fs.access(paths.tokenFile);
|
|
140
166
|
await fs.access(paths.socket);
|
|
141
167
|
}
|
|
142
168
|
catch (error) {
|
|
143
|
-
return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ?
|
|
169
|
+
return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ? unreachable() : { state: 'unknown', reason: messageOf(error) };
|
|
144
170
|
}
|
|
145
171
|
const client = this.clientFor(paths);
|
|
146
172
|
let timer;
|
|
@@ -159,41 +185,105 @@ export class TerminalKeeperClient {
|
|
|
159
185
|
return { state: 'unknown', reason: `protocol ${result.hello.protocolVersion} is outside the daemon's window` };
|
|
160
186
|
}
|
|
161
187
|
catch (error) {
|
|
162
|
-
//
|
|
163
|
-
return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ?
|
|
188
|
+
// Missing endpoints prove absence only when there is no verified live owner.
|
|
189
|
+
return ABSENT_ERRNOS.has(errnoOf(error) ?? '') ? unreachable() : { state: 'unknown', reason: messageOf(error) };
|
|
164
190
|
}
|
|
165
191
|
finally {
|
|
166
192
|
clearTimeout(timer);
|
|
167
193
|
}
|
|
168
194
|
}
|
|
169
|
-
/** `tools.info` terminal fields from a running keeper's hello, else from
|
|
195
|
+
/** `tools.info` terminal fields from a running keeper's hello, else from the caller's unavailable status. Starts nothing. */
|
|
170
196
|
async terminalStatus(fallback) {
|
|
171
197
|
const hello = await this.runningHello();
|
|
172
198
|
return hello ? terminalStatusOf(hello) : fallback();
|
|
173
199
|
}
|
|
174
200
|
/** A failure to reach the keeper (launch, connect, hello) surfaces as {@link KeeperLaunchError}: nothing was sent. */
|
|
175
201
|
async call(method, params, options = {}) {
|
|
202
|
+
this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
|
|
176
203
|
let client;
|
|
177
204
|
try {
|
|
178
205
|
client = await this.ensure();
|
|
179
206
|
}
|
|
180
207
|
catch (error) {
|
|
208
|
+
if (error instanceof RegistrationOwnershipError)
|
|
209
|
+
throw error;
|
|
181
210
|
throw new KeeperLaunchError(error);
|
|
182
211
|
}
|
|
183
|
-
|
|
212
|
+
const input = params;
|
|
213
|
+
const resource = method.startsWith('pty.') ? 'pty' : 'exec';
|
|
214
|
+
const id = resource === 'pty' ? input.id : input.execId;
|
|
215
|
+
if (this.options.launcher.ownership && typeof id === 'string' && method !== 'exec.status') {
|
|
216
|
+
let create = false;
|
|
217
|
+
if (method === 'pty.create')
|
|
218
|
+
create = !(await client.request('pty.list', {})).sessions.some(session => session.id === id);
|
|
219
|
+
if (method === 'exec.start')
|
|
220
|
+
create = !(await client.request('exec.list', {})).execs.some(exec => exec.execId === id);
|
|
221
|
+
this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, resource, id, create);
|
|
222
|
+
}
|
|
223
|
+
const result = await client.request(method, input, options);
|
|
224
|
+
if (this.options.launcher.ownership && typeof id === 'string' && method === 'exec.status') {
|
|
225
|
+
this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, resource, id);
|
|
226
|
+
}
|
|
227
|
+
if (this.options.launcher.ownership && (method === 'pty.list' || method === 'exec.list')) {
|
|
228
|
+
const registrationId = this.options.launcher.ownership.registrationId;
|
|
229
|
+
if (method === 'pty.list')
|
|
230
|
+
result.sessions = result.sessions.filter(session => !session.registrationId || session.registrationId === registrationId);
|
|
231
|
+
else
|
|
232
|
+
result.execs = result.execs.filter(exec => !exec.registrationId || exec.registrationId === registrationId);
|
|
233
|
+
const ids = method === 'pty.list' ? result.sessions.map(session => session.id) : result.execs.map(exec => exec.execId);
|
|
234
|
+
for (const current of ids)
|
|
235
|
+
this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, resource, current);
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
/** Connect only to existing terminal work. Never launch or retry a possibly delivered event. */
|
|
240
|
+
async callNative(method, params, absentResult) {
|
|
241
|
+
this.options.launcher.assertOwnership?.('terminal', this.clientGen ?? this.options.gen);
|
|
242
|
+
// A cached absence may predate another client's create; cleanup needs a fresh absence check.
|
|
243
|
+
const presence = this.hello ? { state: 'running' } : await this.probeOnce(KEEPER_PROBE_TIMEOUT_MS);
|
|
244
|
+
if (presence.state === 'absent') {
|
|
245
|
+
if (absentResult !== undefined)
|
|
246
|
+
return absentResult;
|
|
247
|
+
throw new KeeperRequestError('pty_not_found', 'Terminal session not found; keeper restart loses live PTYs');
|
|
248
|
+
}
|
|
249
|
+
if (presence.state === 'unknown')
|
|
250
|
+
throw new KeeperRequestError('pty_unavailable', `Terminal keeper unavailable: ${presence.reason}`);
|
|
251
|
+
const client = this.client;
|
|
252
|
+
if (!client)
|
|
253
|
+
throw new KeeperRequestError('pty_unavailable', 'Terminal keeper disconnected');
|
|
254
|
+
const input = params;
|
|
255
|
+
if (typeof input.id === 'string')
|
|
256
|
+
this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, 'pty', input.id);
|
|
257
|
+
return client.request(method, input);
|
|
258
|
+
}
|
|
259
|
+
create(params) { return this.call('pty.create', { ...params, ...(this.options.launcher.ownership ? { registrationId: this.options.launcher.ownership.registrationId } : {}) }); }
|
|
260
|
+
attach(params) { return this.callNative('pty.attach', params); }
|
|
261
|
+
poll(params) { return this.callNative('pty.poll', params); }
|
|
262
|
+
input(params) { return this.callNative('pty.input', params); }
|
|
263
|
+
resize(params) { return this.callNative('pty.resize', params); }
|
|
264
|
+
scroll(params) { return this.callNative('pty.scroll', params); }
|
|
265
|
+
detach(params) { return this.callNative('pty.detach', params, { ok: true }); }
|
|
266
|
+
close(params) { return this.callNative('pty.close', params, { ok: true }); }
|
|
267
|
+
/** Listing is a probe even if the ready socket disappears: reconnect, never launch. */
|
|
268
|
+
async list() {
|
|
269
|
+
const presence = await this.probe();
|
|
270
|
+
if (presence.state === 'absent')
|
|
271
|
+
return { schema: 1, limits: { ...TERMINAL_GRID_LIMITS }, sessions: [] };
|
|
272
|
+
if (presence.state === 'unknown')
|
|
273
|
+
throw new KeeperLaunchError(new Error(presence.reason));
|
|
274
|
+
const result = await this.client.request('pty.list', {});
|
|
275
|
+
const registrationId = this.options.launcher.ownership?.registrationId;
|
|
276
|
+
if (registrationId)
|
|
277
|
+
result.sessions = result.sessions.filter(session => !session.registrationId || session.registrationId === registrationId);
|
|
278
|
+
for (const session of result.sessions)
|
|
279
|
+
this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, 'pty', session.id);
|
|
280
|
+
return result;
|
|
184
281
|
}
|
|
185
|
-
create(params) { return this.call('pty.create', params); }
|
|
186
|
-
attach(params) { return this.call('pty.attach', params); }
|
|
187
|
-
poll(params) { return this.call('pty.poll', params); }
|
|
188
|
-
write(params) { return this.call('pty.write', params); }
|
|
189
|
-
resize(params) { return this.call('pty.resize', params); }
|
|
190
|
-
takeover(params) { return this.call('pty.takeover', params); }
|
|
191
|
-
detach(params) { return this.call('pty.detach', params); }
|
|
192
|
-
close(params) { return this.call('pty.close', params); }
|
|
193
|
-
list() { return this.call('pty.list', {}); }
|
|
194
282
|
activity() { return this.call('keeper.activity', {}); }
|
|
195
283
|
// exec.* (spec §4.2). The daemon decides argv/env/deadline/quotas; the keeper only executes them.
|
|
196
|
-
execStart(params, options = {}) {
|
|
284
|
+
execStart(params, options = {}) {
|
|
285
|
+
return this.call('exec.start', { ...params, ...(this.options.launcher.ownership ? { registrationId: this.options.launcher.ownership.registrationId } : {}) }, options);
|
|
286
|
+
}
|
|
197
287
|
execList() { return this.call('exec.list', {}); }
|
|
198
288
|
/** Descriptor plus the latest checkpoint (blob as given to `execCheckpoint`). */
|
|
199
289
|
execStatus(execId, options = {}) { return this.call('exec.status', { execId }, options); }
|
|
@@ -206,7 +296,9 @@ export class TerminalKeeperClient {
|
|
|
206
296
|
* resumes after the last record delivered.
|
|
207
297
|
*/
|
|
208
298
|
async execSubscribe(execId, handler, options) {
|
|
209
|
-
|
|
299
|
+
const client = await this.ensure();
|
|
300
|
+
this.options.launcher.assertResourceOwnership?.('terminal', this.clientGen, 'exec', execId);
|
|
301
|
+
return client.subscribe(execStream(execId), handler, options);
|
|
210
302
|
}
|
|
211
303
|
/** Drops the connection (the keeper and its PTYs keep running). */
|
|
212
304
|
disconnect() { this.client?.close(); this.client = undefined; this.clientGen = undefined; }
|