@yiaany/ghostapi 0.1.9 → 0.2.1

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 (292) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CONTRIBUTING.md +132 -119
  3. package/LICENSE +21 -21
  4. package/PROVENANCE.md +13 -0
  5. package/README.md +78 -84
  6. package/SECURITY.md +56 -53
  7. package/dist/actions/gateway.js +320 -72
  8. package/dist/actions/gateway.js.map +1 -1
  9. package/dist/actions/index.d.ts +2 -2
  10. package/dist/actions/index.js +1 -1
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/agents/agentPrompt.js +7 -5
  13. package/dist/agents/agentPrompt.js.map +1 -1
  14. package/dist/ai/aiClient.js +5 -5
  15. package/dist/ai/aiClient.js.map +1 -1
  16. package/dist/ai/aiGenerator.js +66 -26
  17. package/dist/ai/aiGenerator.js.map +1 -1
  18. package/dist/ai/genericInference.js +151 -36
  19. package/dist/ai/genericInference.js.map +1 -1
  20. package/dist/ai/prompts.js +55 -49
  21. package/dist/ai/prompts.js.map +1 -1
  22. package/dist/approvals/approvalInbox.js +649 -133
  23. package/dist/approvals/approvalInbox.js.map +1 -1
  24. package/dist/approvals/index.d.ts +2 -2
  25. package/dist/approvals/index.js +1 -1
  26. package/dist/approvals/index.js.map +1 -1
  27. package/dist/behavior/behaviorStore.js +34 -14
  28. package/dist/behavior/behaviorStore.js.map +1 -1
  29. package/dist/cache/index.js +14 -5
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +226 -93
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.js +17 -3
  34. package/dist/cli/openUrl.js.map +1 -1
  35. package/dist/cli/parser.js +79 -18
  36. package/dist/cli/parser.js.map +1 -1
  37. package/dist/config/dataPaths.js +4 -2
  38. package/dist/config/dataPaths.js.map +1 -1
  39. package/dist/config/localConfig.js +5 -2
  40. package/dist/config/localConfig.js.map +1 -1
  41. package/dist/config/serverConfig.js +11 -5
  42. package/dist/config/serverConfig.js.map +1 -1
  43. package/dist/contracts/contract.js +311 -83
  44. package/dist/contracts/contract.js.map +1 -1
  45. package/dist/contracts/index.d.ts +2 -2
  46. package/dist/contracts/index.js +1 -1
  47. package/dist/contracts/index.js.map +1 -1
  48. package/dist/credentials/credentialBroker.js +391 -79
  49. package/dist/credentials/credentialBroker.js.map +1 -1
  50. package/dist/credentials/index.d.ts +2 -2
  51. package/dist/credentials/index.js +1 -1
  52. package/dist/credentials/index.js.map +1 -1
  53. package/dist/dashboard/app.js +823 -685
  54. package/dist/dashboard/dashboard.js.map +1 -1
  55. package/dist/dashboard/index.html +704 -267
  56. package/dist/dashboard/styles.css +1040 -349
  57. package/dist/egress/capabilities.js +55 -25
  58. package/dist/egress/capabilities.js.map +1 -1
  59. package/dist/egress/linuxBootstrap.js +47 -11
  60. package/dist/egress/linuxBootstrap.js.map +1 -1
  61. package/dist/egress/run.js +170 -31
  62. package/dist/egress/run.js.map +1 -1
  63. package/dist/errors/errorEngine.js +15 -3
  64. package/dist/errors/errorEngine.js.map +1 -1
  65. package/dist/errors/providerErrors.js.map +1 -1
  66. package/dist/evals/evals.js +304 -78
  67. package/dist/evals/evals.js.map +1 -1
  68. package/dist/evals/index.d.ts +2 -2
  69. package/dist/evals/index.js +1 -1
  70. package/dist/evals/index.js.map +1 -1
  71. package/dist/evidence/index.d.ts +2 -2
  72. package/dist/evidence/index.js +1 -1
  73. package/dist/evidence/index.js.map +1 -1
  74. package/dist/evidence/report.d.ts +1 -1
  75. package/dist/evidence/report.js +263 -79
  76. package/dist/evidence/report.js.map +1 -1
  77. package/dist/fault/faultLab.js +24 -9
  78. package/dist/fault/faultLab.js.map +1 -1
  79. package/dist/index.d.ts +42 -42
  80. package/dist/index.js +21 -21
  81. package/dist/index.js.map +1 -1
  82. package/dist/inventory/analysis.d.ts +1 -1
  83. package/dist/inventory/analysis.js +174 -50
  84. package/dist/inventory/analysis.js.map +1 -1
  85. package/dist/inventory/index.d.ts +4 -4
  86. package/dist/inventory/index.js +3 -3
  87. package/dist/inventory/index.js.map +1 -1
  88. package/dist/inventory/inventoryController.js +362 -115
  89. package/dist/inventory/inventoryController.js.map +1 -1
  90. package/dist/inventory/types.js +680 -140
  91. package/dist/inventory/types.js.map +1 -1
  92. package/dist/ledger/actionLedger.js +355 -78
  93. package/dist/ledger/actionLedger.js.map +1 -1
  94. package/dist/ledger/index.d.ts +2 -2
  95. package/dist/ledger/index.js +1 -1
  96. package/dist/ledger/index.js.map +1 -1
  97. package/dist/mcp/server.js +26 -11
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/policy/index.d.ts +2 -2
  100. package/dist/policy/index.js +1 -1
  101. package/dist/policy/index.js.map +1 -1
  102. package/dist/policy/policy.js +112 -32
  103. package/dist/policy/policy.js.map +1 -1
  104. package/dist/productTelemetry/index.js +71 -20
  105. package/dist/productTelemetry/index.js.map +1 -1
  106. package/dist/providers/conformance.js +10 -5
  107. package/dist/providers/conformance.js.map +1 -1
  108. package/dist/providers/discord.js +2 -2
  109. package/dist/providers/generic.js +3 -3
  110. package/dist/providers/github.js +2 -2
  111. package/dist/providers/index.d.ts +3 -3
  112. package/dist/providers/index.js +2 -2
  113. package/dist/providers/index.js.map +1 -1
  114. package/dist/providers/openai.js +3 -3
  115. package/dist/providers/packs/resendPack.js +51 -25
  116. package/dist/providers/packs/resendPack.js.map +1 -1
  117. package/dist/providers/packs/stripePack.js +590 -121
  118. package/dist/providers/packs/stripePack.js.map +1 -1
  119. package/dist/providers/registry.js +4 -4
  120. package/dist/providers/registry.js.map +1 -1
  121. package/dist/providers/runtime.js +7 -6
  122. package/dist/providers/runtime.js.map +1 -1
  123. package/dist/providers/stripeWebhook.js +39 -10
  124. package/dist/providers/stripeWebhook.js.map +1 -1
  125. package/dist/providers/twilio.js +2 -2
  126. package/dist/proxy/cacheKey.js +9 -2
  127. package/dist/proxy/cacheKey.js.map +1 -1
  128. package/dist/proxy/providerDetector.js +22 -9
  129. package/dist/proxy/providerDetector.js.map +1 -1
  130. package/dist/proxy/proxyHandler.js +57 -22
  131. package/dist/proxy/proxyHandler.js.map +1 -1
  132. package/dist/proxy/requestNormalizer.js +4 -2
  133. package/dist/proxy/requestNormalizer.js.map +1 -1
  134. package/dist/reliability/costGovernance.js +211 -49
  135. package/dist/reliability/costGovernance.js.map +1 -1
  136. package/dist/reliability/index.d.ts +8 -8
  137. package/dist/reliability/index.js +4 -4
  138. package/dist/reliability/index.js.map +1 -1
  139. package/dist/reliability/reconciliation.js +274 -65
  140. package/dist/reliability/reconciliation.js.map +1 -1
  141. package/dist/reliability/runtimeHealth.js +127 -35
  142. package/dist/reliability/runtimeHealth.js.map +1 -1
  143. package/dist/reliability/slo.js +257 -50
  144. package/dist/reliability/slo.js.map +1 -1
  145. package/dist/report/safetyReport.d.ts +6 -0
  146. package/dist/report/safetyReport.js +56 -12
  147. package/dist/report/safetyReport.js.map +1 -1
  148. package/dist/rules/aiRules.js +5 -5
  149. package/dist/rules/aiRules.js.map +1 -1
  150. package/dist/safety/index.d.ts +2 -2
  151. package/dist/safety/index.js +1 -1
  152. package/dist/safety/index.js.map +1 -1
  153. package/dist/safety/safetyController.js +675 -165
  154. package/dist/safety/safetyController.js.map +1 -1
  155. package/dist/scenarios/scenarioBundle.js +204 -71
  156. package/dist/scenarios/scenarioBundle.js.map +1 -1
  157. package/dist/scenarios/scenarioStore.js +88 -29
  158. package/dist/scenarios/scenarioStore.js.map +1 -1
  159. package/dist/security/headerSanitizer.js +52 -10
  160. package/dist/security/headerSanitizer.js.map +1 -1
  161. package/dist/security/secrets.js +10 -5
  162. package/dist/security/secrets.js.map +1 -1
  163. package/dist/server/accessControl.js +37 -10
  164. package/dist/server/accessControl.js.map +1 -1
  165. package/dist/server/createServer.js +1 -1
  166. package/dist/server/createServer.js.map +1 -1
  167. package/dist/server/eventsStore.js +15 -5
  168. package/dist/server/eventsStore.js.map +1 -1
  169. package/dist/server/routes.js +61 -19
  170. package/dist/server/routes.js.map +1 -1
  171. package/dist/server/sse.js +1 -1
  172. package/dist/server/sse.js.map +1 -1
  173. package/dist/setup/sdkDetector.js +7 -2
  174. package/dist/setup/sdkDetector.js.map +1 -1
  175. package/dist/setup/setupGenerator.js +136 -48
  176. package/dist/setup/setupGenerator.js.map +1 -1
  177. package/dist/state/stateExtractor.js +3 -1
  178. package/dist/state/stateExtractor.js.map +1 -1
  179. package/dist/state/stateResolver.js +10 -7
  180. package/dist/state/stateResolver.js.map +1 -1
  181. package/dist/state/stateStore.js +13 -4
  182. package/dist/state/stateStore.js.map +1 -1
  183. package/dist/storage/fileStore.js +12 -4
  184. package/dist/storage/fileStore.js.map +1 -1
  185. package/dist/teamControl/controlPlane.js +846 -174
  186. package/dist/teamControl/controlPlane.js.map +1 -1
  187. package/dist/teamControl/deployment.js +19 -5
  188. package/dist/teamControl/deployment.js.map +1 -1
  189. package/dist/teamControl/index.d.ts +4 -4
  190. package/dist/teamControl/index.js +2 -2
  191. package/dist/teamControl/index.js.map +1 -1
  192. package/dist/tests/testGenerator.js +15 -7
  193. package/dist/tests/testGenerator.js.map +1 -1
  194. package/dist/trust/index.d.ts +2 -2
  195. package/dist/trust/index.js +1 -1
  196. package/dist/trust/index.js.map +1 -1
  197. package/dist/trust/trustLadder.js +309 -63
  198. package/dist/trust/trustLadder.js.map +1 -1
  199. package/dist/worlds/index.d.ts +2 -2
  200. package/dist/worlds/index.js +1 -1
  201. package/dist/worlds/index.js.map +1 -1
  202. package/dist/worlds/worlds.js +364 -65
  203. package/dist/worlds/worlds.js.map +1 -1
  204. package/docs/assets/dashboard.png +0 -0
  205. package/docs/ci.md +49 -49
  206. package/docs/github-actions.md +84 -84
  207. package/docs/mcp.md +58 -52
  208. package/docs/policy.md +63 -61
  209. package/docs/providers/authoring-packs.md +180 -175
  210. package/docs/providers/stripe-core-pack.md +84 -82
  211. package/docs/releases/README.md +13 -0
  212. package/docs/security/action-gateway-threat-model.md +34 -34
  213. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
  214. package/docs/security/approval-inbox-threat-model.md +35 -35
  215. package/docs/security/credential-broker-threat-model.md +52 -52
  216. package/docs/security/egress-threat-model.md +126 -126
  217. package/docs/security/inventory-threat-model.md +104 -104
  218. package/docs/security/kill-switch-budgets-threat-model.md +37 -37
  219. package/docs/security/reliability-threat-model.md +111 -111
  220. package/docs/security/trust-ladder-threat-model.md +33 -33
  221. package/docs/telemetry.md +14 -0
  222. package/docs/usage.md +680 -571
  223. package/examples/README.md +10 -10
  224. package/examples/agent-instructions/README.md +39 -39
  225. package/examples/ci-smoke/README.md +21 -21
  226. package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
  227. package/examples/ci-smoke/package.json +9 -9
  228. package/examples/ci-smoke/production-egress.mjs +4 -2
  229. package/examples/ci-smoke/safe.mjs +21 -17
  230. package/examples/evals/README.md +24 -24
  231. package/examples/evals/retry-after.eval.json +71 -52
  232. package/examples/generic-rest/README.md +27 -27
  233. package/examples/github-fetch/README.md +38 -35
  234. package/examples/openai-streaming/README.md +16 -16
  235. package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
  236. package/examples/policy/ghostapi.policy.yaml +27 -27
  237. package/examples/record-replay/README.md +19 -19
  238. package/examples/record-replay/replay-requests.json +22 -22
  239. package/examples/record-replay/stripe-checkout.har.json +30 -30
  240. package/examples/resend-node/README.md +22 -22
  241. package/examples/stripe-node/README.md +35 -35
  242. package/examples/stripe-node/checkout-flow.mjs +87 -61
  243. package/examples/twilio-node/README.md +21 -21
  244. package/examples/worlds/README.md +19 -19
  245. package/examples/worlds/subscription-recovery.mjs +21 -12
  246. package/package.json +28 -6
  247. package/dist/landing/assets/index-BXR41fAD.css +0 -1
  248. package/dist/landing/assets/index-CxWsopzP.css +0 -1
  249. package/dist/landing/assets/index-D8_ggMRE.js +0 -55
  250. package/dist/landing/assets/index-DZlpyhqd.js +0 -55
  251. package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
  252. package/dist/landing/index.html +0 -15
  253. package/dist/landing/landing.d.ts +0 -3
  254. package/dist/landing/landing.js +0 -12
  255. package/dist/landing/landing.js.map +0 -1
  256. package/docs/commercial/README.md +0 -29
  257. package/docs/commercial/data-inventory.md +0 -36
  258. package/docs/commercial/manual-invoicing.md +0 -44
  259. package/docs/commercial/metrics.md +0 -51
  260. package/docs/commercial/pricing.md +0 -103
  261. package/docs/commercial/privacy-policy-draft.md +0 -41
  262. package/docs/commercial/terms-topics-for-counsel.md +0 -51
  263. package/docs/design-partners/README.md +0 -52
  264. package/docs/design-partners/demo-narratives.md +0 -33
  265. package/docs/design-partners/design-partner-offer.md +0 -39
  266. package/docs/design-partners/discovery-questions.md +0 -28
  267. package/docs/design-partners/feedback-capture-template.md +0 -48
  268. package/docs/design-partners/icp-one-pager.md +0 -43
  269. package/docs/design-partners/onboarding-checklist.md +0 -33
  270. package/docs/design-partners/pilot-success-criteria.md +0 -27
  271. package/docs/design-partners/pricing-interview-script.md +0 -18
  272. package/docs/design-partners/telemetry-plan.md +0 -48
  273. package/docs/development/baseline.md +0 -114
  274. package/docs/development/onboarding-smoke.md +0 -37
  275. package/docs/development/verification-0.1.8.md +0 -25
  276. package/docs/enterprise-product-roadmap-ru.md +0 -1879
  277. package/docs/fundraising/README.md +0 -87
  278. package/docs/fundraising/data-room-checklist.md +0 -72
  279. package/docs/fundraising/demo-script.md +0 -52
  280. package/docs/fundraising/design-partners-50.md +0 -74
  281. package/docs/fundraising/launch-posts.md +0 -37
  282. package/docs/fundraising/metrics-and-evidence.md +0 -30
  283. package/docs/fundraising/roadmap-12-month.md +0 -25
  284. package/docs/fundraising/technical-due-diligence-index.md +0 -37
  285. package/docs/fundraising/yc-application.md +0 -25
  286. package/docs/hosted-pilot.md +0 -111
  287. package/docs/operations/disaster-recovery-runbook.md +0 -81
  288. package/docs/operations/kill-switch-runbook.md +0 -32
  289. package/docs/release-checklist.md +0 -83
  290. package/docs/release-migration-and-rollback.md +0 -63
  291. package/docs/release-readiness.md +0 -39
  292. package/docs/team-control-plane.md +0 -72
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, mkdir, readFile, realpath, rm } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
4
4
  import { getDataPaths } from "../config/dataPaths.js";
