@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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { DesktopProtocolError, parseDesktopJson } from './protocol.js';
|
|
3
|
+
const sessionId = z.string().regex(/^[0-9a-f]{32}$/);
|
|
4
|
+
const generation = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
|
|
5
|
+
const secret = z.string().regex(/^[A-Za-z0-9_-]{43}$/).refine(s => Buffer.from(s, 'base64url').toString('base64url') === s);
|
|
6
|
+
const role = z.enum(['viewer_control', 'viewer_media', 'device_control', 'device_media']);
|
|
7
|
+
const quic = z.strictObject({ port: z.number().int().min(1).max(65535), certificate: z.string().min(64).max(2048).refine(s => Buffer.from(s, 'base64').toString('base64') === s) });
|
|
8
|
+
const schema = z.discriminatedUnion('type', [
|
|
9
|
+
z.strictObject({ type: z.literal('device_attach'), sessionId, grant: secret }),
|
|
10
|
+
z.strictObject({ type: z.literal('viewer_attach'), sessionId, nonce: secret }),
|
|
11
|
+
z.strictObject({ type: z.literal('attached'), sessionId, role, controlGeneration: generation, overviewToken: secret.optional(), audioToken: secret.optional() }),
|
|
12
|
+
z.strictObject({ type: z.literal('media_grant'), sessionId, controlGeneration: generation, grant: secret, quic: quic.optional() }),
|
|
13
|
+
z.strictObject({ type: z.literal('media_nonce'), sessionId, controlGeneration: generation, nonce: secret, quic: quic.optional() }),
|
|
14
|
+
]);
|
|
15
|
+
export function decodeDesktopTransport(source) {
|
|
16
|
+
try {
|
|
17
|
+
return schema.parse(parseDesktopJson(source, 4096));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new DesktopProtocolError('Invalid desktop transport exchange');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function encodeDesktopTransport(message) {
|
|
24
|
+
const text = JSON.stringify(message);
|
|
25
|
+
decodeDesktopTransport(text);
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { wallpaperAssetSchema } from './lobby.js';
|
|
3
|
+
export const WALLPAPER_MAX_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
export const WALLPAPER_MAX_JSON = 3 * 1024 * 1024;
|
|
5
|
+
export function decodeWallpaper(text) {
|
|
6
|
+
if (Buffer.byteLength(text) > WALLPAPER_MAX_JSON)
|
|
7
|
+
throw new Error('wallpaper_unavailable');
|
|
8
|
+
const result = JSON.parse(text);
|
|
9
|
+
const asset = wallpaperAssetSchema.parse(result.asset);
|
|
10
|
+
if (typeof result.image !== 'string' || result.image.length > Math.ceil(WALLPAPER_MAX_BYTES / 3) * 4)
|
|
11
|
+
throw new Error('wallpaper_unavailable');
|
|
12
|
+
const bytes = Buffer.from(result.image, 'base64');
|
|
13
|
+
if (bytes.length < 20 || bytes.length > WALLPAPER_MAX_BYTES || bytes.toString('base64') !== result.image ||
|
|
14
|
+
bytes.toString('ascii', 0, 4) !== 'RIFF' || bytes.toString('ascii', 8, 12) !== 'WEBP' || bytes.readUInt32LE(4) + 8 !== bytes.length || asset.mimeType !== 'image/webp')
|
|
15
|
+
throw new Error('wallpaper_unavailable');
|
|
16
|
+
let dimensions;
|
|
17
|
+
for (let offset = 12; offset + 8 <= bytes.length;) {
|
|
18
|
+
const kind = bytes.toString('ascii', offset, offset + 4), size = bytes.readUInt32LE(offset + 4), data = offset + 8;
|
|
19
|
+
if (size > bytes.length - data || kind === 'ANIM' || kind === 'ANMF')
|
|
20
|
+
throw new Error('wallpaper_unavailable');
|
|
21
|
+
if (kind === 'VP8X' && (size !== 10 || (bytes[data] & 2)))
|
|
22
|
+
throw new Error('wallpaper_unavailable');
|
|
23
|
+
if (kind === 'VP8 ' && size >= 10 && !(bytes[data] & 1) && bytes[data + 3] === 0x9d && bytes[data + 4] === 1 && bytes[data + 5] === 0x2a)
|
|
24
|
+
dimensions = [bytes.readUInt16LE(data + 6) & 0x3fff, bytes.readUInt16LE(data + 8) & 0x3fff];
|
|
25
|
+
if (kind === 'VP8L' && size >= 5 && bytes[data] === 0x2f) {
|
|
26
|
+
const bits = bytes.readUInt32LE(data + 1);
|
|
27
|
+
dimensions = [(bits & 0x3fff) + 1, ((bits >>> 14) & 0x3fff) + 1];
|
|
28
|
+
}
|
|
29
|
+
offset = data + size + (size & 1);
|
|
30
|
+
}
|
|
31
|
+
if (!dimensions || dimensions[0] !== asset.width || dimensions[1] !== asset.height)
|
|
32
|
+
throw new Error('wallpaper_unavailable');
|
|
33
|
+
const version = createHash('sha256').update(JSON.stringify(asset)).update(bytes).digest('hex');
|
|
34
|
+
return { asset: { ...asset, version }, bytes };
|
|
35
|
+
}
|
|
36
|
+
export class XfceWallpaperReader {
|
|
37
|
+
execute;
|
|
38
|
+
constructor(execute) {
|
|
39
|
+
this.execute = execute;
|
|
40
|
+
}
|
|
41
|
+
async discover() {
|
|
42
|
+
const workspaceValue = await this.execute('xprop', ['-root', '_NET_CURRENT_DESKTOP']);
|
|
43
|
+
let workspace = /_NET_CURRENT_DESKTOP\(CARDINAL\)\s*=\s*(\d+)/.exec(workspaceValue)?.[1];
|
|
44
|
+
if (workspace == null || Number(workspace) > 1024)
|
|
45
|
+
throw new Error('wallpaper_unavailable');
|
|
46
|
+
const listed = await this.execute('xfconf-query', ['-c', 'xfce4-desktop', '-l']);
|
|
47
|
+
const read = (property) => this.execute('xfconf-query', ['-c', 'xfce4-desktop', '-p', property]);
|
|
48
|
+
if (listed.split(/\r?\n/).some(line => line.trim() === '/backdrop/single-workspace-mode') &&
|
|
49
|
+
(await read('/backdrop/single-workspace-mode')).trim() === 'true') {
|
|
50
|
+
workspace = (await read('/backdrop/single-workspace-number')).trim();
|
|
51
|
+
if (!/^\d{1,4}$/.test(workspace) || Number(workspace) > 1024)
|
|
52
|
+
throw new Error('wallpaper_unavailable');
|
|
53
|
+
}
|
|
54
|
+
const available = listed.split(/\r?\n/).map(line => line.trim());
|
|
55
|
+
const properties = available
|
|
56
|
+
.filter(line => new RegExp(`^/backdrop/screen0/monitor[^/]+/workspace${workspace}/last-image$`).test(line));
|
|
57
|
+
let selected = properties[0];
|
|
58
|
+
let defaultImage = false;
|
|
59
|
+
if (properties.length === 0) {
|
|
60
|
+
const version = await this.execute('xfdesktop', ['--version']);
|
|
61
|
+
const monitors = await this.execute('xrandr', ['--listactivemonitors']);
|
|
62
|
+
if (!/xfdesktop version 4\.20(?:\.\d+)?\b/.test(version) || !/^Monitors:\s*1\s*$/m.test(monitors))
|
|
63
|
+
throw new Error('wallpaper_unavailable');
|
|
64
|
+
const output = /^\s*0:\s+\S+\s+\S+\s+(\S+)\s*$/m.exec(monitors)?.[1];
|
|
65
|
+
if (!output || !/^[A-Za-z0-9_.:-]+$/.test(output))
|
|
66
|
+
throw new Error('wallpaper_unavailable');
|
|
67
|
+
selected = `/backdrop/screen0/monitor${output}/workspace${workspace}/last-image`;
|
|
68
|
+
defaultImage = true;
|
|
69
|
+
}
|
|
70
|
+
if (properties.length > 1)
|
|
71
|
+
throw new Error('wallpaper_unavailable');
|
|
72
|
+
const file = defaultImage ? '/usr/share/backgrounds/xfce/xfce-x.svg' : (await read(selected)).trim();
|
|
73
|
+
if (!file.startsWith('/') || file.length > 4096 || /[\r\n\0]/.test(file))
|
|
74
|
+
throw new Error('wallpaper_unavailable');
|
|
75
|
+
const prefix = selected.slice(0, -'last-image'.length);
|
|
76
|
+
const style = available.includes(`${prefix}image-style`) ? (await read(`${prefix}image-style`)).trim() : '5';
|
|
77
|
+
const placement = { '1': 'center', '2': 'tile', '3': 'stretch', '4': 'fit', '5': 'cover' }[style];
|
|
78
|
+
if (!placement)
|
|
79
|
+
throw new Error('wallpaper_unavailable');
|
|
80
|
+
let background = '#000000';
|
|
81
|
+
let fidelity = 'exact';
|
|
82
|
+
if (placement !== 'cover' && placement !== 'stretch') {
|
|
83
|
+
try {
|
|
84
|
+
if ((await read(`${prefix}color-style`)).trim() !== '0')
|
|
85
|
+
throw new Error('unsupported_gradient');
|
|
86
|
+
const values = (await read(`${prefix}rgba1`)).split(/\r?\n/).map(line => line.trim())
|
|
87
|
+
.filter(line => /^\d+(?:\.\d+)?$/.test(line)).map(Number);
|
|
88
|
+
if (values.length !== 4 || values.some(value => value < 0 || value > 1) || values[3] !== 1)
|
|
89
|
+
throw new Error('unknown_fill');
|
|
90
|
+
background = '#' + values.slice(0, 3).map(value => Math.round(value * 255).toString(16).padStart(2, '0')).join('');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
fidelity = 'approximate';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { file, placement, background, fidelity };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { createHash, randomUUID } from 'node:crypto';
|
|
13
13
|
import { KeeperFrameTooLargeError, KeeperNotConnectedError, KeeperReplacementRequiredError, KeeperRequestError } from '../keepers/kit/client.js';
|
|
14
14
|
import { KeeperLaunchError } from '../keepers/terminal/client.js';
|
|
15
|
+
import { RegistrationOwnershipError } from '../runtime/ownership.js';
|
|
15
16
|
import { EXEC_CAPABILITY, EXEC_SIGNALS, EXEC_TERMINAL_TTL_MS, ExecErrorCode } from '../keepers/terminal/protocol.js';
|
|
16
17
|
import { execArgv, execCwd, execDeadline, execEnvironment } from './command.js';
|
|
17
18
|
import { DurableExec, EXEC_KILL_GRACE_MS } from './durable.js';
|
|
@@ -289,6 +290,8 @@ export class ExecPolicy {
|
|
|
289
290
|
hello = (await keeper.ensure()).hello;
|
|
290
291
|
}
|
|
291
292
|
catch (error) {
|
|
293
|
+
if (error instanceof RegistrationOwnershipError)
|
|
294
|
+
throw error;
|
|
292
295
|
// A repeat may be running in a keeper-terminal this daemon cannot reach right now: running it here too could run it twice.
|
|
293
296
|
if (how.repeat) {
|
|
294
297
|
this.log(`exec ${req.id}: repeated request while keeper-terminal is unreachable: ${messageOf(error)}`);
|
|
@@ -323,6 +326,8 @@ export class ExecPolicy {
|
|
|
323
326
|
return { kind: 'durable', exec: DurableExec.follow(keeper, descriptor, { outlivesChannel, ...(wire ? { wire } : {}) }, this.durableOptions()) };
|
|
324
327
|
}
|
|
325
328
|
catch (error) {
|
|
329
|
+
if (error instanceof RegistrationOwnershipError)
|
|
330
|
+
throw error;
|
|
326
331
|
// Another keeper-terminal generation owns an exec of this id (spec §3.1: two generations may coexist): it exists, so it is
|
|
327
332
|
// never started again here and never run in this process.
|
|
328
333
|
if (errorCode(error) === ExecErrorCode.foreign) {
|
|
@@ -78,6 +78,14 @@ export class FilesHostClient {
|
|
|
78
78
|
return 0;
|
|
79
79
|
return this.lastStatus.uploads + this.lastStatus.encodes;
|
|
80
80
|
}
|
|
81
|
+
/** Removal must distinguish an idle host from one that has not supplied a current activity sample. */
|
|
82
|
+
activitySnapshot() {
|
|
83
|
+
return {
|
|
84
|
+
known: this.stopped ? !this.child && !this.spawning : !!this.child && this.hostUp && !this.retiring
|
|
85
|
+
&& !!this.lastStatus && Date.now() - this.lastStatusAt <= this.timings.hungPinMs,
|
|
86
|
+
transfers: this.activity() + this.sent.size + this.waiting,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
81
89
|
/** Process id of the current host (diagnostics and tests). */
|
|
82
90
|
get pid() { return this.child?.pid; }
|
|
83
91
|
get isReady() { return this.ready; }
|
|
@@ -392,7 +400,8 @@ export class FilesHostClient {
|
|
|
392
400
|
// Past the grace, stop feeding it: new requests wait for the restarted host while the ones in flight finish.
|
|
393
401
|
if (overdue)
|
|
394
402
|
this.ready = false;
|
|
395
|
-
if (this.sent.size === 0 && (overdue ||
|
|
403
|
+
if (this.sent.size === 0 && (overdue || !!this.lastStatus
|
|
404
|
+
&& this.lastStatus.uploads === 0 && this.lastStatus.encodes === 0 && this.lastStatus.inFlight === 0)) {
|
|
396
405
|
this.restartRequestedAt = null;
|
|
397
406
|
this.log('[files-host] workspaces changed; restarting');
|
|
398
407
|
void this.shutdownChild(true);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { FileError } from './errors.js';
|
|
5
|
+
/*
|
|
6
|
+
* Credential storage the platform agent file tools (`file_*_request`, the request-root scope) never read, list or
|
|
7
|
+
* change. This is a security boundary, not a policy an approval can lift: an agent that lacks a capability must not go
|
|
8
|
+
* looking for another client's identity, a token or a key to work around it.
|
|
9
|
+
*
|
|
10
|
+
* The check runs on canonical paths (the request root after realpath, components below it already refused when they
|
|
11
|
+
* are links), and again inside the `pinned()` section on what the kernel resolves: the pinned directory's realpath and
|
|
12
|
+
* each existing leaf's realpath, which return the names stored on disk. A relative path, a symlinked root, a directory
|
|
13
|
+
* swapped for a link after validation, or a leaf spelled so that a case-insensitive file system resolves it to a store
|
|
14
|
+
* (`.git-credentialſ`, `.ßh`) cannot bypass it. Names are compared folded: NFC, then full case folding (`ſ`→`s`,
|
|
15
|
+
* `ß`→`ss`, `K`→`k`), so a missing leaf created under such a spelling is refused too.
|
|
16
|
+
*
|
|
17
|
+
* The stores are named below the process's home, below any other home-like directory (`/Users/<name>`, `/home/<name>`,
|
|
18
|
+
* `/root`, `/var/root`: a daemon whose HOME is not the workspace owner's), and below the configured client
|
|
19
|
+
* directories. The lists equal `contracts/credential-stores/credential-stores.json` (a test checks it), which the
|
|
20
|
+
* server's early refusal (axom-core `ProtectedCredentialPaths`) matches too.
|
|
21
|
+
*
|
|
22
|
+
* Only the user-facing Files surfaces (configured roots, read-only previews, the owner's file manager) are not fenced
|
|
23
|
+
* here: they act for the user at the keyboard, not for an agent.
|
|
24
|
+
*/
|
|
25
|
+
export const PROTECTED_CREDENTIAL_MESSAGE = 'This path is protected credential storage. Agents may not read, list or change it by any means. Tell the user what is needed instead of looking for another way.';
|
|
26
|
+
/** Below a home directory; each entry covers itself and everything below it; a trailing `*` matches a name prefix. */
|
|
27
|
+
export const HOME_ENTRIES = [
|
|
28
|
+
// Axomwork's own identities: the desktop/app clients, the CLI's auth stores and config.
|
|
29
|
+
'.axom/clients', '.axom/auth', '.axom/config.json', '.axom/device-id',
|
|
30
|
+
// SSH, GPG, password managers and desktop keyrings.
|
|
31
|
+
'.ssh', '.gnupg', '.password-store', '.local/share/keyrings', '.config/op',
|
|
32
|
+
// Agent CLIs' credentials and session data (their instructions and settings stay readable).
|
|
33
|
+
'.claude.json', '.claude/.credentials.json', '.claude/projects', '.claude/sessions', '.claude/shell-snapshots',
|
|
34
|
+
'.claude/history.jsonl', '.claude/backups', '.claude/session-env', '.claude/file-history', '.claude/ide',
|
|
35
|
+
'.codex/auth.json', '.codex/sessions', '.codex/history.jsonl',
|
|
36
|
+
'.acpx',
|
|
37
|
+
'.gemini/oauth_creds.json', '.gemini/jetski-standalone-oauth-token', '.gemini/antigravity*',
|
|
38
|
+
'.commandcode/auth.json', '.commandcode/projects', '.commandcode/history.jsonl', '.commandcode/file-history',
|
|
39
|
+
'.qoder/.auth', '.kimi', '.zcode',
|
|
40
|
+
// Developer, database and cloud credentials.
|
|
41
|
+
'.config/gh/hosts.yml', '.git-credentials', '.netrc', '.npmrc', '.pypirc', '.docker/config.json',
|
|
42
|
+
'.pgpass', '.my.cnf', '.vault-token', '.cargo/credentials', '.cargo/credentials.toml', '.gem/credentials',
|
|
43
|
+
'.m2/settings-security.xml', '.terraform.d/credentials.tfrc.json', '.gradle/gradle.properties',
|
|
44
|
+
'.aws', '.azure', '.kube', '.config/gcloud',
|
|
45
|
+
// macOS keychains and the Axomwork credential broker's state.
|
|
46
|
+
'Library/Keychains', 'Library/Caches/com.platless.axom-credentials',
|
|
47
|
+
];
|
|
48
|
+
/**
|
|
49
|
+
* The workstation client directory holds the binding to the server (keys, identity, provider tokens, keeper sockets,
|
|
50
|
+
* the agent browser's profiles). Its logs, caches and update state are operational and stay readable.
|
|
51
|
+
*/
|
|
52
|
+
export const WORKSTATION_ENTRIES = [
|
|
53
|
+
'client.key', 'server.pub', 'config.yml', 'binding-history.json', 'operation.json', 'desktop-provider.json',
|
|
54
|
+
'desktop-preferences.json', 'control', 'keepers', 'agents', 'files-host', 'browser-profiles',
|
|
55
|
+
];
|
|
56
|
+
export const ABSOLUTE_ENTRIES = ['/Library/Keychains'];
|
|
57
|
+
/** Home-like directories besides the process's own; `*` is one path component. */
|
|
58
|
+
export const HOME_ROOTS = ['/Users/*', '/home/*', '/root', '/var/root', '/private/var/root'];
|
|
59
|
+
/** `/tmp/com.platless.axom-credentials.<uid>.sock` (credentialBroker.ts) and anything else the broker names so. */
|
|
60
|
+
export const BROKER_SOCKET_PREFIX = 'com.platless.axom-credentials.';
|
|
61
|
+
export const TEMP_DIRECTORIES = ['/tmp', '/private/tmp'];
|
|
62
|
+
/** NFC, then full case folding the way a case-insensitive file system compares (`ſ`→`s`, `ß`→`ss`, `K`→`k`). */
|
|
63
|
+
export function foldName(value) { return value.normalize('NFC').toUpperCase().toLowerCase(); }
|
|
64
|
+
function segments(absolute) { return foldName(path.resolve(absolute)).split(path.sep).filter(Boolean); }
|
|
65
|
+
function canonical(target) {
|
|
66
|
+
try {
|
|
67
|
+
return realpathSync(target);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return path.resolve(target);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const patterns = (entries) => entries.map(entry => foldName(entry).split('/').filter(Boolean));
|
|
74
|
+
const HOME_PATTERNS = patterns(HOME_ENTRIES);
|
|
75
|
+
const WORKSTATION_PATTERNS = patterns(WORKSTATION_ENTRIES);
|
|
76
|
+
const ABSOLUTE_PATTERNS = patterns(ABSOLUTE_ENTRIES);
|
|
77
|
+
const HOME_ROOT_PATTERNS = patterns(HOME_ROOTS);
|
|
78
|
+
const WORKSTATION_BELOW_HOME = ['.axom', 'workstation'];
|
|
79
|
+
/** [target] starts with [pattern]; `*` alone is any one component, a trailing `*` a name prefix. */
|
|
80
|
+
function startsWith(target, pattern) {
|
|
81
|
+
if (target.length < pattern.length)
|
|
82
|
+
return false;
|
|
83
|
+
return pattern.every((part, index) => part === '*' || (part.endsWith('*') ? target[index].startsWith(part.slice(0, -1)) : target[index] === part));
|
|
84
|
+
}
|
|
85
|
+
function coveredBelow(target, base, entries) {
|
|
86
|
+
if (!startsWith(target, base))
|
|
87
|
+
return false;
|
|
88
|
+
const rest = target.slice(base.length);
|
|
89
|
+
return entries.some(entry => startsWith(rest, entry));
|
|
90
|
+
}
|
|
91
|
+
let cached;
|
|
92
|
+
/** Both spellings of [value]: as given (a relative one resolved as the client resolves it) and canonical. */
|
|
93
|
+
function spellings(value) {
|
|
94
|
+
if (!value)
|
|
95
|
+
return [];
|
|
96
|
+
const unique = new Map();
|
|
97
|
+
for (const spelling of [path.resolve(value), canonical(value)])
|
|
98
|
+
unique.set(spelling, segments(spelling));
|
|
99
|
+
return [...unique.values()];
|
|
100
|
+
}
|
|
101
|
+
function bases(home) {
|
|
102
|
+
const key = JSON.stringify([home, process.env.AXOM_WORKSTATION_CONFIG_DIR ?? null, process.env.AXOM_CONFIG_DIR ?? null]);
|
|
103
|
+
if (cached?.key !== key) {
|
|
104
|
+
cached = {
|
|
105
|
+
key,
|
|
106
|
+
bases: {
|
|
107
|
+
homes: spellings(home),
|
|
108
|
+
workstations: spellings(process.env.AXOM_WORKSTATION_CONFIG_DIR),
|
|
109
|
+
// AXOM_CONFIG_DIR stands in for `~/.axom`: its config.json and device-id are the CLI's.
|
|
110
|
+
cliConfigs: spellings(process.env.AXOM_CONFIG_DIR),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return cached.bases;
|
|
115
|
+
}
|
|
116
|
+
const CLI_CONFIG_PATTERNS = patterns(['config.json', 'device-id']);
|
|
117
|
+
const TEMP_PATTERNS = patterns(TEMP_DIRECTORIES);
|
|
118
|
+
const BROKER_PREFIX = foldName(BROKER_SOCKET_PREFIX);
|
|
119
|
+
/** True when [target] (absolute) is, or lies below, protected credential storage. */
|
|
120
|
+
export function isCredentialPath(target, home = os.homedir()) {
|
|
121
|
+
const folded = segments(target);
|
|
122
|
+
if (ABSOLUTE_PATTERNS.some(entry => startsWith(folded, entry)))
|
|
123
|
+
return true;
|
|
124
|
+
if (TEMP_PATTERNS.some(dir => folded.length > dir.length && startsWith(folded, dir) && folded[dir.length].startsWith(BROKER_PREFIX)))
|
|
125
|
+
return true;
|
|
126
|
+
const { homes, workstations, cliConfigs } = bases(home);
|
|
127
|
+
// Any home-like directory the target is below, besides the process's own (a daemon running as another user).
|
|
128
|
+
const homeLike = HOME_ROOT_PATTERNS.filter(root => startsWith(folded, root)).map(root => folded.slice(0, root.length));
|
|
129
|
+
for (const base of [...homes, ...homeLike]) {
|
|
130
|
+
if (coveredBelow(folded, base, HOME_PATTERNS))
|
|
131
|
+
return true;
|
|
132
|
+
if (coveredBelow(folded, [...base, ...WORKSTATION_BELOW_HOME], WORKSTATION_PATTERNS))
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (workstations.some(base => coveredBelow(folded, base, WORKSTATION_PATTERNS)))
|
|
136
|
+
return true;
|
|
137
|
+
return cliConfigs.some(base => coveredBelow(folded, base, CLI_CONFIG_PATTERNS));
|
|
138
|
+
}
|
|
139
|
+
export function assertNotCredentialPath(target) {
|
|
140
|
+
if (isCredentialPath(target))
|
|
141
|
+
throw new FileError('permission_denied', PROTECTED_CREDENTIAL_MESSAGE);
|
|
142
|
+
}
|
|
@@ -19,6 +19,7 @@ let tools;
|
|
|
19
19
|
let initializing = false;
|
|
20
20
|
let closing;
|
|
21
21
|
const inFlight = new Map();
|
|
22
|
+
const operations = new Set();
|
|
22
23
|
function send(message) {
|
|
23
24
|
if (!process.connected)
|
|
24
25
|
return;
|
|
@@ -64,7 +65,7 @@ function run(request) {
|
|
|
64
65
|
inFlight.set(request.id, controller);
|
|
65
66
|
const deadline = request.deadlineAt === null ? undefined : setTimeout(() => controller.abort(), Math.max(0, request.deadlineAt - Date.now()));
|
|
66
67
|
const current = tools;
|
|
67
|
-
|
|
68
|
+
const operation = (async () => {
|
|
68
69
|
let reply;
|
|
69
70
|
try {
|
|
70
71
|
// runFileFrame never throws: errors (and cancellation) come back as the result frame's `error`.
|
|
@@ -89,6 +90,8 @@ function run(request) {
|
|
|
89
90
|
status();
|
|
90
91
|
send(reply);
|
|
91
92
|
})();
|
|
93
|
+
operations.add(operation);
|
|
94
|
+
void operation.finally(() => operations.delete(operation));
|
|
92
95
|
}
|
|
93
96
|
/** Refuses new work, lets running calls finish within a budget (a hung IO cannot hold the exit forever), then exits. */
|
|
94
97
|
function shutdown(code) {
|
|
@@ -96,7 +99,11 @@ function shutdown(code) {
|
|
|
96
99
|
for (const controller of inFlight.values())
|
|
97
100
|
controller.abort();
|
|
98
101
|
const budget = new Promise(resolve => { setTimeout(resolve, DISPOSE_BUDGET_MS).unref(); });
|
|
99
|
-
|
|
102
|
+
const dispose = async () => {
|
|
103
|
+
await Promise.allSettled([...operations]);
|
|
104
|
+
await tools?.dispose();
|
|
105
|
+
};
|
|
106
|
+
await Promise.race([dispose().catch(() => { }), budget]);
|
|
100
107
|
process.exit(code);
|
|
101
108
|
})();
|
|
102
109
|
return closing;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { constants } from 'node:fs';
|
|
3
|
-
import { link, lstat, mkdir, open, readdir, rename, stat, unlink } from 'node:fs/promises';
|
|
3
|
+
import { link, lstat, mkdir, open, readdir, realpath, rename, stat, unlink } from 'node:fs/promises';
|
|
4
4
|
import { basename, dirname, join } from 'node:path';
|
|
5
5
|
import { canonicalRoot, FileError, pinned, resolvePinnedDir, resolvePinnedDirCreate, resolvePinnedLeaf, resolveRelativeDir, resolveRelativeDirCreate, resolveRelativeFile, throwIfAborted, toFileError, withinRoot, } from './fence.js';
|
|
6
|
+
import { assertNotCredentialPath } from './credentials.js';
|
|
6
7
|
import { assertScopeAllows } from './scope.js';
|
|
7
8
|
/** Text file semantics of the platform agent tools (`file_*_request`): request-root scope only. */
|
|
8
9
|
export const MAX_READ_BYTES = 8 * 1024 * 1024;
|
|
@@ -306,14 +307,37 @@ function requireRequestRoot(scope, operation) {
|
|
|
306
307
|
* keeps the file worker's contract: a request cancelled once it is under way reports `outcome_unknown` for mutations
|
|
307
308
|
* and `workstation_offline` for reads.
|
|
308
309
|
*/
|
|
309
|
-
async function anchored(operation, root, anchor, signal, hooks, run) {
|
|
310
|
+
async function anchored(operation, root, anchor, leaves, signal, hooks, run) {
|
|
311
|
+
const insideRoot = withinRoot(root);
|
|
310
312
|
try {
|
|
311
313
|
await hooks?.afterValidation?.();
|
|
312
314
|
return await pinned(anchor, async () => {
|
|
313
315
|
await hooks?.afterPinned?.();
|
|
314
316
|
throwIfAborted(signal);
|
|
317
|
+
// What each existing leaf resolves to, by the name stored on disk: a spelling a case-insensitive file system
|
|
318
|
+
// maps onto a store (`.git-credentialſ`) is judged by the store it opens, not by how it was written.
|
|
319
|
+
for (const leaf of leaves) {
|
|
320
|
+
let stored;
|
|
321
|
+
try {
|
|
322
|
+
stored = await realpath(leaf);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
assertNotCredentialPath(stored);
|
|
328
|
+
}
|
|
315
329
|
return run();
|
|
316
|
-
}, {
|
|
330
|
+
}, {
|
|
331
|
+
// The directory the kernel now holds, and what the operation touches below it, are checked again: a directory
|
|
332
|
+
// swapped for a link after validation must not reach credential storage (credentials.ts).
|
|
333
|
+
verify: (pinnedReal, parent) => {
|
|
334
|
+
insideRoot(pinnedReal, parent);
|
|
335
|
+
assertNotCredentialPath(pinnedReal);
|
|
336
|
+
for (const leaf of leaves)
|
|
337
|
+
assertNotCredentialPath(join(pinnedReal, leaf));
|
|
338
|
+
},
|
|
339
|
+
signal,
|
|
340
|
+
});
|
|
317
341
|
}
|
|
318
342
|
catch (error) {
|
|
319
343
|
if (signal?.aborted)
|
|
@@ -325,32 +349,42 @@ export async function readText(scope, request, signal, hooks) {
|
|
|
325
349
|
requireRequestRoot(scope, 'read');
|
|
326
350
|
const root = await canonicalRoot(scope.root);
|
|
327
351
|
const target = await resolveRelativeFile(root, request.path, false);
|
|
352
|
+
assertNotCredentialPath(target);
|
|
328
353
|
throwIfAborted(signal);
|
|
329
|
-
//
|
|
330
|
-
|
|
354
|
+
// The pinned read sees only the leaf; the result names the workspace-relative path it was asked for.
|
|
355
|
+
const data = await anchored('read', root, dirname(target), [basename(target)], signal, hooks, () => readTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf));
|
|
356
|
+
return { ...data, path: request.path };
|
|
331
357
|
}
|
|
332
358
|
export async function writeText(scope, request, signal, hooks) {
|
|
333
359
|
requireRequestRoot(scope, 'write');
|
|
334
360
|
const root = await canonicalRoot(scope.root);
|
|
335
361
|
const target = await resolveRelativeFile(root, request.path, request.mode === 'create');
|
|
336
|
-
|
|
362
|
+
assertNotCredentialPath(target);
|
|
363
|
+
const data = await serialTarget(target, () => throwIfAbortedThen(signal, () => anchored('write', root, dirname(target), [basename(target)], signal, hooks, () => writeTextFile({ ...request, root: '.', path: basename(target) }, signal, resolvePinnedLeaf))));
|
|
364
|
+
return { ...data, path: request.path };
|
|
337
365
|
}
|
|
338
366
|
export async function listDir(scope, request, signal, hooks) {
|
|
339
367
|
requireRequestRoot(scope, 'list');
|
|
340
368
|
const root = await canonicalRoot(scope.root);
|
|
341
369
|
const target = await resolveRelativeDir(root, request.path);
|
|
370
|
+
assertNotCredentialPath(target);
|
|
342
371
|
throwIfAborted(signal);
|
|
343
372
|
// Listing the root pins the root itself; '' is the zero-component leaf.
|
|
344
373
|
const anchor = target === root ? root : dirname(target);
|
|
345
374
|
const leaf = target === root ? '' : basename(target);
|
|
346
|
-
const data = await anchored('list', root, anchor, signal, hooks, () => listTextDir({ ...request, root: '.', path: leaf }, signal, resolvePinnedDir));
|
|
375
|
+
const data = await anchored('list', root, anchor, leaf === '' ? [] : [leaf], signal, hooks, () => listTextDir({ ...request, root: '.', path: leaf }, signal, resolvePinnedDir));
|
|
347
376
|
return { ...data, path: request.path };
|
|
348
377
|
}
|
|
349
378
|
export async function makeDir(scope, request, signal, hooks) {
|
|
350
379
|
requireRequestRoot(scope, 'mkdir');
|
|
351
380
|
const root = await canonicalRoot(scope.root);
|
|
352
381
|
const plan = await resolveRelativeDirCreate(root, request.path, request.parents);
|
|
382
|
+
// Every directory this would create, not only the last one.
|
|
383
|
+
const created = plan.missing.map((_, index) => plan.missing.slice(0, index + 1).join('/'));
|
|
384
|
+
assertNotCredentialPath(plan.anchor);
|
|
385
|
+
for (const leaf of created)
|
|
386
|
+
assertNotCredentialPath(join(plan.anchor, leaf));
|
|
353
387
|
throwIfAborted(signal);
|
|
354
|
-
const data = await anchored('mkdir', root, plan.anchor, signal, hooks, () => createTextDir({ ...request, root: '.', path: plan.missing.join('/') }, signal, resolvePinnedDirCreate));
|
|
388
|
+
const data = await anchored('mkdir', root, plan.anchor, created, signal, hooks, () => createTextDir({ ...request, root: '.', path: plan.missing.join('/') }, signal, resolvePinnedDirCreate));
|
|
355
389
|
return { ...data, path: request.path };
|
|
356
390
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import fs from 'node:fs/promises';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
+
import { assertRegistrationOwner } from './runtime/ownership.js';
|
|
18
19
|
const SUFFIX = '.jsonl';
|
|
19
20
|
function parseEntry(line) {
|
|
20
21
|
try {
|
|
@@ -58,7 +59,7 @@ export async function listLegacyJournalRefs(dir) {
|
|
|
58
59
|
return refs.sort();
|
|
59
60
|
}
|
|
60
61
|
/** Every readable entry of `sessionRef`'s journal, in file order (unreadable lines skipped). A missing file is empty. */
|
|
61
|
-
export async function readLegacyJournal(dir, sessionRef) {
|
|
62
|
+
export async function readLegacyJournal(dir, sessionRef, ownership) {
|
|
62
63
|
let text;
|
|
63
64
|
try {
|
|
64
65
|
text = await fs.readFile(fileOf(dir, sessionRef), 'utf8');
|
|
@@ -73,8 +74,10 @@ export async function readLegacyJournal(dir, sessionRef) {
|
|
|
73
74
|
if (line.length === 0)
|
|
74
75
|
continue;
|
|
75
76
|
const entry = parseEntry(line);
|
|
76
|
-
if (entry)
|
|
77
|
+
if (entry) {
|
|
78
|
+
assertRegistrationOwner(JSON.parse(line), ownership, { directory: dir, kind: 'journal' });
|
|
77
79
|
entries.push(entry);
|
|
80
|
+
}
|
|
78
81
|
}
|
|
79
82
|
return entries;
|
|
80
83
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Daemon-side: a typed `acp.*` client for keeper-acp (launch through KeeperLauncher, one long-lived KeeperClient per generation).
|
|
2
2
|
// Exempt from the keeper-side import guard (it runs in the daemon), though it imports nothing but the kit and this keeper's protocol.
|
|
3
3
|
import { KeeperClient } from '../kit/client.js';
|
|
4
|
+
import { assertRegistrationOwner, RegistrationOwnershipError } from '../../runtime/ownership.js';
|
|
4
5
|
import { protocolWindow } from '../kit/protocol.js';
|
|
5
6
|
import { ACP_PROTOCOL_VERSION, acpStream, } from './protocol.js';
|
|
6
7
|
/** keeper-acp could not be reached (launch, connect or hello failed): the request was never sent. */
|
|
@@ -23,7 +24,7 @@ export class AcpKeeperClient {
|
|
|
23
24
|
}
|
|
24
25
|
get daemonProtocol() { return this.options.daemonProtocol ?? protocolWindow(ACP_PROTOCOL_VERSION); }
|
|
25
26
|
clientFor(paths) {
|
|
26
|
-
if (this.client && this.clientGen === paths.gen)
|
|
27
|
+
if (this.client && this.client.state !== 'closed' && this.clientGen === paths.gen)
|
|
27
28
|
return this.client;
|
|
28
29
|
this.client?.close();
|
|
29
30
|
this.client = new KeeperClient({
|
|
@@ -48,6 +49,7 @@ export class AcpKeeperClient {
|
|
|
48
49
|
get gen() { return this.options.gen; }
|
|
49
50
|
/** The connected client of the current build, launching keeper-acp only when needed (a `gen`-bound client only connects). */
|
|
50
51
|
async ensure() {
|
|
52
|
+
this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
|
|
51
53
|
const current = this.client;
|
|
52
54
|
if (current && current.state === 'ready')
|
|
53
55
|
return current;
|
|
@@ -64,14 +66,43 @@ export class AcpKeeperClient {
|
|
|
64
66
|
return client;
|
|
65
67
|
}
|
|
66
68
|
async call(method, params, options = {}) {
|
|
69
|
+
this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
|
|
67
70
|
let client;
|
|
68
71
|
try {
|
|
69
72
|
client = await this.ensure();
|
|
70
73
|
}
|
|
71
74
|
catch (error) {
|
|
75
|
+
if (error instanceof RegistrationOwnershipError)
|
|
76
|
+
throw error;
|
|
72
77
|
throw new AcpKeeperUnavailableError(error);
|
|
73
78
|
}
|
|
74
|
-
|
|
79
|
+
const ownership = this.options.launcher.ownership;
|
|
80
|
+
const input = params;
|
|
81
|
+
if (ownership && typeof input.sessionKey === 'string') {
|
|
82
|
+
const listed = await client.request('acp.list', {});
|
|
83
|
+
const existing = listed.sessions.find(session => session.sessionKey === input.sessionKey);
|
|
84
|
+
if (existing) {
|
|
85
|
+
if (this.options.launcher.assertSessionOwnership)
|
|
86
|
+
this.options.launcher.assertSessionOwnership(existing.label, this.clientGen);
|
|
87
|
+
else
|
|
88
|
+
assertRegistrationOwner(existing.label, ownership);
|
|
89
|
+
}
|
|
90
|
+
else if (method !== 'acp.spawn')
|
|
91
|
+
throw new RegistrationOwnershipError('Registration session owner is unknown');
|
|
92
|
+
}
|
|
93
|
+
if (ownership && method === 'acp.spawn')
|
|
94
|
+
params = { ...input, label: { ...input.label, registrationId: ownership.registrationId } };
|
|
95
|
+
const result = await client.request(method, params, options);
|
|
96
|
+
if (ownership && method === 'acp.list') {
|
|
97
|
+
const listed = result;
|
|
98
|
+
for (const session of listed.sessions) {
|
|
99
|
+
if (this.options.launcher.assertSessionOwnership)
|
|
100
|
+
this.options.launcher.assertSessionOwnership(session.label, this.clientGen);
|
|
101
|
+
else
|
|
102
|
+
assertRegistrationOwner(session.label, ownership);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
75
106
|
}
|
|
76
107
|
spawn(params, options = {}) { return this.call('acp.spawn', params, options); }
|
|
77
108
|
list() { return this.call('acp.list', {}); }
|
|
@@ -87,13 +118,18 @@ export class AcpKeeperClient {
|
|
|
87
118
|
release(sessionKey) { return this.call('acp.release', { sessionKey }); }
|
|
88
119
|
activity() { return this.call('keeper.activity', {}); }
|
|
89
120
|
async subscribe(sessionKey, stream, handler, options) {
|
|
121
|
+
this.options.launcher.assertOwnership?.('acp', this.clientGen ?? this.options.gen);
|
|
90
122
|
let client;
|
|
91
123
|
try {
|
|
92
124
|
client = await this.ensure();
|
|
93
125
|
}
|
|
94
126
|
catch (error) {
|
|
127
|
+
if (error instanceof RegistrationOwnershipError)
|
|
128
|
+
throw error;
|
|
95
129
|
throw new AcpKeeperUnavailableError(error);
|
|
96
130
|
}
|
|
131
|
+
if (this.options.launcher.ownership)
|
|
132
|
+
await this.attach(sessionKey);
|
|
97
133
|
return client.subscribe(acpStream(stream, sessionKey), handler, options);
|
|
98
134
|
}
|
|
99
135
|
/** Drops the connection (keeper-acp and its adapters keep running). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { opendirSync, readFileSync, lstatSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, relative, sep } from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* The keeper buildVersion: sha256 over the bundle bytes plus the sorted list of
|
|
@@ -14,22 +14,46 @@ import { join, relative, sep } from 'node:path';
|
|
|
14
14
|
* unchanged keeper code does not change it.
|
|
15
15
|
*/
|
|
16
16
|
const sha256 = (data) => createHash('sha256').update(data).digest('hex');
|
|
17
|
-
function walkFiles(dir) {
|
|
17
|
+
function walkFiles(dir, limits) {
|
|
18
18
|
const out = [];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
let entries = 0;
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
const walk = (directory, depth) => {
|
|
22
|
+
if (limits && depth > limits.maxDepth)
|
|
23
|
+
throw Error('Native bundle depth limit');
|
|
24
|
+
const handle = opendirSync(directory, { bufferSize: 32 });
|
|
25
|
+
try {
|
|
26
|
+
for (;;) {
|
|
27
|
+
const entry = handle.readSync();
|
|
28
|
+
if (!entry)
|
|
29
|
+
break;
|
|
30
|
+
if (limits && ++entries > limits.maxEntries)
|
|
31
|
+
throw Error('Native bundle entry limit');
|
|
32
|
+
const full = join(directory, entry.name);
|
|
33
|
+
if (entry.isDirectory())
|
|
34
|
+
walk(full, depth + 1);
|
|
35
|
+
else {
|
|
36
|
+
if (limits?.maxBytes !== undefined) {
|
|
37
|
+
bytes += lstatSync(full).size;
|
|
38
|
+
if (bytes > limits.maxBytes)
|
|
39
|
+
throw Error('Native bundle byte limit');
|
|
40
|
+
}
|
|
41
|
+
out.push(full);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
handle.closeSync();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
walk(dir, 0);
|
|
26
50
|
return out;
|
|
27
51
|
}
|
|
28
52
|
/** Relative (posix) paths of the native files carried next to a bundle, sorted. */
|
|
29
|
-
export function nativeFilesOf(kindDir) {
|
|
53
|
+
export function nativeFilesOf(kindDir, limits) {
|
|
30
54
|
let files;
|
|
31
55
|
try {
|
|
32
|
-
files = walkFiles(join(kindDir, 'node_modules'));
|
|
56
|
+
files = walkFiles(join(kindDir, 'node_modules'), limits);
|
|
33
57
|
}
|
|
34
58
|
catch (error) {
|
|
35
59
|
if (error.code === 'ENOENT')
|