@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
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* that version, a copy of pristine is patched into `patched/<digest>/`. Launch then runs the package's
|
|
5
5
|
* `bin` directly with the daemon's own Node — no npx, no network, nothing resolved at open time.
|
|
6
6
|
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
7
8
|
import { execFile } from 'node:child_process';
|
|
8
9
|
import { existsSync, statSync } from 'node:fs';
|
|
9
|
-
import { open, readFile } from 'node:fs/promises';
|
|
10
|
+
import { open, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { createPatchedTree, expectedPatchedFiles, patchDigest, selectPackagePatches, verifyPatchedFiles } from '../../managedPatches.js';
|
|
13
|
+
import { installPolicyFor } from '../../patchAssets.js';
|
|
14
|
+
import { assertRuntimeRequest, loadRuntimeRelease, runtimeRecipes, verifyRuntimeTree, verifyManagedRuntime } from '../../managedRuntime.js';
|
|
12
15
|
import { assertSafePackageName, assertSafeVersion, buildIdFor, inside, npmPatchedDir, npmPristineDir, } from '../model.js';
|
|
13
16
|
import { assertBuildRunsOn } from '../nodeGate.js';
|
|
14
17
|
import { publishStaged } from '../store.js';
|
|
@@ -42,9 +45,9 @@ export function runNpmViewLatest(npmCommand, name, timeoutMs) {
|
|
|
42
45
|
});
|
|
43
46
|
}
|
|
44
47
|
/** `npm install --prefix <prefix> --no-save --no-audit --no-fund <spec>`, argv only, never a shell. */
|
|
45
|
-
export function runNpmInstall(npmCommand, prefix, spec, timeoutMs) {
|
|
48
|
+
export function runNpmInstall(npmCommand, prefix, spec, timeoutMs, options = {}) {
|
|
46
49
|
return new Promise((resolve) => {
|
|
47
|
-
execFile(npmCommand, ['install', '--prefix', prefix, '--no-save', '--no-audit', '--no-fund', spec], { timeout: timeoutMs, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
50
|
+
execFile(npmCommand, ['install', '--prefix', prefix, '--no-save', '--no-audit', '--no-fund', ...(options.ignoreScripts ? ['--ignore-scripts'] : []), spec], { timeout: timeoutMs, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
48
51
|
if (!error) {
|
|
49
52
|
resolve({ ok: true });
|
|
50
53
|
return;
|
|
@@ -68,6 +71,36 @@ async function installedVersion(treeRoot, name) {
|
|
|
68
71
|
const metadata = await readPackage(inside(path.join(treeRoot, 'node_modules'), name));
|
|
69
72
|
return metadata?.name === name && typeof metadata.version === 'string' ? metadata.version : undefined;
|
|
70
73
|
}
|
|
74
|
+
/** Download server bytes, verifying npm's base64 sha512 before writing a local archive. */
|
|
75
|
+
export async function fetchVerifiedTarball(options) {
|
|
76
|
+
const response = await fetch(options.url, { signal: AbortSignal.timeout(options.timeoutMs) });
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
throw new Error(`download ${options.url} failed: HTTP ${response.status}`);
|
|
79
|
+
const body = Buffer.from(await response.arrayBuffer());
|
|
80
|
+
if (createHash('sha512').update(body).digest('base64') !== options.sha512)
|
|
81
|
+
throw new Error(`tarball sha512 mismatch for ${options.url}`);
|
|
82
|
+
const file = path.join(options.dir, 'adapter.tgz');
|
|
83
|
+
await writeFile(file, body);
|
|
84
|
+
return file;
|
|
85
|
+
}
|
|
86
|
+
/** Read actual installed top-level package identities, including scoped dependencies. */
|
|
87
|
+
export async function installedDependencies(treeRoot) {
|
|
88
|
+
const modules = path.join(treeRoot, 'node_modules');
|
|
89
|
+
const dependencies = {};
|
|
90
|
+
for (const entry of await readdir(modules, { withFileTypes: true })) {
|
|
91
|
+
if (entry.name.startsWith('.'))
|
|
92
|
+
continue;
|
|
93
|
+
const names = entry.name.startsWith('@')
|
|
94
|
+
? (await readdir(path.join(modules, entry.name))).map((name) => `${entry.name}/${name}`)
|
|
95
|
+
: [entry.name];
|
|
96
|
+
for (const name of names) {
|
|
97
|
+
const metadata = await readPackage(inside(modules, name));
|
|
98
|
+
if (metadata?.name === name && typeof metadata.version === 'string')
|
|
99
|
+
dependencies[name] = metadata.version;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return dependencies;
|
|
103
|
+
}
|
|
71
104
|
/**
|
|
72
105
|
* Make sure `npm/<sha16(name)>/<version>/pristine` exists. `version` undefined = unpinned (npm's
|
|
73
106
|
* `latest`, the install-time ETARGET fallback): the version is read back off what npm installed.
|
|
@@ -76,6 +109,14 @@ async function installedVersion(treeRoot, name) {
|
|
|
76
109
|
export async function ensurePristine(options) {
|
|
77
110
|
const { store, name } = options;
|
|
78
111
|
assertSafePackageName(name);
|
|
112
|
+
if (options.policy) {
|
|
113
|
+
assertRuntimeRequest(options.policy, name, options.version);
|
|
114
|
+
const release = await loadRuntimeRelease(options.policy, options);
|
|
115
|
+
if (options.tarball && options.tarball.sha512 !== release.integrity.slice('sha512-'.length))
|
|
116
|
+
throw new Error('Managed runtime archive integrity differs from bundled release');
|
|
117
|
+
}
|
|
118
|
+
if (options.tarball && options.version === undefined)
|
|
119
|
+
throw new Error('Server adapter tarball requires a pinned version');
|
|
79
120
|
if (options.version === undefined) {
|
|
80
121
|
// Unpinned (the install-time ETARGET fallback): name the version first, so a `latest` that is
|
|
81
122
|
// already installed costs one metadata request and no download.
|
|
@@ -95,13 +136,26 @@ export async function ensurePristine(options) {
|
|
|
95
136
|
}
|
|
96
137
|
if (options.version !== undefined) {
|
|
97
138
|
const final = inside(store.root, npmPristineDir(name, options.version));
|
|
139
|
+
if (existsSync(final) && options.policy) {
|
|
140
|
+
await verifyRuntimeTree({ ...options, policy: options.policy, treeRoot: final, pristine: true });
|
|
141
|
+
return { ok: true, version: options.version, downloaded: false };
|
|
142
|
+
}
|
|
98
143
|
if ((await installedVersion(final, name)) === options.version)
|
|
99
144
|
return { ok: true, version: options.version, downloaded: false };
|
|
100
145
|
}
|
|
101
146
|
const staged = await store.stagingDir();
|
|
102
147
|
try {
|
|
103
148
|
const spec = options.version === undefined ? name : `${name}@${options.version}`;
|
|
104
|
-
|
|
149
|
+
let tarball = options.tarball;
|
|
150
|
+
if (options.policy) {
|
|
151
|
+
const release = await loadRuntimeRelease(options.policy, options);
|
|
152
|
+
const sha512 = release.integrity.slice('sha512-'.length);
|
|
153
|
+
if (tarball && tarball.sha512 !== sha512)
|
|
154
|
+
throw new Error('Managed runtime archive integrity differs from bundled release');
|
|
155
|
+
tarball ??= await runtimeArchive(options.npmCommand, spec, release.integrity, options.timeoutMs);
|
|
156
|
+
}
|
|
157
|
+
const archive = tarball ? await fetchVerifiedTarball({ ...tarball, dir: staged, timeoutMs: options.timeoutMs }) : undefined;
|
|
158
|
+
const ran = await runNpmInstall(options.npmCommand, staged, archive ?? spec, options.timeoutMs, { ignoreScripts: options.policy?.ignore_scripts ?? installPolicyFor(name)?.ignore_scripts });
|
|
105
159
|
if (!ran.ok)
|
|
106
160
|
return ran;
|
|
107
161
|
const version = await installedVersion(staged, name);
|
|
@@ -111,7 +165,14 @@ export async function ensurePristine(options) {
|
|
|
111
165
|
return { ok: false, error: `npm installed ${name}@${version} when ${spec} was asked for`, stderr: '' };
|
|
112
166
|
}
|
|
113
167
|
assertSafeVersion(version);
|
|
168
|
+
if (options.policy)
|
|
169
|
+
await verifyRuntimeTree({ ...options, policy: options.policy, treeRoot: staged, pristine: true, prepareTools: true });
|
|
170
|
+
if (archive)
|
|
171
|
+
await rm(archive);
|
|
114
172
|
const { published } = await publishStaged(staged, inside(store.root, npmPristineDir(name, version)));
|
|
173
|
+
// publishStaged may choose an existing winner: verify that actual tree, never just staging.
|
|
174
|
+
if (options.policy)
|
|
175
|
+
await verifyRuntimeTree({ ...options, policy: options.policy, treeRoot: inside(store.root, npmPristineDir(name, version)), pristine: true });
|
|
115
176
|
// Not published = that version's pristine was already there (we only learned the version now).
|
|
116
177
|
return { ok: true, version, downloaded: published };
|
|
117
178
|
}
|
|
@@ -168,9 +229,12 @@ async function runtimeOf(file) {
|
|
|
168
229
|
*/
|
|
169
230
|
export async function npmBuild(options) {
|
|
170
231
|
const { store, name, version } = options;
|
|
232
|
+
if (options.policy)
|
|
233
|
+
assertRuntimeRequest(options.policy, name, version);
|
|
171
234
|
const pristineRel = npmPristineDir(name, version);
|
|
172
235
|
const pristine = inside(store.root, pristineRel);
|
|
173
|
-
const
|
|
236
|
+
const release = options.policy ? await verifyRuntimeTree({ ...options, policy: options.policy, treeRoot: pristine, pristine: true }) : undefined;
|
|
237
|
+
const recipes = release && options.policy ? runtimeRecipes(options.policy, release, options.recipes) : await selectPackagePatches(name, version, inside(path.join(pristine, 'node_modules'), name), options.recipes, await installedDependencies(pristine));
|
|
174
238
|
let dir = pristineRel;
|
|
175
239
|
let patch = {};
|
|
176
240
|
if (recipes.length) {
|
|
@@ -180,7 +244,7 @@ export async function npmBuild(options) {
|
|
|
180
244
|
if (!existsSync(final)) {
|
|
181
245
|
const staged = await store.stagingDir();
|
|
182
246
|
try {
|
|
183
|
-
await createPatchedTree(pristine, staged, name, version, recipes);
|
|
247
|
+
await createPatchedTree(pristine, staged, name, version, recipes, options.assetRoot);
|
|
184
248
|
await publishStaged(staged, final);
|
|
185
249
|
}
|
|
186
250
|
finally {
|
|
@@ -189,22 +253,24 @@ export async function npmBuild(options) {
|
|
|
189
253
|
}
|
|
190
254
|
// Whoever published it, the tree's receipt must be for exactly this recipe set.
|
|
191
255
|
const receipt = JSON.parse(await readFile(path.join(final, 'axom-patches.json'), 'utf8'));
|
|
256
|
+
if (receipt.patchedFilesBase !== undefined && receipt.patchedFilesBase !== 'node_modules')
|
|
257
|
+
throw new Error('Invalid Axom patched file base');
|
|
192
258
|
if (receipt.digest !== digest)
|
|
193
259
|
throw new Error('Axom adapter patch receipt does not match this CLI');
|
|
194
260
|
// Expected hashes come from the shipped recipes, never from the writable receipt alone.
|
|
195
261
|
const files = expectedPatchedFiles(recipes);
|
|
196
|
-
await verifyPatchedFiles(
|
|
197
|
-
patch = { patchDigest: digest, patches: recipes.map((recipe) => recipe.id), patchedFiles: files };
|
|
262
|
+
await verifyPatchedFiles(path.join(final, 'node_modules'), files);
|
|
263
|
+
patch = { patchedFilesBase: 'node_modules', patchDigest: digest, patches: recipes.map((recipe) => recipe.id), patchedFiles: files };
|
|
198
264
|
}
|
|
199
265
|
const root = inside(store.root, dir);
|
|
200
266
|
const packageRel = `node_modules/${name}`;
|
|
201
267
|
const metadata = await readPackage(inside(root, packageRel));
|
|
202
268
|
if (metadata?.name !== name || metadata.version !== version)
|
|
203
269
|
throw new Error(`Adapter package identity changed under ${dir}`);
|
|
204
|
-
const explicit = [...new Set(recipes.map((recipe) => recipe.bin).filter((bin) => bin !== undefined))];
|
|
270
|
+
const explicit = [...new Set(recipes.filter((recipe) => recipe.package === name).map((recipe) => recipe.bin).filter((bin) => bin !== undefined))];
|
|
205
271
|
if (explicit.length > 1)
|
|
206
272
|
throw new Error('Axom patches disagree about the adapter executable');
|
|
207
|
-
const entryRel = path.posix.normalize(`${packageRel}/${selectBin(name, metadata.bin, explicit[0])}`);
|
|
273
|
+
const entryRel = path.posix.normalize(`${packageRel}/${selectBin(name, metadata.bin, release?.bin.name ?? explicit[0])}`);
|
|
208
274
|
const entry = inside(root, entryRel);
|
|
209
275
|
if (!entryRel.startsWith(`${packageRel}/`) || !statSync(entry).isFile())
|
|
210
276
|
throw new Error(`Adapter executable is missing in ${dir}`);
|
|
@@ -217,6 +283,7 @@ export async function npmBuild(options) {
|
|
|
217
283
|
};
|
|
218
284
|
const build = {
|
|
219
285
|
buildId: buildIdFor(version, patch.patchDigest),
|
|
286
|
+
...(release ? { runtimeReleaseId: release.id } : {}),
|
|
220
287
|
agentId: options.agentId,
|
|
221
288
|
kind: patch.patchDigest ? 'npm-patched' : 'npm',
|
|
222
289
|
version,
|
|
@@ -231,5 +298,27 @@ export async function npmBuild(options) {
|
|
|
231
298
|
};
|
|
232
299
|
// Never hand back (so never commit, never make current) a build its own Node gate refuses.
|
|
233
300
|
await assertBuildRunsOn(build, store.root, options.nodePath);
|
|
301
|
+
if (options.policy)
|
|
302
|
+
await verifyManagedRuntime({ ...options, policy: options.policy, build, storeRoot: store.root, initialize: true });
|
|
234
303
|
return build;
|
|
235
304
|
}
|
|
305
|
+
/** Registry metadata supplies only a location; its digest must equal the bundled immutable release. */
|
|
306
|
+
async function runtimeArchive(npmCommand, spec, integrity, timeoutMs) {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
execFile(npmCommand, ['view', spec, 'dist', '--json'], { timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
309
|
+
if (error) {
|
|
310
|
+
reject(new Error(`Managed runtime exact archive lookup failed: ${stderr.toString().slice(-2048) || error.message}`));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
const dist = JSON.parse(stdout.toString());
|
|
315
|
+
if (dist.integrity !== integrity || typeof dist.tarball !== 'string' || !/^https?:\/\//.test(dist.tarball))
|
|
316
|
+
throw new Error('Managed runtime archive metadata integrity mismatch');
|
|
317
|
+
resolve({ url: dist.tarball, sha512: integrity.slice('sha512-'.length) });
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
reject(error);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `processor/initialize.test.ts` then guards the processor's *wiring* (the right line, the right
|
|
14
14
|
* result), not a second copy of these rules.
|
|
15
15
|
*
|
|
16
|
-
* The
|
|
16
|
+
* The runtime profile in `_meta` is deliberately not read here: it belongs to the connection's
|
|
17
17
|
* runtime diagnostics, which the processor takes over in Task 17b/17c.
|
|
18
18
|
*/
|
|
19
19
|
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
|
|
@@ -81,6 +81,7 @@ export function initializeRequestParams() {
|
|
|
81
81
|
// on another machine cannot do; the standard `authenticate` methods (device-code style) are
|
|
82
82
|
// unaffected and never needed this key.
|
|
83
83
|
'terminal-auth': true,
|
|
84
|
+
terminal_output_delta: true,
|
|
84
85
|
'subagent-transcript': true,
|
|
85
86
|
...airCapabilityMeta(AIR_NATIVE_SUBAGENT_SESSIONS_CAPABILITY, AIR_ASYNC_TASKS_CAPABILITY),
|
|
86
87
|
},
|
|
@@ -110,6 +111,9 @@ export function parseInitializeResult(initResponse) {
|
|
|
110
111
|
if (initResponse.protocolVersion !== PROTOCOL_VERSION) {
|
|
111
112
|
return { protocolSupported: false, protocolVersion: initResponse.protocolVersion };
|
|
112
113
|
}
|
|
114
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
115
|
+
const axomMeta = isRecord(initResponse._meta) && isRecord(initResponse._meta.axom) ? initResponse._meta.axom : undefined;
|
|
116
|
+
const cachedReconnect = axomMeta?.cachedReconnect === 'resume' ? 'resume' : undefined;
|
|
113
117
|
const sessionCapabilities = initResponse.agentCapabilities?.sessionCapabilities;
|
|
114
118
|
const capabilities = {
|
|
115
119
|
// Capability markers are empty objects (`{ _meta? }`); supplying `{}` means "supported", and
|
|
@@ -155,6 +159,7 @@ export function parseInitializeResult(initResponse) {
|
|
|
155
159
|
}
|
|
156
160
|
return {
|
|
157
161
|
protocolSupported: true,
|
|
162
|
+
...(cachedReconnect ? { cachedReconnect } : {}),
|
|
158
163
|
capabilities,
|
|
159
164
|
authMethods,
|
|
160
165
|
droppedAuthMethods: advertisedAuthMethods.length - authMethods.length,
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
7
|
import { constants } from 'node:fs';
|
|
8
|
-
import { cp,
|
|
8
|
+
import { cp, lstat, mkdir, open, realpath, writeFile } from 'node:fs/promises';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { assertSafePackageName, inside } from './builds/model.js';
|
|
12
|
+
import { packagePatchMatch, packagePatches, patchAssetRoot, validatePackagePatch, validatePatchPath } from './patchAssets.js';
|
|
12
13
|
const sha = (content) => createHash('sha256').update(content).digest('hex');
|
|
13
14
|
const warned = new Set();
|
|
14
15
|
function warnOnce(key, message) {
|
|
@@ -18,30 +19,149 @@ function warnOnce(key, message) {
|
|
|
18
19
|
console.warn(message);
|
|
19
20
|
}
|
|
20
21
|
function within(root, relative) {
|
|
22
|
+
validatePatchPath(relative);
|
|
21
23
|
const target = path.resolve(root, relative);
|
|
22
24
|
if (!target.startsWith(path.resolve(root) + path.sep))
|
|
23
25
|
throw new Error('Axom patch path escapes its package');
|
|
24
26
|
return target;
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Check every component, including the supplied root and its ancestors, without following links.
|
|
30
|
+
* The one links crossed are root-owned system symlinks (`/var -> /private/var` on macOS): stock
|
|
31
|
+
* macOS hands out per-user tmp dirs behind one, and refusing it would reject every tmp-scoped
|
|
32
|
+
* tree. A root-owned link to a root-owned, non-group-writable directory is not plantable by the
|
|
33
|
+
* running user; every user-owned symlink is still refused.
|
|
34
|
+
*/
|
|
35
|
+
async function noSymlinks(filename, missing = false) {
|
|
36
|
+
const absolute = path.resolve(filename);
|
|
37
|
+
let current = path.parse(absolute).root;
|
|
38
|
+
const systemSymlink = async (uid) => {
|
|
39
|
+
if (uid !== 0 || process.getuid?.() === 0)
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
const target = await realpath(current);
|
|
43
|
+
const stat = await lstat(target);
|
|
44
|
+
return stat.isDirectory() && stat.uid === 0 && (stat.mode & 0o022) === 0;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
for (const part of absolute.slice(current.length).split(path.sep).filter(Boolean)) {
|
|
51
|
+
current = path.join(current, part);
|
|
52
|
+
try {
|
|
53
|
+
const info = await lstat(current);
|
|
54
|
+
if (info.isSymbolicLink() && !(await systemSymlink(info.uid)))
|
|
55
|
+
throw new Error(`Axom patch symlink is forbidden: ${current}`);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (missing && error.code === 'ENOENT')
|
|
59
|
+
return;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function readOwnedFile(filename) {
|
|
65
|
+
await noSymlinks(filename);
|
|
66
|
+
const handle = await open(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
67
|
+
try {
|
|
68
|
+
if (!(await handle.stat()).isFile())
|
|
69
|
+
throw new Error(`Axom patch requires a regular file: ${filename}`);
|
|
70
|
+
return await handle.readFile();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function makeOwnedDirectories(directory) {
|
|
77
|
+
await noSymlinks(directory, true);
|
|
78
|
+
const parent = path.dirname(directory);
|
|
79
|
+
if (parent !== directory) {
|
|
80
|
+
try {
|
|
81
|
+
await lstat(directory);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code !== 'ENOENT')
|
|
85
|
+
throw error;
|
|
86
|
+
await makeOwnedDirectories(parent);
|
|
87
|
+
try {
|
|
88
|
+
await mkdir(directory);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code !== 'EEXIST')
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
await noSymlinks(directory);
|
|
97
|
+
if (!(await lstat(directory)).isDirectory())
|
|
98
|
+
throw new Error(`Axom patch parent is not a directory: ${directory}`);
|
|
99
|
+
}
|
|
26
100
|
/** A matching version and exact original bytes; already-patched or changed originals are rejected. */
|
|
27
|
-
export async function applyPackagePatches(
|
|
101
|
+
export async function applyPackagePatches(nodeModulesDir, recipes, assetRoot = patchAssetRoot) {
|
|
28
102
|
const output = {};
|
|
103
|
+
// Validate the complete declaration and asset closure before any writes.
|
|
104
|
+
expectedPatchedFiles(recipes);
|
|
105
|
+
const assets = new Map();
|
|
29
106
|
for (const recipe of recipes) {
|
|
30
|
-
const
|
|
107
|
+
const packageDir = inside(nodeModulesDir, recipe.package);
|
|
108
|
+
const metadata = JSON.parse((await readOwnedFile(path.join(packageDir, 'package.json'))).toString('utf8'));
|
|
31
109
|
if (metadata.name !== recipe.package || !packagePatchMatch(recipe, metadata.version)) {
|
|
32
110
|
throw new Error(`Axom patch '${recipe.id}' does not support ${metadata.name}@${metadata.version}`);
|
|
33
111
|
}
|
|
112
|
+
for (const edit of recipe.edits)
|
|
113
|
+
await noSymlinks(within(packageDir, edit.file));
|
|
114
|
+
for (const asset of recipe.assets ?? []) {
|
|
115
|
+
const filename = within(packageDir, asset.target);
|
|
116
|
+
await noSymlinks(filename, true);
|
|
117
|
+
try {
|
|
118
|
+
await lstat(filename);
|
|
119
|
+
throw new Error(`Axom patch asset collision: target already exists: ${asset.target}`);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== 'ENOENT')
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const bytes = await readOwnedFile(within(assetRoot instanceof URL ? fileURLToPath(assetRoot) : assetRoot, asset.source));
|
|
126
|
+
if (sha(bytes) !== asset.sha256)
|
|
127
|
+
throw new Error(`Axom patch asset hash mismatch: ${asset.source}`);
|
|
128
|
+
assets.set(`${recipe.package}/${asset.target}`, bytes);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const recipe of recipes) {
|
|
132
|
+
const packageDir = inside(nodeModulesDir, recipe.package);
|
|
34
133
|
for (const edit of recipe.edits) {
|
|
35
134
|
const filename = within(packageDir, edit.file);
|
|
36
|
-
const original = await
|
|
135
|
+
const original = (await readOwnedFile(filename)).toString('utf8');
|
|
37
136
|
if (sha(original) !== edit.before_sha256 || !edit.before || original.split(edit.before).length !== 2) {
|
|
38
137
|
throw new Error(`Axom patch '${recipe.id}': original bytes changed in ${edit.file}`);
|
|
39
138
|
}
|
|
40
139
|
const patched = original.replace(edit.before, edit.after);
|
|
41
140
|
if (sha(patched) !== edit.after_sha256)
|
|
42
141
|
throw new Error(`Axom patch '${recipe.id}': invalid expected output`);
|
|
43
|
-
await
|
|
44
|
-
|
|
142
|
+
await noSymlinks(filename);
|
|
143
|
+
const handle = await open(filename, constants.O_WRONLY | constants.O_NOFOLLOW);
|
|
144
|
+
try {
|
|
145
|
+
await handle.truncate(0);
|
|
146
|
+
await handle.writeFile(patched, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
await handle.close();
|
|
150
|
+
}
|
|
151
|
+
output[`${recipe.package}/${edit.file}`] = sha(patched);
|
|
152
|
+
}
|
|
153
|
+
for (const asset of recipe.assets ?? []) {
|
|
154
|
+
const filename = within(packageDir, asset.target);
|
|
155
|
+
await makeOwnedDirectories(path.dirname(filename));
|
|
156
|
+
await noSymlinks(filename, true);
|
|
157
|
+
const handle = await open(filename, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o644);
|
|
158
|
+
try {
|
|
159
|
+
await handle.writeFile(assets.get(`${recipe.package}/${asset.target}`));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await handle.close();
|
|
163
|
+
}
|
|
164
|
+
output[`${recipe.package}/${asset.target}`] = asset.sha256;
|
|
45
165
|
}
|
|
46
166
|
}
|
|
47
167
|
return output;
|
|
@@ -53,7 +173,7 @@ async function originalsFit(packageDir, recipe) {
|
|
|
53
173
|
let content = files.get(edit.file);
|
|
54
174
|
if (content === undefined) {
|
|
55
175
|
try {
|
|
56
|
-
content = await
|
|
176
|
+
content = (await readOwnedFile(within(packageDir, edit.file))).toString('utf8');
|
|
57
177
|
}
|
|
58
178
|
catch {
|
|
59
179
|
return false;
|
|
@@ -71,22 +191,25 @@ async function originalsFit(packageDir, recipe) {
|
|
|
71
191
|
* patch step loudly), range recipes only when the pristine bytes fit (otherwise upstream runs, with a
|
|
72
192
|
* warning). An empty answer means the build is the pristine tree itself.
|
|
73
193
|
*/
|
|
74
|
-
export async function selectPackagePatches(name, version, pristinePackageDir, recipes = packagePatches) {
|
|
75
|
-
const candidates = recipes.filter((patch) => patch.package === name);
|
|
194
|
+
export async function selectPackagePatches(name, version, pristinePackageDir, recipes = packagePatches, dependencies = {}) {
|
|
195
|
+
const candidates = recipes.filter((patch) => patch.package === name || Object.hasOwn(dependencies, patch.package));
|
|
76
196
|
const matched = candidates.flatMap((recipe) => {
|
|
77
|
-
const match = packagePatchMatch(recipe, version);
|
|
197
|
+
const match = packagePatchMatch(recipe, recipe.package === name ? version : dependencies[recipe.package]);
|
|
78
198
|
return match ? [{ recipe, match }] : [];
|
|
79
199
|
});
|
|
80
200
|
if (matched.length && (!/^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/i.test(name) || !/^[0-9a-z.+-]+$/i.test(version))) {
|
|
81
201
|
throw new Error('Invalid package identity for Axom adapter patch');
|
|
82
202
|
}
|
|
83
203
|
const selected = [];
|
|
204
|
+
const nodeModulesDir = path.resolve(pristinePackageDir, ...name.split('/').map(() => '..'));
|
|
84
205
|
for (const { recipe, match } of matched) {
|
|
206
|
+
assertSafePackageName(recipe.package);
|
|
207
|
+
const packageDir = recipe.package === name ? pristinePackageDir : inside(nodeModulesDir, recipe.package);
|
|
85
208
|
if (match === 'exact') {
|
|
86
209
|
selected.push(recipe);
|
|
87
210
|
continue;
|
|
88
211
|
}
|
|
89
|
-
if (await originalsFit(
|
|
212
|
+
if (await originalsFit(packageDir, recipe))
|
|
90
213
|
selected.push(recipe);
|
|
91
214
|
else
|
|
92
215
|
warnOnce(`${recipe.id}@${name}@${version}`, `[axom-patch] ${recipe.id} does not fit ${name}@${version}; using the upstream package. Review the patch inventory.`);
|
|
@@ -104,39 +227,80 @@ export function patchDigest(recipes) {
|
|
|
104
227
|
* Copy the pristine `node_modules` (reflink where the filesystem supports it — `COPYFILE_FICLONE`
|
|
105
228
|
* falls back to an ordinary copy by itself; relative symlinks such as `.bin/*` are kept relative so
|
|
106
229
|
* the copy never points back into pristine) into `stagedRoot`, patch the copy, and write a receipt.
|
|
107
|
-
* Returns the sha256 of every patched file,
|
|
230
|
+
* Returns the sha256 of every patched file, qualified by its package name under node_modules.
|
|
108
231
|
*/
|
|
109
|
-
export async function createPatchedTree(pristineRoot, stagedRoot, name, version, recipes) {
|
|
232
|
+
export async function createPatchedTree(pristineRoot, stagedRoot, name, version, recipes, assetRoot = patchAssetRoot) {
|
|
233
|
+
const original = path.resolve(pristineRoot, 'node_modules');
|
|
234
|
+
const copy = path.resolve(stagedRoot, 'node_modules');
|
|
235
|
+
if (original === copy || original.startsWith(copy + path.sep) || copy.startsWith(original + path.sep)) {
|
|
236
|
+
throw new Error('Axom patch staged tree must not overlap pristine');
|
|
237
|
+
}
|
|
238
|
+
await noSymlinks(original);
|
|
239
|
+
const receipt = path.join(stagedRoot, 'axom-patches.json');
|
|
240
|
+
for (const destination of [copy, receipt]) {
|
|
241
|
+
await noSymlinks(destination, true);
|
|
242
|
+
try {
|
|
243
|
+
await lstat(destination);
|
|
244
|
+
throw new Error(`Axom patch staged target already exists: ${destination}`);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (error.code !== 'ENOENT')
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
110
251
|
await cp(path.join(pristineRoot, 'node_modules'), path.join(stagedRoot, 'node_modules'), {
|
|
111
252
|
recursive: true, verbatimSymlinks: true, mode: constants.COPYFILE_FICLONE,
|
|
112
253
|
});
|
|
113
|
-
const
|
|
114
|
-
const files = await applyPackagePatches(packageDir, recipes);
|
|
254
|
+
const files = await applyPackagePatches(path.join(stagedRoot, 'node_modules'), recipes, assetRoot);
|
|
115
255
|
const expected = expectedPatchedFiles(recipes);
|
|
116
256
|
if (JSON.stringify(sortKeys(files)) !== JSON.stringify(sortKeys(expected)))
|
|
117
257
|
throw new Error('Incomplete Axom adapter patch');
|
|
118
|
-
await
|
|
119
|
-
|
|
120
|
-
|
|
258
|
+
await noSymlinks(receipt, true);
|
|
259
|
+
await writeFile(receipt, JSON.stringify({
|
|
260
|
+
patchedFilesBase: 'node_modules', digest: patchDigest(recipes), package: name, version, patches: recipes.map((recipe) => recipe.id), files,
|
|
261
|
+
}) + '\n', { flag: 'wx' });
|
|
121
262
|
console.info(`[axom-patch] Applied ${recipes.map((recipe) => recipe.id).join(', ')} to ${name}@${version}`);
|
|
122
263
|
return files;
|
|
123
264
|
}
|
|
124
265
|
/** The files a recipe set rewrites and their expected sha256, derived from the shipped recipes. */
|
|
125
266
|
export function expectedPatchedFiles(recipes) {
|
|
126
|
-
|
|
267
|
+
const files = {};
|
|
268
|
+
const assetTargets = new Set();
|
|
269
|
+
const editTargets = new Set();
|
|
270
|
+
for (const recipe of recipes) {
|
|
271
|
+
assertSafePackageName(recipe.package);
|
|
272
|
+
validatePackagePatch(recipe);
|
|
273
|
+
for (const edit of recipe.edits) {
|
|
274
|
+
const key = `${recipe.package}/${edit.file}`;
|
|
275
|
+
editTargets.add(key);
|
|
276
|
+
files[key] = edit.after_sha256;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const recipe of recipes)
|
|
280
|
+
for (const asset of recipe.assets ?? []) {
|
|
281
|
+
const key = `${recipe.package}/${asset.target}`;
|
|
282
|
+
if ([...assetTargets, ...editTargets].some(other => other === key || other.startsWith(key + '/') || key.startsWith(other + '/'))) {
|
|
283
|
+
throw new Error(`Axom patch asset collision: ${key}`);
|
|
284
|
+
}
|
|
285
|
+
assetTargets.add(key);
|
|
286
|
+
files[key] = asset.sha256;
|
|
287
|
+
}
|
|
288
|
+
return files;
|
|
127
289
|
}
|
|
128
290
|
function sortKeys(record) {
|
|
129
291
|
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
130
292
|
}
|
|
131
|
-
/**
|
|
293
|
+
/** Tamper check against an explicit base: node_modules for qualified keys, adapter package for legacy keys. */
|
|
132
294
|
export async function verifyPatchedFiles(packageDir, files) {
|
|
133
295
|
for (const [file, hash] of Object.entries(files)) {
|
|
134
296
|
let content;
|
|
135
297
|
try {
|
|
136
|
-
content = await
|
|
298
|
+
content = await readOwnedFile(within(packageDir, file));
|
|
137
299
|
}
|
|
138
|
-
catch {
|
|
139
|
-
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error.code === 'ENOENT')
|
|
302
|
+
throw new Error(`Axom patched adapter is missing ${file}`);
|
|
303
|
+
throw new Error(`Axom patched adapter was modified: ${file} (${error.message})`);
|
|
140
304
|
}
|
|
141
305
|
if (sha(content) !== hash)
|
|
142
306
|
throw new Error(`Axom patched adapter was modified: ${file}`);
|