5
5
  import { runEgressCommand } from "../egress/run.js";
6
- import { generateEvidenceReport, loadEvidenceReport } from "../evidence/index.js";
6
+ import { generateEvidenceReport, loadEvidenceReport, } from "../evidence/index.js";
7
7
  import { sanitizeSecretString } from "../security/secrets.js";
8
- import { atomicWriteJson, ensurePrivateDirectory } from "../storage/fileStore.js";
8
+ import { atomicWriteJson, ensurePrivateDirectory, } from "../storage/fileStore.js";
9
9
  const MAX_SPEC_BYTES = 128 * 1024;
10
10
  const MAX_REPORT_BYTES = 512 * 1024;
11
11
  const MAX_ITEMS = 100;
@@ -18,34 +18,88 @@ export class EvalError extends Error {
18
18
  }
19
19
  export const builtinEvalTemplates = {
20
20
  "retry-after": template("retry-after", "Retry honors Retry-After", [
21
- { id: "rate-limit-scenario", type: "scenario-completed", value: "stripe.rate_limited", points: 40 },
21
+ {
22
+ id: "rate-limit-scenario",
23
+ type: "scenario-completed",
24
+ value: "stripe.rate_limited",
25
+ points: 40,
26
+ },
22
27
  { id: "retry-observed", type: "retry-observed", points: 30 },
23
28
  { id: "no-production-egress", type: "no-production-egress", points: 20 },
24
- { id: "run-finished", type: "run-finished", points: 10 }
25
- ], [{ id: "production-egress", type: "production-egress" }, { id: "secret-leak", type: "secret-leak" }], [{ id: "stripe.rate_limited", type: "rate-limit", provider: "stripe", statusCode: 429, retryAfterMs: 2000 }], ["stripe.rate_limited"]),
29
+ { id: "run-finished", type: "run-finished", points: 10 },
30
+ ], [
31
+ { id: "production-egress", type: "production-egress" },
32
+ { id: "secret-leak", type: "secret-leak" },
33
+ ], [
34
+ {
35
+ id: "stripe.rate_limited",
36
+ type: "rate-limit",
37
+ provider: "stripe",
38
+ statusCode: 429,
39
+ retryAfterMs: 2000,
40
+ },
41
+ ], ["stripe.rate_limited"]),
26
42
  "duplicate-payment": template("duplicate-payment", "Duplicate payment prevention", [
27
- { id: "duplicate-payment-scenario", type: "scenario-completed", value: "stripe.duplicate_payment", points: 50 },
28
- { id: "stripe-covered", type: "provider-covered", value: "stripe", points: 20 },
29
- { id: "no-production-egress", type: "no-production-egress", points: 30 }
30
- ], [{ id: "production-egress", type: "production-egress" }, { id: "provider-failure", type: "provider-failure" }], [{ id: "stripe.duplicate_payment", type: "timeout", provider: "stripe" }], ["stripe.duplicate_payment"]),
43
+ {
44
+ id: "duplicate-payment-scenario",
45
+ type: "scenario-completed",
46
+ value: "stripe.duplicate_payment",
47
+ points: 50,
48
+ },
49
+ {
50
+ id: "stripe-covered",
51
+ type: "provider-covered",
52
+ value: "stripe",
53
+ points: 20,
54
+ },
55
+ { id: "no-production-egress", type: "no-production-egress", points: 30 },
56
+ ], [
57
+ { id: "production-egress", type: "production-egress" },
58
+ { id: "provider-failure", type: "provider-failure" },
59
+ ], [{ id: "stripe.duplicate_payment", type: "timeout", provider: "stripe" }], ["stripe.duplicate_payment"]),
31
60
  "webhook-signature": template("webhook-signature", "Webhook signature validation", [
32
- { id: "signature-scenario", type: "scenario-completed", value: "stripe.webhook_signature_invalid", points: 50 },
61
+ {
62
+ id: "signature-scenario",
63
+ type: "scenario-completed",
64
+ value: "stripe.webhook_signature_invalid",
65
+ points: 50,
66
+ },
33
67
  { id: "no-secrets", type: "no-secrets", points: 25 },
34
- { id: "no-production-egress", type: "no-production-egress", points: 25 }
35
- ], [{ id: "production-egress", type: "production-egress" }, { id: "secret-leak", type: "secret-leak" }], [{ id: "stripe.webhook_signature_invalid", type: "invalid-webhook-signature", provider: "stripe" }], ["stripe.webhook_signature_invalid"]),
68
+ { id: "no-production-egress", type: "no-production-egress", points: 25 },
69
+ ], [
70
+ { id: "production-egress", type: "production-egress" },
71
+ { id: "secret-leak", type: "secret-leak" },
72
+ ], [
73
+ {
74
+ id: "stripe.webhook_signature_invalid",
75
+ type: "invalid-webhook-signature",
76
+ provider: "stripe",
77
+ },
78
+ ], ["stripe.webhook_signature_invalid"]),
36
79
  "no-secret-logs": template("no-secret-logs", "No secret in logs", [
37
80
  { id: "no-secrets", type: "no-secrets", points: 70 },
38
- { id: "evidence-passed", type: "evidence-passed", points: 30 }
39
- ], [{ id: "secret-leak", type: "secret-leak" }, { id: "production-egress", type: "production-egress" }], [], ["security.no_secret_logs"]),
81
+ { id: "evidence-passed", type: "evidence-passed", points: 30 },
82
+ ], [
83
+ { id: "secret-leak", type: "secret-leak" },
84
+ { id: "production-egress", type: "production-egress" },
85
+ ], [], ["security.no_secret_logs"]),
40
86
  "timeout-recovery": template("timeout-recovery", "Timeout recovery", [
41
- { id: "timeout-scenario", type: "scenario-completed", value: "provider.timeout", points: 40 },
87
+ {
88
+ id: "timeout-scenario",
89
+ type: "scenario-completed",
90
+ value: "provider.timeout",
91
+ points: 40,
92
+ },
42
93
  { id: "run-finished", type: "run-finished", points: 30 },
43
- { id: "no-production-egress", type: "no-production-egress", points: 30 }
94
+ { id: "no-production-egress", type: "no-production-egress", points: 30 },
44
95
  ], [{ id: "production-egress", type: "production-egress" }], [{ id: "provider.timeout", type: "timeout" }], ["provider.timeout"]),
