@platless/axom-cli 0.3.13 → 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 +119 -9
- package/dist/axom-patch/cli/account-limits/README.md +27 -0
- package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
- package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
- package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
- package/dist/axom-patch/cli/account-limits/generate.py +123 -0
- package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
- package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
- package/dist/axom-patch/cli/claude-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-account-limits.json +37 -0
- package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
- package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
- 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 +194 -0
- package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
- package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
- 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/pi-subagent-subwork.mjs +211 -0
- package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -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 -5
- package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
- 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 +39 -15
- package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
- package/dist/axom-patch/cli/zcode-models.json +37 -0
- package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
- package/dist/cli.js +4 -9
- 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/keepers/acp/keeper.mjs +3083 -0
- package/dist/keepers/acp/manifest.json +9 -0
- package/dist/keepers/terminal/keeper.mjs +8469 -0
- package/dist/keepers/terminal/manifest.json +9 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
- package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
- package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
- package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
- package/dist/mcp.js +1 -1
- package/dist/native/darwin-arm64/axom-quic +0 -0
- package/dist/test/keeperKit.js +67 -0
- package/dist/workstation/acp/acpAgents.js +2192 -636
- package/dist/workstation/acp/acpClient.js +84 -495
- package/dist/workstation/acp/adoption.js +10 -396
- package/dist/workstation/acp/binaryInstall.js +100 -27
- package/dist/workstation/acp/builds/gc.js +245 -0
- package/dist/workstation/acp/builds/install/binary.js +28 -0
- package/dist/workstation/acp/builds/install/npm.js +324 -0
- package/dist/workstation/acp/builds/install/uv.js +254 -0
- package/dist/workstation/acp/builds/migrate.js +195 -0
- package/dist/workstation/acp/builds/model.js +128 -0
- package/dist/workstation/acp/builds/nodeGate.js +100 -0
- package/dist/workstation/acp/builds/store.js +352 -0
- package/dist/workstation/acp/commandTableProbe.js +68 -0
- package/dist/workstation/acp/configOptions.js +103 -1
- package/dist/workstation/acp/envSecrets.js +27 -0
- package/dist/workstation/acp/initializeResponse.js +290 -0
- package/dist/workstation/acp/installErrors.js +12 -2
- package/dist/workstation/acp/installProbe.js +1 -1
- package/dist/workstation/acp/managedPatches.js +239 -159
- package/dist/workstation/acp/managedRuntime.js +314 -0
- package/dist/workstation/acp/outbox.js +330 -0
- package/dist/workstation/acp/patchAssets.js +60 -3
- package/dist/workstation/acp/permission.js +42 -10
- package/dist/workstation/acp/processor/checkpoint.js +78 -0
- package/dist/workstation/acp/processor/commands.js +35 -0
- package/dist/workstation/acp/processor/control.js +282 -0
- package/dist/workstation/acp/processor/emit.js +26 -0
- package/dist/workstation/acp/processor/jsonrpc.js +172 -0
- package/dist/workstation/acp/processor/memoryLog.js +49 -0
- package/dist/workstation/acp/processor/origins.js +48 -0
- package/dist/workstation/acp/processor/output.js +67 -0
- package/dist/workstation/acp/processor/projection.js +42 -0
- package/dist/workstation/acp/processor/record.js +128 -0
- package/dist/workstation/acp/processor/runtime.js +105 -0
- package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
- package/dist/workstation/acp/processor/session.js +496 -0
- package/dist/workstation/acp/processor/state.js +67 -0
- package/dist/workstation/acp/processor/step.js +442 -0
- package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
- package/dist/workstation/acp/processor/test/harness.js +120 -0
- package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
- package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
- package/dist/workstation/acp/processor/transport/generations.js +76 -0
- package/dist/workstation/acp/processor/transport/keeper.js +130 -0
- package/dist/workstation/acp/processor/transport/local.js +269 -0
- package/dist/workstation/acp/processor/transport/types.js +78 -0
- package/dist/workstation/acp/processor/updates.js +390 -0
- package/dist/workstation/acp/processorSession.js +1348 -0
- package/dist/workstation/acp/registry/catalog.js +189 -41
- package/dist/workstation/acp/registry/launch.js +76 -119
- package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
- package/dist/workstation/acp/steering.js +58 -0
- package/dist/workstation/acp/switching.js +111 -0
- package/dist/workstation/acp/terminalLogin.js +16 -42
- package/dist/workstation/acp/terminalLoginSpec.js +41 -0
- package/dist/workstation/acp/test/adapterFixtures.js +256 -0
- package/dist/workstation/acp/test/daemonLife.js +106 -0
- package/dist/workstation/acp/test/fakeAgent.js +13 -0
- package/dist/workstation/acp/test/keeperWorld.js +295 -0
- package/dist/workstation/acp/test/memoryOutbox.js +55 -0
- package/dist/workstation/acp/test/raw/fixtures.js +196 -0
- package/dist/workstation/acp/test/raw/sanitize.js +344 -0
- package/dist/workstation/acp/test/raw/sources.js +135 -0
- package/dist/workstation/acp/test/realAdapter.js +331 -0
- package/dist/workstation/acp/test/scriptAgent.js +214 -0
- package/dist/workstation/acp/test/sessionFrames.js +42 -0
- package/dist/workstation/acp/timedCommand.js +4 -2
- package/dist/workstation/acp/uvxInstall.js +72 -122
- package/dist/workstation/agentFlow.js +103 -0
- package/dist/workstation/agentSessions.js +1289 -544
- package/dist/workstation/channels/agent.js +7 -5
- package/dist/workstation/channels/computer.js +69 -21
- package/dist/workstation/channels/computerExecs.js +200 -0
- package/dist/workstation/channels/management.js +172 -8
- 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 +393 -12
- package/dist/workstation/daemonctl.js +103 -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/exec/attach.js +126 -0
- package/dist/workstation/exec/command.js +40 -0
- package/dist/workstation/exec/durable.js +517 -0
- package/dist/workstation/exec/fallback.js +106 -0
- package/dist/workstation/exec/policy.js +837 -0
- package/dist/workstation/exec/shaping.js +211 -0
- package/dist/workstation/exec.js +3 -112
- package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
- package/dist/workstation/files/adapters/toolRequests.js +121 -0
- package/dist/workstation/files/client.js +455 -0
- package/dist/workstation/files/credentials.js +142 -0
- package/dist/workstation/files/errors.js +28 -0
- package/dist/workstation/files/fence.js +350 -0
- package/dist/workstation/files/find/fileIndex.js +153 -0
- package/dist/workstation/files/find/fuzzy.js +155 -0
- package/dist/workstation/files/host.js +122 -0
- package/dist/workstation/files/hostProtocol.js +2 -0
- package/dist/workstation/files/media/cache.js +620 -0
- package/dist/workstation/files/media/encoding.js +308 -0
- package/dist/workstation/files/media/estimator.js +100 -0
- package/dist/workstation/files/media/media.js +325 -0
- package/dist/workstation/files/media/packedAudio.js +23 -0
- package/dist/workstation/files/media/scheduler.js +551 -0
- package/dist/workstation/files/media/types.js +5 -0
- package/dist/workstation/files/scope.js +45 -0
- package/dist/workstation/files/structured.js +284 -0
- package/dist/workstation/{fileOps.js → files/text.js} +152 -39
- package/dist/workstation/files/uploads.js +351 -0
- package/dist/workstation/journal.js +65 -236
- package/dist/workstation/keepers/acp/client.js +101 -0
- package/dist/workstation/keepers/acp/envelope.js +62 -0
- package/dist/workstation/keepers/acp/host.js +419 -0
- package/dist/workstation/keepers/acp/main.js +18 -0
- package/dist/workstation/keepers/acp/memoryStream.js +121 -0
- package/dist/workstation/keepers/acp/params.js +137 -0
- package/dist/workstation/keepers/acp/protocol.js +66 -0
- package/dist/workstation/keepers/acp/recover.js +120 -0
- package/dist/workstation/keepers/acp/session.js +584 -0
- package/dist/workstation/keepers/acp/stderr.js +154 -0
- package/dist/workstation/keepers/kit/activity.js +103 -0
- package/dist/workstation/keepers/kit/bootstrap.js +164 -0
- package/dist/workstation/keepers/kit/buildVersion.js +51 -0
- package/dist/workstation/keepers/kit/bundle.js +124 -0
- package/dist/workstation/keepers/kit/client.js +432 -0
- package/dist/workstation/keepers/kit/identity.js +21 -0
- package/dist/workstation/keepers/kit/ipc.js +112 -0
- package/dist/workstation/keepers/kit/launchConfig.js +8 -0
- package/dist/workstation/keepers/kit/launcher.js +289 -0
- package/dist/workstation/keepers/kit/lock.js +123 -0
- package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
- package/dist/workstation/keepers/kit/paths.js +76 -0
- package/dist/workstation/keepers/kit/protocol.js +43 -0
- package/dist/workstation/keepers/kit/server.js +518 -0
- package/dist/workstation/keepers/kit/streamLog.js +697 -0
- package/dist/workstation/keepers/legacy.js +296 -0
- package/dist/workstation/keepers/policy.js +576 -0
- package/dist/workstation/keepers/stop.js +34 -0
- package/dist/workstation/keepers/terminal/client.js +213 -0
- package/dist/workstation/keepers/terminal/exec.js +981 -0
- package/dist/workstation/keepers/terminal/generations.js +129 -0
- package/dist/workstation/keepers/terminal/keeper.js +62 -0
- package/dist/workstation/keepers/terminal/main.js +43 -0
- package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
- package/dist/workstation/keepers/terminal/params.js +27 -0
- package/dist/workstation/keepers/terminal/protocol.js +56 -0
- package/dist/workstation/keepers/terminal/pty.js +330 -0
- package/dist/workstation/managedInstall.js +57 -0
- package/dist/workstation/protocol/accountLimits.js +47 -0
- package/dist/workstation/protocol/agent.js +50 -19
- package/dist/workstation/protocol/common.js +30 -0
- package/dist/workstation/protocol/computer.js +15 -4
- package/dist/workstation/protocol/management.js +141 -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 +154 -93
- package/dist/workstation/toolchain.js +94 -0
- package/dist/workstation/tools/browser.js +28 -4
- package/dist/workstation/tools/client.js +9 -1
- package/dist/workstation/tools/info.js +50 -0
- package/dist/workstation/tools/keeperProcess.js +15 -5
- package/dist/workstation/tools/router.js +55 -0
- package/dist/workstation/tools/terminalRouter.js +222 -0
- package/dist/workstation/tools/validation.js +37 -1
- package/dist/workstation/update/artifact.js +2 -1
- package/dist/workstation/update/coordinator.js +3 -0
- package/dist/workstation/update/globalInstall.js +116 -0
- package/dist/workstation/update/npmInstaller.js +49 -23
- package/dist/workstation/update/ptyPreflight.js +21 -0
- package/dist/workstation/update/rdRollback.js +145 -0
- package/dist/workstation/update/runUpdate.js +3 -2
- package/dist/workstation/update/taskRegistry.js +105 -11
- package/dist/workstation/update/watchdog.js +16790 -334
- package/package.json +11 -8
- 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 -49
- 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/workstation/acp/acpSession.js +0 -1475
- package/dist/workstation/acp/contract.js +0 -499
- package/dist/workstation/acp/delegationMcp.js +0 -24
- package/dist/workstation/acp/draftInterceptor.js +0 -163
- package/dist/workstation/acp/qoderStartup.js +0 -45
- package/dist/workstation/acp/sessionScope.js +0 -19
- package/dist/workstation/filePaths.js +0 -174
- package/dist/workstation/fileWorker.js +0 -81
- package/dist/workstation/fileWorkerClient.js +0 -121
- package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
- package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
- package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
- package/dist/workstation/qoderAdapter/contract.js +0 -78
- package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
- package/dist/workstation/qoderAdapter/history.js +0 -87
- package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
- package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
- package/dist/workstation/qoderAdapter/main.js +0 -93
- 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 -49
- package/dist/workstation/qoderAdapter/session.js +0 -319
- package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
- package/dist/workstation/qoderAdapter/subwork.js +0 -77
- package/dist/workstation/qoderAdapter/taskControl.js +0 -38
- package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
- package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
- package/dist/workstation/qoderAdapter/usage.js +0 -37
- package/dist/workstation/tools/core.js +0 -50
- package/dist/workstation/tools/files.js +0 -525
- package/dist/workstation/tools/terminal.js +0 -214
- /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { base64, CHUNK_BYTES, identifier, integer, keys, string } from '../tools/validation.js';
|
|
4
|
+
import { openMediaSource, pinned, resolveAbsolute } from './fence.js';
|
|
5
|
+
import { configuredRoots, filesystemScope, inside, READ_ONLY } from './scope.js';
|
|
6
|
+
import { MAX_UPLOAD_BYTES, TEMP_PREFIX, UPLOAD_TTL_MS, UploadStore } from './uploads.js';
|
|
7
|
+
import { FileIndexCache } from './find/fileIndex.js';
|
|
8
|
+
import { rankPaths } from './find/fuzzy.js';
|
|
9
|
+
export const LIST_MAX_ENTRIES = 2000;
|
|
10
|
+
export const LIST_MAX_VISITED = 10000;
|
|
11
|
+
export const FIND_DEFAULT_LIMIT = 50;
|
|
12
|
+
export const FIND_MAX_LIMIT = 200;
|
|
13
|
+
/**
|
|
14
|
+
* The Files tab's file semantics (`files.*` tool requests): absolute paths in the `configured-roots` scope, or in the
|
|
15
|
+
* `read-only` scope when a request says `readOnly: true`. Errors are thrown with the messages callers see today
|
|
16
|
+
* (`tool_result.error`). Tools never compose a shell command.
|
|
17
|
+
*/
|
|
18
|
+
export class StructuredFiles {
|
|
19
|
+
scope;
|
|
20
|
+
uploads;
|
|
21
|
+
// Home and its ancestors walk instead of asking git: a dotfiles repository at `~` (often ignoring
|
|
22
|
+
// `*`) would answer for the whole home with just its tracked dotfiles.
|
|
23
|
+
indexes = new FileIndexCache(name => name.startsWith(TEMP_PREFIX), Date.now, scope => { const relative = path.relative(scope, this.home); return !relative.startsWith('..') && !path.isAbsolute(relative); });
|
|
24
|
+
home;
|
|
25
|
+
roots;
|
|
26
|
+
downloads;
|
|
27
|
+
constructor(scope, ttl, stateDir) {
|
|
28
|
+
this.scope = scope;
|
|
29
|
+
this.home = scope.home;
|
|
30
|
+
this.roots = [...scope.roots];
|
|
31
|
+
this.downloads = path.join(scope.home, 'Downloads');
|
|
32
|
+
this.uploads = new UploadStore(scope.roots, ttl, stateDir);
|
|
33
|
+
}
|
|
34
|
+
static async create(home, roots, ttl = UPLOAD_TTL_MS, stateDir, filesystem = false) {
|
|
35
|
+
const scope = filesystem ? await filesystemScope(home) : await configuredRoots(home, roots);
|
|
36
|
+
if (stateDir)
|
|
37
|
+
await UploadStore.prepareStateDir(stateDir);
|
|
38
|
+
const files = new StructuredFiles(scope, ttl, stateDir);
|
|
39
|
+
await files.uploads.recover((raw, missingLeaf, mutation) => files.resolve(raw, missingLeaf, mutation));
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
/** Uploads in flight. */
|
|
43
|
+
get activeUploads() { return this.uploads.size; }
|
|
44
|
+
scopeFor(readOnly) { return readOnly ? READ_ONLY : this.scope; }
|
|
45
|
+
/** Validates an absolute path in this instance's configured-roots scope (or the read-only scope). */
|
|
46
|
+
resolve(raw, missingLeaf = false, mutation = false, readOnly = false, followLeaf = true) {
|
|
47
|
+
return resolveAbsolute(this.scopeFor(readOnly), raw, missingLeaf, mutation, followLeaf);
|
|
48
|
+
}
|
|
49
|
+
/** The descriptor stays pinned after cwd is restored; media children inherit it, never re-open the path. */
|
|
50
|
+
openMediaSource(raw, readOnly = false) {
|
|
51
|
+
return openMediaSource(this.scopeFor(readOnly), raw);
|
|
52
|
+
}
|
|
53
|
+
async dispatch(method, p) {
|
|
54
|
+
if (p.readOnly !== undefined && typeof p.readOnly !== 'boolean')
|
|
55
|
+
throw new Error('Invalid readOnly');
|
|
56
|
+
if (p.readOnly !== undefined && !['files.list', 'files.find', 'files.read'].includes(method))
|
|
57
|
+
throw new Error('Read-only file access cannot modify files');
|
|
58
|
+
await this.expire();
|
|
59
|
+
if (method === 'files.find')
|
|
60
|
+
return this.find(p);
|
|
61
|
+
// Any write may add, move or remove a path a cached search list still holds. Invalidate before and again after
|
|
62
|
+
// the mutation: a search building its list concurrently (find takes no pin) then never caches a pre-mutation list.
|
|
63
|
+
if (!['files.list', 'files.read'].includes(method)) {
|
|
64
|
+
this.indexes.invalidate();
|
|
65
|
+
try {
|
|
66
|
+
return await this.locate(method, p);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
this.indexes.invalidate();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return this.locate(method, p);
|
|
73
|
+
}
|
|
74
|
+
async locate(method, p) {
|
|
75
|
+
let parent;
|
|
76
|
+
if (method === 'files.upload.chunk' || method === 'files.upload.finish' || method === 'files.upload.cancel') {
|
|
77
|
+
const upload = this.uploads.get(identifier(p, 'uploadId'));
|
|
78
|
+
if (!upload && method === 'files.upload.cancel') {
|
|
79
|
+
keys(p, ['uploadId']);
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
if (!upload)
|
|
83
|
+
throw new Error('Upload not found or expired');
|
|
84
|
+
parent = path.dirname(upload.target);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const target = await this.resolve(string(p, 'path', 4096, method === 'files.list' ? this.home : undefined), method === 'files.mkdir' || method === 'files.upload.begin', !['files.list', 'files.read'].includes(method), p.readOnly === true, !['files.rename', 'files.delete'].includes(method));
|
|
88
|
+
parent = method === 'files.list' ? target : path.dirname(target);
|
|
89
|
+
}
|
|
90
|
+
return pinned(parent, async (pin) => {
|
|
91
|
+
if (method.startsWith('files.upload.') && method !== 'files.upload.begin') {
|
|
92
|
+
// Re-read: the upload may have finished, been cancelled or expired while this call waited for the pin.
|
|
93
|
+
const upload = this.uploads.get(identifier(p, 'uploadId'));
|
|
94
|
+
if (!upload && method === 'files.upload.cancel') {
|
|
95
|
+
keys(p, ['uploadId']);
|
|
96
|
+
return { ok: true };
|
|
97
|
+
}
|
|
98
|
+
if (!upload)
|
|
99
|
+
throw new Error('Upload not found or expired');
|
|
100
|
+
const stat = await fs.stat('.');
|
|
101
|
+
if (stat.dev !== upload.parent.dev || stat.ino !== upload.parent.ino) {
|
|
102
|
+
if (method === 'files.upload.cancel') {
|
|
103
|
+
await this.uploads.abandon(upload);
|
|
104
|
+
return { ok: true };
|
|
105
|
+
}
|
|
106
|
+
throw new Error('Upload parent changed');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return this.perform(method, p, pin);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async perform(method, p, pin) {
|
|
113
|
+
switch (method) {
|
|
114
|
+
case 'files.list': {
|
|
115
|
+
keys(p, ['path', 'readOnly']);
|
|
116
|
+
const target = await this.resolve(string(p, 'path', 4096, this.home), false, false, p.readOnly === true);
|
|
117
|
+
const displayedPath = this.scope.kind === 'filesystem' ? string(p, 'path', 4096, this.home) : target;
|
|
118
|
+
const listing = await fs.opendir(pin.leaf(target));
|
|
119
|
+
const entries = [];
|
|
120
|
+
let visited = 0;
|
|
121
|
+
try {
|
|
122
|
+
for await (const entry of listing) {
|
|
123
|
+
if (++visited > LIST_MAX_VISITED || entries.length >= LIST_MAX_ENTRIES)
|
|
124
|
+
throw new Error('Directory entry limit exceeded');
|
|
125
|
+
if (entry.name.startsWith(TEMP_PREFIX) || entry.isSymbolicLink())
|
|
126
|
+
continue;
|
|
127
|
+
const entryPath = path.join(target, entry.name);
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
await this.resolve(entryPath, false, false, p.readOnly === true);
|
|
131
|
+
stat = await fs.lstat(pin.leaf(entryPath));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!stat.isFile() && !stat.isDirectory())
|
|
137
|
+
continue;
|
|
138
|
+
entries.push({ name: entry.name, path: path.join(displayedPath, entry.name), directory: stat.isDirectory(), size: stat.size, modifiedAt: stat.mtimeMs, createdAt: stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.ctimeMs > 0 ? stat.ctimeMs : stat.mtimeMs,
|
|
139
|
+
...(this.scope.kind === 'filesystem' ? { writable: await fs.access(entryPath, constants.W_OK).then(() => true, () => false) } : {}) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await listing.close().catch(() => { });
|
|
144
|
+
}
|
|
145
|
+
entries.sort((a, b) => Number(b.directory) - Number(a.directory) || String(a.name).localeCompare(String(b.name)));
|
|
146
|
+
const parent = path.dirname(displayedPath);
|
|
147
|
+
return { path: displayedPath, parent: p.readOnly === true || this.scope.kind === 'filesystem' ? (parent === displayedPath ? null : parent) : this.roots.includes(target) || !this.roots.some(root => inside(parent, root)) ? null : parent, entries, roots: this.roots };
|
|
148
|
+
}
|
|
149
|
+
case 'files.read': {
|
|
150
|
+
keys(p, ['path', 'offset', 'length', 'readOnly']);
|
|
151
|
+
const offset = integer(p, 'offset', 0, Number.MAX_SAFE_INTEGER, 0);
|
|
152
|
+
const length = integer(p, 'length', 1, CHUNK_BYTES, CHUNK_BYTES);
|
|
153
|
+
const target = await this.resolve(string(p, 'path'), false, false, p.readOnly === true);
|
|
154
|
+
const handle = await pin.regularHandle(target);
|
|
155
|
+
try {
|
|
156
|
+
const stat = await handle.stat();
|
|
157
|
+
const buffer = Buffer.alloc(length);
|
|
158
|
+
const { bytesRead } = await handle.read(buffer, 0, length, offset);
|
|
159
|
+
return { data: buffer.subarray(0, bytesRead).toString('base64'), size: stat.size, eof: offset + bytesRead >= stat.size, name: path.basename(target),
|
|
160
|
+
...(this.scope.kind === 'filesystem' ? { writable: await fs.access(pin.leaf(target), constants.W_OK).then(() => true, () => false) } : {}) };
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
await handle.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
case 'files.mkdir': {
|
|
167
|
+
keys(p, ['path']);
|
|
168
|
+
const target = await this.resolve(string(p, 'path'), true, true);
|
|
169
|
+
await fs.mkdir(pin.leaf(target));
|
|
170
|
+
return { ok: true };
|
|
171
|
+
}
|
|
172
|
+
case 'files.rename': {
|
|
173
|
+
keys(p, ['path', 'name']);
|
|
174
|
+
const target = await this.resolve(string(p, 'path'), false, true, false, false);
|
|
175
|
+
const name = string(p, 'name', 255);
|
|
176
|
+
if (name === '.' || name === '..' || /[/\\]/.test(name) || name.startsWith(TEMP_PREFIX))
|
|
177
|
+
throw new Error('Invalid new name');
|
|
178
|
+
const destination = await this.resolve(path.join(path.dirname(target), name), true, true);
|
|
179
|
+
// Node rename() overwrites files. Hard-link commits are exclusive for files; for directories,
|
|
180
|
+
// reserve the destination with mkdir and replace only that empty reservation atomically.
|
|
181
|
+
const stat = await fs.lstat(pin.leaf(target));
|
|
182
|
+
if (stat.isFile()) {
|
|
183
|
+
await fs.link(pin.leaf(target), pin.leaf(destination));
|
|
184
|
+
await fs.unlink(pin.leaf(target));
|
|
185
|
+
}
|
|
186
|
+
else if (stat.isDirectory()) {
|
|
187
|
+
await fs.mkdir(pin.leaf(destination), { mode: stat.mode });
|
|
188
|
+
try {
|
|
189
|
+
await fs.rename(pin.leaf(target), pin.leaf(destination));
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await fs.rmdir(pin.leaf(destination)).catch(() => { });
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else
|
|
197
|
+
throw new Error('Not a regular file or directory');
|
|
198
|
+
return { ok: true };
|
|
199
|
+
}
|
|
200
|
+
case 'files.delete': {
|
|
201
|
+
keys(p, ['path']);
|
|
202
|
+
const target = await this.resolve(string(p, 'path'), false, true, false, false);
|
|
203
|
+
const stat = await fs.lstat(pin.leaf(target));
|
|
204
|
+
if (stat.isDirectory())
|
|
205
|
+
await fs.rmdir(pin.leaf(target));
|
|
206
|
+
else if (stat.isFile())
|
|
207
|
+
await fs.unlink(pin.leaf(target));
|
|
208
|
+
else
|
|
209
|
+
throw new Error('Not a regular file or directory');
|
|
210
|
+
return { ok: true };
|
|
211
|
+
}
|
|
212
|
+
case 'files.upload.begin': {
|
|
213
|
+
keys(p, ['path', 'size', 'overwrite']);
|
|
214
|
+
const size = integer(p, 'size', 0, MAX_UPLOAD_BYTES);
|
|
215
|
+
if (p.overwrite !== undefined && typeof p.overwrite !== 'boolean')
|
|
216
|
+
throw new Error('Invalid overwrite');
|
|
217
|
+
return this.uploads.begin(pin, () => this.resolve(string(p, 'path'), true, true), size, p.overwrite === true, this.scope.kind === 'filesystem', string(p, 'path'));
|
|
218
|
+
}
|
|
219
|
+
case 'files.upload.chunk': {
|
|
220
|
+
keys(p, ['uploadId', 'offset', 'data']);
|
|
221
|
+
const upload = this.uploads.use(identifier(p, 'uploadId'));
|
|
222
|
+
const offset = integer(p, 'offset', 0, MAX_UPLOAD_BYTES);
|
|
223
|
+
const bytes = base64(p.data);
|
|
224
|
+
return this.uploads.chunk(upload, offset, bytes);
|
|
225
|
+
}
|
|
226
|
+
case 'files.upload.finish': {
|
|
227
|
+
keys(p, ['uploadId']);
|
|
228
|
+
const upload = this.uploads.use(identifier(p, 'uploadId'));
|
|
229
|
+
return this.uploads.finish(pin, upload, (raw, missingLeaf, mutation) => this.resolve(raw, missingLeaf, mutation));
|
|
230
|
+
}
|
|
231
|
+
case 'files.upload.cancel': {
|
|
232
|
+
keys(p, ['uploadId']);
|
|
233
|
+
return this.uploads.cancel(identifier(p, 'uploadId'));
|
|
234
|
+
}
|
|
235
|
+
default: throw new Error('Unsupported file tool');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Fuzzy file-name search below one directory (`path`, default home). Ranking runs over the cached
|
|
240
|
+
* relative-path list; only the best candidates are re-validated against the same root/symlink fence
|
|
241
|
+
* as `files.list` before they are returned, so a path git still remembers but the disk lost, or one
|
|
242
|
+
* that became a symbolic link, never reaches the client. Uses no cwd: absolute paths throughout, so it
|
|
243
|
+
* never enters (or waits for) a `pinned()` section.
|
|
244
|
+
*/
|
|
245
|
+
async find(p) {
|
|
246
|
+
keys(p, ['path', 'query', 'limit', 'readOnly']);
|
|
247
|
+
const readOnly = p.readOnly === true;
|
|
248
|
+
const query = string(p, 'query', 256).trim();
|
|
249
|
+
if (!query)
|
|
250
|
+
throw new Error('Invalid query');
|
|
251
|
+
const limit = integer(p, 'limit', 1, FIND_MAX_LIMIT, FIND_DEFAULT_LIMIT);
|
|
252
|
+
const scope = await this.resolve(string(p, 'path', 4096, this.home), false, false, readOnly);
|
|
253
|
+
const displayedScope = this.scope.kind === 'filesystem' ? string(p, 'path', 4096, this.home) : scope;
|
|
254
|
+
if (!(await fs.stat(scope)).isDirectory())
|
|
255
|
+
throw new Error('Search scope is not a directory');
|
|
256
|
+
const index = await this.indexes.get(scope);
|
|
257
|
+
const ranked = rankPaths(index.paths, query);
|
|
258
|
+
const entries = [];
|
|
259
|
+
let checked = 0;
|
|
260
|
+
// Bounded: a stale list full of vanished paths must not turn one search into a disk scan.
|
|
261
|
+
for (const hit of ranked.slice(0, limit * 4)) {
|
|
262
|
+
if (entries.length >= limit)
|
|
263
|
+
break;
|
|
264
|
+
checked++;
|
|
265
|
+
const relative = index.paths[hit.index];
|
|
266
|
+
const target = path.join(scope, relative);
|
|
267
|
+
let stat;
|
|
268
|
+
try {
|
|
269
|
+
await this.resolve(target, false, false, readOnly);
|
|
270
|
+
stat = await fs.lstat(target);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!stat.isFile() && !stat.isDirectory())
|
|
276
|
+
continue;
|
|
277
|
+
entries.push({ name: path.basename(relative), path: path.join(displayedScope, relative), directory: stat.isDirectory(), match: relative, positions: hit.positions });
|
|
278
|
+
}
|
|
279
|
+
// Candidates the fence dropped are not matches; the unchecked tail is counted as is.
|
|
280
|
+
return { path: displayedScope, entries, total: entries.length + ranked.length - checked, truncated: index.truncated, source: index.source };
|
|
281
|
+
}
|
|
282
|
+
async expire() { await this.uploads.expire(); }
|
|
283
|
+
async dispose() { await this.uploads.dispose(); }
|
|
284
|
+
}
|
|
@@ -1,28 +1,28 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { canonicalRoot, FileError, pinned, resolvePinnedDir, resolvePinnedDirCreate, resolvePinnedLeaf, resolveRelativeDir, resolveRelativeDirCreate, resolveRelativeFile, throwIfAborted, toFileError, withinRoot, } from './fence.js';
|
|
6
|
+
import { assertNotCredentialPath } from './credentials.js';
|
|
7
|
+
import { assertScopeAllows } from './scope.js';
|
|
8
|
+
/** Text file semantics of the platform agent tools (`file_*_request`): request-root scope only. */
|
|
6
9
|
export const MAX_READ_BYTES = 8 * 1024 * 1024;
|
|
7
10
|
export const MAX_PAGE_BYTES = 256 * 1024;
|
|
8
11
|
export const MAX_WRITE_BYTES = 1024 * 1024;
|
|
9
|
-
// Transport-free
|
|
10
|
-
//
|
|
11
|
-
// is retained for unit tests and trusted callers, not as a
|
|
12
|
+
// Transport-free engines (formerly fileOps.ts). Untrusted requests must go through the scope entry points at the
|
|
13
|
+
// bottom of this file (or the legacy fork-per-request worker), which pin the parent directory before invoking these
|
|
14
|
+
// with a pinned leaf resolver; pathname-only resolution here is retained for unit tests and trusted callers, not as a
|
|
15
|
+
// security boundary against rename races.
|
|
12
16
|
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
-
function throwIfAborted(signal) {
|
|
14
|
-
if (signal?.aborted)
|
|
15
|
-
throw new ComputerFileError('workstation_offline', 'File operation was cancelled');
|
|
16
|
-
}
|
|
17
17
|
async function snapshotFile(file, signal) {
|
|
18
18
|
let handle;
|
|
19
19
|
try {
|
|
20
20
|
handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
21
21
|
const before = await handle.stat({ bigint: true });
|
|
22
22
|
if (!before.isFile())
|
|
23
|
-
throw new
|
|
23
|
+
throw new FileError('not_regular_file', 'Target is not a regular file');
|
|
24
24
|
if (before.size > BigInt(MAX_READ_BYTES))
|
|
25
|
-
throw new
|
|
25
|
+
throw new FileError('too_large', 'File exceeds 8 MiB');
|
|
26
26
|
const parts = [];
|
|
27
27
|
let total = 0;
|
|
28
28
|
while (true) {
|
|
@@ -33,25 +33,25 @@ async function snapshotFile(file, signal) {
|
|
|
33
33
|
break;
|
|
34
34
|
total += bytesRead;
|
|
35
35
|
if (total > MAX_READ_BYTES)
|
|
36
|
-
throw new
|
|
36
|
+
throw new FileError('too_large', 'File exceeds 8 MiB');
|
|
37
37
|
parts.push(chunk.subarray(0, bytesRead));
|
|
38
38
|
}
|
|
39
39
|
const after = await handle.stat({ bigint: true });
|
|
40
40
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs || BigInt(total) !== after.size) {
|
|
41
|
-
throw new
|
|
41
|
+
throw new FileError('conflict', 'File changed during read');
|
|
42
42
|
}
|
|
43
43
|
return Buffer.concat(parts, total);
|
|
44
44
|
}
|
|
45
45
|
catch (error) {
|
|
46
|
-
throw
|
|
46
|
+
throw toFileError(error);
|
|
47
47
|
}
|
|
48
48
|
finally {
|
|
49
49
|
await handle?.close();
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
export async function
|
|
52
|
+
export async function readTextFile(request, signal, resolvePath = resolveRelativeFile) {
|
|
53
53
|
if (!Number.isSafeInteger(request.startLine) || request.startLine < 1 || !Number.isSafeInteger(request.maxLines) || request.maxLines < 1 || request.maxLines > 2000) {
|
|
54
|
-
throw new
|
|
54
|
+
throw new FileError('invalid_argument', 'Invalid line range');
|
|
55
55
|
}
|
|
56
56
|
const file = await resolvePath(request.root, request.path, false);
|
|
57
57
|
let bytes;
|
|
@@ -59,7 +59,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
|
|
|
59
59
|
bytes = await snapshotFile(file, signal);
|
|
60
60
|
}
|
|
61
61
|
catch (error) {
|
|
62
|
-
if (!(error instanceof
|
|
62
|
+
if (!(error instanceof FileError) || error.code !== 'conflict')
|
|
63
63
|
throw error;
|
|
64
64
|
bytes = await snapshotFile(file, signal);
|
|
65
65
|
}
|
|
@@ -68,7 +68,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
|
|
|
68
68
|
source = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
69
69
|
}
|
|
70
70
|
catch {
|
|
71
|
-
throw new
|
|
71
|
+
throw new FileError('invalid_encoding', 'File is not valid UTF-8');
|
|
72
72
|
}
|
|
73
73
|
const lines = [];
|
|
74
74
|
for (let begin = 0; begin < source.length;) {
|
|
@@ -84,7 +84,7 @@ export async function readComputerFile(request, signal, resolvePath = resolveCom
|
|
|
84
84
|
for (let index = first; index < lines.length && selected < request.maxLines; index++) {
|
|
85
85
|
const lineBytes = Buffer.byteLength(lines[index], 'utf8');
|
|
86
86
|
if (lineBytes > MAX_PAGE_BYTES)
|
|
87
|
-
throw new
|
|
87
|
+
throw new FileError('line_too_long', 'A line exceeds 256 KiB');
|
|
88
88
|
if (pageBytes + lineBytes > MAX_PAGE_BYTES)
|
|
89
89
|
break;
|
|
90
90
|
content += lines[index];
|
|
@@ -129,17 +129,17 @@ function validUnicode(content) {
|
|
|
129
129
|
}
|
|
130
130
|
return true;
|
|
131
131
|
}
|
|
132
|
-
export async function
|
|
132
|
+
export async function writeTextFile(request, signal, resolvePath = resolveRelativeFile) {
|
|
133
133
|
if (request.content.includes('\0') || !validUnicode(request.content)) {
|
|
134
|
-
throw new
|
|
134
|
+
throw new FileError('invalid_encoding', 'Content must be valid UTF-8 text without NUL');
|
|
135
135
|
}
|
|
136
136
|
const bytes = Buffer.from(request.content, 'utf8');
|
|
137
137
|
if (bytes.length > MAX_WRITE_BYTES)
|
|
138
|
-
throw new
|
|
138
|
+
throw new FileError('too_large', 'Content exceeds 1 MiB');
|
|
139
139
|
if (request.mode !== 'create' && request.mode !== 'replace')
|
|
140
|
-
throw new
|
|
140
|
+
throw new FileError('invalid_argument', 'Invalid write mode');
|
|
141
141
|
if (request.mode === 'create' ? request.expectedSha256 !== undefined : !/^[0-9a-f]{64}$/.test(request.expectedSha256 ?? '')) {
|
|
142
|
-
throw new
|
|
142
|
+
throw new FileError('invalid_argument', 'create forbids expected_sha256; replace requires lowercase SHA-256');
|
|
143
143
|
}
|
|
144
144
|
throwIfAborted(signal);
|
|
145
145
|
const file = await resolvePath(request.root, request.path, request.mode === 'create');
|
|
@@ -150,12 +150,12 @@ export async function writeComputerFile(request, signal, resolvePath = resolveCo
|
|
|
150
150
|
if (request.mode === 'replace') {
|
|
151
151
|
const current = await snapshotFile(file, signal);
|
|
152
152
|
if (hash(current) !== request.expectedSha256)
|
|
153
|
-
throw new
|
|
153
|
+
throw new FileError('conflict', 'File changed since it was read');
|
|
154
154
|
try {
|
|
155
155
|
mode = (await stat(file)).mode & 0o777;
|
|
156
156
|
}
|
|
157
157
|
catch (error) {
|
|
158
|
-
throw
|
|
158
|
+
throw toFileError(error);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
const parent = dirname(file);
|
|
@@ -178,18 +178,18 @@ export async function writeComputerFile(request, signal, resolvePath = resolveCo
|
|
|
178
178
|
return { path: request.path, sizeBytes: bytes.length, sha256: hash(bytes), created: request.mode === 'create' };
|
|
179
179
|
}
|
|
180
180
|
catch (error) {
|
|
181
|
-
throw
|
|
181
|
+
throw toFileError(error);
|
|
182
182
|
}
|
|
183
183
|
finally {
|
|
184
184
|
await handle?.close();
|
|
185
185
|
await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
|
|
186
|
-
throw
|
|
186
|
+
throw toFileError(error); });
|
|
187
187
|
}
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
|
-
export async function
|
|
190
|
+
export async function listTextDir(request, signal, resolvePath = resolveRelativeDir) {
|
|
191
191
|
if (!Number.isSafeInteger(request.maxEntries) || request.maxEntries < 1 || request.maxEntries > 2000) {
|
|
192
|
-
throw new
|
|
192
|
+
throw new FileError('invalid_argument', 'Invalid entry range');
|
|
193
193
|
}
|
|
194
194
|
throwIfAborted(signal);
|
|
195
195
|
const dir = await resolvePath(request.root, request.path);
|
|
@@ -220,7 +220,7 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
|
|
|
220
220
|
}
|
|
221
221
|
catch (error) {
|
|
222
222
|
if (error.code !== 'ENOENT')
|
|
223
|
-
throw
|
|
223
|
+
throw toFileError(error);
|
|
224
224
|
continue;
|
|
225
225
|
}
|
|
226
226
|
}
|
|
@@ -232,7 +232,7 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
|
|
|
232
232
|
catch (error) {
|
|
233
233
|
// A file that vanished between readdir and lstat simply drops out of the snapshot.
|
|
234
234
|
if (error.code !== 'ENOENT')
|
|
235
|
-
throw
|
|
235
|
+
throw toFileError(error);
|
|
236
236
|
continue;
|
|
237
237
|
}
|
|
238
238
|
}
|
|
@@ -241,16 +241,16 @@ export async function listComputerDir(request, signal, resolvePath = resolveComp
|
|
|
241
241
|
const nextAfter = truncated ? entries[entries.length - 1]?.name : undefined;
|
|
242
242
|
return { path: request.path, entries, truncated, ...(nextAfter !== undefined ? { nextAfter } : {}) };
|
|
243
243
|
}
|
|
244
|
-
export async function
|
|
244
|
+
export async function createTextDir(request, signal, resolvePlan = resolveRelativeDirCreate) {
|
|
245
245
|
throwIfAborted(signal);
|
|
246
246
|
const plan = await resolvePlan(request.root, request.path, request.parents);
|
|
247
247
|
if (plan.missing.length === 0) {
|
|
248
248
|
if (!request.parents)
|
|
249
|
-
throw new
|
|
249
|
+
throw new FileError('conflict', 'Target already exists');
|
|
250
250
|
return { path: request.path, created: false };
|
|
251
251
|
}
|
|
252
252
|
if (!request.parents && plan.missing.length > 1) {
|
|
253
|
-
throw new
|
|
253
|
+
throw new FileError('not_found', 'Parent directory not found');
|
|
254
254
|
}
|
|
255
255
|
let cursor = plan.anchor;
|
|
256
256
|
let created = false;
|
|
@@ -263,15 +263,128 @@ export async function createComputerDir(request, signal, resolvePlan = resolveCo
|
|
|
263
263
|
}
|
|
264
264
|
catch (error) {
|
|
265
265
|
if (error.code !== 'EEXIST')
|
|
266
|
-
throw
|
|
266
|
+
throw toFileError(error);
|
|
267
267
|
if (!request.parents)
|
|
268
|
-
throw new
|
|
268
|
+
throw new FileError('conflict', 'Target already exists');
|
|
269
269
|
}
|
|
270
270
|
// A component we just made (or found racing us) must be a real directory, never a symlink stand-in.
|
|
271
|
-
const entry = await lstat(target).catch(error => { throw
|
|
271
|
+
const entry = await lstat(target).catch(error => { throw toFileError(error); });
|
|
272
272
|
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
273
|
-
throw new
|
|
273
|
+
throw new FileError('conflict', 'Path is not a directory');
|
|
274
274
|
cursor = target;
|
|
275
275
|
}
|
|
276
276
|
return { path: request.path, created };
|
|
277
277
|
}
|
|
278
|
+
const targetLocks = new Map();
|
|
279
|
+
async function serialTarget(target, action) {
|
|
280
|
+
const previous = targetLocks.get(target) ?? Promise.resolve();
|
|
281
|
+
let release;
|
|
282
|
+
const gate = new Promise(resolve => { release = resolve; });
|
|
283
|
+
const queued = previous.then(() => gate);
|
|
284
|
+
targetLocks.set(target, queued);
|
|
285
|
+
await previous;
|
|
286
|
+
try {
|
|
287
|
+
return await action();
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
release();
|
|
291
|
+
if (targetLocks.get(target) === queued)
|
|
292
|
+
targetLocks.delete(target);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// The worker client checked for cancellation after pathname validation, when it was about to start the operation.
|
|
296
|
+
async function throwIfAbortedThen(signal, run) {
|
|
297
|
+
throwIfAborted(signal);
|
|
298
|
+
return run();
|
|
299
|
+
}
|
|
300
|
+
function requireRequestRoot(scope, operation) {
|
|
301
|
+
if (scope?.kind !== 'request-root')
|
|
302
|
+
throw new FileError('invalid_argument', 'Text file operations require a request root');
|
|
303
|
+
assertScopeAllows(scope, operation);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Runs `run` with the cwd pinned to `anchor`, which must still be inside the canonical root after chdir. Cancellation
|
|
307
|
+
* keeps the file worker's contract: a request cancelled once it is under way reports `outcome_unknown` for mutations
|
|
308
|
+
* and `workstation_offline` for reads.
|
|
309
|
+
*/
|
|
310
|
+
async function anchored(operation, root, anchor, leaves, signal, hooks, run) {
|
|
311
|
+
const insideRoot = withinRoot(root);
|
|
312
|
+
try {
|
|
313
|
+
await hooks?.afterValidation?.();
|
|
314
|
+
return await pinned(anchor, async () => {
|
|
315
|
+
await hooks?.afterPinned?.();
|
|
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
|
+
}
|
|
329
|
+
return run();
|
|
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
|
+
});
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
if (signal?.aborted)
|
|
344
|
+
throw new FileError(operation === 'write' || operation === 'mkdir' ? 'outcome_unknown' : 'workstation_offline', 'File operation was cancelled');
|
|
345
|
+
throw toFileError(error);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
export async function readText(scope, request, signal, hooks) {
|
|
349
|
+
requireRequestRoot(scope, 'read');
|
|
350
|
+
const root = await canonicalRoot(scope.root);
|
|
351
|
+
const target = await resolveRelativeFile(root, request.path, false);
|
|
352
|
+
assertNotCredentialPath(target);
|
|
353
|
+
throwIfAborted(signal);
|
|
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 };
|
|
357
|
+
}
|
|
358
|
+
export async function writeText(scope, request, signal, hooks) {
|
|
359
|
+
requireRequestRoot(scope, 'write');
|
|
360
|
+
const root = await canonicalRoot(scope.root);
|
|
361
|
+
const target = await resolveRelativeFile(root, request.path, request.mode === 'create');
|
|
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 };
|
|
365
|
+
}
|
|
366
|
+
export async function listDir(scope, request, signal, hooks) {
|
|
367
|
+
requireRequestRoot(scope, 'list');
|
|
368
|
+
const root = await canonicalRoot(scope.root);
|
|
369
|
+
const target = await resolveRelativeDir(root, request.path);
|
|
370
|
+
assertNotCredentialPath(target);
|
|
371
|
+
throwIfAborted(signal);
|
|
372
|
+
// Listing the root pins the root itself; '' is the zero-component leaf.
|
|
373
|
+
const anchor = target === root ? root : dirname(target);
|
|
374
|
+
const leaf = target === root ? '' : basename(target);
|
|
375
|
+
const data = await anchored('list', root, anchor, leaf === '' ? [] : [leaf], signal, hooks, () => listTextDir({ ...request, root: '.', path: leaf }, signal, resolvePinnedDir));
|
|
376
|
+
return { ...data, path: request.path };
|
|
377
|
+
}
|
|
378
|
+
export async function makeDir(scope, request, signal, hooks) {
|
|
379
|
+
requireRequestRoot(scope, 'mkdir');
|
|
380
|
+
const root = await canonicalRoot(scope.root);
|
|
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));
|
|
387
|
+
throwIfAborted(signal);
|
|
388
|
+
const data = await anchored('mkdir', root, plan.anchor, created, signal, hooks, () => createTextDir({ ...request, root: '.', path: plan.missing.join('/') }, signal, resolvePinnedDirCreate));
|
|
389
|
+
return { ...data, path: request.path };
|
|
390
|
+
}
|