@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.
- package/CHANGELOG.md +21 -0
- package/CONTRIBUTING.md +132 -119
- package/LICENSE +21 -21
- package/PROVENANCE.md +13 -0
- package/README.md +78 -84
- package/SECURITY.md +56 -53
- package/dist/actions/gateway.js +320 -72
- package/dist/actions/gateway.js.map +1 -1
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/index.js +1 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/agents/agentPrompt.js +7 -5
- package/dist/agents/agentPrompt.js.map +1 -1
- package/dist/ai/aiClient.js +5 -5
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.js +66 -26
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/genericInference.js +151 -36
- package/dist/ai/genericInference.js.map +1 -1
- package/dist/ai/prompts.js +55 -49
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.js +649 -133
- package/dist/approvals/approvalInbox.js.map +1 -1
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +1 -1
- package/dist/approvals/index.js.map +1 -1
- package/dist/behavior/behaviorStore.js +34 -14
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.js +14 -5
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +226 -93
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.js +17 -3
- package/dist/cli/openUrl.js.map +1 -1
- package/dist/cli/parser.js +79 -18
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.js +4 -2
- package/dist/config/dataPaths.js.map +1 -1
- package/dist/config/localConfig.js +5 -2
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.js +11 -5
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.js +311 -83
- package/dist/contracts/contract.js.map +1 -1
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/credentials/credentialBroker.js +391 -79
- package/dist/credentials/credentialBroker.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -2
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/index.js.map +1 -1
- package/dist/dashboard/app.js +823 -685
- package/dist/dashboard/dashboard.js.map +1 -1
- package/dist/dashboard/index.html +704 -267
- package/dist/dashboard/styles.css +1040 -349
- package/dist/egress/capabilities.js +55 -25
- package/dist/egress/capabilities.js.map +1 -1
- package/dist/egress/linuxBootstrap.js +47 -11
- package/dist/egress/linuxBootstrap.js.map +1 -1
- package/dist/egress/run.js +170 -31
- package/dist/egress/run.js.map +1 -1
- package/dist/errors/errorEngine.js +15 -3
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/errors/providerErrors.js.map +1 -1
- package/dist/evals/evals.js +304 -78
- package/dist/evals/evals.js.map +1 -1
- package/dist/evals/index.d.ts +2 -2
- package/dist/evals/index.js +1 -1
- package/dist/evals/index.js.map +1 -1
- package/dist/evidence/index.d.ts +2 -2
- package/dist/evidence/index.js +1 -1
- package/dist/evidence/index.js.map +1 -1
- package/dist/evidence/report.d.ts +1 -1
- package/dist/evidence/report.js +263 -79
- package/dist/evidence/report.js.map +1 -1
- package/dist/fault/faultLab.js +24 -9
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +42 -42
- package/dist/index.js +21 -21
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +1 -1
- package/dist/inventory/analysis.js +174 -50
- package/dist/inventory/analysis.js.map +1 -1
- package/dist/inventory/index.d.ts +4 -4
- package/dist/inventory/index.js +3 -3
- package/dist/inventory/index.js.map +1 -1
- package/dist/inventory/inventoryController.js +362 -115
- package/dist/inventory/inventoryController.js.map +1 -1
- package/dist/inventory/types.js +680 -140
- package/dist/inventory/types.js.map +1 -1
- package/dist/ledger/actionLedger.js +355 -78
- package/dist/ledger/actionLedger.js.map +1 -1
- package/dist/ledger/index.d.ts +2 -2
- package/dist/ledger/index.js +1 -1
- package/dist/ledger/index.js.map +1 -1
- package/dist/mcp/server.js +26 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +2 -2
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/policy.js +112 -32
- package/dist/policy/policy.js.map +1 -1
- package/dist/productTelemetry/index.js +71 -20
- package/dist/productTelemetry/index.js.map +1 -1
- package/dist/providers/conformance.js +10 -5
- package/dist/providers/conformance.js.map +1 -1
- package/dist/providers/discord.js +2 -2
- package/dist/providers/generic.js +3 -3
- package/dist/providers/github.js +2 -2
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.js +3 -3
- package/dist/providers/packs/resendPack.js +51 -25
- package/dist/providers/packs/resendPack.js.map +1 -1
- package/dist/providers/packs/stripePack.js +590 -121
- package/dist/providers/packs/stripePack.js.map +1 -1
- package/dist/providers/registry.js +4 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/runtime.js +7 -6
- package/dist/providers/runtime.js.map +1 -1
- package/dist/providers/stripeWebhook.js +39 -10
- package/dist/providers/stripeWebhook.js.map +1 -1
- package/dist/providers/twilio.js +2 -2
- package/dist/proxy/cacheKey.js +9 -2
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.js +22 -9
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +57 -22
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.js +4 -2
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.js +211 -49
- package/dist/reliability/costGovernance.js.map +1 -1
- package/dist/reliability/index.d.ts +8 -8
- package/dist/reliability/index.js +4 -4
- package/dist/reliability/index.js.map +1 -1
- package/dist/reliability/reconciliation.js +274 -65
- package/dist/reliability/reconciliation.js.map +1 -1
- package/dist/reliability/runtimeHealth.js +127 -35
- package/dist/reliability/runtimeHealth.js.map +1 -1
- package/dist/reliability/slo.js +257 -50
- package/dist/reliability/slo.js.map +1 -1
- package/dist/report/safetyReport.d.ts +6 -0
- package/dist/report/safetyReport.js +56 -12
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/rules/aiRules.js +5 -5
- package/dist/rules/aiRules.js.map +1 -1
- package/dist/safety/index.d.ts +2 -2
- package/dist/safety/index.js +1 -1
- package/dist/safety/index.js.map +1 -1
- package/dist/safety/safetyController.js +675 -165
- package/dist/safety/safetyController.js.map +1 -1
- package/dist/scenarios/scenarioBundle.js +204 -71
- package/dist/scenarios/scenarioBundle.js.map +1 -1
- package/dist/scenarios/scenarioStore.js +88 -29
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.js +52 -10
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +10 -5
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.js +37 -10
- package/dist/server/accessControl.js.map +1 -1
- package/dist/server/createServer.js +1 -1
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.js +15 -5
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +61 -19
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.js +1 -1
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/sdkDetector.js +7 -2
- package/dist/setup/sdkDetector.js.map +1 -1
- package/dist/setup/setupGenerator.js +136 -48
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateExtractor.js +3 -1
- package/dist/state/stateExtractor.js.map +1 -1
- package/dist/state/stateResolver.js +10 -7
- package/dist/state/stateResolver.js.map +1 -1
- package/dist/state/stateStore.js +13 -4
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.js +12 -4
- package/dist/storage/fileStore.js.map +1 -1
- package/dist/teamControl/controlPlane.js +846 -174
- package/dist/teamControl/controlPlane.js.map +1 -1
- package/dist/teamControl/deployment.js +19 -5
- package/dist/teamControl/deployment.js.map +1 -1
- package/dist/teamControl/index.d.ts +4 -4
- package/dist/teamControl/index.js +2 -2
- package/dist/teamControl/index.js.map +1 -1
- package/dist/tests/testGenerator.js +15 -7
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -2
- package/dist/trust/index.js +1 -1
- package/dist/trust/index.js.map +1 -1
- package/dist/trust/trustLadder.js +309 -63
- package/dist/trust/trustLadder.js.map +1 -1
- package/dist/worlds/index.d.ts +2 -2
- package/dist/worlds/index.js +1 -1
- package/dist/worlds/index.js.map +1 -1
- package/dist/worlds/worlds.js +364 -65
- package/dist/worlds/worlds.js.map +1 -1
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -49
- package/docs/github-actions.md +84 -84
- package/docs/mcp.md +58 -52
- package/docs/policy.md +63 -61
- package/docs/providers/authoring-packs.md +180 -175
- package/docs/providers/stripe-core-pack.md +84 -82
- package/docs/releases/README.md +13 -0
- package/docs/security/action-gateway-threat-model.md +34 -34
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -42
- package/docs/security/approval-inbox-threat-model.md +35 -35
- package/docs/security/credential-broker-threat-model.md +52 -52
- package/docs/security/egress-threat-model.md +126 -126
- package/docs/security/inventory-threat-model.md +104 -104
- package/docs/security/kill-switch-budgets-threat-model.md +37 -37
- package/docs/security/reliability-threat-model.md +111 -111
- package/docs/security/trust-ladder-threat-model.md +33 -33
- package/docs/telemetry.md +14 -0
- package/docs/usage.md +680 -571
- package/examples/README.md +10 -10
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -21
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -22
- package/examples/ci-smoke/package.json +9 -9
- package/examples/ci-smoke/production-egress.mjs +4 -2
- package/examples/ci-smoke/safe.mjs +21 -17
- package/examples/evals/README.md +24 -24
- package/examples/evals/retry-after.eval.json +71 -52
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +38 -35
- package/examples/openai-streaming/README.md +16 -16
- package/examples/openai-streaming/streaming-tool-call.mjs +44 -31
- package/examples/policy/ghostapi.policy.yaml +27 -27
- package/examples/record-replay/README.md +19 -19
- package/examples/record-replay/replay-requests.json +22 -22
- package/examples/record-replay/stripe-checkout.har.json +30 -30
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -35
- package/examples/stripe-node/checkout-flow.mjs +87 -61
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -19
- package/examples/worlds/subscription-recovery.mjs +21 -12
- package/package.json +28 -6
- package/dist/landing/assets/index-BXR41fAD.css +0 -1
- package/dist/landing/assets/index-CxWsopzP.css +0 -1
- package/dist/landing/assets/index-D8_ggMRE.js +0 -55
- package/dist/landing/assets/index-DZlpyhqd.js +0 -55
- package/dist/landing/assets/index-Dk-Xezs3.js +0 -55
- package/dist/landing/index.html +0 -15
- package/dist/landing/landing.d.ts +0 -3
- package/dist/landing/landing.js +0 -12
- package/dist/landing/landing.js.map +0 -1
- package/docs/commercial/README.md +0 -29
- package/docs/commercial/data-inventory.md +0 -36
- package/docs/commercial/manual-invoicing.md +0 -44
- package/docs/commercial/metrics.md +0 -51
- package/docs/commercial/pricing.md +0 -103
- package/docs/commercial/privacy-policy-draft.md +0 -41
- package/docs/commercial/terms-topics-for-counsel.md +0 -51
- package/docs/design-partners/README.md +0 -52
- package/docs/design-partners/demo-narratives.md +0 -33
- package/docs/design-partners/design-partner-offer.md +0 -39
- package/docs/design-partners/discovery-questions.md +0 -28
- package/docs/design-partners/feedback-capture-template.md +0 -48
- package/docs/design-partners/icp-one-pager.md +0 -43
- package/docs/design-partners/onboarding-checklist.md +0 -33
- package/docs/design-partners/pilot-success-criteria.md +0 -27
- package/docs/design-partners/pricing-interview-script.md +0 -18
- package/docs/design-partners/telemetry-plan.md +0 -48
- package/docs/development/baseline.md +0 -114
- package/docs/development/onboarding-smoke.md +0 -37
- package/docs/development/verification-0.1.8.md +0 -25
- package/docs/enterprise-product-roadmap-ru.md +0 -1879
- package/docs/fundraising/README.md +0 -87
- package/docs/fundraising/data-room-checklist.md +0 -72
- package/docs/fundraising/demo-script.md +0 -52
- package/docs/fundraising/design-partners-50.md +0 -74
- package/docs/fundraising/launch-posts.md +0 -37
- package/docs/fundraising/metrics-and-evidence.md +0 -30
- package/docs/fundraising/roadmap-12-month.md +0 -25
- package/docs/fundraising/technical-due-diligence-index.md +0 -37
- package/docs/fundraising/yc-application.md +0 -25
- package/docs/hosted-pilot.md +0 -111
- package/docs/operations/disaster-recovery-runbook.md +0 -81
- package/docs/operations/kill-switch-runbook.md +0 -32
- package/docs/release-checklist.md +0 -83
- package/docs/release-migration-and-rollback.md +0 -63
- package/docs/release-readiness.md +0 -39
- package/docs/team-control-plane.md +0 -72
package/dist/evals/evals.js
CHANGED
|
@@ -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
|
-
{
|
|
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
|
-
], [
|
|
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
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
{
|
|
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
|
-
], [
|
|
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
|
-
], [
|
|
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
|
-
{
|
|
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
|
-
], [
|
|
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({
|
|
76
|
-
|
|
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, {
|
|
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
|
|
91
|
-
|
|
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 {
|
|
95
|
-
|
|
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
|
|
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: {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 =
|
|
109
|
-
|
|
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" &&
|
|
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" &&
|
|
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) &&
|
|
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
|
|
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" &&
|
|
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
|
-
|
|
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, [
|
|
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" ||
|
|
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) ||
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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) ||
|
|
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 (![
|
|
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)) ||
|
|
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) &&
|
|
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 (![
|
|
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" &&
|
|
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) ||
|
|
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) ||
|
|
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) ||
|
|
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) ||
|
|
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) ||
|
|
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) ||
|
|
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
|
|
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" &&
|
|
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: {
|
|
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: {
|
|
337
|
-
|
|
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
|
|
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)
|
|
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 !== ".." &&
|
|
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) ||
|
|
622
|
+
return (isInside(realProjectRoot, realTarget) ||
|
|
623
|
+
(dataRoot !== null && isInside(dataRoot, realTarget)));
|
|
408
624
|
}
|
|
409
625
|
function readIdentifierArray(value, label) {
|
|
410
|
-
if (!Array.isArray(value) ||
|
|
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")
|
|
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)
|
|
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
|
|
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)
|
|
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;
|