45
96
  "no-production-bypass": template("no-production-bypass", "No production bypass", [
46
97
  { id: "no-production-egress", type: "no-production-egress", points: 80 },
47
- { id: "run-finished", type: "run-finished", points: 20 }
48
- ], [{ id: "production-egress", type: "production-egress" }, { id: "secret-leak", type: "secret-leak" }], [], ["security.no_production_bypass"])
98
+ { id: "run-finished", type: "run-finished", points: 20 },
99
+ ], [
100
+ { id: "production-egress", type: "production-egress" },
101
+ { id: "secret-leak", type: "secret-leak" },
102
+ ], [], ["security.no_production_bypass"]),
49
103
  };
50
104
  export async function loadEvalSpec(path, projectRoot = process.cwd()) {
51
105
  const resolved = await resolveExistingPath(path, projectRoot);
@@ -72,8 +126,16 @@ export async function runEval(options = {}) {
72
126
  if (spec.injectedFailures.length > 0) {
73
127
  throw new EvalError("Eval specs with injectedFailures require --evidence until GhostAPI can apply the declared failures to an isolated synthetic world.");
74
128
  }
75
- const run = await runEgressCommand({ command: spec.task.command, allowHosts: [], timeoutMs: spec.limits.timeoutMs, maxOutputBytes: spec.limits.maxOutputBytes });
76
- const generated = await generateEvidenceReport({ runPath: run.evidencePath, generatedAt: options.generatedAt });
129
+ const run = await runEgressCommand({
130
+ command: spec.task.command,
131
+ allowHosts: [],
132
+ timeoutMs: spec.limits.timeoutMs,
133
+ maxOutputBytes: spec.limits.maxOutputBytes,
134
+ });
135
+ const generated = await generateEvidenceReport({
136
+ runPath: run.evidencePath,
137
+ generatedAt: options.generatedAt,
138
+ });
77
139
  evidencePath = generated.path;
78
140
  evidence = generated.report;
79
141
  }
@@ -81,32 +143,85 @@ export async function runEval(options = {}) {
81
143
  evidencePath = options.evidencePath;
82
144
  evidence = await loadEvidenceReport(evidencePath, projectRoot);
83
145
  }
84
- const report = scoreEval(spec, evidence, { evidencePath, generatedAt: options.generatedAt ?? new Date().toISOString() });
146
+ const report = scoreEval(spec, evidence, {
147
+ evidencePath,
148
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
149
+ });
85
150
  const path = await writeEvalReport(report, projectRoot, options.outPath);
86
151
  return { report, path };
87
152
  }
