@platless/axom-cli 0.5.9 → 0.6.36
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 +18 -5
- package/dist/axom-patch/cli/README.md +100 -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/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/codex-terminal-output.mjs +33 -0
- package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
- package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
- package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -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 +16 -18
- package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
- package/dist/axom-patch/cli/zcode-models.json +1 -2
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
- package/dist/cli.js +4 -36
- package/dist/commands/knowledge.js +47 -0
- package/dist/commands/update.js +65 -3
- package/dist/commands/workstation.js +73 -7
- package/dist/install/claude/commands.js +13 -13
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/workstation/acp/acpAgents.js +102 -98
- 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/patchAssets.js +40 -0
- package/dist/workstation/acp/processor/checkpoint.js +25 -34
- package/dist/workstation/acp/processor/control.js +5 -78
- package/dist/workstation/acp/processor/runtime.js +5 -81
- package/dist/workstation/acp/processor/session.js +51 -80
- package/dist/workstation/acp/processor/state.js +6 -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/updates.js +131 -105
- package/dist/workstation/acp/processorSession.js +8 -172
- 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 +28 -0
- 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 +38 -143
- package/dist/workstation/channels/management.js +95 -11
- package/dist/workstation/channels/url.js +1 -1
- package/dist/workstation/config.js +16 -4
- package/dist/workstation/connection/encryptedChannel.js +4 -1
- package/dist/workstation/control/bindingStore.js +200 -0
- package/dist/workstation/control/commands.js +124 -0
- package/dist/workstation/control/daemonIdentity.js +53 -0
- package/dist/workstation/control/enrollment.js +106 -0
- package/dist/workstation/control/inspect.js +71 -0
- package/dist/workstation/control/locks.js +63 -0
- package/dist/workstation/control/protocol.js +40 -0
- package/dist/workstation/control/restartGuard.js +46 -0
- package/dist/workstation/control/runtimeClient.js +97 -0
- package/dist/workstation/control/runtimeHost.js +148 -0
- package/dist/workstation/control/serverIdentity.js +35 -0
- package/dist/workstation/daemon.js +106 -6
- package/dist/workstation/daemonctl.js +77 -14
- package/dist/workstation/desktopControl/audio.js +99 -0
- package/dist/workstation/desktopControl/capabilities.js +92 -0
- package/dist/workstation/desktopControl/captureModes.js +9 -0
- package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
- package/dist/workstation/desktopControl/deviceSockets.js +246 -0
- package/dist/workstation/desktopControl/helperProcess.js +353 -0
- package/dist/workstation/desktopControl/lobby.js +129 -0
- package/dist/workstation/desktopControl/macProvider.js +155 -0
- package/dist/workstation/desktopControl/media.js +331 -0
- package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
- package/dist/workstation/desktopControl/networkEvidence.js +43 -0
- package/dist/workstation/desktopControl/overview.js +58 -0
- package/dist/workstation/desktopControl/protocol.js +302 -0
- package/dist/workstation/desktopControl/quicMedia.js +240 -0
- package/dist/workstation/desktopControl/rateController.js +226 -0
- package/dist/workstation/desktopControl/supervisor.js +1681 -0
- package/dist/workstation/desktopControl/transport.js +27 -0
- package/dist/workstation/desktopControl/wallpaper.js +78 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/text.js +42 -8
- package/dist/workstation/keepers/kit/bundle.js +21 -17
- package/dist/workstation/keepers/kit/launcher.js +47 -7
- package/dist/workstation/keepers/kit/paths.js +2 -1
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/agent.js +2 -15
- package/dist/workstation/protocol/management.js +93 -4
- package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
- package/dist/workstation/rd/artifact.js +119 -0
- package/dist/workstation/rd/control.js +119 -0
- package/dist/workstation/rd/display.js +29 -0
- package/dist/workstation/rd/provider.js +222 -0
- package/dist/workstation/rd/sessions.js +391 -0
- package/dist/workstation/rd/state.js +45 -0
- package/dist/workstation/rd/update.js +171 -0
- package/dist/workstation/rd/upkeep.js +38 -0
- package/dist/workstation/register.js +27 -37
- package/dist/workstation/service/installer.js +104 -13
- package/dist/workstation/service/launchd.js +1 -0
- package/dist/workstation/storageMetrics.js +118 -0
- package/dist/workstation/systemMetrics.js +44 -92
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +5 -5
- 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/provider.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,78 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { wallpaperAssetSchema } from './lobby.js';
|
|
3
|
+
export function decodeWallpaper(text) {
|
|
4
|
+
if (Buffer.byteLength(text) > 100_000)
|
|
5
|
+
throw new Error('wallpaper_unavailable');
|
|
6
|
+
const result = JSON.parse(text);
|
|
7
|
+
const asset = wallpaperAssetSchema.parse(result.asset);
|
|
8
|
+
if (typeof result.jpeg !== 'string' || result.jpeg.length > 87384)
|
|
9
|
+
throw new Error('wallpaper_unavailable');
|
|
10
|
+
const jpeg = Buffer.from(result.jpeg, 'base64');
|
|
11
|
+
if (jpeg.length < 4 || jpeg.length > 65536 || jpeg.toString('base64') !== result.jpeg || jpeg[0] !== 255 || jpeg[1] !== 216)
|
|
12
|
+
throw new Error('wallpaper_unavailable');
|
|
13
|
+
const version = createHash('sha256').update(JSON.stringify(asset)).update(jpeg).digest('hex');
|
|
14
|
+
return { asset: { ...asset, version }, jpeg };
|
|
15
|
+
}
|
|
16
|
+
export class XfceWallpaperReader {
|
|
17
|
+
execute;
|
|
18
|
+
constructor(execute) {
|
|
19
|
+
this.execute = execute;
|
|
20
|
+
}
|
|
21
|
+
async discover() {
|
|
22
|
+
const workspaceValue = await this.execute('xprop', ['-root', '_NET_CURRENT_DESKTOP']);
|
|
23
|
+
let workspace = /_NET_CURRENT_DESKTOP\(CARDINAL\)\s*=\s*(\d+)/.exec(workspaceValue)?.[1];
|
|
24
|
+
if (workspace == null || Number(workspace) > 1024)
|
|
25
|
+
throw new Error('wallpaper_unavailable');
|
|
26
|
+
const listed = await this.execute('xfconf-query', ['-c', 'xfce4-desktop', '-l']);
|
|
27
|
+
const read = (property) => this.execute('xfconf-query', ['-c', 'xfce4-desktop', '-p', property]);
|
|
28
|
+
if (listed.split(/\r?\n/).some(line => line.trim() === '/backdrop/single-workspace-mode') &&
|
|
29
|
+
(await read('/backdrop/single-workspace-mode')).trim() === 'true') {
|
|
30
|
+
workspace = (await read('/backdrop/single-workspace-number')).trim();
|
|
31
|
+
if (!/^\d{1,4}$/.test(workspace) || Number(workspace) > 1024)
|
|
32
|
+
throw new Error('wallpaper_unavailable');
|
|
33
|
+
}
|
|
34
|
+
const available = listed.split(/\r?\n/).map(line => line.trim());
|
|
35
|
+
const properties = available
|
|
36
|
+
.filter(line => new RegExp(`^/backdrop/screen0/monitor[^/]+/workspace${workspace}/last-image$`).test(line));
|
|
37
|
+
let selected = properties[0];
|
|
38
|
+
let defaultImage = false;
|
|
39
|
+
if (properties.length === 0) {
|
|
40
|
+
const version = await this.execute('xfdesktop', ['--version']);
|
|
41
|
+
const monitors = await this.execute('xrandr', ['--listactivemonitors']);
|
|
42
|
+
if (!/xfdesktop version 4\.20(?:\.\d+)?\b/.test(version) || !/^Monitors:\s*1\s*$/m.test(monitors))
|
|
43
|
+
throw new Error('wallpaper_unavailable');
|
|
44
|
+
const output = /^\s*0:\s+\S+\s+\S+\s+(\S+)\s*$/m.exec(monitors)?.[1];
|
|
45
|
+
if (!output || !/^[A-Za-z0-9_.:-]+$/.test(output))
|
|
46
|
+
throw new Error('wallpaper_unavailable');
|
|
47
|
+
selected = `/backdrop/screen0/monitor${output}/workspace${workspace}/last-image`;
|
|
48
|
+
defaultImage = true;
|
|
49
|
+
}
|
|
50
|
+
if (properties.length > 1)
|
|
51
|
+
throw new Error('wallpaper_unavailable');
|
|
52
|
+
const file = defaultImage ? '/usr/share/backgrounds/xfce/xfce-x.svg' : (await read(selected)).trim();
|
|
53
|
+
if (!file.startsWith('/') || file.length > 4096 || /[\r\n\0]/.test(file))
|
|
54
|
+
throw new Error('wallpaper_unavailable');
|
|
55
|
+
const prefix = selected.slice(0, -'last-image'.length);
|
|
56
|
+
const style = available.includes(`${prefix}image-style`) ? (await read(`${prefix}image-style`)).trim() : '5';
|
|
57
|
+
const placement = { '1': 'center', '2': 'tile', '3': 'stretch', '4': 'fit', '5': 'cover' }[style];
|
|
58
|
+
if (!placement)
|
|
59
|
+
throw new Error('wallpaper_unavailable');
|
|
60
|
+
let background = '#000000';
|
|
61
|
+
let fidelity = 'exact';
|
|
62
|
+
if (placement !== 'cover' && placement !== 'stretch') {
|
|
63
|
+
try {
|
|
64
|
+
if ((await read(`${prefix}color-style`)).trim() !== '0')
|
|
65
|
+
throw new Error('unsupported_gradient');
|
|
66
|
+
const values = (await read(`${prefix}rgba1`)).split(/\r?\n/).map(line => line.trim())
|
|
67
|
+
.filter(line => /^\d+(?:\.\d+)?$/.test(line)).map(Number);
|
|
68
|
+
if (values.length !== 4 || values.some(value => value < 0 || value > 1) || values[3] !== 1)
|
|
69
|
+
throw new Error('unknown_fill');
|
|
70
|
+
background = '#' + values.slice(0, 3).map(value => Math.round(value * 255).toString(16).padStart(2, '0')).join('');
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
fidelity = 'approximate';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { file, placement, background, fidelity };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -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 } 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';
|
|
@@ -49,17 +50,55 @@ export class KeeperLauncher {
|
|
|
49
50
|
get distDir() { return this.options.distDir ?? defaultDistDir(); }
|
|
50
51
|
probe(paths, timeoutMs) { return this.options.probe ? this.options.probe(paths) : pingKeeper(paths, timeoutMs); }
|
|
51
52
|
/** Paths of generation `gen` of `kind` (no build needed). */
|
|
52
|
-
pathsFor(kind, gen) {
|
|
53
|
+
pathsFor(kind, gen) {
|
|
54
|
+
const scoped = keeperPaths(this.options, kind, gen);
|
|
55
|
+
try {
|
|
56
|
+
lstatSync(scoped.pidFile);
|
|
57
|
+
return scoped;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code !== 'ENOENT')
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const legacy = keeperPaths(this.options, kind, gen, { legacyLayout: true });
|
|
64
|
+
let descriptor;
|
|
65
|
+
try {
|
|
66
|
+
descriptor = openSync(legacy.pidFile, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
67
|
+
const stat = fstatSync(descriptor);
|
|
68
|
+
if (!stat.isFile() || stat.size > 4096 || process.getuid && stat.uid !== process.getuid())
|
|
69
|
+
throw new Error('Unsafe keeper PID manifest');
|
|
70
|
+
const manifest = JSON.parse(readFileSync(descriptor, 'utf8'));
|
|
71
|
+
if (manifest.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen)
|
|
72
|
+
return legacy;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code !== 'ENOENT')
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (descriptor !== undefined)
|
|
80
|
+
closeSync(descriptor);
|
|
81
|
+
}
|
|
82
|
+
return scoped;
|
|
83
|
+
}
|
|
53
84
|
/** The generation the current build of `kind` runs as (`dist/keepers/<kind>/manifest.json`); starts nothing. */
|
|
54
85
|
async currentGen(kind) {
|
|
55
86
|
return keeperGen((await readKeeperManifest(path.join(this.distDir, kind))).buildVersion);
|
|
56
87
|
}
|
|
57
88
|
/** Generations of `kind` whose pid manifest names a live keeper of that generation (`<root>/run/<gen>`); starts nothing. */
|
|
58
89
|
async liveGenerations(kind) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
90
|
+
const scoped = keeperPaths(this.options, kind, 'probe');
|
|
91
|
+
const legacy = keeperPaths(this.options, kind, 'probe', { legacyLayout: true });
|
|
92
|
+
const roots = [scoped.root, legacy.root];
|
|
93
|
+
const gens = new Set();
|
|
94
|
+
for (const root of roots)
|
|
95
|
+
for (const gen of await fs.readdir(path.join(root, 'run')).catch(() => [])) {
|
|
96
|
+
const manifest = await readPidManifest(path.join(root, 'run', gen, 'keeper.pid.json')).catch(() => null);
|
|
97
|
+
if (manifest?.identity === scoped.identity && manifest.kind === kind && manifest.gen === gen)
|
|
98
|
+
gens.add(gen);
|
|
99
|
+
}
|
|
61
100
|
const live = [];
|
|
62
|
-
for (const gen of gens.sort())
|
|
101
|
+
for (const gen of [...gens].sort())
|
|
63
102
|
if (await liveManifest(this.pathsFor(kind, gen)).catch(() => false))
|
|
64
103
|
live.push(gen);
|
|
65
104
|
return live;
|
|
@@ -76,7 +115,7 @@ export class KeeperLauncher {
|
|
|
76
115
|
async start(kind) {
|
|
77
116
|
const deadline = Date.now() + (this.options.startTimeoutMs ?? 7500);
|
|
78
117
|
const manifest = await readKeeperManifest(path.join(this.distDir, kind));
|
|
79
|
-
const paths =
|
|
118
|
+
const paths = this.pathsFor(kind, keeperGen(manifest.buildVersion));
|
|
80
119
|
await ensureKeeperDirectories(paths);
|
|
81
120
|
// A keeper that already answers needs no snapshot check: it runs from the snapshot it was started with.
|
|
82
121
|
const alive = await this.probe(paths);
|
|
@@ -165,9 +204,10 @@ export class KeeperLauncher {
|
|
|
165
204
|
* socket. Sockets under configDir are left alone. Returns how many were touched; never throws.
|
|
166
205
|
*/
|
|
167
206
|
async touchSockets(kind) {
|
|
168
|
-
const
|
|
207
|
+
const root = keeperPaths(this.options, kind, 'probe').root;
|
|
208
|
+
const gens = new Set([...await fs.readdir(path.join(root, 'run')).catch(() => []), ...await this.liveGenerations(kind)]);
|
|
169
209
|
let touched = 0;
|
|
170
|
-
for (const gen of
|
|
210
|
+
for (const gen of gens) {
|
|
171
211
|
const paths = this.pathsFor(kind, gen);
|
|
172
212
|
if (paths.socketFallback && await touchSocket(paths).catch(() => false))
|
|
173
213
|
touched += 1;
|
|
@@ -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;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { LocalControlError } from './control/protocol.js';
|
|
5
|
+
const schema = z.object({ schemaVersion: z.literal(1), source: z.literal('managed'), prefix: z.string(), nodePath: z.string(), npmPath: z.string(), cliPath: z.string(), entryPath: z.string(), runtimeVersion: z.string().regex(/^24\.\d+\.\d+$/), architecture: z.enum(['arm64', 'x64']) }).strict();
|
|
6
|
+
function plannedRealPath(value) {
|
|
7
|
+
try {
|
|
8
|
+
return realpathSync(value);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.code !== 'ENOENT' || path.dirname(value) === value)
|
|
12
|
+
throw error;
|
|
13
|
+
return path.join(plannedRealPath(path.dirname(value)), path.basename(value));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** This synchronous variant is shared by the existing synchronous service-context constructors. */
|
|
17
|
+
export function loadManagedCliInstallSync(packageRoot) {
|
|
18
|
+
let root;
|
|
19
|
+
try {
|
|
20
|
+
root = realpathSync(packageRoot);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT')
|
|
24
|
+
return null;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
const prefix = path.resolve(root, '../../../..');
|
|
28
|
+
if (root !== path.join(prefix, 'lib/node_modules/@platless/axom-cli'))
|
|
29
|
+
return null;
|
|
30
|
+
const file = path.join(prefix, 'install.json');
|
|
31
|
+
let found = false;
|
|
32
|
+
try {
|
|
33
|
+
const stat = lstatSync(file);
|
|
34
|
+
found = true;
|
|
35
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16384 || (process.getuid && stat.uid !== process.getuid()) || (stat.mode & 0o077))
|
|
36
|
+
throw Error('Unsafe metadata');
|
|
37
|
+
const value = schema.parse(JSON.parse(readFileSync(file, 'utf8')));
|
|
38
|
+
const home = path.resolve(prefix, '../..');
|
|
39
|
+
const runtime = path.join(home, '.axom/runtime/node', `${value.runtimeVersion}-darwin-${value.architecture}`);
|
|
40
|
+
if (prefix !== path.join(home, '.axom/cli') || realpathSync(value.prefix) !== prefix || realpathSync(value.cliPath) !== path.join(root, 'dist/cli.js') || realpathSync(value.nodePath) !== path.join(runtime, 'bin/node') || realpathSync(value.npmPath) !== path.join(runtime, 'bin/npm') || plannedRealPath(value.entryPath) !== path.join(home, '.local/bin/axom'))
|
|
41
|
+
throw Error('Metadata does not describe this installation');
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (!found && error.code === 'ENOENT')
|
|
46
|
+
return null;
|
|
47
|
+
throw new LocalControlError('UNSUPPORTED_INSTALLATION', 'The managed CLI installation needs repair');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function loadManagedCliInstall(packageRoot) { return loadManagedCliInstallSync(packageRoot); }
|
|
51
|
+
export function npmExecutionContext(install, inherited) {
|
|
52
|
+
if (!install)
|
|
53
|
+
return { ...inherited };
|
|
54
|
+
const result = { ...inherited, NPM_CONFIG_PREFIX: install.prefix, PATH: `${path.dirname(install.nodePath)}:${inherited.PATH ?? '/usr/bin:/bin'}` };
|
|
55
|
+
delete result.npm_config_prefix;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { accountLimitsSchema } from './accountLimits.js';
|
|
2
|
-
import { isRuntimeDiagnostics } from '
|
|
2
|
+
import { isRuntimeDiagnostics } from './runtimeDiagnostics.js';
|
|
3
3
|
import { decodeChannelFrame, hasOptional, hasRequired, isBoolean, isBridgeInfoArray, isNullableAuthMethodInfoArray, isNullableString, isNumber, isObject, isString, isStringArray, isStringRecord, } from './common.js';
|
|
4
4
|
/** Current version and feature floors for the independently negotiated agent protocol. */
|
|
5
5
|
export const AGENT_WIRE_VERSION = 27;
|
|
@@ -14,9 +14,7 @@ export const AGENT_WIRE_SWITCH_IDLE_VERSION = 26;
|
|
|
14
14
|
export const AGENT_WIRE_NATIVE_DELETE_VERSION = 24;
|
|
15
15
|
export const AGENT_WIRE_PROVIDER_TASK_VERSION = 23;
|
|
16
16
|
export const AGENT_WIRE_RUNTIME_PROFILE_VERSION = 22;
|
|
17
|
-
export const AGENT_WIRE_DELEGATION_RENEW_VERSION = 21;
|
|
18
17
|
export const AGENT_WIRE_TURN_IDENTITY_VERSION = 20;
|
|
19
|
-
export const AGENT_WIRE_DELEGATION_MCP_VERSION = 19;
|
|
20
18
|
/** Scoped replay/history is enabled only when both peers preserve native origin. */
|
|
21
19
|
export const AGENT_WIRE_SCOPED_ORIGIN_VERSION = 18;
|
|
22
20
|
/** agent_open.requireSessionLoad: reject unsupported/failed load without session/new fallback. */
|
|
@@ -246,9 +244,6 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
246
244
|
env: isStringRecord, pathPrepend: isStringArray, agentEnv: isStringRecord, command: isNullableString,
|
|
247
245
|
args: isStringArray, nativeSessionId: isNullableString,
|
|
248
246
|
resumeFromSeq: isNumber, backfill: isBoolean, requireSessionLoad: isBoolean,
|
|
249
|
-
delegationMcp: (value) => isObject(value) &&
|
|
250
|
-
isString(value.url) && isStringRecord(value.headers) && isNumber(value.expiresAtEpochMs) &&
|
|
251
|
-
(value.bindingId === undefined || isString(value.bindingId)),
|
|
252
247
|
},
|
|
253
248
|
},
|
|
254
249
|
agent_opened: {
|
|
@@ -265,9 +260,6 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
265
260
|
// `reloaded` is `true`-or-absent for the same reason as `supportsSteering`: a `false` would
|
|
266
261
|
// be a second spelling of what its absence already says.
|
|
267
262
|
reloaded: isVerifiedTrue,
|
|
268
|
-
delegationExpiresAtEpochMs: isNumber,
|
|
269
|
-
delegationRefreshPending: isVerifiedTrue,
|
|
270
|
-
delegationInstanceId: isString, delegationBindingId: isString, delegationRenewalRevision: isNumber,
|
|
271
263
|
agentInfo: isAgentInfo, supportsImageInput: isVerifiedTrue, configOptions: isNullableSessionConfigOptionArray,
|
|
272
264
|
// Same guard as `agent_verified.authMethods`, and the same absent-vs-`[]` reading: a
|
|
273
265
|
// half-decoded method table is worse than none, because the reader would offer a login the
|
|
@@ -286,10 +278,6 @@ export const AGENT_FRAME_SHAPES = {
|
|
|
286
278
|
},
|
|
287
279
|
},
|
|
288
280
|
agent_prompt: { required: { sessionRef: isString, text: isString }, optional: { images: isImageBlockArray, platformTurnId: isString } },
|
|
289
|
-
agent_delegation_renew: { required: { id: isString, sessionRef: isString, nativeSessionId: isString,
|
|
290
|
-
instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
|
|
291
|
-
agent_delegation_renewed: { required: { id: isString, ok: isBoolean }, optional: { error: isString,
|
|
292
|
-
nativeSessionId: isString, instanceId: isString, bindingId: isString, renewalRevision: isNumber, expiresAtEpochMs: isNumber } },
|
|
293
281
|
agent_answer: { required: { sessionRef: isString, requestId: isString, outcome: isString } },
|
|
294
282
|
// `content`'s interior is never validated here -- it is ACP's free-form elicitation payload, and
|
|
295
283
|
// this table is transport, not translator. `isRecord` only rejects a non-object.
|
|
@@ -773,7 +761,6 @@ export function outboundFailureReply(frame, reason) {
|
|
|
773
761
|
case 'agent_steered': return { t: 'agent_steered', id, ok: false, error };
|
|
774
762
|
case 'agent_account_read_result': return { t: 'agent_account_read_result', id, ok: false, error };
|
|
775
763
|
case 'agent_config_option_set': return { t: 'agent_config_option_set', id, ok: false, error };
|
|
776
|
-
case 'agent_delegation_renewed': return { t: 'agent_delegation_renewed', id, ok: false, error };
|
|
777
764
|
case 'agent_switch_idle_result': return { t: 'agent_switch_idle_result', id, ok: false, error };
|
|
778
765
|
case 'agent_provider_task_stopped': return { t: 'agent_provider_task_stopped', id, state: 'unknown', error };
|
|
779
766
|
case 'agent_backfill_done':
|
|
@@ -790,7 +777,7 @@ export function decodeAgentFrame(text) {
|
|
|
790
777
|
if ((frame.state === 'completed' || frame.state === 'failed' || frame.state === 'stopped') && frame.live)
|
|
791
778
|
throw new TypeError('Invalid provider task terminal live state');
|
|
792
779
|
if (frame.status !== undefined &&
|
|
793
|
-
(frame.status !== frame.state || !['completed', 'failed', 'stopped'].
|
|
780
|
+
(frame.status !== frame.state || !['completed', 'failed', 'stopped'].some(status => status === frame.status)))
|
|
794
781
|
throw new TypeError('Invalid provider task terminal status');
|
|
795
782
|
if (frame.summary !== undefined && frame.status === undefined)
|
|
796
783
|
throw new TypeError('Provider task summary requires terminal status');
|