open-multi-agent-kit 0.90.8 → 0.91.0
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 +41 -0
- package/README.md +21 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +140 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1179 -95
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/command-safety.d.ts.map +1 -1
- package/dist/core/command-safety.js +0 -8
- package/dist/core/command-safety.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +19 -0
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +46 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +7 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +12 -0
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +7 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/metrics-sampler.d.ts +20 -0
- package/dist/core/metrics-sampler.d.ts.map +1 -1
- package/dist/core/metrics-sampler.js +52 -0
- package/dist/core/metrics-sampler.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +4 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +16 -7
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/provider-resilience.d.ts +51 -0
- package/dist/core/provider-resilience.d.ts.map +1 -0
- package/dist/core/provider-resilience.js +86 -0
- package/dist/core/provider-resilience.js.map +1 -0
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1045 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +106 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +445 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +52 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +54 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +47 -0
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/omp-pure-seams.d.ts +41 -0
- package/dist/core/tools/omp-pure-seams.d.ts.map +1 -0
- package/dist/core/tools/omp-pure-seams.js +142 -0
- package/dist/core/tools/omp-pure-seams.js.map +1 -0
- package/dist/core/tools/omp-seam-runtime.d.ts +100 -0
- package/dist/core/tools/omp-seam-runtime.d.ts.map +1 -0
- package/dist/core/tools/omp-seam-runtime.js +40 -0
- package/dist/core/tools/omp-seam-runtime.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +34 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +39 -15
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +255 -97
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +11 -8
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/omk-aurora-dark.json +80 -0
- package/dist/modes/interactive/theme/omk-aurora-light.json +80 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +5 -0
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +211 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/adaptorch-preview.md +1 -1
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/provider-resilience.md +87 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +80 -2
- package/docs/themes.md +2 -0
- package/docs/usage.md +38 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +22 -13
- package/package.json +6 -5
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { closeSync, constants as fsConstants, fstatSync, lstatSync, openSync, readFileSync, realpathSync, } from "node:fs";
|
|
4
|
+
import { isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
6
|
+
const WORKSPACE_MANIFEST_DOMAIN = "omk:evidence:workspace-fingerprint:v1\0";
|
|
7
|
+
const GIT_DIRTY_DOMAIN = "omk:evidence:workspace-fingerprint:git-dirty:v1\0";
|
|
8
|
+
/** Full SHA-1 or SHA-256 git object names. */
|
|
9
|
+
const GIT_COMMIT_HEX = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
10
|
+
/** Hard bound on bytes accepted from one git child process. */
|
|
11
|
+
const MAX_GIT_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const GIT_CAPTURE_TIMEOUT_MS = 30_000;
|
|
13
|
+
/** Inherited variables that could redirect git away from the trusted root; always stripped. */
|
|
14
|
+
const UNSAFE_GIT_ENV_VARS = [
|
|
15
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
16
|
+
"GIT_CEILING_DIRECTORIES",
|
|
17
|
+
"GIT_COMMON_DIR",
|
|
18
|
+
"GIT_DIFF_OPTS",
|
|
19
|
+
"GIT_DIR",
|
|
20
|
+
"GIT_EXTERNAL_DIFF",
|
|
21
|
+
"GIT_INDEX_FILE",
|
|
22
|
+
"GIT_NAMESPACE",
|
|
23
|
+
"GIT_OBJECT_DIRECTORY",
|
|
24
|
+
"GIT_PREFIX",
|
|
25
|
+
"GIT_WORK_TREE",
|
|
26
|
+
];
|
|
27
|
+
/** Deterministic scope-limited diff rendering, independent of local prefix/ext-diff config. */
|
|
28
|
+
const GIT_DIFF_ARGS = [
|
|
29
|
+
"--full-index",
|
|
30
|
+
"--binary",
|
|
31
|
+
"--no-color",
|
|
32
|
+
"--no-ext-diff",
|
|
33
|
+
"--no-textconv",
|
|
34
|
+
"--no-renames",
|
|
35
|
+
"--ignore-submodules=none",
|
|
36
|
+
"--src-prefix=a/",
|
|
37
|
+
"--dst-prefix=b/",
|
|
38
|
+
];
|
|
39
|
+
function errorCode(error) {
|
|
40
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
41
|
+
? String(error.code)
|
|
42
|
+
: undefined;
|
|
43
|
+
}
|
|
44
|
+
function sha256(value) {
|
|
45
|
+
return createHash("sha256").update(value).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function exactObject(value, label, keys) {
|
|
48
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
49
|
+
throw new Error(`${label} must be an object`);
|
|
50
|
+
}
|
|
51
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
52
|
+
const actual = Object.entries(descriptors)
|
|
53
|
+
.filter(([, descriptor]) => descriptor.enumerable)
|
|
54
|
+
.map(([key]) => key)
|
|
55
|
+
.sort();
|
|
56
|
+
const expected = [...keys].sort();
|
|
57
|
+
if (actual.length !== expected.length ||
|
|
58
|
+
actual.some((key, index) => key !== expected[index]) ||
|
|
59
|
+
keys.some((key) => {
|
|
60
|
+
const descriptor = descriptors[key];
|
|
61
|
+
return descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor);
|
|
62
|
+
})) {
|
|
63
|
+
throw new Error(`${label} has an invalid key set`);
|
|
64
|
+
}
|
|
65
|
+
const snapshot = {};
|
|
66
|
+
for (const key of actual) {
|
|
67
|
+
const descriptor = descriptors[key];
|
|
68
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
69
|
+
throw new Error(`${label} has an invalid key set`);
|
|
70
|
+
}
|
|
71
|
+
snapshot[key] = descriptor.value;
|
|
72
|
+
}
|
|
73
|
+
return snapshot;
|
|
74
|
+
}
|
|
75
|
+
function ownEnumerableDataProperty(record, label, key) {
|
|
76
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
77
|
+
if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) {
|
|
78
|
+
throw new Error(`${label} has an invalid key set`);
|
|
79
|
+
}
|
|
80
|
+
return descriptor.value;
|
|
81
|
+
}
|
|
82
|
+
function exactArray(value, label) {
|
|
83
|
+
if (!Array.isArray(value))
|
|
84
|
+
throw new Error(`${label} must be an array`);
|
|
85
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
86
|
+
const lengthDescriptor = descriptors.length;
|
|
87
|
+
if (lengthDescriptor === undefined || !("value" in lengthDescriptor)) {
|
|
88
|
+
throw new Error(`${label} has invalid index properties`);
|
|
89
|
+
}
|
|
90
|
+
const rawLength = lengthDescriptor.value;
|
|
91
|
+
if (!Number.isSafeInteger(rawLength) || rawLength < 0) {
|
|
92
|
+
throw new Error(`${label} has invalid index properties`);
|
|
93
|
+
}
|
|
94
|
+
const length = rawLength;
|
|
95
|
+
const snapshot = [];
|
|
96
|
+
for (let index = 0; index < length; index++) {
|
|
97
|
+
const descriptor = descriptors[String(index)];
|
|
98
|
+
if (descriptor === undefined || descriptor.enumerable !== true || !("value" in descriptor)) {
|
|
99
|
+
throw new Error(`${label} must use own enumerable data index properties`);
|
|
100
|
+
}
|
|
101
|
+
snapshot.push(descriptor.value);
|
|
102
|
+
}
|
|
103
|
+
const enumerableKeys = Object.entries(descriptors)
|
|
104
|
+
.filter(([, descriptor]) => descriptor.enumerable)
|
|
105
|
+
.map(([key]) => key);
|
|
106
|
+
if (enumerableKeys.length !== length || enumerableKeys.some((key, index) => key !== String(index))) {
|
|
107
|
+
throw new Error(`${label} must use own enumerable data index properties`);
|
|
108
|
+
}
|
|
109
|
+
return snapshot;
|
|
110
|
+
}
|
|
111
|
+
function assertNormalizedArtifactPath(path, label = "artifact path") {
|
|
112
|
+
if (typeof path !== "string" ||
|
|
113
|
+
path.length === 0 ||
|
|
114
|
+
path.includes("\0") ||
|
|
115
|
+
path.includes("\\") ||
|
|
116
|
+
isAbsolute(path) ||
|
|
117
|
+
win32.isAbsolute(path) ||
|
|
118
|
+
path === "." ||
|
|
119
|
+
posix.normalize(path) !== path ||
|
|
120
|
+
path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
121
|
+
throw new Error(`${label} must be a normalized root-relative path`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function assertSortedUniquePaths(paths, label) {
|
|
125
|
+
for (let index = 0; index < paths.length; index++) {
|
|
126
|
+
assertNormalizedArtifactPath(paths[index], `${label}[${index}]`);
|
|
127
|
+
if (index > 0 && paths[index - 1] >= paths[index]) {
|
|
128
|
+
throw new Error(`${label} must be sorted and contain no duplicates`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function parseSha256(value, label) {
|
|
133
|
+
if (typeof value !== "string" || !SHA256_HEX.test(value)) {
|
|
134
|
+
throw new Error(`${label} must be a lowercase SHA-256 hex digest`);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
function parseScope(value) {
|
|
139
|
+
const scope = exactObject(value, "workspace scope", ["root", "artifactPaths"]);
|
|
140
|
+
if (typeof scope.root !== "string" || scope.root.length === 0 || !isAbsolute(scope.root)) {
|
|
141
|
+
throw new Error("workspace scope root must be an absolute path");
|
|
142
|
+
}
|
|
143
|
+
const rawArtifactPaths = exactArray(scope.artifactPaths, "workspace scope artifactPaths");
|
|
144
|
+
if (!rawArtifactPaths.every((path) => typeof path === "string")) {
|
|
145
|
+
throw new Error("workspace scope artifactPaths must be a string array");
|
|
146
|
+
}
|
|
147
|
+
const artifactPaths = [...rawArtifactPaths];
|
|
148
|
+
assertSortedUniquePaths(artifactPaths, "workspace scope artifactPaths");
|
|
149
|
+
return Object.freeze({ root: scope.root, artifactPaths: Object.freeze(artifactPaths) });
|
|
150
|
+
}
|
|
151
|
+
function parseArtifact(value, index) {
|
|
152
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
153
|
+
throw new Error(`workspace artifact[${index}] must be an object`);
|
|
154
|
+
}
|
|
155
|
+
const candidate = value;
|
|
156
|
+
const state = ownEnumerableDataProperty(candidate, `workspace artifact[${index}]`, "state");
|
|
157
|
+
if (state === "missing") {
|
|
158
|
+
const artifact = exactObject(value, `workspace artifact[${index}]`, ["path", "state"]);
|
|
159
|
+
assertNormalizedArtifactPath(artifact.path, `workspace artifact[${index}].path`);
|
|
160
|
+
return Object.freeze({ path: artifact.path, state: "missing" });
|
|
161
|
+
}
|
|
162
|
+
if (state === "file") {
|
|
163
|
+
const artifact = exactObject(value, `workspace artifact[${index}]`, ["path", "state", "sha256", "size"]);
|
|
164
|
+
assertNormalizedArtifactPath(artifact.path, `workspace artifact[${index}].path`);
|
|
165
|
+
if (!Number.isSafeInteger(artifact.size) || artifact.size < 0) {
|
|
166
|
+
throw new Error(`workspace artifact[${index}].size must be a non-negative safe integer`);
|
|
167
|
+
}
|
|
168
|
+
return Object.freeze({
|
|
169
|
+
path: artifact.path,
|
|
170
|
+
state: "file",
|
|
171
|
+
sha256: parseSha256(artifact.sha256, `workspace artifact[${index}].sha256`),
|
|
172
|
+
size: artifact.size,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
throw new Error(`workspace artifact[${index}].state must be file or missing`);
|
|
176
|
+
}
|
|
177
|
+
function canonicalManifest(scope, artifacts) {
|
|
178
|
+
return JSON.stringify({
|
|
179
|
+
kind: "artifact-set",
|
|
180
|
+
scope: { root: scope.root, artifactPaths: scope.artifactPaths },
|
|
181
|
+
artifacts: artifacts.map((artifact) => artifact.state === "file"
|
|
182
|
+
? { path: artifact.path, state: artifact.state, sha256: artifact.sha256, size: artifact.size }
|
|
183
|
+
: { path: artifact.path, state: artifact.state }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/** Compute the domain-separated digest for an already validated canonical manifest. */
|
|
187
|
+
export function computeWorkspaceManifestSha256(scope, artifacts) {
|
|
188
|
+
return sha256(`${WORKSPACE_MANIFEST_DOMAIN}${canonicalManifest(scope, artifacts)}`);
|
|
189
|
+
}
|
|
190
|
+
/** Canonical digest committing sorted changed paths, both diff digests, and every selected artifact state. */
|
|
191
|
+
export function computeGitWorkspaceDirtySha256(changedPaths, stagedDiffSha256, unstagedDiffSha256, artifacts) {
|
|
192
|
+
const canonical = JSON.stringify({
|
|
193
|
+
changedPaths,
|
|
194
|
+
stagedDiffSha256,
|
|
195
|
+
unstagedDiffSha256,
|
|
196
|
+
artifacts: artifacts.map((artifact) => artifact.state === "file"
|
|
197
|
+
? { path: artifact.path, state: artifact.state, sha256: artifact.sha256, size: artifact.size }
|
|
198
|
+
: { path: artifact.path, state: artifact.state }),
|
|
199
|
+
});
|
|
200
|
+
return sha256(`${GIT_DIRTY_DOMAIN}${canonical}`);
|
|
201
|
+
}
|
|
202
|
+
/** Compute the domain-separated digest for an already validated canonical git manifest. */
|
|
203
|
+
export function computeGitWorkspaceManifestSha256(scope, artifacts, git) {
|
|
204
|
+
const canonical = JSON.stringify({
|
|
205
|
+
kind: "git",
|
|
206
|
+
scope: { root: scope.root, artifactPaths: scope.artifactPaths },
|
|
207
|
+
artifacts: artifacts.map((artifact) => artifact.state === "file"
|
|
208
|
+
? { path: artifact.path, state: artifact.state, sha256: artifact.sha256, size: artifact.size }
|
|
209
|
+
: { path: artifact.path, state: artifact.state }),
|
|
210
|
+
git: {
|
|
211
|
+
headCommit: git.headCommit,
|
|
212
|
+
changedPaths: git.changedPaths,
|
|
213
|
+
stagedDiffSha256: git.stagedDiffSha256,
|
|
214
|
+
unstagedDiffSha256: git.unstagedDiffSha256,
|
|
215
|
+
dirtySha256: git.dirtySha256,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
return sha256(`${WORKSPACE_MANIFEST_DOMAIN}${canonical}`);
|
|
219
|
+
}
|
|
220
|
+
/** A changed path is inside scope when it equals a selected path or lies under a selected directory. */
|
|
221
|
+
function isWithinScope(path, artifactPaths) {
|
|
222
|
+
return artifactPaths.some((artifact) => path === artifact || path.startsWith(`${artifact}/`));
|
|
223
|
+
}
|
|
224
|
+
function digestsEqual(left, right) {
|
|
225
|
+
return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
|
|
226
|
+
}
|
|
227
|
+
function parseGitFingerprint(value) {
|
|
228
|
+
const raw = exactObject(value, "workspace fingerprint", ["kind", "scope", "artifacts", "git", "manifestSha256"]);
|
|
229
|
+
const scope = parseScope(raw.scope);
|
|
230
|
+
const rawArtifacts = exactArray(raw.artifacts, "workspace fingerprint artifacts");
|
|
231
|
+
const artifacts = rawArtifacts.map((artifact, index) => parseArtifact(artifact, index));
|
|
232
|
+
const artifactPaths = artifacts.map((artifact) => artifact.path);
|
|
233
|
+
assertSortedUniquePaths(artifactPaths, "workspace artifact paths");
|
|
234
|
+
if (artifactPaths.length !== scope.artifactPaths.length ||
|
|
235
|
+
artifactPaths.some((path, index) => path !== scope.artifactPaths[index])) {
|
|
236
|
+
throw new Error("workspace artifacts must exactly match the selected scope paths");
|
|
237
|
+
}
|
|
238
|
+
const rawGit = exactObject(raw.git, "workspace git state", [
|
|
239
|
+
"headCommit",
|
|
240
|
+
"changedPaths",
|
|
241
|
+
"stagedDiffSha256",
|
|
242
|
+
"unstagedDiffSha256",
|
|
243
|
+
"dirtySha256",
|
|
244
|
+
]);
|
|
245
|
+
const headCommit = rawGit.headCommit;
|
|
246
|
+
if (headCommit !== null && (typeof headCommit !== "string" || !GIT_COMMIT_HEX.test(headCommit))) {
|
|
247
|
+
throw new Error("workspace git headCommit must be null or a full lowercase hex object name");
|
|
248
|
+
}
|
|
249
|
+
const rawChangedPaths = exactArray(rawGit.changedPaths, "workspace git changedPaths");
|
|
250
|
+
if (!rawChangedPaths.every((path) => typeof path === "string")) {
|
|
251
|
+
throw new Error("workspace git changedPaths must be a string array");
|
|
252
|
+
}
|
|
253
|
+
const changedPaths = [...rawChangedPaths];
|
|
254
|
+
assertSortedUniquePaths(changedPaths, "workspace git changedPaths");
|
|
255
|
+
for (const path of changedPaths) {
|
|
256
|
+
if (!isWithinScope(path, scope.artifactPaths)) {
|
|
257
|
+
throw new Error(`workspace git changed path escapes the selected scope: ${path}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const stagedDiffSha256 = parseSha256(rawGit.stagedDiffSha256, "workspace git stagedDiffSha256");
|
|
261
|
+
const unstagedDiffSha256 = parseSha256(rawGit.unstagedDiffSha256, "workspace git unstagedDiffSha256");
|
|
262
|
+
const dirtySha256 = parseSha256(rawGit.dirtySha256, "workspace git dirtySha256");
|
|
263
|
+
if (!digestsEqual(dirtySha256, computeGitWorkspaceDirtySha256(changedPaths, stagedDiffSha256, unstagedDiffSha256, artifacts))) {
|
|
264
|
+
throw new Error("workspace git dirty digest mismatch");
|
|
265
|
+
}
|
|
266
|
+
const git = Object.freeze({
|
|
267
|
+
headCommit,
|
|
268
|
+
changedPaths: Object.freeze(changedPaths),
|
|
269
|
+
stagedDiffSha256,
|
|
270
|
+
unstagedDiffSha256,
|
|
271
|
+
dirtySha256,
|
|
272
|
+
});
|
|
273
|
+
const manifestSha256 = parseSha256(raw.manifestSha256, "workspace manifestSha256");
|
|
274
|
+
if (!digestsEqual(manifestSha256, computeGitWorkspaceManifestSha256(scope, artifacts, git))) {
|
|
275
|
+
throw new Error("workspace manifest digest mismatch");
|
|
276
|
+
}
|
|
277
|
+
return Object.freeze({
|
|
278
|
+
kind: "git",
|
|
279
|
+
scope,
|
|
280
|
+
artifacts: Object.freeze(artifacts),
|
|
281
|
+
git,
|
|
282
|
+
manifestSha256,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/** Strictly parse and validate a fingerprint of either kind, including its canonical digests. */
|
|
286
|
+
export function parseWorkspaceFingerprint(value) {
|
|
287
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
288
|
+
throw new Error("workspace fingerprint must be an object");
|
|
289
|
+
}
|
|
290
|
+
const kind = ownEnumerableDataProperty(value, "workspace fingerprint", "kind");
|
|
291
|
+
if (kind === "git")
|
|
292
|
+
return parseGitFingerprint(value);
|
|
293
|
+
if (kind !== "artifact-set") {
|
|
294
|
+
throw new Error("workspace fingerprint kind must be artifact-set or git");
|
|
295
|
+
}
|
|
296
|
+
return parseArtifactSetFingerprint(value);
|
|
297
|
+
}
|
|
298
|
+
function parseArtifactSetFingerprint(value) {
|
|
299
|
+
const raw = exactObject(value, "workspace fingerprint", ["kind", "scope", "artifacts", "manifestSha256"]);
|
|
300
|
+
if (raw.kind !== "artifact-set") {
|
|
301
|
+
throw new Error("workspace fingerprint kind must be artifact-set");
|
|
302
|
+
}
|
|
303
|
+
const scope = parseScope(raw.scope);
|
|
304
|
+
const rawArtifacts = exactArray(raw.artifacts, "workspace fingerprint artifacts");
|
|
305
|
+
const artifacts = rawArtifacts.map((artifact, index) => parseArtifact(artifact, index));
|
|
306
|
+
const artifactPaths = artifacts.map((artifact) => artifact.path);
|
|
307
|
+
assertSortedUniquePaths(artifactPaths, "workspace artifact paths");
|
|
308
|
+
if (artifactPaths.length !== scope.artifactPaths.length ||
|
|
309
|
+
artifactPaths.some((path, index) => path !== scope.artifactPaths[index])) {
|
|
310
|
+
throw new Error("workspace artifacts must exactly match the selected scope paths");
|
|
311
|
+
}
|
|
312
|
+
const manifestSha256 = parseSha256(raw.manifestSha256, "workspace manifestSha256");
|
|
313
|
+
const expected = computeWorkspaceManifestSha256(scope, artifacts);
|
|
314
|
+
if (!digestsEqual(manifestSha256, expected)) {
|
|
315
|
+
throw new Error("workspace manifest digest mismatch");
|
|
316
|
+
}
|
|
317
|
+
return Object.freeze({
|
|
318
|
+
kind: "artifact-set",
|
|
319
|
+
scope,
|
|
320
|
+
artifacts: Object.freeze(artifacts),
|
|
321
|
+
manifestSha256,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function statPathWithoutSymlinks(root, artifactPath) {
|
|
325
|
+
let current = root;
|
|
326
|
+
const segments = artifactPath.split("/");
|
|
327
|
+
for (let index = 0; index < segments.length; index++) {
|
|
328
|
+
current = join(current, segments[index]);
|
|
329
|
+
let stat;
|
|
330
|
+
try {
|
|
331
|
+
stat = lstatSync(current);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
if (errorCode(error) === "ENOENT")
|
|
335
|
+
return undefined;
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
if (stat.isSymbolicLink()) {
|
|
339
|
+
throw new Error(`workspace artifact path contains a symlink: ${artifactPath}`);
|
|
340
|
+
}
|
|
341
|
+
if (index < segments.length - 1 && !stat.isDirectory()) {
|
|
342
|
+
throw new Error(`workspace artifact parent is not a directory: ${artifactPath}`);
|
|
343
|
+
}
|
|
344
|
+
if (index === segments.length - 1)
|
|
345
|
+
return stat;
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
function captureParentIdentities(root, artifactPath) {
|
|
350
|
+
const parents = [];
|
|
351
|
+
let current = root;
|
|
352
|
+
for (const segment of artifactPath.split("/").slice(0, -1)) {
|
|
353
|
+
current = join(current, segment);
|
|
354
|
+
const stat = lstatSync(current);
|
|
355
|
+
if (stat.isSymbolicLink()) {
|
|
356
|
+
throw new Error(`workspace artifact path contains a symlink: ${artifactPath}`);
|
|
357
|
+
}
|
|
358
|
+
if (!stat.isDirectory()) {
|
|
359
|
+
throw new Error(`workspace artifact parent is not a directory: ${artifactPath}`);
|
|
360
|
+
}
|
|
361
|
+
parents.push({ path: current, stat });
|
|
362
|
+
}
|
|
363
|
+
return parents;
|
|
364
|
+
}
|
|
365
|
+
function hasFileIdentity(stat) {
|
|
366
|
+
return stat.dev !== 0 || stat.ino !== 0;
|
|
367
|
+
}
|
|
368
|
+
function sameFileIdentity(expected, actual) {
|
|
369
|
+
const expectedHasIdentity = hasFileIdentity(expected);
|
|
370
|
+
const actualHasIdentity = hasFileIdentity(actual);
|
|
371
|
+
return ((!expectedHasIdentity && !actualHasIdentity) ||
|
|
372
|
+
(expectedHasIdentity && actualHasIdentity && expected.dev === actual.dev && expected.ino === actual.ino));
|
|
373
|
+
}
|
|
374
|
+
function isStrictlyUnder(root, path) {
|
|
375
|
+
const relativePath = relative(root, path);
|
|
376
|
+
return (relativePath !== "" && relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath));
|
|
377
|
+
}
|
|
378
|
+
function assertTrustedRoot(root, trustedRootStat) {
|
|
379
|
+
const liveRootStat = lstatSync(root);
|
|
380
|
+
if (liveRootStat.isSymbolicLink() ||
|
|
381
|
+
!liveRootStat.isDirectory() ||
|
|
382
|
+
realpathSync(root) !== root ||
|
|
383
|
+
!sameFileIdentity(trustedRootStat, liveRootStat)) {
|
|
384
|
+
throw new Error("workspace root changed while artifacts were captured");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function assertParentIdentities(parents, artifactPath) {
|
|
388
|
+
for (const parent of parents) {
|
|
389
|
+
const stat = lstatSync(parent.path);
|
|
390
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || !sameFileIdentity(parent.stat, stat)) {
|
|
391
|
+
throw new Error(`workspace artifact parent changed while it was captured: ${artifactPath}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function assertLiveArtifactPath(root, trustedRootStat, trustedParents, artifactPath, fdStat) {
|
|
396
|
+
assertTrustedRoot(root, trustedRootStat);
|
|
397
|
+
assertParentIdentities(trustedParents, artifactPath);
|
|
398
|
+
const pathStat = statPathWithoutSymlinks(root, artifactPath);
|
|
399
|
+
if (pathStat === undefined || !pathStat.isFile() || !sameFileIdentity(fdStat, pathStat)) {
|
|
400
|
+
throw new Error(`workspace artifact path was rebound while it was captured: ${artifactPath}`);
|
|
401
|
+
}
|
|
402
|
+
const fullPath = join(root, ...artifactPath.split("/"));
|
|
403
|
+
const livePath = realpathSync(fullPath);
|
|
404
|
+
if (!isStrictlyUnder(root, livePath)) {
|
|
405
|
+
throw new Error(`workspace artifact resolved outside the trusted root: ${artifactPath}`);
|
|
406
|
+
}
|
|
407
|
+
const livePathStat = lstatSync(livePath);
|
|
408
|
+
if (!livePathStat.isFile() || !sameFileIdentity(fdStat, livePathStat)) {
|
|
409
|
+
throw new Error(`workspace artifact path was rebound while it was captured: ${artifactPath}`);
|
|
410
|
+
}
|
|
411
|
+
assertParentIdentities(trustedParents, artifactPath);
|
|
412
|
+
assertTrustedRoot(root, trustedRootStat);
|
|
413
|
+
}
|
|
414
|
+
function captureFile(root, trustedRootStat, trustedParents, artifactPath) {
|
|
415
|
+
const fullPath = join(root, ...artifactPath.split("/"));
|
|
416
|
+
const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0);
|
|
417
|
+
let fd;
|
|
418
|
+
try {
|
|
419
|
+
fd = openSync(fullPath, flags);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (errorCode(error) === "ELOOP") {
|
|
423
|
+
throw new Error(`workspace artifact path contains a symlink: ${artifactPath}`);
|
|
424
|
+
}
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const before = fstatSync(fd);
|
|
429
|
+
if (!before.isFile()) {
|
|
430
|
+
throw new Error(`workspace artifact is not a regular file: ${artifactPath}`);
|
|
431
|
+
}
|
|
432
|
+
assertLiveArtifactPath(root, trustedRootStat, trustedParents, artifactPath, before);
|
|
433
|
+
const bytes = readFileSync(fd);
|
|
434
|
+
const after = fstatSync(fd);
|
|
435
|
+
assertLiveArtifactPath(root, trustedRootStat, trustedParents, artifactPath, after);
|
|
436
|
+
if (!after.isFile() ||
|
|
437
|
+
!sameFileIdentity(before, after) ||
|
|
438
|
+
before.size !== after.size ||
|
|
439
|
+
before.mtimeMs !== after.mtimeMs ||
|
|
440
|
+
before.ctimeMs !== after.ctimeMs ||
|
|
441
|
+
after.size !== bytes.byteLength) {
|
|
442
|
+
throw new Error(`workspace artifact changed while it was captured: ${artifactPath}`);
|
|
443
|
+
}
|
|
444
|
+
return Object.freeze({ path: artifactPath, state: "file", sha256: sha256(bytes), size: bytes.byteLength });
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
closeSync(fd);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function captureSelectedArtifact(root, trustedRootStat, artifactPath, options) {
|
|
451
|
+
assertTrustedRoot(root, trustedRootStat);
|
|
452
|
+
const stat = statPathWithoutSymlinks(root, artifactPath);
|
|
453
|
+
if (stat === undefined) {
|
|
454
|
+
assertTrustedRoot(root, trustedRootStat);
|
|
455
|
+
return Object.freeze({ path: artifactPath, state: "missing" });
|
|
456
|
+
}
|
|
457
|
+
if (!stat.isFile()) {
|
|
458
|
+
throw new Error(`workspace artifact is not a regular file: ${artifactPath}`);
|
|
459
|
+
}
|
|
460
|
+
const trustedParents = captureParentIdentities(root, artifactPath);
|
|
461
|
+
options.faultInjector?.("before-artifact-open", artifactPath);
|
|
462
|
+
return captureFile(root, trustedRootStat, trustedParents, artifactPath);
|
|
463
|
+
}
|
|
464
|
+
/** True when the trusted root itself hosts a `.git` entry; a symlinked `.git` fails closed. */
|
|
465
|
+
function isGitWorkspaceRoot(root) {
|
|
466
|
+
let stat;
|
|
467
|
+
try {
|
|
468
|
+
stat = lstatSync(join(root, ".git"));
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (errorCode(error) === "ENOENT")
|
|
472
|
+
return false;
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
if (stat.isSymbolicLink()) {
|
|
476
|
+
throw new Error("workspace root .git must not be a symlink");
|
|
477
|
+
}
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
function gitCaptureEnv() {
|
|
481
|
+
const env = { ...process.env };
|
|
482
|
+
for (const name of UNSAFE_GIT_ENV_VARS)
|
|
483
|
+
delete env[name];
|
|
484
|
+
env.GIT_OPTIONAL_LOCKS = "0";
|
|
485
|
+
env.LC_ALL = "C";
|
|
486
|
+
return env;
|
|
487
|
+
}
|
|
488
|
+
/** Run one git subcommand with argv boundaries (never a shell) and bounded, time-limited output. */
|
|
489
|
+
function runGitCapture(root, args, allowedExitCodes = [0]) {
|
|
490
|
+
const result = spawnSync("git", ["-C", root, ...args], {
|
|
491
|
+
env: gitCaptureEnv(),
|
|
492
|
+
maxBuffer: MAX_GIT_OUTPUT_BYTES,
|
|
493
|
+
timeout: GIT_CAPTURE_TIMEOUT_MS,
|
|
494
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
495
|
+
windowsHide: true,
|
|
496
|
+
});
|
|
497
|
+
if (result.error !== undefined) {
|
|
498
|
+
const code = errorCode(result.error);
|
|
499
|
+
throw new Error(`workspace git capture failed to run git: ${code ?? result.error.message}`);
|
|
500
|
+
}
|
|
501
|
+
if (result.status === null || !allowedExitCodes.includes(result.status)) {
|
|
502
|
+
throw new Error(`workspace git capture: git exited with status ${String(result.status)}`);
|
|
503
|
+
}
|
|
504
|
+
return { status: result.status, stdout: result.stdout };
|
|
505
|
+
}
|
|
506
|
+
function parseGitStatusOutput(stdout, artifactPaths) {
|
|
507
|
+
const changed = new Set();
|
|
508
|
+
for (const entry of stdout.toString("utf8").split("\0")) {
|
|
509
|
+
if (entry.length === 0)
|
|
510
|
+
continue;
|
|
511
|
+
if (entry.length < 4 || entry[2] !== " ") {
|
|
512
|
+
throw new Error("workspace git capture: unexpected status entry shape");
|
|
513
|
+
}
|
|
514
|
+
const path = entry.slice(3);
|
|
515
|
+
assertNormalizedArtifactPath(path, "workspace git status path");
|
|
516
|
+
if (!isWithinScope(path, artifactPaths)) {
|
|
517
|
+
throw new Error(`workspace git status path escapes the selected scope: ${path}`);
|
|
518
|
+
}
|
|
519
|
+
changed.add(path);
|
|
520
|
+
}
|
|
521
|
+
return { changedPaths: [...changed].sort() };
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Capture HEAD plus a scope-limited canonical dirty digest for a git work-tree root.
|
|
525
|
+
* All git invocations are argv-only, env-sanitized, bounded, and pathspec-limited with
|
|
526
|
+
* literal magic. Every exact selected path is also captured with symlink-free,
|
|
527
|
+
* root-contained file reads, independent of ignore rules and index flags.
|
|
528
|
+
*/
|
|
529
|
+
function captureGitWorkspaceFingerprint(root, trustedRootStat, scope, options) {
|
|
530
|
+
const toplevel = runGitCapture(root, ["rev-parse", "--show-toplevel"]).stdout.toString("utf8").replace(/\n$/, "");
|
|
531
|
+
if (toplevel.length === 0 || realpathSync(toplevel) !== root) {
|
|
532
|
+
throw new Error("workspace root must be the git work-tree top level");
|
|
533
|
+
}
|
|
534
|
+
const head = runGitCapture(root, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], [0, 1]);
|
|
535
|
+
let headCommit = null;
|
|
536
|
+
if (head.status === 0) {
|
|
537
|
+
const hex = head.stdout.toString("utf8").trim();
|
|
538
|
+
if (!GIT_COMMIT_HEX.test(hex)) {
|
|
539
|
+
throw new Error("workspace git capture: HEAD is not a full hex object name");
|
|
540
|
+
}
|
|
541
|
+
headCommit = hex;
|
|
542
|
+
}
|
|
543
|
+
const pathspecs = scope.artifactPaths.map((path) => `:(literal)${path}`);
|
|
544
|
+
let status = { changedPaths: [] };
|
|
545
|
+
let stagedDiff = Buffer.alloc(0);
|
|
546
|
+
let unstagedDiff = Buffer.alloc(0);
|
|
547
|
+
if (pathspecs.length > 0) {
|
|
548
|
+
status = parseGitStatusOutput(runGitCapture(root, [
|
|
549
|
+
"-c",
|
|
550
|
+
"core.fsmonitor=false",
|
|
551
|
+
"status",
|
|
552
|
+
"--porcelain=v1",
|
|
553
|
+
"-z",
|
|
554
|
+
"--untracked-files=all",
|
|
555
|
+
"--no-renames",
|
|
556
|
+
"--ignore-submodules=none",
|
|
557
|
+
"--",
|
|
558
|
+
...pathspecs,
|
|
559
|
+
]).stdout, scope.artifactPaths);
|
|
560
|
+
stagedDiff = runGitCapture(root, ["diff", "--cached", ...GIT_DIFF_ARGS, "--", ...pathspecs]).stdout;
|
|
561
|
+
unstagedDiff = runGitCapture(root, ["diff", ...GIT_DIFF_ARGS, "--", ...pathspecs]).stdout;
|
|
562
|
+
}
|
|
563
|
+
const artifacts = scope.artifactPaths.map((artifactPath) => captureSelectedArtifact(root, trustedRootStat, artifactPath, options));
|
|
564
|
+
assertTrustedRoot(root, trustedRootStat);
|
|
565
|
+
const changedPaths = Object.freeze([...status.changedPaths]);
|
|
566
|
+
const stagedDiffSha256 = sha256(stagedDiff);
|
|
567
|
+
const unstagedDiffSha256 = sha256(unstagedDiff);
|
|
568
|
+
const git = Object.freeze({
|
|
569
|
+
headCommit,
|
|
570
|
+
changedPaths,
|
|
571
|
+
stagedDiffSha256,
|
|
572
|
+
unstagedDiffSha256,
|
|
573
|
+
dirtySha256: computeGitWorkspaceDirtySha256(changedPaths, stagedDiffSha256, unstagedDiffSha256, artifacts),
|
|
574
|
+
});
|
|
575
|
+
const parsed = parseWorkspaceFingerprint({
|
|
576
|
+
kind: "git",
|
|
577
|
+
scope,
|
|
578
|
+
artifacts: Object.freeze(artifacts),
|
|
579
|
+
git,
|
|
580
|
+
manifestSha256: computeGitWorkspaceManifestSha256(scope, artifacts, git),
|
|
581
|
+
});
|
|
582
|
+
if (parsed.kind !== "git") {
|
|
583
|
+
throw new Error("workspace git capture produced a non-git fingerprint");
|
|
584
|
+
}
|
|
585
|
+
return parsed;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Hash the selected workspace scope. When the trusted root is itself a git work-tree
|
|
589
|
+
* top level, capture HEAD plus a scope-limited canonical dirty digest; otherwise hash
|
|
590
|
+
* the caller-selected artifact set directly and perform no git commands.
|
|
591
|
+
* Phase A assumes a trusted, quiescent workspace: pathname identity checks reject
|
|
592
|
+
* observed rebinds, but do not prove an immutable snapshot against a same-UID adversary.
|
|
593
|
+
*/
|
|
594
|
+
export function captureWorkspaceFingerprint(scope, options = {}) {
|
|
595
|
+
const rawScope = exactObject(scope, "workspace scope", ["root", "artifactPaths"]);
|
|
596
|
+
const rawRoot = rawScope.root;
|
|
597
|
+
if (typeof rawRoot !== "string" || rawRoot.length === 0) {
|
|
598
|
+
throw new Error("workspace scope root must be a non-empty string");
|
|
599
|
+
}
|
|
600
|
+
const rawArtifactPaths = exactArray(rawScope.artifactPaths, "workspace scope artifactPaths");
|
|
601
|
+
const seen = new Set();
|
|
602
|
+
for (let index = 0; index < rawArtifactPaths.length; index++) {
|
|
603
|
+
const artifactPath = rawArtifactPaths[index];
|
|
604
|
+
assertNormalizedArtifactPath(artifactPath, `workspace scope artifactPaths[${index}]`);
|
|
605
|
+
if (seen.has(artifactPath)) {
|
|
606
|
+
throw new Error(`duplicate workspace artifact path: ${artifactPath}`);
|
|
607
|
+
}
|
|
608
|
+
seen.add(artifactPath);
|
|
609
|
+
}
|
|
610
|
+
const resolvedRoot = resolve(rawRoot);
|
|
611
|
+
const rootStat = lstatSync(resolvedRoot);
|
|
612
|
+
if (rootStat.isSymbolicLink()) {
|
|
613
|
+
throw new Error("workspace root must not be a symlink");
|
|
614
|
+
}
|
|
615
|
+
if (!rootStat.isDirectory()) {
|
|
616
|
+
throw new Error("workspace root must be a directory");
|
|
617
|
+
}
|
|
618
|
+
const root = realpathSync(resolvedRoot);
|
|
619
|
+
const trustedRootStat = lstatSync(root);
|
|
620
|
+
const artifactPaths = rawArtifactPaths.sort();
|
|
621
|
+
const canonicalScope = Object.freeze({ root, artifactPaths: Object.freeze(artifactPaths) });
|
|
622
|
+
if (isGitWorkspaceRoot(root)) {
|
|
623
|
+
return captureGitWorkspaceFingerprint(root, trustedRootStat, canonicalScope, options);
|
|
624
|
+
}
|
|
625
|
+
const artifacts = artifactPaths.map((artifactPath) => captureSelectedArtifact(root, trustedRootStat, artifactPath, options));
|
|
626
|
+
const fingerprint = {
|
|
627
|
+
kind: "artifact-set",
|
|
628
|
+
scope: canonicalScope,
|
|
629
|
+
artifacts: Object.freeze(artifacts),
|
|
630
|
+
manifestSha256: computeWorkspaceManifestSha256(canonicalScope, artifacts),
|
|
631
|
+
};
|
|
632
|
+
return parseWorkspaceFingerprint(fingerprint);
|
|
633
|
+
}
|
|
634
|
+
//# sourceMappingURL=workspace-fingerprint.js.map
|