88
153
  export function scoreEval(spec, evidence, options = {}) {
89
154
  const normalizedSpec = normalizeEvalSpec(spec);
90
- const forbiddenTriggered = normalizedSpec.forbidden.filter((forbidden) => isForbiddenTriggered(forbidden, evidence)).map((forbidden) => forbidden.id).sort();
91
- const components = normalizedSpec.rubric.components.map((component) => {
155
+ const forbiddenTriggered = normalizedSpec.forbidden
156
+ .filter((forbidden) => isForbiddenTriggered(forbidden, evidence))
157
+ .map((forbidden) => forbidden.id)
158
+ .sort();
159
+ const components = normalizedSpec.rubric.components
160
+ .map((component) => {
92
161
  const expectation = normalizedSpec.expectations.find((candidate) => candidate.id === component.expectationId);
93
162
  const passed = expectation !== undefined && evaluateExpectation(expectation, evidence);
94
- return { id: component.id, expectationId: component.expectationId, passed, points: passed ? component.points : 0, maxPoints: component.points, reason: sanitizedText(passed ? component.reason : failedReason(expectation, evidence)) };
95
- }).sort((left, right) => left.id.localeCompare(right.id));
163
+ return {
164
+ id: component.id,
165
+ expectationId: component.expectationId,
166
+ passed,
167
+ points: passed ? component.points : 0,
168
+ maxPoints: component.points,
169
+ reason: sanitizedText(passed ? component.reason : failedReason(expectation, evidence)),
170
+ };
171
+ })
172
+ .sort((left, right) => left.id.localeCompare(right.id));
96
173
  const rawScore = components.reduce((sum, component) => sum + component.points, 0);
97
- const core = forbiddenTriggered.length > 0 ? 0 : Math.min(normalizedSpec.rubric.maxScore, rawScore);
174
+ const core = forbiddenTriggered.length > 0
175
+ ? 0
176
+ : Math.min(normalizedSpec.rubric.maxScore, rawScore);
98
177
  const reportWithoutHash = {
99
178
  schemaVersion: 1,
100
- artifact: { generatedAt: options.generatedAt ?? new Date().toISOString(), logicalHash: "", canonicalization: "json-stable-sorted-keys-v1" },
101
- eval: { id: normalizedSpec.id, title: sanitizedText(normalizedSpec.title), specHash: hashStable(normalizedSpec) },
102
- evidence: { path: options.evidencePath === undefined ? undefined : sanitizedText(options.evidencePath), logicalHash: evidence.artifact.logicalHash, runId: evidence.run.id, links: options.evidencePath === undefined ? [] : [sanitizedText(options.evidencePath)] },
103
- score: { core, max: normalizedSpec.rubric.maxScore, passed: forbiddenTriggered.length === 0 && core >= normalizedSpec.rubric.maxScore, forbiddenTriggered, components },
104
- repeatability: { deterministicInputs: true, evidenceLogicalHash: evidence.artifact.logicalHash, evalLogicalHash: "", notes: ["Core score uses deterministic evidence only; LLM-as-judge is not part of the security score."] },
105
- judge: { used: false, reason: "LLM-as-judge is optional and disabled for core security scoring." },
106
- warnings: evidence.warnings.map(sanitizedText)
179
+ artifact: {
180
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
181
+ logicalHash: "",
182
+ canonicalization: "json-stable-sorted-keys-v1",
183
+ },
184
+ eval: {
185
+ id: normalizedSpec.id,
186
+ title: sanitizedText(normalizedSpec.title),
187
+ specHash: hashStable(normalizedSpec),
188
+ },
189
+ evidence: {
190
+ path: options.evidencePath === undefined
191
+ ? undefined
192
+ : sanitizedText(options.evidencePath),
193
+ logicalHash: evidence.artifact.logicalHash,
194
+ runId: evidence.run.id,
195
+ links: options.evidencePath === undefined
196
+ ? []
197
+ : [sanitizedText(options.evidencePath)],
198
+ },
199
+ score: {
200
+ core,
201
+ max: normalizedSpec.rubric.maxScore,
202
+ passed: forbiddenTriggered.length === 0 &&
203
+ core >= normalizedSpec.rubric.maxScore,
204
+ forbiddenTriggered,
205
+ components,
206
+ },
207
+ repeatability: {
208
+ deterministicInputs: true,
209
+ evidenceLogicalHash: evidence.artifact.logicalHash,
210
+ evalLogicalHash: "",
211
+ notes: [
212
+ "Core score uses deterministic evidence only; LLM-as-judge is not part of the security score.",
213
+ ],
214
+ },
215
+ judge: {
216
+ used: false,
217
+ reason: "LLM-as-judge is optional and disabled for core security scoring.",
218
+ },
219
+ warnings: evidence.warnings.map(sanitizedText),
107
220
  };
108
- reportWithoutHash.repeatability.evalLogicalHash = hashLogicalReport(reportWithoutHash);
109
- reportWithoutHash.artifact.logicalHash = reportWithoutHash.repeatability.evalLogicalHash;
221
+ reportWithoutHash.repeatability.evalLogicalHash =
222
+ hashLogicalReport(reportWithoutHash);
223
+ reportWithoutHash.artifact.logicalHash =
224
+ reportWithoutHash.repeatability.evalLogicalHash;
110
225
  return reportWithoutHash;
111
226
  }
