@aiwg/cli 2026.9.24 → 2026.10.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.
Files changed (207) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +2 -1
  2. package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
  3. package/dist/src/artifacts/discovery-shadow.js +195 -0
  4. package/dist/src/artifacts/index-builder.js +47 -9
  5. package/dist/src/artifacts/query-engine.js +94 -1
  6. package/dist/src/artifacts/types.js +1 -0
  7. package/dist/src/cli/handlers/decision.js +88 -0
  8. package/dist/src/cli/handlers/gates.js +154 -0
  9. package/dist/src/cli/handlers/index.js +5 -1
  10. package/dist/src/cli/handlers/sessions.js +10 -7
  11. package/dist/src/cli/handlers/utilities.js +20 -0
  12. package/dist/src/config/aiwg-config.js +15 -2
  13. package/dist/src/decision/acceptance.js +25 -0
  14. package/dist/src/decision/adapters/jev.js +24 -2
  15. package/dist/src/decision/calibration/registry.js +9 -0
  16. package/dist/src/decision/compose.js +74 -15
  17. package/dist/src/decision/context-live-promotion.js +344 -0
  18. package/dist/src/decision/context-live-qualification.js +591 -0
  19. package/dist/src/decision/context-pruning.js +847 -0
  20. package/dist/src/decision/driver.js +430 -0
  21. package/dist/src/decision/egress-live-qualification.js +874 -0
  22. package/dist/src/decision/ensemble/contract.js +64 -1
  23. package/dist/src/decision/ensemble/index.js +1 -0
  24. package/dist/src/decision/ensemble/report.js +8 -42
  25. package/dist/src/decision/ensemble/runtime.js +571 -0
  26. package/dist/src/decision/ensemble-study/artifacts.js +85 -0
  27. package/dist/src/decision/ensemble-study/calibration.js +443 -0
  28. package/dist/src/decision/ensemble-study/corpus.js +229 -0
  29. package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
  30. package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
  31. package/dist/src/decision/ensemble-study/multifact.js +241 -0
  32. package/dist/src/decision/ensemble-study/protocol.js +77 -0
  33. package/dist/src/decision/ensemble-study/score.js +462 -0
  34. package/dist/src/decision/ensemble-study/staged.js +87 -0
  35. package/dist/src/decision/ensemble-study/statistics.js +84 -0
  36. package/dist/src/decision/evaluate.js +227 -14
  37. package/dist/src/decision/export.js +4 -0
  38. package/dist/src/decision/feature-export.js +1074 -0
  39. package/dist/src/decision/graph-index.js +16 -0
  40. package/dist/src/decision/graph-live-qualification.js +1041 -0
  41. package/dist/src/decision/graph-live-workload.js +245 -0
  42. package/dist/src/decision/graph-skill-bridge.js +21 -3
  43. package/dist/src/decision/heldout/calibration.js +140 -0
  44. package/dist/src/decision/heldout/collector.js +430 -0
  45. package/dist/src/decision/heldout/contract.js +309 -0
  46. package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
  47. package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
  48. package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
  49. package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
  50. package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
  51. package/dist/src/decision/heldout/d29-pools.js +288 -0
  52. package/dist/src/decision/heldout/d29-v6.js +169 -0
  53. package/dist/src/decision/heldout/d29-v7.js +202 -0
  54. package/dist/src/decision/heldout/d29-v8.js +586 -0
  55. package/dist/src/decision/heldout/generator-registry.js +67 -0
  56. package/dist/src/decision/heldout/generators.js +696 -0
  57. package/dist/src/decision/heldout/index.js +8 -0
  58. package/dist/src/decision/heldout/journal.js +346 -0
  59. package/dist/src/decision/heldout/study-generators.js +64 -0
  60. package/dist/src/decision/heldout/types.js +2 -0
  61. package/dist/src/decision/host-policy.js +61 -0
  62. package/dist/src/decision/index.js +11 -0
  63. package/dist/src/decision/interop-profile.v1.json +112 -0
  64. package/dist/src/decision/interop.js +653 -0
  65. package/dist/src/decision/issue-triage/index.js +3 -0
  66. package/dist/src/decision/issue-triage/runtime.js +1069 -0
  67. package/dist/src/decision/issue-triage/types.js +2 -0
  68. package/dist/src/decision/patterns/catalog.js +28 -8
  69. package/dist/src/decision/patterns/playground.js +28 -11
  70. package/dist/src/decision/patterns/specs.js +19 -10
  71. package/dist/src/decision/preprocessed-evidence.js +724 -0
  72. package/dist/src/decision/qualification/capture.js +8 -2
  73. package/dist/src/decision/qualification/gate-evidence.js +19 -4
  74. package/dist/src/decision/qualification/quality.js +57 -49
  75. package/dist/src/decision/qualification/release.js +76 -3
  76. package/dist/src/decision/qualification/runner.js +5 -2
  77. package/dist/src/decision/routing/contract.js +175 -0
  78. package/dist/src/decision/routing/index.js +5 -0
  79. package/dist/src/decision/routing/report.js +216 -0
  80. package/dist/src/decision/routing/runtime.js +685 -0
  81. package/dist/src/decision/routing/types.js +2 -0
  82. package/dist/src/decision/sdlc-screening.js +1157 -0
  83. package/dist/src/decision/sensitivity/comparative.js +154 -0
  84. package/dist/src/decision/sensitivity/contract.js +218 -0
  85. package/dist/src/decision/sensitivity/index.js +5 -0
  86. package/dist/src/decision/sensitivity/runtime.js +912 -0
  87. package/dist/src/decision/sensitivity/types.js +2 -0
  88. package/dist/src/decision/validate.js +26 -0
  89. package/dist/src/extensions/commands/definitions.js +38 -0
  90. package/dist/src/extensions/manifest.js +44 -0
  91. package/dist/src/gates/discovery.js +315 -0
  92. package/dist/src/gates/driver.js +388 -0
  93. package/dist/src/gates/evaluate.js +719 -0
  94. package/dist/src/gates/floors.js +255 -0
  95. package/dist/src/gates/index.js +17 -0
  96. package/dist/src/gates/providers/evidence.js +38 -0
  97. package/dist/src/gates/providers/index.js +21 -0
  98. package/dist/src/gates/providers/isolated-child-source.js +446 -0
  99. package/dist/src/gates/providers/loader.js +1069 -0
  100. package/dist/src/gates/providers/paired.js +52 -0
  101. package/dist/src/gates/providers/proportion.js +60 -0
  102. package/dist/src/gates/providers/registry.js +40 -0
  103. package/dist/src/gates/providers/scalar.js +41 -0
  104. package/dist/src/gates/providers/screening.js +53 -0
  105. package/dist/src/gates/providers/types.js +25 -0
  106. package/dist/src/gates/registry.js +1003 -0
  107. package/dist/src/gates/report.js +102 -0
  108. package/dist/src/gates/schema.js +56 -0
  109. package/dist/src/gates/stats/binomial.js +26 -0
  110. package/dist/src/gates/stats/bootstrap.js +62 -0
  111. package/dist/src/gates/stats/clopper-pearson.js +95 -0
  112. package/dist/src/gates/stats/digest.js +26 -0
  113. package/dist/src/gates/stats/error.js +5 -0
  114. package/dist/src/gates/stats/index.js +9 -0
  115. package/dist/src/gates/stats/normal.js +35 -0
  116. package/dist/src/gates/stats/paired.js +120 -0
  117. package/dist/src/gates/stats/splits.js +30 -0
  118. package/dist/src/gates/types.js +8 -0
  119. package/dist/src/governance/redaction.js +14 -4
  120. package/dist/src/mcp/muse-native-config.mjs +123 -0
  121. package/dist/src/mcp/tools/decision.mjs +188 -0
  122. package/dist/src/mcp/tools/subsystems.mjs +2 -0
  123. package/dist/src/providers/capability-matrix.yaml +2 -1
  124. package/dist/src/providers/provider-definitions.js +2 -1
  125. package/dist/src/sessions/adapters/muse.js +336 -39
  126. package/dist/src/sessions/batch-import.js +7 -0
  127. package/dist/src/sessions/workspace-discovery.js +13 -2
  128. package/package.json +1 -1
  129. package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
  130. package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
  131. package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
  132. package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
  133. package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
  134. package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
  135. package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
  136. package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
  137. package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
  138. package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
  139. package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
  140. package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
  141. package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
  142. package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
  143. package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
  144. package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
  145. package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
  146. package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
  147. package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
  148. package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
  149. package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
  150. package/schemas/decision/D29Study.v1.schema.json +1903 -0
  151. package/schemas/decision/D29Study.v2.schema.json +1344 -0
  152. package/schemas/decision/D29Study.v3.schema.json +609 -0
  153. package/schemas/decision/D29Study.v4.schema.json +1133 -0
  154. package/schemas/decision/D29Study.v6.schema.json +1205 -0
  155. package/schemas/decision/D29Study.v7.schema.json +1471 -0
  156. package/schemas/decision/D29Study.v8.schema.json +671 -0
  157. package/schemas/decision/D29Study.v9.schema.json +778 -0
  158. package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
  159. package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
  160. package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
  161. package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
  162. package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
  163. package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
  164. package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
  165. package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
  166. package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
  167. package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
  168. package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
  169. package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
  170. package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
  171. package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
  172. package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
  173. package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
  174. package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
  175. package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
  176. package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
  177. package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
  178. package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
  179. package/schemas/decision/DecisionRuleset.schema.json +8 -2
  180. package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
  181. package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
  182. package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
  183. package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
  184. package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
  185. package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
  186. package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
  187. package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
  188. package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
  189. package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
  190. package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
  191. package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
  192. package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
  193. package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
  194. package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
  195. package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
  196. package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
  197. package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
  198. package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
  199. package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
  200. package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
  201. package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
  202. package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
  203. package/tools/agents/deploy-agents.mjs +1 -1
  204. package/tools/agents/providers/base.mjs +26 -7
  205. package/tools/agents/providers/factory.mjs +10 -2
  206. package/tools/agents/providers/muse-hooks.mjs +1 -29
  207. package/tools/agents/providers/muse.mjs +6 -3
