@relayflows/sdk 2.0.29 → 2.0.31
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/dist/agent-artifacts.d.ts.map +1 -1
- package/dist/agent-artifacts.js +2 -9
- package/dist/agent-artifacts.js.map +1 -1
- package/dist/agent-cwd.d.ts +69 -0
- package/dist/agent-cwd.d.ts.map +1 -0
- package/dist/agent-cwd.js +140 -0
- package/dist/agent-cwd.js.map +1 -0
- package/dist/artifact-scan-policy.d.ts +35 -0
- package/dist/artifact-scan-policy.d.ts.map +1 -0
- package/dist/artifact-scan-policy.js +49 -0
- package/dist/artifact-scan-policy.js.map +1 -0
- package/dist/authored-completion-record.d.ts +35 -0
- package/dist/authored-completion-record.d.ts.map +1 -0
- package/dist/authored-completion-record.js +92 -0
- package/dist/authored-completion-record.js.map +1 -0
- package/dist/authored-completion.d.ts +99 -0
- package/dist/authored-completion.d.ts.map +1 -0
- package/dist/authored-completion.js +184 -0
- package/dist/authored-completion.js.map +1 -0
- package/dist/authored-flow-error.d.ts +13 -1
- package/dist/authored-flow-error.d.ts.map +1 -1
- package/dist/authored-flow-error.js +12 -0
- package/dist/authored-flow-error.js.map +1 -1
- package/dist/authored-flow-executor.d.ts +7 -30
- package/dist/authored-flow-executor.d.ts.map +1 -1
- package/dist/authored-flow-executor.js +64 -48
- package/dist/authored-flow-executor.js.map +1 -1
- package/dist/authored-human.d.ts.map +1 -1
- package/dist/authored-human.js +1 -3
- package/dist/authored-human.js.map +1 -1
- package/dist/authored-node-entry.js +4 -1
- package/dist/authored-node-entry.js.map +1 -1
- package/dist/authored-node-runner.d.ts +9 -1
- package/dist/authored-node-runner.d.ts.map +1 -1
- package/dist/authored-node-runner.js +92 -17
- package/dist/authored-node-runner.js.map +1 -1
- package/dist/authored-root.d.ts +5 -0
- package/dist/authored-root.d.ts.map +1 -1
- package/dist/authored-root.js +20 -2
- package/dist/authored-root.js.map +1 -1
- package/dist/authored-step-output.d.ts +35 -10
- package/dist/authored-step-output.d.ts.map +1 -1
- package/dist/authored-step-output.js +108 -40
- package/dist/authored-step-output.js.map +1 -1
- package/dist/authored-verdict.d.ts +28 -0
- package/dist/authored-verdict.d.ts.map +1 -0
- package/dist/authored-verdict.js +79 -0
- package/dist/authored-verdict.js.map +1 -0
- package/dist/authored-worker-step.d.ts +5 -2
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +48 -11
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/bundle-extensions.d.ts.map +1 -1
- package/dist/bundle-extensions.js +4 -7
- package/dist/bundle-extensions.js.map +1 -1
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +41 -7
- package/dist/bundle.js.map +1 -1
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +18 -6
- package/dist/canonical.js.map +1 -1
- package/dist/cli/check-worker-surface.d.ts +27 -0
- package/dist/cli/check-worker-surface.d.ts.map +1 -0
- package/dist/cli/check-worker-surface.js +63 -0
- package/dist/cli/check-worker-surface.js.map +1 -0
- package/dist/cli/check.d.ts +19 -2
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +12 -4
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cloud-format.d.ts +9 -0
- package/dist/cli/cloud-format.d.ts.map +1 -0
- package/dist/cli/cloud-format.js +77 -0
- package/dist/cli/cloud-format.js.map +1 -0
- package/dist/cli/cloud-live.d.ts +39 -0
- package/dist/cli/cloud-live.d.ts.map +1 -0
- package/dist/cli/cloud-live.js +325 -0
- package/dist/cli/cloud-live.js.map +1 -0
- package/dist/cli/cloud-read.d.ts +3 -7
- package/dist/cli/cloud-read.d.ts.map +1 -1
- package/dist/cli/cloud-read.js +14 -266
- package/dist/cli/cloud-read.js.map +1 -1
- package/dist/cli/cloud-refusal.d.ts +39 -0
- package/dist/cli/cloud-refusal.d.ts.map +1 -0
- package/dist/cli/cloud-refusal.js +95 -0
- package/dist/cli/cloud-refusal.js.map +1 -0
- package/dist/cli/cloud-status-view.d.ts +7 -0
- package/dist/cli/cloud-status-view.d.ts.map +1 -0
- package/dist/cli/cloud-status-view.js +214 -0
- package/dist/cli/cloud-status-view.js.map +1 -0
- package/dist/cli/direct-run.d.ts.map +1 -1
- package/dist/cli/direct-run.js +16 -2
- package/dist/cli/direct-run.js.map +1 -1
- package/dist/cli/hn-monitor.d.ts +0 -13
- package/dist/cli/hn-monitor.d.ts.map +1 -1
- package/dist/cli/hn-monitor.js +2 -1
- package/dist/cli/hn-monitor.js.map +1 -1
- package/dist/cli/local-agent-remedy.d.ts +80 -0
- package/dist/cli/local-agent-remedy.d.ts.map +1 -0
- package/dist/cli/local-agent-remedy.js +116 -0
- package/dist/cli/local-agent-remedy.js.map +1 -0
- package/dist/cli/observer-session.d.ts +41 -0
- package/dist/cli/observer-session.d.ts.map +1 -0
- package/dist/cli/observer-session.js +78 -0
- package/dist/cli/observer-session.js.map +1 -0
- package/dist/cli/run.d.ts +30 -2
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +186 -24
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/status.d.ts +9 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +31 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/step-evidence.d.ts +90 -0
- package/dist/cli/step-evidence.d.ts.map +1 -0
- package/dist/cli/step-evidence.js +259 -0
- package/dist/cli/step-evidence.js.map +1 -0
- package/dist/cli/step-failure.d.ts +14 -4
- package/dist/cli/step-failure.d.ts.map +1 -1
- package/dist/cli/step-failure.js +41 -76
- package/dist/cli/step-failure.js.map +1 -1
- package/dist/cli-commands.d.ts +6 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +2 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +10 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -42
- package/dist/cli.js.map +1 -1
- package/dist/cloud-read.d.ts +15 -0
- package/dist/cloud-read.d.ts.map +1 -1
- package/dist/cloud-read.js +20 -1
- package/dist/cloud-read.js.map +1 -1
- package/dist/cloud-run-record.d.ts +27 -0
- package/dist/cloud-run-record.d.ts.map +1 -0
- package/dist/cloud-run-record.js +56 -0
- package/dist/cloud-run-record.js.map +1 -0
- package/dist/cloud-run.d.ts +2 -9
- package/dist/cloud-run.d.ts.map +1 -1
- package/dist/cloud-run.js +3 -20
- package/dist/cloud-run.js.map +1 -1
- package/dist/communication/local.d.ts.map +1 -1
- package/dist/communication/local.js +2 -1
- package/dist/communication/local.js.map +1 -1
- package/dist/communication/worker.d.ts +1 -1
- package/dist/communication/worker.d.ts.map +1 -1
- package/dist/communication/worker.js +13 -5
- package/dist/communication/worker.js.map +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +28 -6
- package/dist/compile.js.map +1 -1
- package/dist/direct-input.d.ts.map +1 -1
- package/dist/direct-input.js +21 -1
- package/dist/direct-input.js.map +1 -1
- package/dist/failure-kinds.d.ts +96 -6
- package/dist/failure-kinds.d.ts.map +1 -1
- package/dist/failure-kinds.js +29 -2
- package/dist/failure-kinds.js.map +1 -1
- package/dist/flow-extension-compat.d.ts.map +1 -1
- package/dist/flow-extension-compat.js +26 -8
- package/dist/flow-extension-compat.js.map +1 -1
- package/dist/flow-extension-loader.d.ts +7 -0
- package/dist/flow-extension-loader.d.ts.map +1 -1
- package/dist/flow-extension-loader.js +48 -22
- package/dist/flow-extension-loader.js.map +1 -1
- package/dist/flow-requirements.d.ts.map +1 -1
- package/dist/flow-requirements.js +24 -9
- package/dist/flow-requirements.js.map +1 -1
- package/dist/fs-descriptor.d.ts +17 -0
- package/dist/fs-descriptor.d.ts.map +1 -0
- package/dist/fs-descriptor.js +96 -0
- package/dist/fs-descriptor.js.map +1 -0
- package/dist/gate-contract.d.ts +7 -0
- package/dist/gate-contract.d.ts.map +1 -1
- package/dist/gate-contract.js +6 -1
- package/dist/gate-contract.js.map +1 -1
- package/dist/helper-preflight.d.ts.map +1 -1
- package/dist/helper-preflight.js +10 -3
- package/dist/helper-preflight.js.map +1 -1
- package/dist/helper-reference.d.ts +25 -0
- package/dist/helper-reference.d.ts.map +1 -0
- package/dist/helper-reference.js +129 -0
- package/dist/helper-reference.js.map +1 -0
- package/dist/hosted-base-snapshot.d.ts +25 -0
- package/dist/hosted-base-snapshot.d.ts.map +1 -0
- package/dist/hosted-base-snapshot.js +278 -0
- package/dist/hosted-base-snapshot.js.map +1 -0
- package/dist/hosted-extension-declarations.d.ts +10 -0
- package/dist/hosted-extension-declarations.d.ts.map +1 -0
- package/dist/hosted-extension-declarations.js +272 -0
- package/dist/hosted-extension-declarations.js.map +1 -0
- package/dist/hosted-extension-isolation.d.ts +82 -0
- package/dist/hosted-extension-isolation.d.ts.map +1 -0
- package/dist/hosted-extension-isolation.js +322 -0
- package/dist/hosted-extension-isolation.js.map +1 -0
- package/dist/hosted-extension-manifest.d.ts +9 -0
- package/dist/hosted-extension-manifest.d.ts.map +1 -0
- package/dist/hosted-extension-manifest.js +235 -0
- package/dist/hosted-extension-manifest.js.map +1 -0
- package/dist/hosted-extension-protocol.d.ts +16 -0
- package/dist/hosted-extension-protocol.d.ts.map +1 -0
- package/dist/hosted-extension-protocol.js +224 -0
- package/dist/hosted-extension-protocol.js.map +1 -0
- package/dist/hosted-extension-runtime.d.ts +30 -0
- package/dist/hosted-extension-runtime.d.ts.map +1 -0
- package/dist/hosted-extension-runtime.js +167 -0
- package/dist/hosted-extension-runtime.js.map +1 -0
- package/dist/hosted-extension-sandbox-source.d.ts +9 -0
- package/dist/hosted-extension-sandbox-source.d.ts.map +1 -0
- package/dist/hosted-extension-sandbox-source.js +95 -0
- package/dist/hosted-extension-sandbox-source.js.map +1 -0
- package/dist/hosted-extension-sandbox.d.ts +39 -0
- package/dist/hosted-extension-sandbox.d.ts.map +1 -0
- package/dist/hosted-extension-sandbox.js +418 -0
- package/dist/hosted-extension-sandbox.js.map +1 -0
- package/dist/hosted-project.d.ts +3 -0
- package/dist/hosted-project.d.ts.map +1 -0
- package/dist/hosted-project.js +19 -0
- package/dist/hosted-project.js.map +1 -0
- package/dist/hosted-promise-safety.d.ts +12 -0
- package/dist/hosted-promise-safety.d.ts.map +1 -0
- package/dist/hosted-promise-safety.js +63 -0
- package/dist/hosted-promise-safety.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/intrinsic-array.d.ts +3 -0
- package/dist/intrinsic-array.d.ts.map +1 -0
- package/dist/intrinsic-array.js +14 -0
- package/dist/intrinsic-array.js.map +1 -0
- package/dist/journal-client.d.ts +3 -1
- package/dist/journal-client.d.ts.map +1 -1
- package/dist/journal-client.js +17 -2
- package/dist/journal-client.js.map +1 -1
- package/dist/journal-projection.d.ts +16 -0
- package/dist/journal-projection.d.ts.map +1 -0
- package/dist/journal-projection.js +108 -0
- package/dist/journal-projection.js.map +1 -0
- package/dist/json-value.d.ts +6 -1
- package/dist/json-value.d.ts.map +1 -1
- package/dist/json-value.js +155 -36
- package/dist/json-value.js.map +1 -1
- package/dist/llm-worker.d.ts +9 -0
- package/dist/llm-worker.d.ts.map +1 -1
- package/dist/llm-worker.js +16 -2
- package/dist/llm-worker.js.map +1 -1
- package/dist/local-agent.d.ts +1 -1
- package/dist/local-agent.d.ts.map +1 -1
- package/dist/local-agent.js +4 -3
- package/dist/local-agent.js.map +1 -1
- package/dist/named-gate-lowering.js +35 -5
- package/dist/named-gate-lowering.js.map +1 -1
- package/dist/named-gate-preflight.d.ts +27 -0
- package/dist/named-gate-preflight.d.ts.map +1 -0
- package/dist/named-gate-preflight.js +47 -0
- package/dist/named-gate-preflight.js.map +1 -0
- package/dist/named-gates.d.ts +1 -1
- package/dist/named-gates.d.ts.map +1 -1
- package/dist/named-gates.js +3 -0
- package/dist/named-gates.js.map +1 -1
- package/dist/observer-link.d.ts +7 -0
- package/dist/observer-link.d.ts.map +1 -1
- package/dist/observer-link.js +3 -1
- package/dist/observer-link.js.map +1 -1
- package/dist/plugin-store.d.ts +8 -2
- package/dist/plugin-store.d.ts.map +1 -1
- package/dist/plugin-store.js +233 -56
- package/dist/plugin-store.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +7 -0
- package/dist/preflight.js.map +1 -1
- package/dist/protocol.d.ts +2 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/redact.d.ts +42 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +128 -0
- package/dist/redact.js.map +1 -1
- package/dist/run-projection.d.ts +83 -0
- package/dist/run-projection.d.ts.map +1 -0
- package/dist/run-projection.js +199 -0
- package/dist/run-projection.js.map +1 -0
- package/dist/semver-range.d.ts.map +1 -1
- package/dist/semver-range.js +27 -15
- package/dist/semver-range.js.map +1 -1
- package/dist/shell-word.d.ts +19 -0
- package/dist/shell-word.d.ts.map +1 -0
- package/dist/shell-word.js +23 -0
- package/dist/shell-word.js.map +1 -0
- package/dist/spec.d.ts +52 -3
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/step-fields.d.ts +1 -1
- package/dist/step-fields.js +1 -1
- package/dist/step-fields.js.map +1 -1
- package/dist/steps-green.d.ts +27 -0
- package/dist/steps-green.d.ts.map +1 -0
- package/dist/steps-green.js +164 -0
- package/dist/steps-green.js.map +1 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +39 -8
- package/dist/validate.js.map +1 -1
- package/dist/worker-lease.d.ts +8 -1
- package/dist/worker-lease.d.ts.map +1 -1
- package/dist/worker-lease.js +34 -4
- package/dist/worker-lease.js.map +1 -1
- package/dist/worker.d.ts +8 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +17 -9
- package/dist/worker.js.map +1 -1
- package/package.json +3 -7
- package/src/agent-artifacts.ts +2 -10
- package/src/agent-cwd.ts +139 -0
- package/src/artifact-scan-policy.ts +51 -0
- package/src/authored-completion-record.ts +119 -0
- package/src/authored-completion.ts +203 -0
- package/src/authored-flow-error.ts +13 -1
- package/src/authored-flow-executor.ts +101 -59
- package/src/authored-human.ts +1 -4
- package/src/authored-node-entry.ts +4 -1
- package/src/authored-node-runner.ts +96 -22
- package/src/authored-root.ts +20 -2
- package/src/authored-step-output.ts +139 -42
- package/src/authored-verdict.ts +87 -0
- package/src/authored-worker-step.ts +60 -12
- package/src/bundle-extensions.ts +6 -5
- package/src/bundle.ts +52 -7
- package/src/canonical.ts +20 -6
- package/src/cli/check-worker-surface.ts +66 -0
- package/src/cli/check.ts +35 -4
- package/src/cli/cloud-format.ts +82 -0
- package/src/cli/cloud-live.ts +366 -0
- package/src/cli/cloud-read.ts +13 -282
- package/src/cli/cloud-refusal.ts +113 -0
- package/src/cli/cloud-status-view.ts +213 -0
- package/src/cli/direct-run.ts +19 -2
- package/src/cli/hn-monitor.ts +2 -1
- package/src/cli/local-agent-remedy.ts +160 -0
- package/src/cli/observer-session.ts +107 -0
- package/src/cli/run.ts +233 -28
- package/src/cli/status.ts +59 -3
- package/src/cli/step-evidence.ts +310 -0
- package/src/cli/step-failure.ts +57 -81
- package/src/cli-commands.ts +2 -0
- package/src/cli.ts +64 -44
- package/src/cloud-read.ts +25 -1
- package/src/cloud-run-record.ts +64 -0
- package/src/cloud-run.ts +5 -24
- package/src/communication/local.ts +2 -1
- package/src/communication/worker.ts +13 -5
- package/src/compile.ts +28 -6
- package/src/direct-input.ts +22 -1
- package/src/failure-kinds.ts +104 -2
- package/src/flow-extension-compat.ts +32 -7
- package/src/flow-extension-loader.ts +56 -21
- package/src/flow-requirements.ts +24 -9
- package/src/fs-descriptor.ts +127 -0
- package/src/gate-contract.ts +13 -1
- package/src/helper-preflight.ts +10 -3
- package/src/helper-reference.ts +130 -0
- package/src/hosted-base-snapshot.ts +351 -0
- package/src/hosted-extension-declarations.ts +387 -0
- package/src/hosted-extension-isolation.ts +457 -0
- package/src/hosted-extension-manifest.ts +249 -0
- package/src/hosted-extension-protocol.ts +262 -0
- package/src/hosted-extension-runtime.ts +266 -0
- package/src/hosted-extension-sandbox-source.ts +94 -0
- package/src/hosted-extension-sandbox.ts +489 -0
- package/src/hosted-project.ts +18 -0
- package/src/hosted-promise-safety.ts +68 -0
- package/src/index.ts +20 -0
- package/src/intrinsic-array.ts +14 -0
- package/src/journal-client.ts +19 -2
- package/src/journal-projection.ts +106 -0
- package/src/json-value.ts +184 -32
- package/src/llm-worker.ts +16 -2
- package/src/local-agent.ts +4 -2
- package/src/named-gate-lowering.ts +35 -5
- package/src/named-gate-preflight.ts +47 -0
- package/src/named-gates.ts +3 -0
- package/src/observer-link.ts +10 -1
- package/src/plugin-store.ts +323 -56
- package/src/preflight.ts +7 -0
- package/src/protocol.ts +2 -0
- package/src/redact.ts +111 -0
- package/src/run-projection.ts +247 -0
- package/src/semver-range.ts +39 -14
- package/src/shell-word.ts +24 -0
- package/src/spec.ts +54 -3
- package/src/step-fields.ts +1 -1
- package/src/steps-green.ts +168 -0
- package/src/validate.ts +35 -8
- package/src/worker-lease.ts +35 -5
- package/src/worker.ts +27 -11
package/src/plugin-store.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { payloadManifest, safePath, sha256 } from './bundle.js';
|
|
5
|
+
import {
|
|
6
|
+
closeDescriptor,
|
|
7
|
+
closeDirectory,
|
|
8
|
+
descriptorIsDirectory,
|
|
9
|
+
descriptorIsFile,
|
|
10
|
+
directoryEntryIsDirectory,
|
|
11
|
+
directoryEntryIsFile,
|
|
12
|
+
lstatPath,
|
|
13
|
+
openDirectory,
|
|
14
|
+
openDescriptor,
|
|
15
|
+
readDescriptor,
|
|
16
|
+
readDirectoryEntry,
|
|
17
|
+
statDescriptor,
|
|
18
|
+
} from './fs-descriptor.js';
|
|
19
|
+
import { frozenHostedPromiseValue, hostedPromiseValue } from './hosted-promise-safety.js';
|
|
20
|
+
import { appendIntrinsicArray } from './intrinsic-array.js';
|
|
21
|
+
import { snapshotJsonValue, type JsonValue } from './json-value.js';
|
|
22
|
+
import { MAX_PLUGIN_FILE_BYTES, MAX_PLUGIN_FILES, MAX_PLUGIN_TOTAL_BYTES } from './plugin-github.js';
|
|
4
23
|
import { PluginError } from './plugin-manifest.js';
|
|
5
24
|
|
|
6
25
|
/**
|
|
@@ -13,93 +32,341 @@ import { PluginError } from './plugin-manifest.js';
|
|
|
13
32
|
* refusal, not a surprise.
|
|
14
33
|
*/
|
|
15
34
|
export const PLUGIN_STORE = '.flows/plugins';
|
|
35
|
+
const MKDIR = mkdir;
|
|
36
|
+
const MKDTEMP = mkdtemp;
|
|
37
|
+
const RENAME = rename;
|
|
38
|
+
const RM = rm;
|
|
39
|
+
const WRITE_FILE = writeFile;
|
|
40
|
+
const PATH_DIRNAME = dirname;
|
|
41
|
+
const PATH_JOIN = join;
|
|
42
|
+
const PATH_RESOLVE = resolve;
|
|
43
|
+
const PROCESS_PLATFORM = process.platform;
|
|
44
|
+
const ERROR = Error;
|
|
45
|
+
const MAX_PLUGIN_MANIFEST_BYTES = 4_000_000;
|
|
46
|
+
const MAX_PLUGIN_STORE_ENTRIES = 10_000;
|
|
47
|
+
const ARRAY_IS_ARRAY = Array.isArray;
|
|
48
|
+
const BIG_INT = BigInt;
|
|
49
|
+
const BUFFER_ALLOC_UNSAFE = Buffer.allocUnsafe;
|
|
50
|
+
const BUFFER_TO_STRING = Function.prototype.call.bind(Buffer.prototype.toString) as (
|
|
51
|
+
value: Buffer,
|
|
52
|
+
encoding: BufferEncoding,
|
|
53
|
+
) => string;
|
|
54
|
+
const JSON_PARSE = JSON.parse;
|
|
55
|
+
const NUMBER = Number;
|
|
56
|
+
const NUMBER_IS_SAFE_INTEGER = Number.isSafeInteger;
|
|
57
|
+
const OBJECT_CREATE = Object.create;
|
|
58
|
+
const OBJECT_FREEZE = Object.freeze;
|
|
59
|
+
const OBJECT_GET_OWN_PROPERTY_DESCRIPTOR = Object.getOwnPropertyDescriptor;
|
|
60
|
+
const REGEXP_TEST = Function.prototype.call.bind(RegExp.prototype.test) as (pattern: RegExp, value: string) => boolean;
|
|
61
|
+
const SET = Set;
|
|
62
|
+
const SET_ADD = Function.prototype.call.bind(Set.prototype.add) as <T>(set: Set<T>, value: T) => Set<T>;
|
|
63
|
+
const SET_FOR_EACH = Function.prototype.call.bind(Set.prototype.forEach) as <T>(
|
|
64
|
+
set: Set<T>,
|
|
65
|
+
callback: (value: T) => void,
|
|
66
|
+
) => void;
|
|
67
|
+
const SET_HAS = Function.prototype.call.bind(Set.prototype.has) as <T>(set: Set<T>, value: T) => boolean;
|
|
68
|
+
const STRING_SPLIT = Function.prototype.call.bind(String.prototype.split) as (
|
|
69
|
+
value: string,
|
|
70
|
+
separator: string,
|
|
71
|
+
) => string[];
|
|
72
|
+
const STRING_STARTS_WITH = Function.prototype.call.bind(String.prototype.startsWith) as (
|
|
73
|
+
value: string,
|
|
74
|
+
search: string,
|
|
75
|
+
) => boolean;
|
|
76
|
+
const READ_FLAGS = constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0);
|
|
16
77
|
|
|
17
78
|
export function pluginStoreDirectory(root: string, name: string, digest: string): string {
|
|
18
|
-
return
|
|
79
|
+
return PATH_JOIN(PATH_RESOLVE(root), PLUGIN_STORE, `${name}@sha256:${digest}`);
|
|
19
80
|
}
|
|
20
81
|
|
|
21
|
-
export interface StoredPluginFile {
|
|
82
|
+
export interface StoredPluginFile {
|
|
83
|
+
readonly path: string;
|
|
84
|
+
readonly data: Uint8Array;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @internal Deterministic race seams for the bounded store reader. */
|
|
88
|
+
export interface StoredPluginReadTestHooks {
|
|
89
|
+
readonly beforeOpen?: (path: string) => Promise<void>;
|
|
90
|
+
readonly afterStat?: (path: string) => Promise<void>;
|
|
91
|
+
readonly beforeDirectoryStat?: () => Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const EMPTY_STORED_PLUGIN_HOOKS = OBJECT_FREEZE(
|
|
95
|
+
OBJECT_CREATE(null),
|
|
96
|
+
) as StoredPluginReadTestHooks;
|
|
22
97
|
|
|
23
98
|
/** Write the files atomically; an existing directory is verified instead of overwritten. */
|
|
24
|
-
export async function materializePlugin(
|
|
99
|
+
export async function materializePlugin(
|
|
100
|
+
root: string,
|
|
101
|
+
name: string,
|
|
102
|
+
files: readonly StoredPluginFile[],
|
|
103
|
+
): Promise<{ directory: string; digest: string }> {
|
|
25
104
|
const manifest = payloadManifest(files);
|
|
26
105
|
const digest = sha256(manifest);
|
|
27
106
|
const directory = pluginStoreDirectory(root, name, digest);
|
|
28
107
|
let exists = false;
|
|
29
|
-
try { await lstat(directory); exists = true; }
|
|
30
|
-
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
31
|
-
if (exists) { await verifyStoredPlugin(directory, digest); return { directory, digest }; }
|
|
32
|
-
const parent = dirname(directory);
|
|
33
|
-
await mkdir(parent, { recursive: true });
|
|
34
|
-
const staging = await mkdtemp(join(parent, '.install-'));
|
|
35
108
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
109
|
+
await lstatPath(directory);
|
|
110
|
+
exists = true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
113
|
+
}
|
|
114
|
+
if (exists) {
|
|
115
|
+
await verifyStoredPlugin(directory, digest);
|
|
116
|
+
return frozenHostedPromiseValue({ directory, digest });
|
|
117
|
+
}
|
|
118
|
+
const parent = PATH_DIRNAME(directory);
|
|
119
|
+
await MKDIR(parent, { recursive: true });
|
|
120
|
+
const staging = await MKDTEMP(PATH_JOIN(parent, '.install-'));
|
|
121
|
+
try {
|
|
122
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
123
|
+
const file = files[index]!;
|
|
124
|
+
if (!safePath(file.path) || file.path === 'manifest.json')
|
|
125
|
+
throw new PluginError('plugin_path_invalid', `${file.path}: invalid plugin path.`);
|
|
126
|
+
await MKDIR(PATH_DIRNAME(PATH_JOIN(staging, file.path)), {
|
|
127
|
+
recursive: true,
|
|
128
|
+
});
|
|
129
|
+
await WRITE_FILE(PATH_JOIN(staging, file.path), file.data, {
|
|
130
|
+
mode: 0o644,
|
|
131
|
+
});
|
|
40
132
|
}
|
|
41
|
-
await
|
|
42
|
-
try {
|
|
43
|
-
|
|
44
|
-
|
|
133
|
+
await WRITE_FILE(PATH_JOIN(staging, 'manifest.json'), manifest);
|
|
134
|
+
try {
|
|
135
|
+
await RENAME(staging, directory);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
138
|
+
if (code !== 'EEXIST' && code !== 'ENOTEMPTY') throw error;
|
|
45
139
|
await verifyStoredPlugin(directory, digest);
|
|
46
140
|
}
|
|
47
|
-
} finally {
|
|
48
|
-
|
|
141
|
+
} finally {
|
|
142
|
+
await RM(staging, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
return frozenHostedPromiseValue({ directory, digest });
|
|
49
145
|
}
|
|
50
146
|
|
|
51
|
-
async function regularFile(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
147
|
+
async function regularFile(
|
|
148
|
+
root: string,
|
|
149
|
+
path: string,
|
|
150
|
+
maxBytes: number,
|
|
151
|
+
expectedBytes?: number,
|
|
152
|
+
hooks: StoredPluginReadTestHooks = EMPTY_STORED_PLUGIN_HOOKS,
|
|
153
|
+
): Promise<Buffer> {
|
|
154
|
+
const absolute = PATH_JOIN(root, path);
|
|
155
|
+
const handle = await openStoredFile(root, path, hooks);
|
|
156
|
+
try {
|
|
157
|
+
const before = await statDescriptor(handle, { bigint: true });
|
|
158
|
+
if (
|
|
159
|
+
!descriptorIsFile(before) ||
|
|
160
|
+
before.size < 0n ||
|
|
161
|
+
before.size > BIG_INT(maxBytes) ||
|
|
162
|
+
(expectedBytes !== undefined && before.size !== BIG_INT(expectedBytes))
|
|
163
|
+
) {
|
|
164
|
+
throw new PluginError('plugin_source_drift', `${path}: expected a bounded regular file.`);
|
|
165
|
+
}
|
|
166
|
+
await ownHook(hooks, 'afterStat')?.(absolute);
|
|
167
|
+
const size = NUMBER(before.size);
|
|
168
|
+
const bytes = BUFFER_ALLOC_UNSAFE(size);
|
|
169
|
+
let offset = 0;
|
|
170
|
+
while (offset < size) {
|
|
171
|
+
const bytesRead = await readDescriptor(handle, bytes, offset, size - offset, offset);
|
|
172
|
+
if (bytesRead === 0) {
|
|
173
|
+
throw new PluginError('plugin_source_drift', `${path}: changed while reading.`);
|
|
174
|
+
}
|
|
175
|
+
offset += bytesRead;
|
|
176
|
+
}
|
|
177
|
+
const extra = BUFFER_ALLOC_UNSAFE(1);
|
|
178
|
+
if ((await readDescriptor(handle, extra, 0, 1, size)) !== 0) {
|
|
179
|
+
throw new PluginError('plugin_source_drift', `${path}: changed while reading.`);
|
|
180
|
+
}
|
|
181
|
+
const after = await statDescriptor(handle, { bigint: true });
|
|
182
|
+
if (after.size !== before.size || after.mtimeNs !== before.mtimeNs || after.ctimeNs !== before.ctimeNs) {
|
|
183
|
+
throw new PluginError('plugin_source_drift', `${path}: changed while reading.`);
|
|
184
|
+
}
|
|
185
|
+
return hostedPromiseValue(bytes);
|
|
186
|
+
} finally {
|
|
187
|
+
await closeDescriptor(handle);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function openStoredFile(root: string, path: string, hooks: StoredPluginReadTestHooks): Promise<number> {
|
|
192
|
+
const parts = STRING_SPLIT(path, '/');
|
|
193
|
+
if (PROCESS_PLATFORM !== 'linux') {
|
|
194
|
+
for (let i = 1; i < parts.length; i++) {
|
|
195
|
+
let parent = root;
|
|
196
|
+
for (let index = 0; index < i; index += 1) parent = PATH_JOIN(parent, parts[index]!);
|
|
197
|
+
if (!descriptorIsDirectory(await lstatPath(parent))) {
|
|
198
|
+
throw new PluginError('plugin_source_drift', `${path}: expected a regular file, without symlinks.`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
await ownHook(hooks, 'beforeOpen')?.(PATH_JOIN(root, path));
|
|
202
|
+
return await openDescriptor(PATH_JOIN(root, path), READ_FLAGS);
|
|
203
|
+
}
|
|
204
|
+
let directory = await openDescriptor(PATH_RESOLVE(root), READ_FLAGS);
|
|
205
|
+
try {
|
|
206
|
+
if (!descriptorIsDirectory(await statDescriptor(directory, { bigint: true }))) {
|
|
207
|
+
throw new PluginError('plugin_source_drift', `${path}: plugin store root is not a directory.`);
|
|
208
|
+
}
|
|
209
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
210
|
+
const part = parts[index]!;
|
|
211
|
+
const child = await openDescriptor(`/proc/self/fd/${directory}/${part}`, READ_FLAGS);
|
|
212
|
+
let adopted = false;
|
|
213
|
+
try {
|
|
214
|
+
await ownHook(hooks, 'beforeDirectoryStat')?.();
|
|
215
|
+
if (!descriptorIsDirectory(await statDescriptor(child, { bigint: true }))) {
|
|
216
|
+
throw new PluginError('plugin_source_drift', `${path}: expected a regular file, without symlinks.`);
|
|
217
|
+
}
|
|
218
|
+
await closeDescriptor(directory);
|
|
219
|
+
directory = child;
|
|
220
|
+
adopted = true;
|
|
221
|
+
} finally {
|
|
222
|
+
if (!adopted) await closeDescriptor(child);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
await ownHook(hooks, 'beforeOpen')?.(PATH_JOIN(root, path));
|
|
226
|
+
return await openDescriptor(`/proc/self/fd/${directory}/${parts[parts.length - 1]!}`, READ_FLAGS);
|
|
227
|
+
} finally {
|
|
228
|
+
await closeDescriptor(directory);
|
|
56
229
|
}
|
|
57
|
-
return readFile(join(root, path));
|
|
58
230
|
}
|
|
59
231
|
|
|
60
232
|
/** Re-hash a materialized plugin and compare with the digest the lockfile recorded. */
|
|
61
|
-
export async function verifyStoredPlugin(
|
|
62
|
-
|
|
63
|
-
|
|
233
|
+
export async function verifyStoredPlugin(
|
|
234
|
+
directory: string,
|
|
235
|
+
expectedDigest: string,
|
|
236
|
+
hooks: StoredPluginReadTestHooks = EMPTY_STORED_PLUGIN_HOOKS,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
await readVerifiedStoredPluginFiles(directory, expectedDigest, hooks);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function readVerifiedStoredPluginFiles(
|
|
242
|
+
directory: string,
|
|
243
|
+
expectedDigest: string,
|
|
244
|
+
hooks: StoredPluginReadTestHooks,
|
|
245
|
+
): Promise<readonly { path: string; data: Buffer }[]> {
|
|
246
|
+
const drift = (message: string): never => {
|
|
247
|
+
throw new PluginError('plugin_source_drift', `${directory}: ${message}`);
|
|
248
|
+
};
|
|
249
|
+
let manifest: Buffer;
|
|
64
250
|
try {
|
|
65
|
-
if (!(await
|
|
66
|
-
|
|
67
|
-
} catch (error) {
|
|
251
|
+
if (!descriptorIsDirectory(await lstatPath(directory))) return drift('not a directory');
|
|
252
|
+
manifest = await regularFile(directory, 'manifest.json', MAX_PLUGIN_MANIFEST_BYTES, undefined, hooks);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return drift(error instanceof PluginError ? error.message : 'manifest.json is missing');
|
|
255
|
+
}
|
|
256
|
+
const raw = BUFFER_TO_STRING(manifest, 'utf8');
|
|
68
257
|
if (sha256(raw) !== expectedDigest) return drift('manifest.json digest differs from the lockfile');
|
|
69
|
-
let entries:
|
|
70
|
-
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
258
|
+
let entries: JsonValue[];
|
|
259
|
+
try {
|
|
260
|
+
const value = snapshotJsonValue(JSON_PARSE(raw), 'stored plugin manifest', {
|
|
261
|
+
maxBytes: MAX_PLUGIN_MANIFEST_BYTES,
|
|
262
|
+
maxDepth: 3,
|
|
263
|
+
maxNodes: 1 + MAX_PLUGIN_FILES * 4,
|
|
264
|
+
});
|
|
265
|
+
if (!ARRAY_IS_ARRAY(value)) throw new ERROR();
|
|
266
|
+
entries = value;
|
|
267
|
+
} catch {
|
|
268
|
+
return drift('manifest.json is not a manifest');
|
|
269
|
+
}
|
|
270
|
+
if (entries.length > MAX_PLUGIN_FILES) return drift(`manifest.json lists more than ${MAX_PLUGIN_FILES} files`);
|
|
271
|
+
const paths = new SET<string>();
|
|
272
|
+
const files = [{ path: 'manifest.json', data: manifest }];
|
|
273
|
+
let totalBytes = 0;
|
|
274
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
275
|
+
const entry = entries[index]!;
|
|
276
|
+
if (
|
|
277
|
+
typeof entry !== 'object' ||
|
|
278
|
+
entry === null ||
|
|
279
|
+
ARRAY_IS_ARRAY(entry)
|
|
280
|
+
)
|
|
281
|
+
return drift('manifest.json lists an invalid file');
|
|
282
|
+
const path = entry.path;
|
|
283
|
+
const digest = entry.sha256;
|
|
284
|
+
const bytes = entry.bytes;
|
|
285
|
+
if (
|
|
286
|
+
typeof path !== 'string' ||
|
|
287
|
+
!safePath(path) ||
|
|
288
|
+
typeof digest !== 'string' ||
|
|
289
|
+
!REGEXP_TEST(/^[a-f0-9]{64}$/, digest) ||
|
|
290
|
+
typeof bytes !== 'number' ||
|
|
291
|
+
!NUMBER_IS_SAFE_INTEGER(bytes) ||
|
|
292
|
+
bytes < 0 ||
|
|
293
|
+
bytes > MAX_PLUGIN_FILE_BYTES ||
|
|
294
|
+
SET_HAS(paths, path)
|
|
295
|
+
)
|
|
296
|
+
return drift('manifest.json lists an invalid file');
|
|
297
|
+
totalBytes += bytes;
|
|
298
|
+
if (totalBytes > MAX_PLUGIN_TOTAL_BYTES) return drift(`plugin exceeds ${MAX_PLUGIN_TOTAL_BYTES} bytes`);
|
|
75
299
|
let data: Buffer;
|
|
76
|
-
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
300
|
+
try {
|
|
301
|
+
data = await regularFile(directory, path, MAX_PLUGIN_FILE_BYTES, bytes, hooks);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return drift(error instanceof PluginError ? error.message : `${path} is missing`);
|
|
304
|
+
}
|
|
305
|
+
if (sha256(data) !== digest) return drift(`${path} changed since installation`);
|
|
306
|
+
SET_ADD(paths, path);
|
|
307
|
+
appendIntrinsicArray(files, { path, data });
|
|
308
|
+
}
|
|
309
|
+
SET_ADD(paths, 'manifest.json');
|
|
310
|
+
await rejectExtras(directory, '', paths, drift);
|
|
311
|
+
const frozen: Readonly<{ path: string; data: Buffer }>[] = [];
|
|
312
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
313
|
+
appendIntrinsicArray(frozen, OBJECT_FREEZE(files[index]!));
|
|
80
314
|
}
|
|
81
|
-
|
|
315
|
+
return frozenHostedPromiseValue(frozen);
|
|
82
316
|
}
|
|
83
317
|
|
|
84
318
|
/** Re-verify, then return every stored file including the payload `manifest.json`. */
|
|
85
|
-
export async function readStoredPluginFiles(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
319
|
+
export async function readStoredPluginFiles(
|
|
320
|
+
directory: string,
|
|
321
|
+
expectedDigest: string,
|
|
322
|
+
hooks: StoredPluginReadTestHooks = EMPTY_STORED_PLUGIN_HOOKS,
|
|
323
|
+
): Promise<readonly { path: string; data: Buffer }[]> {
|
|
324
|
+
return await readVerifiedStoredPluginFiles(directory, expectedDigest, hooks);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function ownHook(
|
|
328
|
+
hooks: StoredPluginReadTestHooks,
|
|
329
|
+
name: keyof StoredPluginReadTestHooks,
|
|
330
|
+
): ((...args: unknown[]) => Promise<void>) | undefined {
|
|
331
|
+
const descriptor = OBJECT_GET_OWN_PROPERTY_DESCRIPTOR(hooks, name);
|
|
332
|
+
return descriptor !== undefined && 'value' in descriptor && typeof descriptor.value === 'function'
|
|
333
|
+
? descriptor.value as (...args: unknown[]) => Promise<void>
|
|
334
|
+
: undefined;
|
|
92
335
|
}
|
|
93
336
|
|
|
94
337
|
/** Drop a materialized plugin directory. Missing is a no-op. */
|
|
95
338
|
export async function removeStoredPlugin(directory: string): Promise<void> {
|
|
96
|
-
await
|
|
339
|
+
await RM(directory, { recursive: true, force: true });
|
|
97
340
|
}
|
|
98
341
|
|
|
99
|
-
async function rejectExtras(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
342
|
+
async function rejectExtras(
|
|
343
|
+
root: string,
|
|
344
|
+
prefix: string,
|
|
345
|
+
paths: Set<string>,
|
|
346
|
+
drift: (m: string) => never,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
let entries = 0;
|
|
349
|
+
async function visit(currentPrefix: string): Promise<void> {
|
|
350
|
+
const directory = await openDirectory(PATH_JOIN(root, currentPrefix));
|
|
351
|
+
try {
|
|
352
|
+
for (;;) {
|
|
353
|
+
const entry = await readDirectoryEntry(directory);
|
|
354
|
+
if (entry === null) break;
|
|
355
|
+
entries += 1;
|
|
356
|
+
if (entries > MAX_PLUGIN_STORE_ENTRIES) drift('plugin store contains too many entries');
|
|
357
|
+
const path = currentPrefix + entry.name;
|
|
358
|
+
let declaredDescendant = false;
|
|
359
|
+
SET_FOR_EACH(paths, file => {
|
|
360
|
+
if (STRING_STARTS_WITH(file, `${path}/`)) declaredDescendant = true;
|
|
361
|
+
});
|
|
362
|
+
if (directoryEntryIsDirectory(entry) && declaredDescendant) await visit(`${path}/`);
|
|
363
|
+
else if (!directoryEntryIsFile(entry) || !SET_HAS(paths, path)) {
|
|
364
|
+
drift(`${path}: unlisted file or unsupported file type`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
} finally {
|
|
368
|
+
await closeDirectory(directory);
|
|
369
|
+
}
|
|
104
370
|
}
|
|
371
|
+
await visit(prefix);
|
|
105
372
|
}
|
package/src/preflight.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { compileSpec, CompileError } from './compile.js';
|
|
|
12
12
|
import { helperCall } from './yaml-helpers.js';
|
|
13
13
|
import { resolveCliModelSelection, type CliModelSource } from './cli-adapter.js';
|
|
14
14
|
import { isNamedGate, NAMED_GATE_FAILURE_KINDS, type NamedGateFailureKind } from './named-gates.js';
|
|
15
|
+
import { namedGateScanCoverageDiagnostics } from './named-gate-preflight.js';
|
|
15
16
|
import { compileScopes, type ScopeInput, type MountRegistry } from './scope-compiler.js';
|
|
16
17
|
import { readMountRegistry } from './mount-registry.js';
|
|
17
18
|
import type {
|
|
@@ -242,6 +243,12 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
|
|
|
242
243
|
// permissions they declared on the same file are not enforced.
|
|
243
244
|
diagnostics.push(...permissionsDiagnostics(compiled));
|
|
244
245
|
diagnostics.push(...scopeDiagnostics(compiled, options));
|
|
246
|
+
// Same rule, same reason: whether the bundled worker's artifact scan can
|
|
247
|
+
// reach an `artifact_exists` path is decidable from the snapshot, so it is
|
|
248
|
+
// collected here rather than beside `probeNamedGate` below — which CLI
|
|
249
|
+
// resolution, an unknown model, a bad scope or a budget refusal all return
|
|
250
|
+
// before.
|
|
251
|
+
diagnostics.push(...compiled.steps.flatMap(namedGateScanCoverageDiagnostics));
|
|
245
252
|
for (const server of new Set(options.mcpServers ?? [])) {
|
|
246
253
|
if (options.mcp !== undefined && Object.hasOwn(options.mcp, server)) continue;
|
|
247
254
|
diagnostics.push({ severity: 'refusal', kind: 'mcp_undeclared_server', server,
|
package/src/protocol.ts
CHANGED
|
@@ -96,6 +96,8 @@ export type StepUsage =
|
|
|
96
96
|
export interface RunStartParams {
|
|
97
97
|
/** Caller-owned retry identity. Reuse with a different spec is refused. */
|
|
98
98
|
admission_key?: string;
|
|
99
|
+
/** Push the new run's entries to this connection from `run.spawned` on, as `run.watch` does. */
|
|
100
|
+
watch?: boolean;
|
|
99
101
|
reuse_from_run_id?: string;
|
|
100
102
|
/**
|
|
101
103
|
* The kernel spec dialect — the ONE boundary shape `RunSpec::parse`
|
package/src/redact.ts
CHANGED
|
@@ -104,3 +104,114 @@ export function redactRelayError(message: string, env: NodeJS.ProcessEnv = proce
|
|
|
104
104
|
}
|
|
105
105
|
return redact(message, env);
|
|
106
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Credential heads whose value may continue past a line boundary.
|
|
110
|
+
*
|
|
111
|
+
* Every NAMED_VALUE pattern puts `\s` between the name and the value, and
|
|
112
|
+
* `\s` matches newlines: `authorization:` alone at the end of a poll is a
|
|
113
|
+
* complete, unredacted line, while the opaque value that lands on the next
|
|
114
|
+
* poll arrives with no recognizable credential context and would print
|
|
115
|
+
* verbatim. A trailing header — the colon plus only whitespace, or for
|
|
116
|
+
* `authorization` one scheme word — is therefore an open context. A header
|
|
117
|
+
* already followed by a non-space word has its value on that line and is not
|
|
118
|
+
* open. `Bearer` is the one bare scheme word the patterns treat as a name.
|
|
119
|
+
*/
|
|
120
|
+
const OPEN_HEADER_PATTERNS: readonly RegExp[] = [
|
|
121
|
+
/\bauthorization:(?:\s+\w+)?\s*$/i,
|
|
122
|
+
/\bx-callback-token:\s*$/i,
|
|
123
|
+
/\bx-nightcto-evidence-token:\s*$/i,
|
|
124
|
+
/\bBearer\s*$/,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/** A quoted JSON field name; the value side is inspected by the scanner. */
|
|
128
|
+
const JSON_FIELD_HEAD = /"([A-Za-z0-9_.\-]{1,64})"/g;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Where a named-credential context may still be half-arrived at the end of
|
|
132
|
+
* `text`.
|
|
133
|
+
*
|
|
134
|
+
* `openSecretStart` covers secret env *values*; this covers the credential
|
|
135
|
+
* *names* whose patterns can span a line break: a header released on one poll
|
|
136
|
+
* leaves its late-arriving value unrecognizable on the next, and a credential
|
|
137
|
+
* JSON field whose value string is not yet closed leaks its first fragment.
|
|
138
|
+
* Returns the earliest index at which such a context is open — the point past
|
|
139
|
+
* which nothing may be released until more of the stream arrives — or
|
|
140
|
+
* `text.length` when none is open. Holding back is line-granular upstream, so
|
|
141
|
+
* a false positive delays one line one poll rather than dropping it.
|
|
142
|
+
*/
|
|
143
|
+
export function openCredentialStart(text: string): number {
|
|
144
|
+
let hold = text.length;
|
|
145
|
+
for (const pattern of OPEN_HEADER_PATTERNS) {
|
|
146
|
+
const match = pattern.exec(text);
|
|
147
|
+
if (match && match.index < hold) hold = match.index;
|
|
148
|
+
}
|
|
149
|
+
JSON_FIELD_HEAD.lastIndex = 0;
|
|
150
|
+
let head: RegExpExecArray | null;
|
|
151
|
+
while ((head = JSON_FIELD_HEAD.exec(text)) !== null) {
|
|
152
|
+
if (!isCredentialName(head[1]!)) continue;
|
|
153
|
+
let i = head.index + head[0].length;
|
|
154
|
+
while (i < text.length && /\s/.test(text[i]!)) i++;
|
|
155
|
+
if (i >= text.length) { hold = Math.min(hold, head.index); continue; }
|
|
156
|
+
if (text[i] !== ':') continue;
|
|
157
|
+
i++;
|
|
158
|
+
while (i < text.length && /\s/.test(text[i]!)) i++;
|
|
159
|
+
if (i >= text.length) { hold = Math.min(hold, head.index); continue; }
|
|
160
|
+
if (text.startsWith('[redacted]', i) || text[i] !== '"') continue;
|
|
161
|
+
i++;
|
|
162
|
+
let closed = false;
|
|
163
|
+
while (i < text.length) {
|
|
164
|
+
if (text[i] === '\\') { i += 2; continue; }
|
|
165
|
+
if (text[i] === '"') { closed = true; break; }
|
|
166
|
+
i++;
|
|
167
|
+
}
|
|
168
|
+
if (!closed) hold = Math.min(hold, head.index);
|
|
169
|
+
}
|
|
170
|
+
return hold;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Does `text` end on a bare credential header — a named credential line whose
|
|
175
|
+
* value, if it has one, begins on the NEXT line?
|
|
176
|
+
*
|
|
177
|
+
* The stream releaser needs this apart from `openCredentialStart`: that check
|
|
178
|
+
* sees only a header at the very end of the consumed text, but the danger is
|
|
179
|
+
* wider — a released block that *ends* on `x-callback-token:` prints the
|
|
180
|
+
* header now and its value, arriving on the next line in some later poll,
|
|
181
|
+
* without the context `redact` needs. `redact` folds the header's newline into
|
|
182
|
+
* its own named-pattern match, so the releaser's block-prefix check cannot
|
|
183
|
+
* detect the dependency on its own. Trailing whitespace-only lines after the
|
|
184
|
+
* header count as part of it — the value is still to come.
|
|
185
|
+
*/
|
|
186
|
+
export function endsWithOpenCredentialHeader(text: string): boolean {
|
|
187
|
+
const stripped = text.replace(/\s+$/, '');
|
|
188
|
+
const lastLine = stripped.slice(stripped.lastIndexOf('\n') + 1);
|
|
189
|
+
return OPEN_HEADER_PATTERNS.some((pattern) => pattern.test(lastLine));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Where a secret value may still be half-arrived at the end of `text`.
|
|
194
|
+
*
|
|
195
|
+
* A stream is redacted in pieces, and `redact` only replaces a secret it can
|
|
196
|
+
* see whole: releasing text up to a point where a secret has begun but not
|
|
197
|
+
* ended would print the first half of it and never match the second. This
|
|
198
|
+
* returns the smallest index `i` for which `text.slice(i)` is a proper prefix
|
|
199
|
+
* of some secret env value — the point past which nothing may be released
|
|
200
|
+
* until more of the stream arrives — or `text.length` when no value is open.
|
|
201
|
+
*
|
|
202
|
+
* Values only, not the token and header *shapes*: those are bounded by
|
|
203
|
+
* whitespace, so a reader that releases whole lines cannot cut one in half.
|
|
204
|
+
*/
|
|
205
|
+
export function openSecretStart(text: string, env: NodeJS.ProcessEnv = process.env): number {
|
|
206
|
+
let start = text.length;
|
|
207
|
+
for (const [, value] of secretEnvValues(env)) {
|
|
208
|
+
// A proper prefix is shorter than the value, so it can only begin inside
|
|
209
|
+
// the last `value.length - 1` characters; the first character narrows the
|
|
210
|
+
// scan to the few positions worth comparing.
|
|
211
|
+
const first = Math.max(0, text.length - value.length + 1);
|
|
212
|
+
for (let at = text.indexOf(value[0]!, first); at >= 0 && at < start; at = text.indexOf(value[0]!, at + 1)) {
|
|
213
|
+
if (value.startsWith(text.slice(at))) { start = at; break; }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return start;
|
|
217
|
+
}
|