112
227
  export function formatEvalReport(report) {
@@ -120,7 +235,7 @@ export function formatEvalReport(report) {
120
235
  "Components:",
121
236
  ...report.score.components.map((component) => ` ${component.passed ? "PASS" : "FAIL"} ${escapeTerminal(component.id)}: ${component.points}/${component.maxPoints} - ${escapeTerminal(component.reason)}`),
122
237
  `Judge: ${report.judge.used ? "used" : "not used"} - ${escapeTerminal(report.judge.reason)}`,
123
- ...report.warnings.map((warning) => ` WARNING eval: ${escapeTerminal(warning)}`)
238
+ ...report.warnings.map((warning) => ` WARNING eval: ${escapeTerminal(warning)}`),
124
239
  ].join("\n");
125
240
  }
126
241
  function evaluateExpectation(expectation, evidence) {
@@ -129,9 +244,11 @@ function evaluateExpectation(expectation, evidence) {
129
244
  if (expectation.type === "run-exit-code")
130
245
  return evidence.run.exitCode === expectation.value;
131
246
  if (expectation.type === "scenario-completed")
132
- return typeof expectation.value === "string" && evidence.coverage.scenarios.includes(expectation.value);
247
+ return (typeof expectation.value === "string" &&
248
+ evidence.coverage.scenarios.includes(expectation.value));
133
249
  if (expectation.type === "provider-covered")
134
- return typeof expectation.value === "string" && evidence.coverage.providers.includes(expectation.value);
250
+ return (typeof expectation.value === "string" &&
251
+ evidence.coverage.providers.includes(expectation.value));
135
252
  if (expectation.type === "retry-observed")
136
253
  return hasObservedRetry(evidence);
137
254
  if (expectation.type === "no-provider-failures")
@@ -139,17 +256,20 @@ function evaluateExpectation(expectation, evidence) {
139
256
  if (expectation.type === "no-secrets")
140
257
  return evidence.secrets.matches === 0;
141
258
  if (expectation.type === "no-production-egress")
142
- return hasEnforcedCompletedRun(evidence) && evidence.egress.productionAttempts === 0;
259
+ return (hasEnforcedCompletedRun(evidence) &&
260
+ evidence.egress.productionAttempts === 0);
143
261
  return evidence.summary.passed;
144
262
  }
145
263
  function isForbiddenTriggered(forbidden, evidence) {
146
264
  if (forbidden.type === "production-egress")
147
- return evidence.egress.productionAttempts > 0;
265
+ return (typeof evidence.egress.productionAttempts === "number" &&
266
+ evidence.egress.productionAttempts > 0);
148
267
  if (forbidden.type === "secret-leak")
149
268
  return evidence.secrets.matches > 0;
150
269
  if (forbidden.type === "provider-failure")
151
270
  return evidence.retriesAndFailures.failureCount > 0;
152
- return typeof forbidden.value === "string" && !evidence.coverage.scenarios.includes(forbidden.value);
271
+ return (typeof forbidden.value === "string" &&
272
+ !evidence.coverage.scenarios.includes(forbidden.value));
153
273
  }
154
274
  function failedReason(expectation, evidence) {
155
275
  if (expectation === undefined)
@@ -168,14 +288,32 @@ function failedReason(expectation, evidence) {
168
288
  return `${evidence.retriesAndFailures.failureCount} provider failure(s) were observed.`;
169
289
  if (expectation.type === "no-secrets")
170
290
  return `${evidence.secrets.matches} secret-shaped value(s) were detected.`;
171
- if (expectation.type === "no-production-egress")
172
- return hasEnforcedCompletedRun(evidence) ? `${evidence.egress.productionAttempts} production egress attempt(s) were observed.` : "A completed Linux namespace run is required before zero production egress can be credited.";
291
+ if (expectation.type === "no-production-egress") {
292
+ if (!hasEnforcedCompletedRun(evidence))
293
+ return "A completed Linux namespace run is required before production-egress evidence can be considered.";
294
+ if (evidence.egress.productionAttempts === null)
295
+ return "Production egress attempts were not measured by this evidence backend.";
296
+ return `${evidence.egress.productionAttempts} production egress attempt(s) were observed.`;
297
+ }
173
298
  return "Evidence report did not pass.";
174
299
  }
175
300
  function normalizeEvalSpec(value) {
176
301
  if (!isPlainObject(value))
177
302
  throw new EvalError("Eval spec must be an object.");
178
- assertKeys(value, ["schemaVersion", "kind", "id", "title", "syntheticWorld", "task", "injectedFailures", "expectations", "forbidden", "limits", "rubric", "judge"], "eval spec");
303
+ assertKeys(value, [
304
+ "schemaVersion",
305
+ "kind",
306
+ "id",
307
+ "title",
308
+ "syntheticWorld",
309
+ "task",
310
+ "injectedFailures",
311
+ "expectations",
312
+ "forbidden",
313
+ "limits",
314
+ "rubric",
315
+ "judge",
316
+ ], "eval spec");
179
317
  const spec = value;
180
318
  if (spec.schemaVersion !== 1 || spec.kind !== "ghostapi.eval")
181
319
  throw new EvalError("Unsupported eval spec schema.");
@@ -207,7 +345,15 @@ function normalizeWorldReference(value) {
207
345
  if (!isPlainObject(value))
208
346
  throw new EvalError("syntheticWorld.world must be an object.");
209
347
  assertKeys(value, ["id", "version", "seed"], "syntheticWorld.world");
210
- if (typeof value.id !== "string" || !IDENTIFIER.test(value.id) || typeof value.version !== "string" || !/^\d+\.\d+\.\d+$/.test(value.version) || typeof value.seed !== "string" || value.seed.trim() === "" || value.seed.length > 128 || /[\r\n\t]/.test(value.seed) || sanitizeSecretString(value.seed) !== value.seed) {
348
+ if (typeof value.id !== "string" ||
349
+ !IDENTIFIER.test(value.id) ||
350
+ typeof value.version !== "string" ||
351
+ !/^\d+\.\d+\.\d+$/.test(value.version) ||
352
+ typeof value.seed !== "string" ||
353
+ value.seed.trim() === "" ||
354
+ value.seed.length > 128 ||
355
+ /[\r\n\t]/.test(value.seed) ||
356
+ sanitizeSecretString(value.seed) !== value.seed) {
211
357
  throw new EvalError("syntheticWorld.world must reference a safe world id, semantic version and non-secret seed.");
212
358
  }
213
359
  }
@@ -217,7 +363,10 @@ function normalizeTask(value) {
217
363
  assertKeys(value, ["description", "command"], "task");
218
364
  if (typeof value.description !== "string")
219
365
  throw new EvalError("task.description must be a string.");
220
- if (!Array.isArray(value.command) || value.command.length === 0 || value.command.length > 64 || !value.command.every((entry) => typeof entry === "string" && entry.trim() !== ""))
366
+ if (!Array.isArray(value.command) ||
367
+ value.command.length === 0 ||
368
+ value.command.length > 64 ||
369
+ !value.command.every((entry) => typeof entry === "string" && entry.trim() !== ""))
221
370
  throw new EvalError("task.command must be a non-empty string array.");
222
371
  }
223
372
  function normalizeInjectedFailures(values) {
@@ -229,16 +378,25 @@ function normalizeInjectedFailures(values) {
229
378
  assertKeys(value, ["id", "type", "provider", "statusCode", "retryAfterMs"], "injected failure");
230
379
  if (!IDENTIFIER.test(value.id) || !IDENTIFIER.test(value.type))
231
380
  throw new EvalError("injected failure id/type is invalid.");
232
- if (value.provider !== undefined && (typeof value.provider !== "string" || !IDENTIFIER.test(value.provider)))
381
+ if (value.provider !== undefined &&
382
+ (typeof value.provider !== "string" || !IDENTIFIER.test(value.provider)))
233
383
  throw new EvalError("injected failure provider is invalid.");
234
- if (value.statusCode !== undefined && (!Number.isInteger(value.statusCode) || value.statusCode < 400 || value.statusCode > 599))
384
+ if (value.statusCode !== undefined &&
385
+ (!Number.isInteger(value.statusCode) ||
386
+ value.statusCode < 400 ||
387
+ value.statusCode > 599))
235
388
  throw new EvalError("injected failure statusCode must be an error status.");
236
- if (value.retryAfterMs !== undefined && (!Number.isInteger(value.retryAfterMs) || value.retryAfterMs < 0 || value.retryAfterMs > 300_000))
389
+ if (value.retryAfterMs !== undefined &&
390
+ (!Number.isInteger(value.retryAfterMs) ||
391
+ value.retryAfterMs < 0 ||
392
+ value.retryAfterMs > 300_000))
237
393
  throw new EvalError("injected failure retryAfterMs is invalid.");
238
394
  }
239
395
  }
240
396
  function normalizeExpectations(values) {
241
- if (!Array.isArray(values) || values.length === 0 || values.length > MAX_ITEMS)
397
+ if (!Array.isArray(values) ||
398
+ values.length === 0 ||
399
+ values.length > MAX_ITEMS)
242
400
  throw new EvalError("expectations must be a non-empty bounded array.");
243
401
  const ids = new Set();
244
402
  for (const value of values) {
@@ -248,11 +406,25 @@ function normalizeExpectations(values) {
248
406
  if (!IDENTIFIER.test(value.id) || ids.has(value.id))
249
407
  throw new EvalError("expectation id must be unique and stable.");
250
408
  ids.add(value.id);
251
- if (!["run-finished", "run-exit-code", "scenario-completed", "provider-covered", "retry-observed", "no-provider-failures", "no-secrets", "no-production-egress", "evidence-passed"].includes(value.type))
409
+ if (![
410
+ "run-finished",
411
+ "run-exit-code",
412
+ "scenario-completed",
413
+ "provider-covered",
414
+ "retry-observed",
415
+ "no-provider-failures",
416
+ "no-secrets",
417
+ "no-production-egress",
418
+ "evidence-passed",
419
+ ].includes(value.type))
252
420
  throw new EvalError(`Unsupported expectation type: ${String(value.type)}`);
253
- if ((value.type === "run-exit-code" && !Number.isInteger(value.value)) || ((value.type === "scenario-completed" || value.type === "provider-covered") && (typeof value.value !== "string" || !IDENTIFIER.test(value.value))))
421
+ if ((value.type === "run-exit-code" && !Number.isInteger(value.value)) ||
422
+ ((value.type === "scenario-completed" ||
423
+ value.type === "provider-covered") &&
424
+ (typeof value.value !== "string" || !IDENTIFIER.test(value.value))))
254
425
  throw new EvalError(`Expectation ${value.id} has an invalid value.`);
255
- if (!["run-exit-code", "scenario-completed", "provider-covered"].includes(value.type) && value.value !== undefined)
426
+ if (!["run-exit-code", "scenario-completed", "provider-covered"].includes(value.type) &&
427
+ value.value !== undefined)
256
428
  throw new EvalError(`Expectation ${value.id} does not accept a value.`);
257
429
  }
258
430
  }
@@ -265,9 +437,15 @@ function normalizeForbidden(values) {
265
437
  assertKeys(value, ["id", "type", "value"], "forbidden action");
266
438
  if (!IDENTIFIER.test(value.id))
267
439
  throw new EvalError("forbidden action id is invalid.");
268
- if (!["production-egress", "secret-leak", "provider-failure", "missing-scenario"].includes(value.type))
440
+ if (![
441
+ "production-egress",
442
+ "secret-leak",
443
+ "provider-failure",
444
+ "missing-scenario",
445
+ ].includes(value.type))
269
446
  throw new EvalError(`Unsupported forbidden action type: ${String(value.type)}`);
270
- if (value.type === "missing-scenario" && (typeof value.value !== "string" || !IDENTIFIER.test(value.value)))
447
+ if (value.type === "missing-scenario" &&
448
+ (typeof value.value !== "string" || !IDENTIFIER.test(value.value)))
271
449
  throw new EvalError("missing-scenario forbidden actions require a scenario identifier.");
272
450
  if (value.type !== "missing-scenario" && value.value !== undefined)
273
451
  throw new EvalError(`${value.type} forbidden actions do not accept a value.`);
@@ -277,18 +455,26 @@ function normalizeLimits(value) {
277
455
  if (!isPlainObject(value))
278
456
  throw new EvalError("limits must be an object.");
279
457
  assertKeys(value, ["timeoutMs", "maxOutputBytes"], "limits");
280
- if (!Number.isInteger(value.timeoutMs) || value.timeoutMs < 100 || value.timeoutMs > 300_000)
458
+ if (!Number.isInteger(value.timeoutMs) ||
459
+ value.timeoutMs < 100 ||
460
+ value.timeoutMs > 300_000)
281
461
  throw new EvalError("limits.timeoutMs must be between 100 and 300000.");
282
- if (!Number.isInteger(value.maxOutputBytes) || value.maxOutputBytes < 0 || value.maxOutputBytes > 10 * 1024 * 1024)
462
+ if (!Number.isInteger(value.maxOutputBytes) ||
463
+ value.maxOutputBytes < 0 ||
464
+ value.maxOutputBytes > 10 * 1024 * 1024)
283
465
  throw new EvalError("limits.maxOutputBytes must be bounded.");
284
466
  }
285
467
  function normalizeRubric(value, expectations) {
286
468
  if (!isPlainObject(value))
287
469
  throw new EvalError("rubric must be an object.");
288
470
  assertKeys(value, ["maxScore", "components"], "rubric");
289
- if (!Number.isInteger(value.maxScore) || value.maxScore < 1 || value.maxScore > 1000)
471
+ if (!Number.isInteger(value.maxScore) ||
472
+ value.maxScore < 1 ||
473
+ value.maxScore > 1000)
290
474
  throw new EvalError("rubric.maxScore must be a positive bounded integer.");
291
- if (!Array.isArray(value.components) || value.components.length === 0 || value.components.length > MAX_ITEMS)
475
+ if (!Array.isArray(value.components) ||
476
+ value.components.length === 0 ||
477
+ value.components.length > MAX_ITEMS)
292
478
  throw new EvalError("rubric.components must be a non-empty bounded array.");
293
479
  const expectationIds = new Set(expectations.map((expectation) => expectation.id));
294
480
  const componentIds = new Set();
@@ -298,9 +484,14 @@ function normalizeRubric(value, expectations) {
298
484
  if (!isPlainObject(component))
299
485
  throw new EvalError("rubric component must be an object.");
300
486
  assertKeys(component, ["id", "expectationId", "points", "reason"], "rubric component");
301
- if (!IDENTIFIER.test(component.id) || componentIds.has(component.id) || !expectationIds.has(component.expectationId) || referencedExpectations.has(component.expectationId))
487
+ if (!IDENTIFIER.test(component.id) ||
488
+ componentIds.has(component.id) ||
489
+ !expectationIds.has(component.expectationId) ||
490
+ referencedExpectations.has(component.expectationId))
302
491
  throw new EvalError("rubric component id or expectation reference is duplicated or unknown.");
303
- if (!Number.isInteger(component.points) || component.points < 0 || component.points > value.maxScore)
492
+ if (!Number.isInteger(component.points) ||
493
+ component.points < 0 ||
494
+ component.points > value.maxScore)
304
495
  throw new EvalError("rubric component points are invalid.");
305
496
  if (typeof component.reason !== "string" || component.reason.trim() === "")
306
497
  throw new EvalError("rubric component reason is required.");
@@ -315,11 +506,18 @@ function hasObservedRetry(evidence) {
315
506
  return evidence.egress.allowedAttempts.some((attempt, index, attempts) => {
316
507
  if (attempt.statusCode !== 429 && attempt.statusCode < 500)
317
508
  return false;
318
- return attempts.slice(index + 1).some((later) => later.provider === attempt.provider && later.method === attempt.method && later.path === attempt.path);
509
+ return attempts
510
+ .slice(index + 1)
511
+ .some((later) => later.provider === attempt.provider &&
512
+ later.method === attempt.method &&
513
+ later.path === attempt.path);
319
514
  });
320
515
  }
321
516
  function hasEnforcedCompletedRun(evidence) {
322
- return evidence.run.status === "finished" && evidence.enforcement.mode === "linux-network-namespace" && evidence.enforcement.isolated && !evidence.enforcement.degraded;
517
+ return (evidence.run.status === "finished" &&
518
+ evidence.enforcement.mode === "linux-network-namespace" &&
519
+ evidence.enforcement.isolated &&
520
+ !evidence.enforcement.degraded);
323
521
  }
324
522
  function template(id, title, expectations, forbidden, failures, scenarios) {
325
523
  return {
@@ -328,13 +526,24 @@ function template(id, title, expectations, forbidden, failures, scenarios) {
328
526
  id,
329
527
  title,
330
528
  syntheticWorld: { providers: ["stripe"], scenarios },
331
- task: { description: `Run an agent workflow for ${title}.`, command: ["node", "agent-workflow.mjs"] },
529
+ task: {
530
+ description: `Run an agent workflow for ${title}.`,
531
+ command: ["node", "agent-workflow.mjs"],
532
+ },
332
533
  injectedFailures: failures,
333
534
  expectations,
334
535
  forbidden,
335
536
  limits: { timeoutMs: 60_000, maxOutputBytes: 512 * 1024 },
336
- rubric: { maxScore: 100, components: expectations.map((expectation) => ({ id: expectation.id, expectationId: expectation.id, points: expectation.points ?? 0, reason: `${expectation.type} satisfied.` })) },
337
- judge: { llmAsJudge: false }
537
+ rubric: {
538
+ maxScore: 100,
539
+ components: expectations.map((expectation) => ({
540
+ id: expectation.id,
541
+ expectationId: expectation.id,
542
+ points: expectation.points ?? 0,
543
+ reason: `${expectation.type} satisfied.`,
544
+ })),
545
+ },
546
+ judge: { llmAsJudge: false },
338
547
  };
339
548
  }
340
549
  function readBuiltinTemplate(name) {
@@ -344,7 +553,9 @@ function readBuiltinTemplate(name) {
344
553
  return template;
345
554
  }
346
555
  async function writeEvalReport(report, projectRoot, outPath) {
347
- const target = outPath === undefined ? join(getDataPaths().reports, `${report.eval.id}.eval.json`) : await resolveOutputPath(outPath, projectRoot);
556
+ const target = outPath === undefined
557
+ ? join(getDataPaths().reports, `${report.eval.id}.eval.json`)
558
+ : await resolveOutputPath(outPath, projectRoot);
348
559
  await ensurePrivateDirectory(dirname(target));
349
560
  await atomicWriteJson(target, report);
350
561
  const source = await readFile(target, "utf8");
@@ -355,13 +566,15 @@ async function writeEvalReport(report, projectRoot, outPath) {
355
566
  return target;
356
567
  }
357
568
  async function resolveOutputPath(outPath, projectRoot) {
358
- const target = isAbsolute(outPath) ? resolve(outPath) : resolve(projectRoot, outPath);
569
+ const target = isAbsolute(outPath)
570
+ ? resolve(outPath)
571
+ : resolve(projectRoot, outPath);
359
572
  assertPathInsideAllowedRoots(projectRoot, target);
360
573
  await mkdir(dirname(target), { recursive: true, mode: 0o700 });
361
574
  const parent = await lstat(dirname(target));
362
575
  if (!parent.isDirectory() || parent.isSymbolicLink())
363
576
  throw new EvalError("Eval output parent must be a real directory, not a symlink.");
364
- if (!await isRealPathInsideAllowedRoots(dirname(target), projectRoot))
577
+ if (!(await isRealPathInsideAllowedRoots(dirname(target), projectRoot)))
365
578
  throw new EvalError("Eval output parent resolves outside the project root or GHOSTAPI_DATA_DIR through a symlink.");
366
579
  const existing = await lstat(target).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
367
580
  if (existing?.isSymbolicLink())
@@ -374,7 +587,7 @@ async function resolveExistingPath(path, projectRoot) {
374
587
  const info = await lstat(target);
375
588
  if (!info.isFile() || info.isSymbolicLink())
376
589
  throw new EvalError("Eval input path must be a regular non-symlink file.");
377
- if (!await isRealPathInsideAllowedRoots(target, projectRoot))
590
+ if (!(await isRealPathInsideAllowedRoots(target, projectRoot)))
378
591
  throw new EvalError("Eval input resolves outside the project root or GHOSTAPI_DATA_DIR through a symlink.");
379
592
  return target;
380
593
  }
@@ -398,16 +611,21 @@ function assertPathInsideAllowedRoots(projectRoot, target) {
398
611
  }
399
612
  function isInside(root, target) {
400
613
  const relativePath = relative(resolve(root), target);
401
- return relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
614
+ return (relativePath !== ".." &&
615
+ !relativePath.startsWith(`..${sep}`) &&
616
+ !isAbsolute(relativePath));
402
617
  }
403
618
  async function isRealPathInsideAllowedRoots(target, projectRoot) {
404
619
  const realTarget = await realpath(target);
405
620
  const realProjectRoot = await realpath(projectRoot);
406
621
  const dataRoot = await realpath(getDataPaths().root).catch(() => null);
407
- return isInside(realProjectRoot, realTarget) || (dataRoot !== null && isInside(dataRoot, realTarget));
622
+ return (isInside(realProjectRoot, realTarget) ||
623
+ (dataRoot !== null && isInside(dataRoot, realTarget)));
408
624
  }
409
625
  function readIdentifierArray(value, label) {
410
- if (!Array.isArray(value) || value.length > MAX_ITEMS || !value.every((entry) => typeof entry === "string" && IDENTIFIER.test(entry)))
626
+ if (!Array.isArray(value) ||
627
+ value.length > MAX_ITEMS ||
628
+ !value.every((entry) => typeof entry === "string" && IDENTIFIER.test(entry)))
411
629
  throw new EvalError(`${label} must be a bounded array of identifiers.`);
412
630
  }
413
631
  function assertKeys(value, allowed, label) {
@@ -441,7 +659,9 @@ function hashLogicalReport(report) {
441
659
  return hashStable(logical);
442
660
  }
443
661
  function hashStable(value) {
444
- return createHash("sha256").update(stableStringify(value), "utf8").digest("hex");
662
+ return createHash("sha256")
663
+ .update(stableStringify(value), "utf8")
664
+ .digest("hex");
445
665
  }
446
666
  function stableStringify(value) {
447
667
  if (value === undefined)
@@ -450,15 +670,21 @@ function stableStringify(value) {
450
670
  return JSON.stringify(value);
451
671
  if (Array.isArray(value))
452
672
  return `[${value.map(stableStringify).join(",")}]`;
453
- const entries = Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right));
673
+ const entries = Object.entries(value)
674
+ .filter(([, entry]) => entry !== undefined)
675
+ .sort(([left], [right]) => left.localeCompare(right));
454
676
  return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableStringify(entry)}`).join(",")}}`;
455
677
  }
456
678
  function sanitizedText(value) {
457
679
  const sanitized = sanitizeSecretString(value).replace(/[\r\n\t]/g, " ");
458
- return sanitized.length > 2_000 ? `${sanitized.slice(0, 2_000)}[truncated]` : sanitized;
680
+ return sanitized.length > 2_000
681
+ ? `${sanitized.slice(0, 2_000)}[truncated]`
682
+ : sanitized;
459
683
  }
460
684
  function escapeTerminal(value) {
461
- return sanitizedText(value).replace(/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g, "").replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
685
+ return sanitizedText(value)
686
+ .replace(/[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g, "")
687
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
462
688
  }
463
689
  function isErrorCode(error, code) {
464
690
  return error instanceof Error && "code" in error && error.code === code;