@@ -0,0 +1,1069 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { closeSync, lstatSync, openSync, readSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync, } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { GateProviderEntrySchema } from '../../extensions/manifest.js';
7
+ import { artifactDigest } from '../../decision/validate.js';
8
+ import { providerSourceDigest } from './types.js';
9
+ import { ISOLATED_CHILD_SOURCE } from './isolated-child-source.js';
10
+ /**
11
+ * Addon/extension metric-provider loading, isolated rework (P1-P5).
12
+ *
13
+ * Providers run ONLY in a separate child Node process inside a bubblewrap
14
+ * sandbox (every namespace unshared, network included; cleared environment;
15
+ * no host filesystem beyond the read-only node runtime; die-with-parent),
16
+ * with node's permission model (`--permission`: no fs, no child processes,
17
+ * no workers) and `--disallow-code-generation-from-strings` as depth. If
18
+ * bwrap is missing or fails its capability self-test, bundle providers are
19
+ * REFUSED (`sandbox-unavailable`); there is no unsandboxed fallback. Provider
20
+ * code is evaluated in a `vm` context with string code generation disabled,
21
+ * frozen intrinsics and only context-realm objects (frozen records, frozen
22
+ * clock, seeded RNG, minimal pure standard library). The `vm` context is NOT
23
+ * the boundary; the sandboxed child (plus the parent's SIGKILL timeout,
24
+ * output cap and nonce-framed result channel) is.
25
+ *
26
+ * Provider code is the entire declared provider directory, read into an
27
+ * in-memory snapshot (no symlinks: lstat-checked, symlinks and non-regular
28
+ * files refuse); the code digest covers the snapshot's sorted module paths
29
+ * and bytes. The loader keeps those bytes in a private record keyed by the
30
+ * frozen provider object it issues; every run re-digests its own copy and
31
+ * sends exactly those bytes to the child over stdin. Modules
32
+ * must be self-contained relative `.mjs` with static string specifiers only:
33
+ * dynamic `import()`, `require`, bare/external imports and non-`.mjs`
34
+ * relatives refuse at load. Bare package imports are FORBIDDEN in phase 1.
35
+ *
36
+ * The trust root is the project config (`aiwg.config` `gates.providers`
37
+ * allowlist): a provider registers only with a matching entry (bundle id,
38
+ * provider id, code digest, reviewer, review timestamp). In-bundle review
39
+ * files are informational (integrity-checked for consistency) only.
40
+ * Unknown or mismatched entries refuse.
41
+ *
42
+ * Loading requires BOTH an explicit `allowBundleProviders: true` option AND
43
+ * an allowlist entry. No environment variable activates it. With no opt-in,
44
+ * resolution and evaluation stay byte-identical to the core-only path.
45
+ */
46
+ export const GATE_PROVIDER_MAX_FILE_BYTES = 256 * 1024;
47
+ export const GATE_PROVIDER_MAX_FILES = 20;
48
+ export const GATE_PROVIDER_MAX_TOTAL_BYTES = 1024 * 1024;
49
+ export const GATE_PROVIDER_MAX_REVIEW_BYTES = 8 * 1024;
50
+ export const GATE_PROVIDER_DEFAULT_TIMEOUT_MS = 5000;
51
+ export const GATE_PROVIDER_MAX_TIMEOUT_MS = 30000;
52
+ export const GATE_PROVIDER_DEFAULT_MAX_RECORDS = 50000;
53
+ /** Largest accepted isolated-run stdout body (single JSON message). */
54
+ export const GATE_PROVIDER_MAX_OUTPUT_BYTES = 1024 * 1024;
55
+ export class GateProviderError extends Error {
56
+ code;
57
+ constructor(message, code) {
58
+ super(message);
59
+ this.code = code;
60
+ this.name = 'GateProviderError';
61
+ }
62
+ }
63
+ const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
64
+ const MODULE_PATTERN = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*\.mjs$/;
65
+ const REVIEW_PATTERN = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*\.json$/;
66
+ const HARNESS_PREFIX = '__aiwg_';
67
+ export function isBundleProvidersEnabled(options) {
68
+ return options?.allowBundleProviders === true;
69
+ }
70
+ function bundleRootReal(bundleRoot) {
71
+ const resolved = resolve(bundleRoot);
72
+ try {
73
+ return realpathSync(resolved);
74
+ }
75
+ catch {
76
+ throw new GateProviderError(`bundle root is unavailable: ${bundleRoot}`, 'path-escape');
77
+ }
78
+ }
79
+ function assertDeclaredPath(value, pattern, label) {
80
+ if (typeof value !== 'string' || !pattern.test(value)) {
81
+ throw new GateProviderError(`${label} escapes the bundle: ${String(value)}`, 'path-escape');
82
+ }
83
+ if (isAbsolute(value))
84
+ throw new GateProviderError(`${label} must be bundle-relative: ${value}`, 'path-escape');
85
+ return value;
86
+ }
87
+ /** Deterministic seed for the isolated RNG: pinned by code and records digests. */
88
+ export function deriveIsolatedSeed(codeDigest, recordsDigest) {
89
+ return parseInt(createHash('sha256').update(`${codeDigest}${recordsDigest}`).digest('hex').slice(0, 8), 16);
90
+ }
91
+ /**
92
+ * Reads the provider directory (the directory containing the entry module)
93
+ * into an in-memory snapshot. Every entry is lstat-checked: symlinks (files
94
+ * or dirs) and non-regular files refuse; only regular `.mjs` files are
95
+ * captured (other regular files are inert: the loader never serves them, so
96
+ * they are neither captured nor digested). Module bytes must be valid UTF-8
97
+ * (they round-trip exactly to the source text the sandbox compiles). The
98
+ * walk is sorted, so the snapshot (and its digest) is deterministic. Nothing
99
+ * is written to disk: runs receive the bytes over stdin.
100
+ */
101
+ export function snapshotProviderDir(bundleRoot, entryModule) {
102
+ const rel = assertDeclaredPath(entryModule, MODULE_PATTERN, 'provider module');
103
+ const rootReal = bundleRootReal(bundleRoot);
104
+ const entryAbs = resolve(rootReal, rel);
105
+ let entryReal;
106
+ try {
107
+ entryReal = realpathSync(entryAbs);
108
+ }
109
+ catch {
110
+ throw new GateProviderError(`provider module is missing inside the bundle: ${rel}`, 'path-escape');
111
+ }
112
+ if (entryReal !== rootReal && !entryReal.startsWith(rootReal + sep)) {
113
+ throw new GateProviderError(`provider module escapes the bundle root: ${rel}`, 'path-escape');
114
+ }
115
+ let entryStatus;
116
+ try {
117
+ entryStatus = lstatSync(entryReal);
118
+ }
119
+ catch {
120
+ throw new GateProviderError(`provider module is missing inside the bundle: ${rel}`, 'path-escape');
121
+ }
122
+ if (entryStatus.isSymbolicLink() || !entryStatus.isFile()) {
123
+ throw new GateProviderError(`provider module must be a regular file: ${rel}`, 'path-escape');
124
+ }
125
+ const providerDir = dirname(entryReal);
126
+ const walk = { modules: [] };
127
+ let total = 0;
128
+ const visit = (dirAbs) => {
129
+ let names;
130
+ try {
131
+ names = readdirSync(dirAbs).sort();
132
+ }
133
+ catch {
134
+ throw new GateProviderError(`provider directory is unreadable: ${relative(rootReal, dirAbs)}`, 'path-escape');
135
+ }
136
+ for (const name of names) {
137
+ if (name.startsWith(HARNESS_PREFIX)) {
138
+ throw new GateProviderError(`provider file uses the reserved prefix: ${name}`, 'invalid-module');
139
+ }
140
+ const abs = join(dirAbs, name);
141
+ let status;
142
+ try {
143
+ status = lstatSync(abs);
144
+ }
145
+ catch {
146
+ throw new GateProviderError(`provider file is unreadable: ${relative(rootReal, abs)}`, 'path-escape');
147
+ }
148
+ if (status.isSymbolicLink()) {
149
+ throw new GateProviderError(`provider file must not be a symlink: ${relative(rootReal, abs)}`, 'path-escape');
150
+ }
151
+ if (status.isDirectory()) {
152
+ if (name.endsWith('.mjs')) {
153
+ throw new GateProviderError(`provider module must be a regular file: ${relative(rootReal, abs)}`, 'invalid-module');
154
+ }
155
+ visit(abs);
156
+ continue;
157
+ }
158
+ if (!status.isFile()) {
159
+ throw new GateProviderError(`provider file is not a regular file: ${relative(rootReal, abs)}`, 'path-escape');
160
+ }
161
+ if (!name.endsWith('.mjs'))
162
+ continue;
163
+ if (walk.modules.length >= GATE_PROVIDER_MAX_FILES) {
164
+ throw new GateProviderError(`provider exceeds ${GATE_PROVIDER_MAX_FILES} files`, 'oversize');
165
+ }
166
+ let bytes;
167
+ try {
168
+ bytes = readFileSync(abs);
169
+ }
170
+ catch {
171
+ throw new GateProviderError(`provider file is unreadable: ${relative(rootReal, abs)}`, 'path-escape');
172
+ }
173
+ if (bytes.length > GATE_PROVIDER_MAX_FILE_BYTES) {
174
+ throw new GateProviderError(`provider file exceeds size limit: ${relative(rootReal, abs)}`, 'oversize');
175
+ }
176
+ if (!Buffer.from(bytes.toString('utf8'), 'utf8').equals(bytes)) {
177
+ throw new GateProviderError(`provider module is not valid UTF-8: ${relative(rootReal, abs)}`, 'invalid-module');
178
+ }
179
+ total += bytes.length;
180
+ if (total > GATE_PROVIDER_MAX_TOTAL_BYTES) {
181
+ throw new GateProviderError('provider exceeds total size limit', 'oversize');
182
+ }
183
+ walk.modules.push({ path: relative(rootReal, abs).split(sep).join('/'), bytes });
184
+ }
185
+ };
186
+ visit(providerDir);
187
+ walk.modules.sort((a, b) => (a.path < b.path ? -1 : 1));
188
+ const entryRel = relative(rootReal, entryReal).split(sep).join('/');
189
+ if (!walk.modules.some(file => file.path === entryRel)) {
190
+ throw new GateProviderError(`provider module is missing from its snapshot: ${rel}`, 'path-escape');
191
+ }
192
+ const files = walk.modules.map(file => ({
193
+ path: file.path, sha256: createHash('sha256').update(file.bytes).digest('hex'),
194
+ }));
195
+ const codeDigest = artifactDigest({ externals: [], files });
196
+ return { files, codeDigest, entryRel, modules: walk.modules };
197
+ }
198
+ /**
199
+ * Digest of provider CODE: the snapshot bytes (sorted module paths plus
200
+ * content hashes) of the entire provider directory. Any byte change anywhere
201
+ * in the directory moves the digest, so a binding pinned to the old digest
202
+ * refuses until re-pinned. Bare/external imports are forbidden in phase 1;
203
+ * the sandboxed linker is authoritative at load/invoke. Pure read: no
204
+ * snapshot is written anywhere.
205
+ */
206
+ export function computeProviderCodeDigest(bundleRoot, entryModule) {
207
+ const snapshot = snapshotProviderDir(bundleRoot, entryModule);
208
+ return { codeDigest: snapshot.codeDigest, files: snapshot.files, externals: [] };
209
+ }
210
+ const METRIC_KINDS = new Set(['proportion', 'paired', 'scalar', 'differences', 'evidence']);
211
+ function readReview(rootReal, entry) {
212
+ if (entry.review !== undefined) {
213
+ if (typeof entry.review.codeDigest !== 'string') {
214
+ throw new GateProviderError(`provider ${entry.id} review digest is unknown`, 'missing-attestation');
215
+ }
216
+ return {
217
+ reviewer: entry.review.reviewer,
218
+ reviewedAt: entry.review.reviewedAt,
219
+ codeDigest: entry.review.codeDigest,
220
+ };
221
+ }
222
+ const rel = assertDeclaredPath(entry.reviewFile, REVIEW_PATTERN, 'provider review');
223
+ const abs = resolve(rootReal, rel);
224
+ let real;
225
+ try {
226
+ real = realpathSync(abs);
227
+ }
228
+ catch {
229
+ throw new GateProviderError(`provider ${entry.id} review file is unreadable: ${rel}`, 'missing-attestation');
230
+ }
231
+ if (real !== rootReal && !real.startsWith(rootReal + sep)) {
232
+ throw new GateProviderError(`provider review escapes the bundle root: ${rel}`, 'path-escape');
233
+ }
234
+ let raw;
235
+ try {
236
+ if (statSync(real).size > GATE_PROVIDER_MAX_REVIEW_BYTES) {
237
+ throw new GateProviderError(`provider review exceeds size limit: ${rel}`, 'oversize');
238
+ }
239
+ raw = readFileSync(real, 'utf8');
240
+ }
241
+ catch (error) {
242
+ if (error instanceof GateProviderError)
243
+ throw error;
244
+ throw new GateProviderError(`provider ${entry.id} review file is unreadable: ${rel}`, 'missing-attestation');
245
+ }
246
+ let parsed;
247
+ try {
248
+ parsed = JSON.parse(raw);
249
+ }
250
+ catch {
251
+ throw new GateProviderError(`provider ${entry.id} review file is not JSON: ${rel}`, 'missing-attestation');
252
+ }
253
+ const validated = GateProviderEntrySchema.safeParse({ ...entry, review: parsed, reviewFile: undefined });
254
+ if (!validated.success) {
255
+ throw new GateProviderError(`provider ${entry.id} review file is invalid`, 'missing-attestation');
256
+ }
257
+ if (validated.data.review === undefined) {
258
+ throw new GateProviderError(`provider ${entry.id} review file is invalid`, 'missing-attestation');
259
+ }
260
+ return {
261
+ reviewer: validated.data.review.reviewer,
262
+ reviewedAt: validated.data.review.reviewedAt,
263
+ codeDigest: validated.data.review.codeDigest,
264
+ };
265
+ }
266
+ const ISSUED_PROVIDERS = new WeakMap();
267
+ /** True only for the exact provider objects returned by `loadGateBundleProviders`. */
268
+ export function isLoaderIssuedProvider(provider) {
269
+ return typeof provider === 'object' && provider !== null && ISSUED_PROVIDERS.has(provider);
270
+ }
271
+ function issuedRecordOrThrow(provider) {
272
+ const record = typeof provider === 'object' && provider !== null ? ISSUED_PROVIDERS.get(provider) : undefined;
273
+ if (record === undefined) {
274
+ const id = typeof provider === 'object' && provider !== null ? String(provider.id) : 'unknown';
275
+ throw new GateProviderError(`provider ${id} was not issued by the bundle loader: refusing to run caller-supplied provider objects`, 'unregistered');
276
+ }
277
+ return record;
278
+ }
279
+ function digestModules(modules) {
280
+ const files = [...modules]
281
+ .sort((a, b) => (a.path < b.path ? -1 : 1))
282
+ .map(file => ({ path: file.path, sha256: createHash('sha256').update(file.bytes).digest('hex') }));
283
+ return artifactDigest({ externals: [], files });
284
+ }
285
+ /**
286
+ * Builds the stdin payload for one run from the runner's OWN copy of the
287
+ * trusted bytes: the copy is re-digested and must equal the issued digest
288
+ * (and the pinned digest, for evaluator re-runs) before anything executes.
289
+ * The child receives exactly these bytes (UTF-8, round-trip checked at
290
+ * snapshot time) and nothing else; no file is read or shared between runs.
291
+ */
292
+ function prepareIsolatedRun(record, args) {
293
+ const copy = record.modules.map(module => ({ path: module.path, bytes: Buffer.from(module.bytes) }));
294
+ const digest = digestModules(copy);
295
+ if (digest !== record.codeDigest
296
+ || (args.expectedCodeDigest !== undefined && digest !== args.expectedCodeDigest)) {
297
+ throw new GateProviderError(`provider ${record.id} code does not match its pinned digest`, 'attestation-mismatch');
298
+ }
299
+ const modules = {};
300
+ for (const module of copy)
301
+ modules[module.path] = module.bytes.toString('utf8');
302
+ const nonce = randomBytes(32).toString('hex');
303
+ const request = JSON.stringify({
304
+ entry: record.entryRel,
305
+ modules,
306
+ mode: args.mode,
307
+ ...(args.recordsJson === undefined ? {} : { recordsJson: args.recordsJson }),
308
+ clockMs: args.clockMs,
309
+ seed: args.seed,
310
+ expectedId: record.id,
311
+ expectedVersion: record.version,
312
+ });
313
+ return { nonce, input: `${nonce}\n${request}` };
314
+ }
315
+ // ---------------------------------------------------------------------------
316
+ // OS sandbox (C1c/C1d): bubblewrap is the isolation boundary.
317
+ // ---------------------------------------------------------------------------
318
+ const BWRAP_CANDIDATES = ['/usr/bin/bwrap', '/bin/bwrap', '/usr/local/bin/bwrap'];
319
+ const SANDBOX_NODE_PATH = '/aiwg/bin/node';
320
+ const SANDBOX_CWD = '/tmp';
321
+ const SANDBOX_SELF_TEST_TIMEOUT_MS = 15000;
322
+ const SANDBOX_NAMESPACES = ['net', 'pid', 'ipc', 'uts', 'user', 'mnt'];
323
+ const DRIVER_NODE_ARGS = [
324
+ '--experimental-vm-modules', '--permission', '--disallow-code-generation-from-strings',
325
+ '--no-warnings', '--input-type=module', '-e', ISOLATED_CHILD_SOURCE,
326
+ ];
327
+ const RESULT_FRAME = 'AIWG-RESULT ';
328
+ /** Self-test body: run inside the sandbox WITHOUT node --permission, so it observes the OS layer alone. */
329
+ const SANDBOX_SELF_TEST_SOURCE = [
330
+ "import fs from 'node:fs';",
331
+ "import net from 'node:net';",
332
+ "const input = JSON.parse(fs.readFileSync(0, 'utf8'));",
333
+ 'const out = { env: {}, ns: {}, visible: [], connect: null };',
334
+ 'for (const key of Object.keys(process.env)) out.env[key] = String(process.env[key]);',
335
+ "for (const name of input.namespaces) { try { out.ns[name] = fs.readlinkSync('/proc/self/ns/' + name); } catch { out.ns[name] = null; } }",
336
+ 'for (const path of input.paths) { try { fs.lstatSync(path); out.visible.push(path); } catch { /* absent */ } }',
337
+ 'if (typeof input.port === \'number\') {',
338
+ ' out.connect = await new Promise((resolve) => {',
339
+ " const socket = net.connect({ host: '127.0.0.1', port: input.port });",
340
+ " const timer = setTimeout(() => { socket.destroy(); resolve('timeout'); }, 3000);",
341
+ " socket.on('connect', () => { clearTimeout(timer); socket.destroy(); resolve('connected'); });",
342
+ " socket.on('error', (error) => { clearTimeout(timer); resolve('refused:' + error.code); });",
343
+ ' });',
344
+ '}',
345
+ 'process.stdout.write(JSON.stringify(out));',
346
+ ].join('\n');
347
+ let sandboxOverride = null;
348
+ let sandboxVerdict;
349
+ let runtimeBindCache;
350
+ /**
351
+ * Test-only hook: point the runner at another bwrap path (for example a
352
+ * missing binary, or one that fails the self-test) or reset with `null`.
353
+ * It can only make the runner refuse: whatever path is configured must still
354
+ * pass the capability self-test before any provider runs.
355
+ */
356
+ export function configureIsolationSandboxForTests(options) {
357
+ sandboxOverride = options === null ? null : { ...options };
358
+ sandboxVerdict = undefined;
359
+ }
360
+ function resolveBwrapPath() {
361
+ const candidates = sandboxOverride?.bwrapPath !== undefined ? [sandboxOverride.bwrapPath] : BWRAP_CANDIDATES;
362
+ for (const candidate of candidates) {
363
+ try {
364
+ const status = statSync(candidate);
365
+ if (status.isFile() && (status.mode & 0o111) !== 0)
366
+ return candidate;
367
+ }
368
+ catch {
369
+ /* try the next candidate */
370
+ }
371
+ }
372
+ return null;
373
+ }
374
+ /** PT_INTERP of an ELF64 little-endian binary (the dynamic loader path), or null. */
375
+ function elfInterpreter(binary) {
376
+ let fd;
377
+ try {
378
+ fd = openSync(binary, 'r');
379
+ const header = Buffer.alloc(64);
380
+ if (readSync(fd, header, 0, 64, 0) !== 64)
381
+ return null;
382
+ if (header.readUInt32BE(0) !== 0x7f454c46 || header[4] !== 2 || header[5] !== 1)
383
+ return null;
384
+ const phoff = Number(header.readBigUInt64LE(0x20));
385
+ const phentsize = header.readUInt16LE(0x36);
386
+ const phnum = header.readUInt16LE(0x38);
387
+ const table = Buffer.alloc(phentsize * phnum);
388
+ readSync(fd, table, 0, table.length, phoff);
389
+ for (let index = 0; index < phnum; index++) {
390
+ const base = index * phentsize;
391
+ if (table.readUInt32LE(base) !== 3)
392
+ continue;
393
+ const offset = Number(table.readBigUInt64LE(base + 8));
394
+ const size = Number(table.readBigUInt64LE(base + 32));
395
+ if (size <= 0 || size > 4096)
396
+ return null;
397
+ const buffer = Buffer.alloc(size);
398
+ readSync(fd, buffer, 0, size, offset);
399
+ return buffer.toString('utf8').replace(/\0+$/, '');
400
+ }
401
+ return null;
402
+ }
403
+ catch {
404
+ return null;
405
+ }
406
+ finally {
407
+ if (fd !== undefined)
408
+ closeSync(fd);
409
+ }
410
+ }
411
+ /**
412
+ * Read-only binds for the sandbox: the node binary (at a fixed path) and
413
+ * exactly the shared libraries this node process has mapped, plus their
414
+ * soname aliases and the ELF interpreter. No other host path is visible.
415
+ */
416
+ function runtimeBinds() {
417
+ if (runtimeBindCache !== undefined)
418
+ return runtimeBindCache;
419
+ const byTarget = new Map();
420
+ const nodeReal = realpathSync(process.execPath);
421
+ byTarget.set(SANDBOX_NODE_PATH, nodeReal);
422
+ let maps = '';
423
+ try {
424
+ maps = readFileSync('/proc/self/maps', 'utf8');
425
+ }
426
+ catch {
427
+ maps = '';
428
+ }
429
+ const libraries = new Set();
430
+ for (const line of maps.split('\n')) {
431
+ const path = line.trim().split(/\s+/).slice(5).join(' ');
432
+ if (path.startsWith('/') && /\.so(?:\.\d+)*$/.test(path))
433
+ libraries.add(path);
434
+ }
435
+ for (const library of libraries) {
436
+ let real;
437
+ try {
438
+ real = realpathSync(library);
439
+ }
440
+ catch {
441
+ continue;
442
+ }
443
+ byTarget.set(library, real);
444
+ const match = /^(.*\.so)((?:\.\d+)*)$/.exec(basename(library));
445
+ if (match === null)
446
+ continue;
447
+ const versions = match[2].split('.').filter(Boolean);
448
+ for (let keep = versions.length - 1; keep >= 0; keep--) {
449
+ const alias = join(dirname(library), `${match[1]}${versions.slice(0, keep).map(part => `.${part}`).join('')}`);
450
+ try {
451
+ if (realpathSync(alias) === real)
452
+ byTarget.set(alias, real);
453
+ }
454
+ catch {
455
+ /* no such alias */
456
+ }
457
+ }
458
+ }
459
+ const interpreter = elfInterpreter(nodeReal);
460
+ if (interpreter !== null && isAbsolute(interpreter)) {
461
+ try {
462
+ byTarget.set(interpreter, realpathSync(interpreter));
463
+ }
464
+ catch {
465
+ /* the self-test will fail closed */
466
+ }
467
+ }
468
+ runtimeBindCache = [...byTarget.entries()]
469
+ .sort(([a], [b]) => (a < b ? -1 : 1))
470
+ .map(([target, source]) => [source, target]);
471
+ return runtimeBindCache;
472
+ }
473
+ function sandboxCommand(nodeArgs) {
474
+ const bwrap = resolveBwrapPath();
475
+ if (bwrap === null) {
476
+ throw new GateProviderError('bundle providers require bubblewrap (bwrap) for OS-level isolation and none was found: refusing to run', 'sandbox-unavailable');
477
+ }
478
+ const args = [
479
+ '--unshare-all', '--unshare-user', '--disable-userns',
480
+ '--die-with-parent', '--new-session', '--clearenv', '--cap-drop', 'ALL',
481
+ ];
482
+ for (const [source, target] of runtimeBinds())
483
+ args.push('--ro-bind', source, target);
484
+ args.push('--tmpfs', SANDBOX_CWD, '--proc', '/proc', '--dev', '/dev', '--chdir', SANDBOX_CWD, '--', SANDBOX_NODE_PATH, ...nodeArgs);
485
+ return { command: bwrap, args, env: {} };
486
+ }
487
+ /**
488
+ * The exact sandboxed launch used for every provider run: bubblewrap with
489
+ * every namespace unshared (network included), no user namespaces inside,
490
+ * cleared environment, new session, die-with-parent, read-only node runtime
491
+ * binds, a private tmpfs, and node with `--permission` (no fs, no child
492
+ * processes, no workers) plus `--disallow-code-generation-from-strings`.
493
+ * The spawn environment itself is empty.
494
+ */
495
+ export function isolationLaunchPlan() {
496
+ return sandboxCommand(DRIVER_NODE_ARGS);
497
+ }
498
+ function hostProbePaths() {
499
+ const targets = [...runtimeBinds().map(([, target]) => target), SANDBOX_CWD, '/proc', '/dev', '/aiwg'];
500
+ const candidates = [homedir(), process.cwd(), '/etc/passwd', '/home', '/root', '/var', '/run', '/srv'];
501
+ return [...new Set(candidates)].filter(path => {
502
+ if (!path || path === '/' || !isAbsolute(path))
503
+ return false;
504
+ if (targets.some(target => target === path || target.startsWith(`${path}/`) || path.startsWith(`${target}/`)))
505
+ return false;
506
+ try {
507
+ lstatSync(path);
508
+ return true;
509
+ }
510
+ catch {
511
+ return false;
512
+ }
513
+ });
514
+ }
515
+ function selfTestInput(port) {
516
+ const paths = hostProbePaths();
517
+ return { paths, input: JSON.stringify({ namespaces: SANDBOX_NAMESPACES, paths, port }) };
518
+ }
519
+ function judgeSelfTest(stdout, status, paths, port) {
520
+ const report = {
521
+ ok: false, reason: '', envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
522
+ };
523
+ let observed;
524
+ try {
525
+ observed = JSON.parse(stdout);
526
+ }
527
+ catch {
528
+ report.reason = `sandbox self-test produced no report (exit ${String(status)})`;
529
+ return report;
530
+ }
531
+ if (status !== 0 || !observed || typeof observed !== 'object') {
532
+ report.reason = `sandbox self-test failed (exit ${String(status)})`;
533
+ return report;
534
+ }
535
+ // bwrap always exports PWD for the sandbox working directory; nothing else may leak.
536
+ const env = observed.env ?? {};
537
+ report.envKeys = Object.keys(env).filter(key => !(key === 'PWD' && env[key] === SANDBOX_CWD)).sort();
538
+ for (const name of SANDBOX_NAMESPACES) {
539
+ let host = null;
540
+ try {
541
+ host = readlinkSync(`/proc/self/ns/${name}`);
542
+ }
543
+ catch {
544
+ host = null;
545
+ }
546
+ const inside = observed.ns?.[name] ?? null;
547
+ if (host === null || inside === null || host === inside)
548
+ report.sharedNamespaces.push(name);
549
+ }
550
+ report.hostPathsVisible = (observed.visible ?? []).filter(path => paths.includes(path));
551
+ report.connect = observed.connect ?? null;
552
+ const problems = [];
553
+ if (report.envKeys.length)
554
+ problems.push(`environment leaks ${report.envKeys.join(',')}`);
555
+ if (report.sharedNamespaces.length)
556
+ problems.push(`namespaces not private: ${report.sharedNamespaces.join(',')}`);
557
+ if (report.hostPathsVisible.length)
558
+ problems.push(`host paths visible: ${report.hostPathsVisible.join(',')}`);
559
+ if (port !== null && report.connect === 'connected')
560
+ problems.push('host loopback network reachable');
561
+ report.ok = problems.length === 0;
562
+ report.reason = report.ok ? 'ok' : `sandbox self-test failed: ${problems.join('; ')}`;
563
+ return report;
564
+ }
565
+ /**
566
+ * Runs the sandbox capability self-test asynchronously and returns the
567
+ * observations (empty env, private namespaces, no host paths, optional
568
+ * loopback connect to a parent listener on `connectPort`).
569
+ */
570
+ export function probeIsolationSandbox(options) {
571
+ const port = typeof options?.connectPort === 'number' ? options.connectPort : null;
572
+ let plan;
573
+ let probe;
574
+ try {
575
+ plan = sandboxCommand(['--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
576
+ probe = selfTestInput(port);
577
+ }
578
+ catch (error) {
579
+ return Promise.resolve({
580
+ ok: false, reason: error instanceof Error ? error.message : 'sandbox unavailable',
581
+ envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
582
+ });
583
+ }
584
+ return new Promise((resolvePromise) => {
585
+ const child = spawn(plan.command, plan.args, { env: plan.env, stdio: ['pipe', 'pipe', 'ignore'] });
586
+ const chunks = [];
587
+ const timer = setTimeout(() => { try {
588
+ child.kill('SIGKILL');
589
+ }
590
+ catch { /* exited */ } }, SANDBOX_SELF_TEST_TIMEOUT_MS);
591
+ child.stdout?.on('data', (chunk) => { chunks.push(chunk); });
592
+ child.stdin?.on('error', () => { });
593
+ child.on('error', (error) => {
594
+ clearTimeout(timer);
595
+ resolvePromise({
596
+ ok: false, reason: `sandbox failed to start: ${error.message}`,
597
+ envKeys: [], sharedNamespaces: [], hostPathsVisible: [], connect: null,
598
+ });
599
+ });
600
+ child.on('close', (code) => {
601
+ clearTimeout(timer);
602
+ resolvePromise(judgeSelfTest(Buffer.concat(chunks).toString('utf8'), code, probe.paths, port));
603
+ });
604
+ child.stdin?.end(probe.input);
605
+ });
606
+ }
607
+ /**
608
+ * Fail-closed gate before every provider run: bwrap must exist and pass the
609
+ * capability self-test (cached per bwrap path for the process lifetime).
610
+ * There is no unsandboxed fallback.
611
+ */
612
+ function ensureIsolationSandbox() {
613
+ const key = resolveBwrapPath() ?? '<missing>';
614
+ if (sandboxVerdict === undefined || sandboxVerdict.key !== key) {
615
+ let verdict;
616
+ try {
617
+ const plan = sandboxCommand(['--no-warnings', '--input-type=module', '-e', SANDBOX_SELF_TEST_SOURCE]);
618
+ const probe = selfTestInput(null);
619
+ const completed = spawnSync(plan.command, plan.args, {
620
+ env: plan.env, input: probe.input, encoding: 'utf8', timeout: SANDBOX_SELF_TEST_TIMEOUT_MS,
621
+ killSignal: 'SIGKILL', maxBuffer: 1024 * 1024, stdio: ['pipe', 'pipe', 'ignore'],
622
+ });
623
+ verdict = completed.error !== undefined
624
+ ? { ok: false, reason: `sandbox failed to start: ${completed.error.message}` }
625
+ : judgeSelfTest(typeof completed.stdout === 'string' ? completed.stdout : '', completed.status, probe.paths, null);
626
+ }
627
+ catch (error) {
628
+ verdict = { ok: false, reason: error instanceof Error ? error.message : 'sandbox unavailable' };
629
+ }
630
+ sandboxVerdict = { key, ...verdict };
631
+ }
632
+ if (!sandboxVerdict.ok) {
633
+ throw new GateProviderError(`bundle providers are refused: the OS isolation sandbox is unavailable (${sandboxVerdict.reason})`, 'sandbox-unavailable');
634
+ }
635
+ }
636
+ /**
637
+ * Whether bundle providers can run here: bwrap present and passing its
638
+ * capability self-test. Never throws; `reason` explains a refusal.
639
+ */
640
+ export function isolationSandboxStatus() {
641
+ try {
642
+ ensureIsolationSandbox();
643
+ return { ok: true, reason: 'ok' };
644
+ }
645
+ catch (error) {
646
+ return { ok: false, reason: error instanceof Error ? error.message : 'sandbox unavailable' };
647
+ }
648
+ }
649
+ function parseIsolatedResponse(body, stderr, providerId) {
650
+ let parsed;
651
+ try {
652
+ parsed = JSON.parse(body);
653
+ }
654
+ catch {
655
+ throw new GateProviderError(`provider ${providerId} returned an unreadable message${stderr ? `: ${stderr.slice(0, 300)}` : ''}`, 'rejected');
656
+ }
657
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
658
+ throw new GateProviderError(`provider ${providerId} returned an unreadable message`, 'rejected');
659
+ }
660
+ const message = parsed;
661
+ if (message.ok !== true) {
662
+ throw new GateProviderError(`provider ${providerId} rejected: ${typeof message.error === 'string' && message.error ? message.error.slice(0, 300) : 'unknown'}`, 'rejected');
663
+ }
664
+ return { descriptor: message.descriptor, result: message.result };
665
+ }
666
+ /**
667
+ * C1e: the parent accepts exactly one line on stdout, framed by the trusted
668
+ * driver with the per-run nonce it read from stdin before any provider code
669
+ * was compiled. Anything else (an unframed message, a wrong nonce, extra or
670
+ * missing bytes) is a forgery attempt and rejects.
671
+ */
672
+ export function parseIsolatedFrame(stdout, nonce, providerId, stderr = '') {
673
+ const prefix = `${RESULT_FRAME}${nonce} `;
674
+ if (!/^[0-9a-f]{64}$/.test(nonce) || !stdout.startsWith(prefix) || !stdout.endsWith('\n')
675
+ || stdout.indexOf('\n') !== stdout.length - 1) {
676
+ throw new GateProviderError(`provider ${providerId} wrote outside its result frame${stderr ? `: ${stderr.slice(0, 300)}` : ''}`, 'rejected');
677
+ }
678
+ return parseIsolatedResponse(stdout.slice(prefix.length, -1), stderr, providerId);
679
+ }
680
+ function runIsolatedRecordSync(record, args, limits) {
681
+ ensureIsolationSandbox();
682
+ const { nonce, input } = prepareIsolatedRun(record, args);
683
+ const plan = isolationLaunchPlan();
684
+ const completed = spawnSync(plan.command, plan.args, {
685
+ env: plan.env,
686
+ input,
687
+ timeout: limits.timeoutMs,
688
+ killSignal: 'SIGKILL',
689
+ maxBuffer: limits.maxOutputBytes,
690
+ encoding: 'utf8',
691
+ stdio: ['pipe', 'pipe', 'pipe'],
692
+ });
693
+ const stderr = typeof completed.stderr === 'string' ? completed.stderr : '';
694
+ if (completed.error !== undefined) {
695
+ const code = completed.error.code;
696
+ if (code === 'ETIMEDOUT' || completed.signal === 'SIGKILL') {
697
+ throw new GateProviderError(`provider ${record.id} timed out`, 'timeout');
698
+ }
699
+ if (code === 'ENOBUFS') {
700
+ throw new GateProviderError(`provider ${record.id} exceeds its output cap`, 'rejected');
701
+ }
702
+ throw new GateProviderError(`provider ${record.id} failed to start: ${completed.error.message}`, 'rejected');
703
+ }
704
+ if (completed.status !== 0) {
705
+ throw new GateProviderError(`provider ${record.id} rejected${stderr ? `: ${stderr.slice(0, 300)}` : ''}`, 'rejected');
706
+ }
707
+ return parseIsolatedFrame(completed.stdout, nonce, record.id, stderr);
708
+ }
709
+ function runIsolatedRecord(record, args, limits) {
710
+ if (limits.signal?.aborted === true) {
711
+ return Promise.reject(new GateProviderError(`provider ${record.id} was cancelled before dispatch`, 'cancelled'));
712
+ }
713
+ let nonce;
714
+ let input;
715
+ let plan;
716
+ try {
717
+ ensureIsolationSandbox();
718
+ ({ nonce, input } = prepareIsolatedRun(record, args));
719
+ plan = isolationLaunchPlan();
720
+ }
721
+ catch (error) {
722
+ return Promise.reject(error);
723
+ }
724
+ return new Promise((resolvePromise, rejectPromise) => {
725
+ let settled = false;
726
+ const settle = (action) => {
727
+ if (settled)
728
+ return;
729
+ settled = true;
730
+ clearTimeout(timer);
731
+ limits.signal?.removeEventListener('abort', onAbort);
732
+ action();
733
+ };
734
+ let child;
735
+ try {
736
+ child = spawn(plan.command, plan.args, { env: plan.env, stdio: ['pipe', 'pipe', 'pipe'] });
737
+ }
738
+ catch (error) {
739
+ rejectPromise(new GateProviderError(`provider ${record.id} failed to start: ${error instanceof Error ? error.message : 'unknown'}`, 'rejected'));
740
+ return;
741
+ }
742
+ const timer = setTimeout(() => {
743
+ try {
744
+ child.kill('SIGKILL');
745
+ }
746
+ catch { /* already exited */ }
747
+ settle(() => rejectPromise(new GateProviderError(`provider ${record.id} timed out`, 'timeout')));
748
+ }, limits.timeoutMs);
749
+ const onAbort = () => {
750
+ try {
751
+ child.kill('SIGKILL');
752
+ }
753
+ catch { /* already exited */ }
754
+ settle(() => rejectPromise(new GateProviderError(`provider ${record.id} was cancelled`, 'cancelled')));
755
+ };
756
+ limits.signal?.addEventListener('abort', onAbort, { once: true });
757
+ const chunks = [];
758
+ let bytes = 0;
759
+ let capped = false;
760
+ child.stdout?.on('data', (chunk) => {
761
+ bytes += chunk.length;
762
+ if (bytes > limits.maxOutputBytes) {
763
+ capped = true;
764
+ try {
765
+ child.kill('SIGKILL');
766
+ }
767
+ catch { /* already exited */ }
768
+ settle(() => rejectPromise(new GateProviderError(`provider ${record.id} exceeds its output cap`, 'rejected')));
769
+ return;
770
+ }
771
+ chunks.push(chunk);
772
+ });
773
+ const errors = [];
774
+ let errorBytes = 0;
775
+ child.stderr?.on('data', (chunk) => {
776
+ if (errorBytes + chunk.length <= 4096) {
777
+ errors.push(chunk);
778
+ errorBytes += chunk.length;
779
+ }
780
+ });
781
+ child.stdin?.on('error', () => { });
782
+ child.on('error', (error) => {
783
+ settle(() => rejectPromise(new GateProviderError(`provider ${record.id} failed to start: ${error.message}`, 'rejected')));
784
+ });
785
+ child.on('close', (code) => {
786
+ if (capped)
787
+ return;
788
+ const stderr = Buffer.concat(errors).toString('utf8');
789
+ if (code !== 0) {
790
+ settle(() => rejectPromise(new GateProviderError(`provider ${record.id} rejected${stderr ? `: ${stderr.slice(0, 300)}` : ''}`, 'rejected')));
791
+ return;
792
+ }
793
+ // Parse BEFORE settling: a throwing parse must still reach the
794
+ // rejection below (settling first would swallow the settlement).
795
+ let parsed;
796
+ try {
797
+ parsed = parseIsolatedFrame(Buffer.concat(chunks).toString('utf8'), nonce, record.id, stderr);
798
+ }
799
+ catch (error) {
800
+ settle(() => rejectPromise(error));
801
+ return;
802
+ }
803
+ settle(() => resolvePromise(parsed));
804
+ });
805
+ child.stdin?.end(input);
806
+ });
807
+ }
808
+ /**
809
+ * Synchronous isolated run of a LOADER-ISSUED provider (evaluator re-runs):
810
+ * sandboxed, SIGKILL timeout, output cap. Caller-supplied provider objects
811
+ * (copies, hand-made objects) refuse; the executed bytes come only from the
812
+ * loader's private record and are re-digested before every run.
813
+ */
814
+ export function runIsolatedProviderSync(provider, args, limits) {
815
+ return runIsolatedRecordSync(issuedRecordOrThrow(provider), args, limits);
816
+ }
817
+ /** Asynchronous isolated run of a LOADER-ISSUED provider: abortable, sandboxed, SIGKILL timeout, output cap. */
818
+ export function runIsolatedProvider(provider, args, limits) {
819
+ let record;
820
+ try {
821
+ record = issuedRecordOrThrow(provider);
822
+ }
823
+ catch (error) {
824
+ return Promise.reject(error);
825
+ }
826
+ return runIsolatedRecord(record, args, limits);
827
+ }
828
+ /**
829
+ * Load and register every `gateProviders` entry from a bundle manifest. The
830
+ * manifest entry is re-validated here so direct callers cannot bypass Zod.
831
+ * Registration requires BOTH an explicit `allowBundleProviders: true` option
832
+ * AND a matching project-config allowlist entry (bundle id, provider id,
833
+ * isolated code digest, reviewer, review timestamp); in-bundle reviews are
834
+ * integrity-checked for consistency but are informational only. The provider
835
+ * module graph is validated by the isolated child (parse + link over the
836
+ * snapshot) before anything registers — nothing executes in-process, ever.
837
+ */
838
+ export async function loadGateBundleProviders(bundleRoot, manifest, options) {
839
+ if (!isBundleProvidersEnabled(options)) {
840
+ throw new GateProviderError('bundle metric providers are disabled: pass allowBundleProviders', 'disabled');
841
+ }
842
+ const entries = Array.isArray(manifest.gateProviders) ? [...manifest.gateProviders] : [];
843
+ if (!Array.isArray(manifest.gateProviders) && manifest.gateProviders !== undefined) {
844
+ throw new GateProviderError('bundle gateProviders must be an array', 'invalid-module');
845
+ }
846
+ if (!entries.length)
847
+ return [];
848
+ const nowMs = Date.parse(options?.now ?? new Date().toISOString());
849
+ if (!Number.isFinite(nowMs))
850
+ throw new GateProviderError('provider review clock is not a valid date-time', 'invalid-records');
851
+ const timeoutMs = options?.timeoutMs ?? GATE_PROVIDER_DEFAULT_TIMEOUT_MS;
852
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > GATE_PROVIDER_MAX_TIMEOUT_MS) {
853
+ throw new GateProviderError('provider timeout must be a positive integer within bounds', 'invalid-records');
854
+ }
855
+ const maxOutputBytes = options?.maxOutputBytes ?? GATE_PROVIDER_MAX_OUTPUT_BYTES;
856
+ if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0 || maxOutputBytes > 8 * 1024 * 1024) {
857
+ throw new GateProviderError('provider output cap is invalid', 'invalid-records');
858
+ }
859
+ const rootReal = bundleRootReal(bundleRoot);
860
+ const allowlist = options?.allowlist ?? [];
861
+ const seen = new Set();
862
+ const loaded = [];
863
+ for (const raw of entries) {
864
+ const parsed = GateProviderEntrySchema.safeParse(raw);
865
+ if (!parsed.success) {
866
+ throw new GateProviderError(`bundle provider declaration is invalid: ${parsed.error.issues[0]?.message ?? 'invalid'}`, 'invalid-module');
867
+ }
868
+ const entry = parsed.data;
869
+ if (seen.has(entry.id))
870
+ throw new GateProviderError(`duplicate bundle provider: ${entry.id}`, 'duplicate');
871
+ seen.add(entry.id);
872
+ const snapshot = snapshotProviderDir(rootReal, entry.module);
873
+ const review = readReview(rootReal, entry);
874
+ if (review.codeDigest !== snapshot.codeDigest) {
875
+ throw new GateProviderError(`provider ${entry.id} review digest does not match its code digest`, 'attestation-mismatch');
876
+ }
877
+ const reviewedMs = Date.parse(review.reviewedAt);
878
+ if (!Number.isFinite(reviewedMs))
879
+ throw new GateProviderError(`provider ${entry.id} review date is invalid`, 'missing-attestation');
880
+ if (reviewedMs > nowMs)
881
+ throw new GateProviderError(`provider ${entry.id} review is dated in the future`, 'missing-attestation');
882
+ // P3 trust root: the project-config allowlist authorizes registration.
883
+ const trusted = allowlist.find(candidate => candidate.bundleId === manifest.id && candidate.providerId === entry.id);
884
+ if (trusted === undefined) {
885
+ throw new GateProviderError(`provider ${entry.id} is not allowlisted for bundle ${manifest.id}: refusing registration`, 'unregistered');
886
+ }
887
+ if (trusted.codeDigest !== snapshot.codeDigest) {
888
+ throw new GateProviderError(`provider ${entry.id} code digest does not match its allowlist entry`, 'attestation-mismatch');
889
+ }
890
+ if (trusted.reviewer !== review.reviewer || trusted.reviewedAt !== review.reviewedAt) {
891
+ throw new GateProviderError(`provider ${entry.id} review does not match its allowlist entry`, 'attestation-mismatch');
892
+ }
893
+ if (!DIGEST_PATTERN.test(snapshot.codeDigest)) {
894
+ throw new GateProviderError(`provider ${entry.id} code digest is unknown`, 'attestation-mismatch');
895
+ }
896
+ // Validate the module graph in the sandboxed child (parse + link over the
897
+ // snapshot, identity-checked against the manifest). Dynamic import(),
898
+ // require, bare/absolute/escaping/non-.mjs specifiers refuse here.
899
+ const record = Object.freeze({
900
+ id: entry.id, version: entry.version, codeDigest: snapshot.codeDigest, entryRel: snapshot.entryRel,
901
+ modules: Object.freeze(snapshot.modules.map(module => Object.freeze({ path: module.path, bytes: Buffer.from(module.bytes) }))),
902
+ });
903
+ let descriptor;
904
+ try {
905
+ const inspected = await runIsolatedRecord(record, {
906
+ mode: 'inspect', clockMs: nowMs, seed: 1,
907
+ }, { timeoutMs, maxOutputBytes, signal: options?.signal });
908
+ if (!inspected.descriptor || typeof inspected.descriptor !== 'object') {
909
+ throw new GateProviderError(`provider ${entry.id} module has no provider export`, 'invalid-module');
910
+ }
911
+ descriptor = inspected.descriptor;
912
+ }
913
+ catch (error) {
914
+ if (error instanceof GateProviderError
915
+ && (error.code === 'timeout' || error.code === 'cancelled' || error.code === 'sandbox-unavailable'))
916
+ throw error;
917
+ throw new GateProviderError(`provider ${entry.id} module is invalid: ${error instanceof Error ? error.message : 'unknown'}`, 'invalid-module');
918
+ }
919
+ const metrics = descriptor.metrics;
920
+ if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) {
921
+ throw new GateProviderError(`provider ${entry.id} metrics declaration is invalid`, 'invalid-module');
922
+ }
923
+ for (const [name, declared] of Object.entries(metrics)) {
924
+ const kind = declared.kind;
925
+ if (typeof name !== 'string' || !name || !METRIC_KINDS.has(kind)) {
926
+ throw new GateProviderError(`provider ${entry.id} metric ${name} has an unknown kind`, 'invalid-module');
927
+ }
928
+ }
929
+ const sourceDigest = providerSourceDigest({ id: entry.id, version: entry.version, metrics });
930
+ const provider = Object.freeze({
931
+ id: entry.id, version: entry.version, description: descriptor.description,
932
+ metrics: deepFreeze(metrics), sourceDigest,
933
+ codeDigest: snapshot.codeDigest,
934
+ review: Object.freeze({ reviewer: trusted.reviewer, reviewedAt: trusted.reviewedAt, codeDigest: snapshot.codeDigest }),
935
+ bundleId: manifest.id, modulePath: entry.module,
936
+ snapshotFiles: Object.freeze(snapshot.files.map(file => file.path)),
937
+ compute: () => {
938
+ throw new GateProviderError(`provider ${entry.id} cannot run in-process: use invokeBundleProvider (isolated child)`, 'invalid-module');
939
+ },
940
+ });
941
+ // C3: the loader-issued token. Only this exact frozen object runs or registers.
942
+ ISSUED_PROVIDERS.set(provider, record);
943
+ loaded.push(provider);
944
+ }
945
+ return loaded;
946
+ }
947
+ function deepFreeze(value) {
948
+ if (value && typeof value === 'object') {
949
+ for (const key of Object.keys(value))
950
+ deepFreeze(value[key]);
951
+ Object.freeze(value);
952
+ }
953
+ return value;
954
+ }
955
+ /**
956
+ * Reserve-then-dispatch isolated provider invocation with a SIGKILL timeout.
957
+ * Only loader-issued providers run; the clock (`clockMs`) is required, so
958
+ * standalone invocations are reproducible (no real-clock default).
959
+ */
960
+ export async function invokeBundleProvider(provider, records, options) {
961
+ const timeoutMs = options?.timeoutMs ?? GATE_PROVIDER_DEFAULT_TIMEOUT_MS;
962
+ const maxRecords = options?.maxRecords ?? GATE_PROVIDER_DEFAULT_MAX_RECORDS;
963
+ const maxOutputBytes = options?.maxOutputBytes ?? GATE_PROVIDER_MAX_OUTPUT_BYTES;
964
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > GATE_PROVIDER_MAX_TIMEOUT_MS) {
965
+ throw new GateProviderError('provider timeout must be a positive integer within bounds', 'invalid-records');
966
+ }
967
+ if (!Number.isSafeInteger(maxRecords) || maxRecords <= 0) {
968
+ throw new GateProviderError('provider record budget is invalid', 'invalid-records');
969
+ }
970
+ if (!Number.isSafeInteger(maxOutputBytes) || maxOutputBytes <= 0 || maxOutputBytes > 8 * 1024 * 1024) {
971
+ throw new GateProviderError('provider output cap is invalid', 'invalid-records');
972
+ }
973
+ if (!Array.isArray(records))
974
+ throw new GateProviderError('provider records must be an array', 'invalid-records');
975
+ if (records.length > maxRecords)
976
+ throw new GateProviderError(`provider ${provider.id} exceeds its record budget`, 'budget-exceeded');
977
+ if (options?.signal?.aborted === true)
978
+ throw new GateProviderError(`provider ${provider.id} was cancelled before dispatch`, 'cancelled');
979
+ let recordsJson;
980
+ try {
981
+ const serialized = JSON.stringify(records);
982
+ if (typeof serialized !== 'string')
983
+ throw new Error('unserializable');
984
+ recordsJson = serialized;
985
+ }
986
+ catch {
987
+ throw new GateProviderError(`provider ${provider.id} records are not JSON-serializable`, 'invalid-records');
988
+ }
989
+ let recordsDigest;
990
+ try {
991
+ recordsDigest = artifactDigest(records);
992
+ }
993
+ catch {
994
+ throw new GateProviderError(`provider ${provider.id} records are not digestible`, 'invalid-records');
995
+ }
996
+ const clockMs = options?.clockMs;
997
+ if (clockMs === undefined) {
998
+ throw new GateProviderError(`provider ${provider.id} invocation requires an explicit clockMs`, 'invalid-records');
999
+ }
1000
+ if (!Number.isSafeInteger(clockMs))
1001
+ throw new GateProviderError('provider clock is not an integer', 'invalid-records');
1002
+ const record = issuedRecordOrThrow(provider);
1003
+ try {
1004
+ const ran = await runIsolatedRecord(record, {
1005
+ mode: 'invoke', recordsJson, clockMs, seed: deriveIsolatedSeed(record.codeDigest, recordsDigest),
1006
+ }, { timeoutMs, maxOutputBytes, signal: options?.signal });
1007
+ if (ran.result === undefined)
1008
+ throw new GateProviderError(`provider ${provider.id} returned no metrics`, 'rejected');
1009
+ return ran.result;
1010
+ }
1011
+ catch (error) {
1012
+ if (error instanceof GateProviderError)
1013
+ throw error;
1014
+ throw new GateProviderError(`provider ${provider.id} rejected: ${error instanceof Error ? error.message : 'unknown'}`, 'rejected');
1015
+ }
1016
+ }
1017
+ /**
1018
+ * Builds the binding pin for a bundle provider over caller-supplied records,
1019
+ * including the records digest the evaluator reproduces (records binding).
1020
+ */
1021
+ export function providerBindingPin(provider, records) {
1022
+ if (typeof provider.codeDigest !== 'string' || !DIGEST_PATTERN.test(provider.codeDigest)) {
1023
+ throw new GateProviderError(`provider ${provider.id} code digest is unknown`, 'attestation-mismatch');
1024
+ }
1025
+ if (!Array.isArray(records))
1026
+ throw new GateProviderError('provider records must be an array', 'invalid-records');
1027
+ let recordsDigest;
1028
+ try {
1029
+ recordsDigest = artifactDigest(records);
1030
+ }
1031
+ catch {
1032
+ throw new GateProviderError(`provider ${provider.id} records are not digestible`, 'invalid-records');
1033
+ }
1034
+ return {
1035
+ id: provider.id, version: provider.version,
1036
+ sourceDigest: provider.sourceDigest, codeDigest: provider.codeDigest, recordsDigest,
1037
+ };
1038
+ }
1039
+ /**
1040
+ * Bind a provider result to its trusted code digest and input-records digest.
1041
+ * The returned section overwrites any self-declared `version`/`sourceDigest`:
1042
+ * callers must use the sealed values, and the evaluator verifies the pin
1043
+ * against the loaded provider, never against provider-declared fields.
1044
+ */
1045
+ export function sealProviderSection(provider, records, result) {
1046
+ if (!Array.isArray(records))
1047
+ throw new GateProviderError('provider records must be an array', 'invalid-records');
1048
+ let recordsDigest;
1049
+ try {
1050
+ recordsDigest = artifactDigest(records);
1051
+ }
1052
+ catch {
1053
+ throw new GateProviderError(`provider ${provider.id} records are not digestible`, 'invalid-records');
1054
+ }
1055
+ if (!result || typeof result !== 'object' || !result.metrics || typeof result.metrics !== 'object') {
1056
+ throw new GateProviderError(`provider ${provider.id} returned no metrics`, 'rejected');
1057
+ }
1058
+ if (typeof provider.codeDigest !== 'string' || !DIGEST_PATTERN.test(provider.codeDigest)) {
1059
+ throw new GateProviderError(`provider ${provider.id} code digest is unknown`, 'attestation-mismatch');
1060
+ }
1061
+ return {
1062
+ version: provider.version,
1063
+ sourceDigest: provider.sourceDigest,
1064
+ codeDigest: provider.codeDigest,
1065
+ recordsDigest,
1066
+ metrics: result.metrics,
1067
+ };
1068
+ }
1069
+ //# sourceMappingURL=loader.js.map