akm-cli 0.9.1 → 0.9.2-alpha.2
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/CHANGELOG.md +103 -28
- package/README.md +3 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/assets/hints/cli-hints-full.md +14 -9
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -1
- package/dist/assets/improve-strategies/reflect-distill.json +1 -1
- package/dist/assets/models.json +35 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +3 -4
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +1 -3
- package/dist/assets/tasks/core/extract.yml +6 -5
- package/dist/assets/tasks/core/improve.yml +6 -5
- package/dist/assets/tasks/core/index-refresh.yml +6 -5
- package/dist/assets/tasks/core/sync.yml +6 -5
- package/dist/assets/tasks/core/version-check.yml +6 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +6 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +6 -5
- package/dist/cli/confirm.js +2 -2
- package/dist/cli/parse-args.js +3 -24
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +2 -2
- package/dist/cli.js +11 -9
- package/dist/commands/agent/agent-dispatch.js +55 -89
- package/dist/commands/agent/contribute-cli.js +12 -45
- package/dist/commands/command/builtin-action.js +32 -0
- package/dist/commands/command/command-cli.js +99 -0
- package/dist/commands/command/command-execution.js +308 -0
- package/dist/commands/command/execution-source-loader.js +176 -0
- package/dist/commands/command/portable-template.js +60 -0
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env.js +4 -2
- package/dist/commands/feedback-cli.js +1 -1
- package/dist/commands/health/checks.js +241 -29
- package/dist/commands/health/html-report.js +0 -14
- package/dist/commands/health/report-view-model.js +0 -1
- package/dist/commands/health/surfaces.js +6 -7
- package/dist/commands/health/types.js +0 -2
- package/dist/commands/health.js +63 -18
- package/dist/commands/improve/collapse-detector.js +5 -6
- package/dist/commands/improve/consolidate.js +251 -214
- package/dist/commands/improve/distill/promote-memory.js +71 -34
- package/dist/commands/improve/distill/quality-gate.js +17 -5
- package/dist/commands/improve/distill.js +232 -155
- package/dist/commands/improve/eligibility.js +112 -79
- package/dist/commands/improve/execution.js +57 -0
- package/dist/commands/improve/extract-cli.js +5 -5
- package/dist/commands/improve/extract-prompt.js +64 -22
- package/dist/commands/improve/extract.js +608 -360
- package/dist/commands/improve/improve-strategies.js +43 -14
- package/dist/commands/improve/improve.js +249 -29
- package/dist/commands/improve/loop-stages.js +11 -17
- package/dist/commands/improve/memory/memory-contradiction-detect.js +90 -66
- package/dist/commands/improve/outcome-loop.js +22 -38
- package/dist/commands/improve/planner.js +134 -0
- package/dist/commands/improve/preparation.js +730 -409
- package/dist/commands/improve/reflect.js +386 -223
- package/dist/commands/improve/run-context.js +3 -4
- package/dist/commands/improve/salience.js +6 -58
- package/dist/commands/improve/session-asset.js +12 -12
- package/dist/commands/lint/index.js +101 -29
- package/dist/commands/migrate-cli.js +11 -69
- package/dist/commands/migration-tool.js +6 -9
- package/dist/commands/models-cli.js +27 -0
- package/dist/commands/proposal/drain.js +258 -186
- package/dist/commands/proposal/proposal-cli.js +32 -10
- package/dist/commands/proposal/proposal.js +2 -5
- package/dist/commands/proposal/propose.js +192 -172
- package/dist/commands/proposal/repository.js +54 -91
- package/dist/commands/proposal/validators/proposal-validators.js +9 -7
- package/dist/commands/read/curate.js +53 -22
- package/dist/commands/read/registry-search.js +25 -9
- package/dist/commands/read/remember-cli.js +14 -2
- package/dist/commands/read/search.js +10 -4
- package/dist/commands/read/show.js +139 -153
- package/dist/commands/registry-cli.js +16 -7
- package/dist/commands/remember.js +33 -18
- package/dist/commands/sources/add-cli.js +19 -178
- package/dist/commands/sources/bundle-cli.js +15 -3
- package/dist/commands/sources/dangerous-env-audit.js +135 -0
- package/dist/commands/sources/info.js +2 -1
- package/dist/commands/sources/installed-stashes.js +901 -177
- package/dist/commands/sources/schema-repair.js +174 -95
- package/dist/commands/sources/self-update.js +30 -74
- package/dist/commands/sources/source-add.js +3 -5
- package/dist/commands/sources/sources-cli.js +2 -15
- package/dist/commands/sources/update-transaction.js +220 -0
- package/dist/commands/tasks/tasks-cli.js +3 -3
- package/dist/commands/tasks/tasks.js +736 -317
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +3 -0
- package/dist/core/adapter/adapters/akm-adapter.js +85 -35
- package/dist/core/adapter/adapters/akm-lint.js +54 -39
- package/dist/core/adapter/adapters/akm-metadata.js +45 -45
- package/dist/core/adapter/adapters/akm-task-adapter.js +32 -49
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +38 -23
- package/dist/core/adapter/adapters/dotenv-adapter.js +30 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +11 -0
- package/dist/core/adapter/adapters/index.js +0 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +4 -0
- package/dist/core/adapter/adapters/okf-adapter.js +4 -0
- package/dist/core/adapter/adapters/opencode-adapter.js +5 -8
- package/dist/core/adapter/adapters/tool-dir-shared.js +63 -6
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -0
- package/dist/core/adapter/execution-source.js +308 -0
- package/dist/core/adapter/recognize-match.js +36 -13
- package/dist/core/adapter/registry.js +0 -9
- package/dist/core/asset/stash-meta.js +94 -4
- package/dist/core/common.js +6 -11
- package/dist/core/config/config-io.js +3 -3
- package/dist/core/config/config-schema.js +18 -40
- package/dist/core/config/config-sources.js +11 -21
- package/dist/core/config/config-walker.js +31 -13
- package/dist/core/config/config.js +23 -26
- package/dist/core/config/schema/engines.js +8 -7
- package/dist/core/config/schema/improve-processes.js +29 -5
- package/dist/core/config/schema/index-config.js +0 -27
- package/dist/core/config/schema/primitives.js +1 -23
- package/dist/core/config/schema/sources-bundles.js +13 -16
- package/dist/core/errors.js +2 -0
- package/dist/core/events.js +68 -32
- package/dist/core/extra-params.js +1 -0
- package/dist/core/improve-result.js +315 -0
- package/dist/core/lesson-lint.js +0 -6
- package/dist/core/maintenance-barrier.js +4 -4
- package/dist/core/network-policy.js +152 -0
- package/dist/core/paths.js +1 -1
- package/dist/core/recognition-util.js +4 -4
- package/dist/core/registry-url.js +456 -0
- package/dist/core/state/migrations.js +161 -47
- package/dist/core/state-db.js +453 -80
- package/dist/core/system-error.js +32 -0
- package/dist/core/time.js +2 -12
- package/dist/core/write-source.js +0 -18
- package/dist/execution/directory-identity.js +52 -0
- package/dist/execution/executable-identity.js +107 -0
- package/dist/execution/guarded-source.js +398 -0
- package/dist/execution/json.js +95 -0
- package/dist/{commands/health/types-session-log.js → execution/limits.js} +2 -1
- package/dist/execution/record.js +55 -0
- package/dist/execution/resolved-request.js +730 -0
- package/dist/execution/source.js +320 -0
- package/dist/indexer/bundle-identity-guard.js +5 -4
- package/dist/indexer/db/graph-db.js +33 -0
- package/dist/indexer/graph/graph-boost.js +3 -4
- package/dist/indexer/graph/graph-extraction.js +562 -373
- package/dist/indexer/index-written-assets.js +78 -39
- package/dist/indexer/indexer.js +471 -432
- package/dist/indexer/installations.js +6 -0
- package/dist/indexer/lookup/adapter-concept-owner.js +283 -0
- package/dist/indexer/materialize-embeddings.js +155 -0
- package/dist/indexer/passes/memory-inference.js +227 -174
- package/dist/indexer/passes/metadata.js +263 -118
- package/dist/indexer/scan/doc-to-entry.js +7 -10
- package/dist/indexer/scan/drain-dir.js +51 -23
- package/dist/indexer/search/db-search.js +156 -50
- package/dist/indexer/search/fts-query.js +40 -40
- package/dist/indexer/search/ranking.js +36 -1
- package/dist/indexer/search/search-attribution.js +3 -1
- package/dist/indexer/search/search-fields.js +23 -14
- package/dist/indexer/search/search-hit-enrichers.js +1 -1
- package/dist/indexer/search/search-source.js +7 -16
- package/dist/indexer/search/semantic-status.js +10 -1
- package/dist/indexer/usage/show-usage.js +105 -0
- package/dist/indexer/usage/usage-events.js +7 -2
- package/dist/indexer/walk/matchers.js +40 -10
- package/dist/indexer/walk/path-resolver.js +5 -2
- package/dist/indexer/walk/walker.js +20 -2
- package/dist/integrations/agent/builder-shared.js +3 -6
- package/dist/integrations/agent/conversation-fallback.js +16 -0
- package/dist/integrations/agent/engine-resolution.js +87 -87
- package/dist/integrations/agent/execution-cascade.js +566 -0
- package/dist/integrations/agent/execution-definitions.js +211 -0
- package/dist/integrations/agent/execution-lowering.js +811 -0
- package/dist/integrations/agent/execution-preparation.js +67 -0
- package/dist/integrations/agent/index.js +0 -2
- package/dist/integrations/agent/inline-execution.js +74 -0
- package/dist/integrations/agent/model-map.js +515 -0
- package/dist/integrations/agent/persona-fallback.js +30 -0
- package/dist/integrations/agent/request-lowering.js +186 -0
- package/dist/integrations/agent/runner-dispatch.js +230 -37
- package/dist/integrations/agent/runner.js +12 -83
- package/dist/integrations/harnesses/aider/agent-builder.js +8 -0
- package/dist/integrations/harnesses/aider/index.js +0 -1
- package/dist/integrations/harnesses/amazonq/agent-builder.js +8 -0
- package/dist/integrations/harnesses/amazonq/index.js +0 -1
- package/dist/integrations/harnesses/claude/agent-builder.js +14 -1
- package/dist/integrations/harnesses/claude/index.js +1 -5
- package/dist/integrations/harnesses/claude/session-log.js +3 -33
- package/dist/integrations/harnesses/codex/agent-builder.js +8 -0
- package/dist/integrations/harnesses/codex/index.js +0 -1
- package/dist/integrations/harnesses/copilot/agent-builder.js +8 -0
- package/dist/integrations/harnesses/copilot/index.js +0 -1
- package/dist/integrations/harnesses/gemini/agent-builder.js +8 -0
- package/dist/integrations/harnesses/gemini/index.js +0 -1
- package/dist/integrations/harnesses/index.js +4 -44
- package/dist/integrations/harnesses/opencode/agent-builder.js +16 -9
- package/dist/integrations/harnesses/opencode/index.js +0 -2
- package/dist/integrations/harnesses/opencode/session-log.js +14 -204
- package/dist/integrations/harnesses/opencode-sdk/harness.js +12 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +40 -42
- package/dist/integrations/harnesses/openhands/agent-builder.js +8 -0
- package/dist/integrations/harnesses/openhands/index.js +0 -1
- package/dist/integrations/harnesses/pi/agent-builder.js +8 -0
- package/dist/integrations/harnesses/pi/index.js +0 -1
- package/dist/integrations/harnesses/shared.js +0 -1
- package/dist/integrations/harnesses/types.js +1 -3
- package/dist/integrations/lockfile.js +82 -79
- package/dist/integrations/session-logs/index.js +6 -17
- package/dist/integrations/session-logs/provider-base.js +1 -29
- package/dist/llm/client.js +10 -5
- package/dist/llm/embedder.js +6 -7
- package/dist/llm/embedders/local.js +37 -88
- package/dist/llm/embedders/types.js +1 -1
- package/dist/llm/graph-extract.js +75 -50
- package/dist/llm/index-passes.js +43 -5
- package/dist/llm/memory-infer.js +8 -6
- package/dist/llm/metadata-enhance.js +5 -3
- package/dist/llm/structured-call.js +122 -25
- package/dist/output/format-exempt.js +1 -1
- package/dist/output/render-registry.js +0 -16
- package/dist/output/renderers.js +12 -7
- package/dist/output/shapes/curate.js +1 -0
- package/dist/output/shapes/helpers.js +10 -2
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/command-format.js +31 -33
- package/dist/output/text/health-format.js +1 -29
- package/dist/output/text/migrate.js +6 -56
- package/dist/output/text/proposal-format.js +16 -1
- package/dist/output/text/workflow-format.js +16 -0
- package/dist/registry/network.js +279 -0
- package/dist/registry/pinned-request-helper.js +247 -0
- package/dist/registry/pinned-transport.js +717 -0
- package/dist/registry/providers/skills-sh.js +18 -6
- package/dist/registry/providers/static-index.js +20 -7
- package/dist/registry/resolve.js +53 -28
- package/dist/scripts/akm-migrate-node.js +19334 -52269
- package/dist/scripts/akm-migrate.js +19270 -51612
- package/dist/setup/registry-stash-loader.js +64 -20
- package/dist/setup/semantic-assets.js +9 -34
- package/dist/setup/setup.js +12 -30
- package/dist/setup/source-identity.js +17 -0
- package/dist/setup/steps/sources.js +36 -15
- package/dist/setup/steps/tasks.js +39 -11
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/providers/npm.js +2 -2
- package/dist/sources/providers/provider-utils.js +4 -3
- package/dist/sources/providers/website.js +11 -7
- package/dist/sources/snapshot-fetchers/host-guard.js +9 -136
- package/dist/sources/snapshot-fetchers/website-ingest.js +25 -109
- package/dist/sources/website-url.js +73 -0
- package/dist/storage/engines/sqlite-migrations.js +81 -26
- package/dist/storage/managed-db.js +27 -24
- package/dist/storage/repositories/events-repository.js +3 -0
- package/dist/storage/repositories/index-connection.js +42 -10
- package/dist/storage/repositories/index-entries-repository.js +203 -229
- package/dist/storage/repositories/index-entry-mapper.js +8 -12
- package/dist/storage/repositories/index-entry-schema.js +255 -0
- package/dist/storage/repositories/index-fts-repository.js +64 -71
- package/dist/storage/repositories/index-llm-cache-repository.js +8 -13
- package/dist/storage/repositories/index-meta-repository.js +0 -11
- package/dist/storage/repositories/index-schema.js +74 -350
- package/dist/storage/repositories/index-utility-repository.js +12 -17
- package/dist/storage/repositories/index-vec-repository.js +56 -7
- package/dist/storage/repositories/proposals-repository.js +4 -127
- package/dist/storage/repositories/registry-cache.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +20 -40
- package/dist/storage/repositories/workflow-runs-repository.js +228 -129
- package/dist/storage/sqlite-read-snapshot.js +148 -0
- package/dist/tasks/backends/cron.js +170 -42
- package/dist/tasks/backends/index.js +1 -1
- package/dist/tasks/backends/launchd.js +787 -202
- package/dist/tasks/backends/schtasks.js +282 -83
- package/dist/tasks/embedded.js +7 -7
- package/dist/tasks/frozen-script.js +50 -0
- package/dist/tasks/resolve-akm-bin.js +5 -1
- package/dist/tasks/runner.js +239 -251
- package/dist/tasks/runtime-v3.js +281 -0
- package/dist/tasks/scheduler-binding.js +272 -0
- package/dist/tasks/scheduler-invocation.js +57 -43
- package/dist/tasks/scheduler-sync.js +654 -0
- package/dist/tasks/source-v3.js +752 -0
- package/dist/tasks/standalone-script-entry.js +5 -0
- package/dist/tasks/task-id.js +29 -0
- package/dist/workflows/authoring/authoring.js +15 -32
- package/dist/workflows/exec/dispatch-redaction.js +14 -8
- package/dist/workflows/exec/exec-unit.js +7 -28
- package/dist/workflows/exec/frozen-judge.js +57 -89
- package/dist/workflows/exec/lowering-notices.js +23 -0
- package/dist/workflows/exec/native-executor.js +301 -458
- package/dist/workflows/exec/param-secrets.js +4 -3
- package/dist/workflows/exec/run-workflow.js +26 -32
- package/dist/workflows/exec/step-work.js +105 -109
- package/dist/workflows/exec/unit-dispatch.js +103 -27
- package/dist/workflows/exec/unit-writer.js +3 -3
- package/dist/workflows/exec/worktree.js +2 -2
- package/dist/workflows/ir/compile.js +86 -72
- package/dist/workflows/ir/environment-v4.js +328 -0
- package/dist/workflows/ir/freeze-v4.js +122 -0
- package/dist/workflows/ir/plan-hash.js +13 -7
- package/dist/workflows/ir/schema-v4.js +525 -0
- package/dist/workflows/ir/schema.js +25 -284
- package/dist/workflows/ir/source-freeze-v4.js +506 -0
- package/dist/workflows/parser.js +27 -24
- package/dist/workflows/program/schema.js +1 -2
- package/dist/workflows/renderer.js +42 -29
- package/dist/workflows/resource-limits.js +4 -5
- package/dist/workflows/runtime/agent-identity.js +11 -13
- package/dist/workflows/runtime/plan-classifier.js +8 -8
- package/dist/workflows/runtime/runs.js +27 -43
- package/dist/workflows/runtime/workflow-asset-loader.js +45 -205
- package/dist/workflows/source-files.js +373 -0
- package/dist/workflows/source-ir/compile.js +196 -0
- package/dist/workflows/source-ir/github-yaml.js +577 -0
- package/dist/workflows/source-ir/ordering.js +38 -0
- package/dist/workflows/source-ir/program.js +50 -0
- package/dist/workflows/source-ir/result.js +26 -0
- package/dist/workflows/source-ir/schema.js +772 -0
- package/dist/workflows/source-ir/semantics.js +242 -0
- package/dist/workflows/source-ir/uses.js +14 -0
- package/docs/README.md +2 -0
- package/docs/migration/README.md +3 -1
- package/docs/migration/release-notes/0.9.2.md +55 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/migration/v0.8-to-v0.9.md +76 -1077
- package/docs/migration/v0.9.0-troubleshooting.md +104 -516
- package/docs/migration/v0.9.1-to-v0.9.2.md +150 -0
- package/docs/reference/README.md +1 -0
- package/docs/reference/cli.md +230 -98
- package/docs/reference/configuration.md +159 -36
- package/docs/reference/data-and-telemetry.md +19 -1
- package/docs/reference/supported-formats.md +23 -3
- package/docs/reference/tasks.md +182 -0
- package/docs/reference/workflow-schema.md +91 -40
- package/docs/reference/workflows.md +33 -6
- package/package.json +10 -6
- package/schemas/akm-config.json +372 -224
- package/schemas/akm-task.json +324 -80
- package/schemas/akm-workflow.json +6 -9
- package/dist/core/migration-operation.js +0 -75
- package/dist/integrations/agent/model-aliases.js +0 -74
- package/dist/tasks/parser.js +0 -380
- package/dist/tasks/schema.js +0 -123
- package/dist/tasks/validator.js +0 -80
- package/dist/workflows/ir/freeze.js +0 -320
- package/dist/workflows/runtime/document-cache.js +0 -13
package/dist/core/time.js
CHANGED
|
@@ -21,23 +21,20 @@ const MONTH_MS = 30 * DAY_MS;
|
|
|
21
21
|
*
|
|
22
22
|
* The grammar is intentionally uniform across the whole CLI:
|
|
23
23
|
* - `m` = MINUTES, `M` = MONTHS (30-day approximation)
|
|
24
|
-
* - `h
|
|
24
|
+
* - `h` = hours, `d` = days
|
|
25
25
|
*
|
|
26
26
|
* Matching is CASE-SENSITIVE (see {@link parseDuration}), which is what lets
|
|
27
27
|
* `m` and `M` mean different things. Historically `akm health --since` and
|
|
28
28
|
* `remember --expires` read a case-insensitive `m` as MONTHS while
|
|
29
29
|
* `consolidate` / `--window-compare` read it as MINUTES; that split is now
|
|
30
30
|
* resolved in favour of the conventional `m`=minutes, with `M` reserved for
|
|
31
|
-
* months.
|
|
32
|
-
* relied on the old case-insensitive parsers (e.g. `"7D"`) keep working.
|
|
31
|
+
* months.
|
|
33
32
|
*/
|
|
34
33
|
export const DURATION_UNITS = {
|
|
35
34
|
m: MINUTE_MS,
|
|
36
35
|
M: MONTH_MS,
|
|
37
36
|
h: HOUR_MS,
|
|
38
|
-
H: HOUR_MS,
|
|
39
37
|
d: DAY_MS,
|
|
40
|
-
D: DAY_MS,
|
|
41
38
|
};
|
|
42
39
|
/**
|
|
43
40
|
* Parse a compact duration shorthand (e.g. `"30d"`, `"12h"`, `"5m"`, `"3M"`)
|
|
@@ -118,10 +115,3 @@ export function parseSinceToIsoLenient(since) {
|
|
|
118
115
|
return since;
|
|
119
116
|
return new Date(Date.now() - ms).toISOString();
|
|
120
117
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Convert an ISO-8601 timestamp string to the SQLite datetime format
|
|
123
|
-
* `"YYYY-MM-DD HH:MM:SS"` used by `datetime('now')`.
|
|
124
|
-
*/
|
|
125
|
-
export function isoToSqlite(iso) {
|
|
126
|
-
return iso.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
127
|
-
}
|
|
@@ -893,24 +893,6 @@ export function prepareWriteTargetForMutation(target, options = {}) {
|
|
|
893
893
|
function gitTargetNotMaterialized(target, contentRoot) {
|
|
894
894
|
return new ConfigError(`Writable Git target "${target.source.name}" is not materialized as a Git checkout at ${contentRoot}; refusing to write without a commit boundary.`, "INVALID_CONFIG_FILE", `Run \`akm bundle update ${target.source.name}\` to materialize it, or point the bundle at a writable Git checkout.`);
|
|
895
895
|
}
|
|
896
|
-
/** Enumerate enabled writable targets, deduplicated by materialized content root. */
|
|
897
|
-
export function resolveWritableTargets(akmConfig) {
|
|
898
|
-
const byRoot = new Map();
|
|
899
|
-
for (const runtime of resolveConfiguredSources(akmConfig)) {
|
|
900
|
-
if (runtime.enabled === false || !resolveWritable({ type: runtime.type, writable: runtime.writable }))
|
|
901
|
-
continue;
|
|
902
|
-
const target = adaptConfiguredSource(runtime);
|
|
903
|
-
const root = path.resolve(target.source.path);
|
|
904
|
-
const existing = byRoot.get(root);
|
|
905
|
-
if (!existing || target.source.name === akmConfig.defaultWriteTarget)
|
|
906
|
-
byRoot.set(root, target);
|
|
907
|
-
}
|
|
908
|
-
if (process.env.AKM_BUNDLE_DIR?.trim()) {
|
|
909
|
-
const target = resolveWorkingStashTarget(akmConfig);
|
|
910
|
-
byRoot.set(path.resolve(target.source.path), target);
|
|
911
|
-
}
|
|
912
|
-
return [...byRoot.values()];
|
|
913
|
-
}
|
|
914
896
|
/**
|
|
915
897
|
* Resolve the destination for a write per locked decision 3:
|
|
916
898
|
*
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { UsageError } from "../core/errors.js";
|
|
7
|
+
export function captureFrozenDirectoryIdentity(rootInput, relativeCwd) {
|
|
8
|
+
const requestedRoot = path.resolve(rootInput);
|
|
9
|
+
const requestedCwd = path.resolve(requestedRoot, relativeCwd ?? ".");
|
|
10
|
+
requireContained(requestedRoot, requestedCwd);
|
|
11
|
+
const realRoot = fs.realpathSync(requestedRoot);
|
|
12
|
+
const realCwd = fs.realpathSync(requestedCwd);
|
|
13
|
+
requireContained(realRoot, realCwd);
|
|
14
|
+
const rootStat = fs.statSync(realRoot, { bigint: true });
|
|
15
|
+
const cwdStat = fs.statSync(realCwd, { bigint: true });
|
|
16
|
+
if (!rootStat.isDirectory() || !cwdStat.isDirectory()) {
|
|
17
|
+
throw new UsageError("Execution root and cwd must both be directories.", "INVALID_FLAG_VALUE");
|
|
18
|
+
}
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
requestedRoot,
|
|
21
|
+
realRoot,
|
|
22
|
+
rootDevice: rootStat.dev.toString(),
|
|
23
|
+
rootInode: rootStat.ino.toString(),
|
|
24
|
+
requestedCwd,
|
|
25
|
+
realCwd,
|
|
26
|
+
cwdDevice: cwdStat.dev.toString(),
|
|
27
|
+
cwdInode: cwdStat.ino.toString(),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function assertFrozenDirectoryIdentity(identity) {
|
|
31
|
+
let actual;
|
|
32
|
+
try {
|
|
33
|
+
const relative = path.relative(identity.requestedRoot, identity.requestedCwd);
|
|
34
|
+
actual = captureFrozenDirectoryIdentity(identity.requestedRoot, relative);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw changed(identity.requestedCwd);
|
|
38
|
+
}
|
|
39
|
+
for (const key of ["realRoot", "rootDevice", "rootInode", "realCwd", "cwdDevice", "cwdInode"]) {
|
|
40
|
+
if (actual[key] !== identity[key])
|
|
41
|
+
throw changed(identity.requestedCwd);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function requireContained(root, candidate) {
|
|
45
|
+
const relative = path.relative(root, candidate);
|
|
46
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
47
|
+
throw new UsageError(`${candidate} escapes its execution root.`, "PATH_ESCAPE_VIOLATION");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function changed(cwd) {
|
|
51
|
+
return new UsageError(`Frozen execution cwd ${cwd} changed after publication.`, "RESOURCE_ALREADY_EXISTS");
|
|
52
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { UsageError } from "../core/errors.js";
|
|
8
|
+
export function freezeExecutableIdentity(requested, options = {}) {
|
|
9
|
+
const absolutePath = resolveExecutable(requested, options.cwd ?? process.cwd(), options.path ?? process.env.PATH ?? "");
|
|
10
|
+
let realPath;
|
|
11
|
+
let stat;
|
|
12
|
+
let bytes;
|
|
13
|
+
try {
|
|
14
|
+
fs.accessSync(absolutePath, fs.constants.X_OK);
|
|
15
|
+
realPath = fs.realpathSync(absolutePath);
|
|
16
|
+
stat = fs.lstatSync(realPath, { bigint: true });
|
|
17
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
18
|
+
throw new Error("not a regular executable file");
|
|
19
|
+
bytes = fs.readFileSync(realPath);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new UsageError(`Executable ${JSON.stringify(requested)} could not be frozen.`, "INVALID_FLAG_VALUE");
|
|
23
|
+
}
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
requested,
|
|
26
|
+
absolutePath,
|
|
27
|
+
realPath,
|
|
28
|
+
device: stat.dev.toString(),
|
|
29
|
+
inode: stat.ino.toString(),
|
|
30
|
+
size: bytes.byteLength,
|
|
31
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function decodeFrozenExecutableIdentity(value, label = "executable") {
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
throw invalid(label);
|
|
37
|
+
const allowed = new Set(["requested", "absolutePath", "realPath", "device", "inode", "size", "sha256"]);
|
|
38
|
+
for (const key of Object.keys(value))
|
|
39
|
+
if (!allowed.has(key))
|
|
40
|
+
throw invalid(label);
|
|
41
|
+
if (typeof value.requested !== "string" ||
|
|
42
|
+
typeof value.absolutePath !== "string" ||
|
|
43
|
+
!path.isAbsolute(value.absolutePath) ||
|
|
44
|
+
typeof value.realPath !== "string" ||
|
|
45
|
+
!path.isAbsolute(value.realPath) ||
|
|
46
|
+
typeof value.device !== "string" ||
|
|
47
|
+
typeof value.inode !== "string" ||
|
|
48
|
+
!Number.isSafeInteger(value.size) ||
|
|
49
|
+
value.size < 0 ||
|
|
50
|
+
typeof value.sha256 !== "string" ||
|
|
51
|
+
!/^[0-9a-f]{64}$/.test(value.sha256)) {
|
|
52
|
+
throw invalid(label);
|
|
53
|
+
}
|
|
54
|
+
const decoded = Object.freeze({
|
|
55
|
+
requested: value.requested,
|
|
56
|
+
absolutePath: value.absolutePath,
|
|
57
|
+
realPath: value.realPath,
|
|
58
|
+
device: value.device,
|
|
59
|
+
inode: value.inode,
|
|
60
|
+
size: value.size,
|
|
61
|
+
sha256: value.sha256,
|
|
62
|
+
});
|
|
63
|
+
assertFrozenExecutableIdentity(decoded, label);
|
|
64
|
+
return decoded;
|
|
65
|
+
}
|
|
66
|
+
export function assertFrozenExecutableIdentity(identity, label = "executable") {
|
|
67
|
+
let actual;
|
|
68
|
+
try {
|
|
69
|
+
actual = freezeExecutableIdentity(identity.absolutePath, { cwd: path.dirname(identity.absolutePath) });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw changed(label);
|
|
73
|
+
}
|
|
74
|
+
for (const key of ["absolutePath", "realPath", "device", "inode", "size", "sha256"]) {
|
|
75
|
+
if (actual[key] !== identity[key])
|
|
76
|
+
throw changed(label);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function resolveExecutable(requested, cwd, pathValue) {
|
|
80
|
+
if (requested.includes("/") || requested.includes("\\"))
|
|
81
|
+
return path.resolve(cwd, requested);
|
|
82
|
+
const extensions = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
83
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
84
|
+
if (!directory)
|
|
85
|
+
continue;
|
|
86
|
+
for (const extension of extensions) {
|
|
87
|
+
const candidate = path.resolve(directory, `${requested}${extension}`);
|
|
88
|
+
try {
|
|
89
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
90
|
+
return candidate;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Continue through the frozen search path.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw new UsageError(`Executable ${JSON.stringify(requested)} was not found.`, "INVALID_FLAG_VALUE");
|
|
98
|
+
}
|
|
99
|
+
function isRecord(value) {
|
|
100
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
function invalid(label) {
|
|
103
|
+
return new UsageError(`Frozen ${label} identity is invalid.`, "INVALID_FLAG_VALUE");
|
|
104
|
+
}
|
|
105
|
+
function changed(label) {
|
|
106
|
+
return new UsageError(`Frozen ${label} changed after publication.`, "RESOURCE_ALREADY_EXISTS");
|
|
107
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { parseFrontmatter } from "../core/asset/frontmatter.js";
|
|
8
|
+
import { UsageError } from "../core/errors.js";
|
|
9
|
+
import { createExecutionSourceIdentity } from "./source.js";
|
|
10
|
+
export const DEFAULT_GUARDED_SOURCE_MAX_BYTES = 1024 * 1024;
|
|
11
|
+
function errorMessage(cause) {
|
|
12
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
13
|
+
}
|
|
14
|
+
function toPosix(value) {
|
|
15
|
+
return value.replaceAll("\\", "/");
|
|
16
|
+
}
|
|
17
|
+
function compareCodePoints(left, right) {
|
|
18
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
function physicalIdentity(realPath, stat) {
|
|
21
|
+
return stat.ino === 0n ? `path:${realPath}` : `inode:${stat.dev}:${stat.ino}`;
|
|
22
|
+
}
|
|
23
|
+
function statVersion(stat) {
|
|
24
|
+
return `${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
25
|
+
}
|
|
26
|
+
function sameBigIntStat(left, right) {
|
|
27
|
+
return (left.dev === right.dev &&
|
|
28
|
+
left.ino === right.ino &&
|
|
29
|
+
left.mode === right.mode &&
|
|
30
|
+
left.size === right.size &&
|
|
31
|
+
left.mtimeNs === right.mtimeNs &&
|
|
32
|
+
left.ctimeNs === right.ctimeNs);
|
|
33
|
+
}
|
|
34
|
+
function containedRelative(root, candidate, allowRoot) {
|
|
35
|
+
const relative = path.relative(root, candidate);
|
|
36
|
+
if ((!allowRoot && relative === "") || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
37
|
+
throw new UsageError(`${candidate} resolves outside its guarded containment root.`, "PATH_ESCAPE_VIOLATION");
|
|
38
|
+
}
|
|
39
|
+
return relative;
|
|
40
|
+
}
|
|
41
|
+
function requireContainmentRoot(containmentRootInput) {
|
|
42
|
+
const containmentRoot = path.resolve(containmentRootInput);
|
|
43
|
+
const containmentRealPath = fs.realpathSync(containmentRoot);
|
|
44
|
+
const containmentStat = fs.statSync(containmentRealPath, { bigint: true });
|
|
45
|
+
if (!containmentStat.isDirectory()) {
|
|
46
|
+
throw new UsageError(`${containmentRootInput} is not a guarded source directory.`, "INVALID_FLAG_VALUE");
|
|
47
|
+
}
|
|
48
|
+
return { containmentRoot, containmentRealPath, containmentStat };
|
|
49
|
+
}
|
|
50
|
+
function numberStat(stat) {
|
|
51
|
+
return new Proxy(stat, {
|
|
52
|
+
get(target, property, receiver) {
|
|
53
|
+
const value = Reflect.get(target, property, receiver);
|
|
54
|
+
return typeof value === "bigint" ? Number(value) : value;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function decodeUtf8(bytes, sourcePath) {
|
|
59
|
+
try {
|
|
60
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new UsageError(`${sourcePath} contains invalid UTF-8 bytes.`, "INVALID_FLAG_VALUE");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function freezeSource(source) {
|
|
67
|
+
const identity = source.identity ? createExecutionSourceIdentity(source.identity) : undefined;
|
|
68
|
+
return Object.freeze({ ...source, ...(identity ? { identity } : {}) });
|
|
69
|
+
}
|
|
70
|
+
function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
|
|
71
|
+
const sourcePath = path.resolve(sourcePathInput);
|
|
72
|
+
const { containmentRoot, containmentRealPath, containmentStat } = requireContainmentRoot(containmentRootInput);
|
|
73
|
+
const lexicalRelative = containedRelative(containmentRoot, sourcePath, false);
|
|
74
|
+
const maxBytes = options.maxBytes ?? DEFAULT_GUARDED_SOURCE_MAX_BYTES;
|
|
75
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
76
|
+
throw new UsageError("Guarded source byte limit must be a non-negative safe integer.", "INVALID_FLAG_VALUE");
|
|
77
|
+
}
|
|
78
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
|
|
79
|
+
let descriptor;
|
|
80
|
+
try {
|
|
81
|
+
descriptor = fs.openSync(sourcePath, fs.constants.O_RDONLY | noFollow);
|
|
82
|
+
const before = fs.fstatSync(descriptor, { bigint: true });
|
|
83
|
+
if (!before.isFile()) {
|
|
84
|
+
throw new UsageError(`${sourcePath} is not a regular guarded source file.`, "INVALID_FLAG_VALUE");
|
|
85
|
+
}
|
|
86
|
+
if (before.size > BigInt(maxBytes)) {
|
|
87
|
+
throw new UsageError(`${sourcePath} exceeds the guarded source size limit (1 MiB; ${maxBytes} bytes).`, "INVALID_FLAG_VALUE");
|
|
88
|
+
}
|
|
89
|
+
const bytes = fs.readFileSync(descriptor);
|
|
90
|
+
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
91
|
+
if (!sameBigIntStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
|
|
92
|
+
throw new UsageError(`${sourcePath} changed while its guarded bytes were read.`, "RESOURCE_ALREADY_EXISTS");
|
|
93
|
+
}
|
|
94
|
+
const pathnameStat = fs.lstatSync(sourcePath, { bigint: true });
|
|
95
|
+
if (pathnameStat.isSymbolicLink()) {
|
|
96
|
+
throw new UsageError(`${sourcePath} is a symbolic source; guarded reads require a regular no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
97
|
+
}
|
|
98
|
+
if (!pathnameStat.isFile() || pathnameStat.dev !== before.dev || pathnameStat.ino !== before.ino) {
|
|
99
|
+
throw new UsageError(`${sourcePath} changed physical identity during its guarded read.`, "RESOURCE_ALREADY_EXISTS");
|
|
100
|
+
}
|
|
101
|
+
const realPath = fs.realpathSync(sourcePath);
|
|
102
|
+
containedRelative(containmentRealPath, realPath, false);
|
|
103
|
+
const content = decodeUtf8(bytes, sourcePath);
|
|
104
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
105
|
+
if (options.identity && options.identity.hash !== sha256) {
|
|
106
|
+
throw new UsageError(`${sourcePath} adapter identity hash does not match its guarded bytes.`, "RESOURCE_ALREADY_EXISTS");
|
|
107
|
+
}
|
|
108
|
+
if (options.identity && options.identity.file !== toPosix(lexicalRelative)) {
|
|
109
|
+
throw new UsageError(`${sourcePath} adapter identity file does not match its guarded relative path.`, "RESOURCE_ALREADY_EXISTS");
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
source: freezeSource({
|
|
113
|
+
sourcePath,
|
|
114
|
+
relativePath: toPosix(lexicalRelative),
|
|
115
|
+
realPath,
|
|
116
|
+
containmentRoot,
|
|
117
|
+
containmentRealPath,
|
|
118
|
+
containmentPhysicalIdentity: physicalIdentity(containmentRealPath, containmentStat),
|
|
119
|
+
physicalIdentity: physicalIdentity(realPath, before),
|
|
120
|
+
size: bytes.byteLength,
|
|
121
|
+
mtimeNs: String(before.mtimeNs),
|
|
122
|
+
ctimeNs: String(before.ctimeNs),
|
|
123
|
+
sha256,
|
|
124
|
+
bytesBase64: bytes.toString("base64"),
|
|
125
|
+
content,
|
|
126
|
+
authored: options.authored ?? false,
|
|
127
|
+
...(options.identity ? { identity: options.identity } : {}),
|
|
128
|
+
}),
|
|
129
|
+
stat: numberStat(before),
|
|
130
|
+
maxBytes,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (cause) {
|
|
134
|
+
if (cause instanceof UsageError)
|
|
135
|
+
throw cause;
|
|
136
|
+
const code = cause.code;
|
|
137
|
+
if (code === "ELOOP") {
|
|
138
|
+
let linked;
|
|
139
|
+
try {
|
|
140
|
+
linked = fs.realpathSync(sourcePath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// The no-follow failure itself is sufficient when the link is broken.
|
|
144
|
+
}
|
|
145
|
+
if (linked) {
|
|
146
|
+
const relative = path.relative(containmentRealPath, linked);
|
|
147
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
148
|
+
throw new UsageError(`${sourcePath} resolves outside its containment root through a symbolic source.`, "PATH_ESCAPE_VIOLATION");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
throw new UsageError(`${sourcePath} is a symbolic source; guarded reads require a regular no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
152
|
+
}
|
|
153
|
+
throw new UsageError(`${sourcePath} could not be guarded as a contained regular source: ${errorMessage(cause)}`, code === "ENOENT" ? "RESOURCE_ALREADY_EXISTS" : "PATH_ESCAPE_VIOLATION");
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
if (descriptor !== undefined)
|
|
157
|
+
fs.closeSync(descriptor);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function captureGuardedExecutionSource(sourcePath, containmentRoot, options = {}) {
|
|
161
|
+
return captureRecord(sourcePath, containmentRoot, options).source;
|
|
162
|
+
}
|
|
163
|
+
export function captureGuardedDirectoryManifest(directoryPathInput, containmentRootInput) {
|
|
164
|
+
const directoryPath = path.resolve(directoryPathInput);
|
|
165
|
+
const { containmentRoot, containmentRealPath, containmentStat } = requireContainmentRoot(containmentRootInput);
|
|
166
|
+
const lexicalRelative = containedRelative(containmentRoot, directoryPath, true);
|
|
167
|
+
try {
|
|
168
|
+
const before = fs.lstatSync(directoryPath, { bigint: true });
|
|
169
|
+
if (before.isSymbolicLink()) {
|
|
170
|
+
throw new UsageError(`${directoryPath} is a symbolic directory with ambiguous guarded identity.`, "RESOURCE_ALREADY_EXISTS");
|
|
171
|
+
}
|
|
172
|
+
if (!before.isDirectory()) {
|
|
173
|
+
throw new UsageError(`${directoryPath} is not a guarded directory.`, "INVALID_FLAG_VALUE");
|
|
174
|
+
}
|
|
175
|
+
const realPath = fs.realpathSync(directoryPath);
|
|
176
|
+
containedRelative(containmentRealPath, realPath, true);
|
|
177
|
+
const entries = fs
|
|
178
|
+
.readdirSync(directoryPath, { withFileTypes: true })
|
|
179
|
+
.sort((left, right) => compareCodePoints(left.name, right.name))
|
|
180
|
+
.map((entry) => {
|
|
181
|
+
const entryPath = path.join(directoryPath, entry.name);
|
|
182
|
+
const entryStat = fs.lstatSync(entryPath, { bigint: true });
|
|
183
|
+
if (entry.isSymbolicLink() || entryStat.isSymbolicLink()) {
|
|
184
|
+
let linked;
|
|
185
|
+
try {
|
|
186
|
+
linked = fs.realpathSync(entryPath);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Broken symbolic entries still have ambiguous ownership.
|
|
190
|
+
}
|
|
191
|
+
if (linked) {
|
|
192
|
+
const relative = path.relative(containmentRealPath, linked);
|
|
193
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
194
|
+
throw new UsageError(`${entryPath} resolves outside the bundle root through a symbolic source.`, "PATH_ESCAPE_VIOLATION");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw new UsageError(`${entryPath} is a symbolic source with a physical source identity collision; guarded reads require one no-follow owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
198
|
+
}
|
|
199
|
+
const kind = entry.isDirectory() && entryStat.isDirectory()
|
|
200
|
+
? "directory"
|
|
201
|
+
: entry.isFile() && entryStat.isFile()
|
|
202
|
+
? "file"
|
|
203
|
+
: undefined;
|
|
204
|
+
if (!kind) {
|
|
205
|
+
throw new UsageError(`${entryPath} is neither a regular file nor directory.`, "INVALID_FLAG_VALUE");
|
|
206
|
+
}
|
|
207
|
+
const entryRealPath = fs.realpathSync(entryPath);
|
|
208
|
+
containedRelative(containmentRealPath, entryRealPath, false);
|
|
209
|
+
return Object.freeze({
|
|
210
|
+
name: entry.name,
|
|
211
|
+
kind,
|
|
212
|
+
physicalIdentity: physicalIdentity(entryRealPath, entryStat),
|
|
213
|
+
version: statVersion(entryStat),
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
const after = fs.lstatSync(directoryPath, { bigint: true });
|
|
217
|
+
if (!sameBigIntStat(before, after)) {
|
|
218
|
+
throw new UsageError(`${directoryPath} changed while its guarded directory manifest was read.`, "RESOURCE_ALREADY_EXISTS");
|
|
219
|
+
}
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
directoryPath,
|
|
222
|
+
relativePath: lexicalRelative === "" ? "." : toPosix(lexicalRelative),
|
|
223
|
+
realPath,
|
|
224
|
+
containmentRoot,
|
|
225
|
+
containmentRealPath,
|
|
226
|
+
containmentPhysicalIdentity: physicalIdentity(containmentRealPath, containmentStat),
|
|
227
|
+
physicalIdentity: physicalIdentity(realPath, before),
|
|
228
|
+
version: statVersion(before),
|
|
229
|
+
entries: Object.freeze(entries),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (cause) {
|
|
233
|
+
if (cause instanceof UsageError)
|
|
234
|
+
throw cause;
|
|
235
|
+
throw new UsageError(`${directoryPath} could not be captured as a guarded directory manifest: ${errorMessage(cause)}`, cause.code === "ENOENT" ? "RESOURCE_ALREADY_EXISTS" : "PATH_ESCAPE_VIOLATION");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function sameSnapshotValue(left, right) {
|
|
239
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
240
|
+
}
|
|
241
|
+
function sourceSortKey(source) {
|
|
242
|
+
const identity = source.identity;
|
|
243
|
+
return identity
|
|
244
|
+
? `${identity.ref}\0${identity.adapter}\0${identity.file}`
|
|
245
|
+
: `${source.containmentRealPath}\0${source.relativePath}`;
|
|
246
|
+
}
|
|
247
|
+
function directorySortKey(manifest) {
|
|
248
|
+
return `${manifest.containmentRealPath}\0${manifest.relativePath}`;
|
|
249
|
+
}
|
|
250
|
+
export class GuardedExecutionSourceCollector {
|
|
251
|
+
#sources = new Map();
|
|
252
|
+
#directories = new Map();
|
|
253
|
+
capture(sourcePath, containmentRoot, options = {}) {
|
|
254
|
+
const key = path.resolve(sourcePath);
|
|
255
|
+
const existing = this.#sources.get(key);
|
|
256
|
+
if (existing) {
|
|
257
|
+
if (options.identity)
|
|
258
|
+
return this.bindIdentity(sourcePath, containmentRoot, options.identity);
|
|
259
|
+
return existing.source;
|
|
260
|
+
}
|
|
261
|
+
const record = captureRecord(sourcePath, containmentRoot, options);
|
|
262
|
+
this.#assertNoPhysicalOwnerAlias(record.source);
|
|
263
|
+
this.#sources.set(key, record);
|
|
264
|
+
return record.source;
|
|
265
|
+
}
|
|
266
|
+
readBytes(sourcePath, containmentRoot, options = {}) {
|
|
267
|
+
return Buffer.from(this.capture(sourcePath, containmentRoot, options).bytesBase64, "base64");
|
|
268
|
+
}
|
|
269
|
+
fileContext(root, file, options = {}) {
|
|
270
|
+
const source = this.capture(file, root, options);
|
|
271
|
+
const record = this.#sources.get(path.resolve(file));
|
|
272
|
+
if (!record)
|
|
273
|
+
throw new Error("guarded source collector lost a captured file");
|
|
274
|
+
const relPath = source.relativePath;
|
|
275
|
+
const ext = path.extname(file).toLowerCase();
|
|
276
|
+
const fileName = path.basename(file);
|
|
277
|
+
const parentDirAbs = path.dirname(path.resolve(file));
|
|
278
|
+
const relDir = path.posix.dirname(relPath);
|
|
279
|
+
const ancestorDirs = relDir === "." ? [] : relDir.split("/").filter(Boolean);
|
|
280
|
+
let frontmatter;
|
|
281
|
+
let parsed = false;
|
|
282
|
+
return {
|
|
283
|
+
absPath: path.resolve(file),
|
|
284
|
+
relPath,
|
|
285
|
+
ext,
|
|
286
|
+
fileName,
|
|
287
|
+
parentDir: path.basename(parentDirAbs),
|
|
288
|
+
parentDirAbs,
|
|
289
|
+
ancestorDirs,
|
|
290
|
+
stashRoot: path.resolve(root),
|
|
291
|
+
content: () => source.content,
|
|
292
|
+
frontmatter: () => {
|
|
293
|
+
if (!parsed) {
|
|
294
|
+
const result = parseFrontmatter(source.content);
|
|
295
|
+
frontmatter = Object.keys(result.data).length > 0 ? result.data : null;
|
|
296
|
+
parsed = true;
|
|
297
|
+
}
|
|
298
|
+
return frontmatter ?? null;
|
|
299
|
+
},
|
|
300
|
+
stat: () => record.stat,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
bindIdentity(sourcePath, containmentRoot, identityInput) {
|
|
304
|
+
const key = path.resolve(sourcePath);
|
|
305
|
+
const existing = this.#sources.get(key) ?? captureRecord(sourcePath, containmentRoot);
|
|
306
|
+
const identity = createExecutionSourceIdentity(identityInput);
|
|
307
|
+
if (identity.file !== existing.source.relativePath || identity.hash !== existing.source.sha256) {
|
|
308
|
+
throw new UsageError(`${sourcePath} adapter identity does not match its guarded file owner or bytes.`, "RESOURCE_ALREADY_EXISTS");
|
|
309
|
+
}
|
|
310
|
+
if (existing.source.identity) {
|
|
311
|
+
if (!sameSnapshotValue(existing.source.identity, identity)) {
|
|
312
|
+
throw new UsageError(`${sourcePath} already has a different guarded logical owner identity.`, "RESOURCE_ALREADY_EXISTS");
|
|
313
|
+
}
|
|
314
|
+
return existing.source;
|
|
315
|
+
}
|
|
316
|
+
const source = freezeSource({ ...existing.source, identity });
|
|
317
|
+
this.#assertNoPhysicalOwnerAlias(source, key);
|
|
318
|
+
this.#sources.set(key, { ...existing, source });
|
|
319
|
+
return source;
|
|
320
|
+
}
|
|
321
|
+
trackDirectory(directoryPath, containmentRoot) {
|
|
322
|
+
const manifest = captureGuardedDirectoryManifest(directoryPath, containmentRoot);
|
|
323
|
+
const key = path.resolve(directoryPath);
|
|
324
|
+
const existing = this.#directories.get(key);
|
|
325
|
+
if (existing && !sameSnapshotValue(existing, manifest)) {
|
|
326
|
+
throw new UsageError(`${directoryPath} changed between guarded directory reads.`, "RESOURCE_ALREADY_EXISTS");
|
|
327
|
+
}
|
|
328
|
+
if (!existing)
|
|
329
|
+
this.#directories.set(key, manifest);
|
|
330
|
+
return existing ?? manifest;
|
|
331
|
+
}
|
|
332
|
+
enumerateTree(directoryPath, containmentRoot) {
|
|
333
|
+
const files = [];
|
|
334
|
+
const visit = (directory) => {
|
|
335
|
+
const manifest = this.trackDirectory(directory, containmentRoot);
|
|
336
|
+
for (const entry of manifest.entries) {
|
|
337
|
+
const candidate = path.join(directory, entry.name);
|
|
338
|
+
if (entry.kind === "directory")
|
|
339
|
+
visit(candidate);
|
|
340
|
+
else
|
|
341
|
+
files.push(candidate);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
visit(path.resolve(directoryPath));
|
|
345
|
+
return Object.freeze(files.sort(compareCodePoints));
|
|
346
|
+
}
|
|
347
|
+
snapshot() {
|
|
348
|
+
return Object.freeze({
|
|
349
|
+
sources: Object.freeze([...this.#sources.values()]
|
|
350
|
+
.map((record) => record.source)
|
|
351
|
+
.sort((left, right) => compareCodePoints(sourceSortKey(left), sourceSortKey(right)))),
|
|
352
|
+
directoryManifests: Object.freeze([...this.#directories.values()].sort((left, right) => compareCodePoints(directorySortKey(left), directorySortKey(right)))),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
revalidate() {
|
|
356
|
+
for (const record of this.#sources.values()) {
|
|
357
|
+
let current;
|
|
358
|
+
try {
|
|
359
|
+
current = captureRecord(record.source.sourcePath, record.source.containmentRoot, {
|
|
360
|
+
authored: record.source.authored,
|
|
361
|
+
maxBytes: record.maxBytes,
|
|
362
|
+
...(record.source.identity ? { identity: record.source.identity } : {}),
|
|
363
|
+
}).source;
|
|
364
|
+
}
|
|
365
|
+
catch (cause) {
|
|
366
|
+
throw new UsageError(`Guarded execution source read set changed before publication: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
|
|
367
|
+
}
|
|
368
|
+
if (!sameSnapshotValue(record.source, current)) {
|
|
369
|
+
throw new UsageError("Guarded execution source read set changed before publication.", "RESOURCE_ALREADY_EXISTS");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
for (const manifest of this.#directories.values()) {
|
|
373
|
+
let current;
|
|
374
|
+
try {
|
|
375
|
+
current = captureGuardedDirectoryManifest(manifest.directoryPath, manifest.containmentRoot);
|
|
376
|
+
}
|
|
377
|
+
catch (cause) {
|
|
378
|
+
throw new UsageError(`Guarded execution source directory manifest changed before publication: ${errorMessage(cause)}`, "RESOURCE_ALREADY_EXISTS");
|
|
379
|
+
}
|
|
380
|
+
if (!sameSnapshotValue(manifest, current)) {
|
|
381
|
+
throw new UsageError("Guarded execution source directory manifest changed before publication.", "RESOURCE_ALREADY_EXISTS");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
#assertNoPhysicalOwnerAlias(candidate, selfKey) {
|
|
386
|
+
if (!candidate.identity)
|
|
387
|
+
return;
|
|
388
|
+
for (const [key, record] of this.#sources) {
|
|
389
|
+
if (key === selfKey || !record.source.identity)
|
|
390
|
+
continue;
|
|
391
|
+
if (record.source.containmentPhysicalIdentity === candidate.containmentPhysicalIdentity &&
|
|
392
|
+
record.source.physicalIdentity === candidate.physicalIdentity &&
|
|
393
|
+
!sameSnapshotValue(record.source.identity, candidate.identity)) {
|
|
394
|
+
throw new UsageError(`${candidate.sourcePath} aliases the same physical source and root under a different logical owner.`, "RESOURCE_ALREADY_EXISTS");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|