@yiaany/ghostapi 0.1.6 → 0.1.8
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 +48 -2
- package/CONTRIBUTING.md +119 -117
- package/LICENSE +21 -21
- package/README.md +208 -185
- package/SECURITY.md +53 -37
- package/dist/actions/gateway.d.ts +179 -0
- package/dist/actions/gateway.js +514 -0
- package/dist/actions/gateway.js.map +1 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/ai/aiClient.js +4 -4
- package/dist/ai/aiClient.js.map +1 -1
- package/dist/ai/aiGenerator.d.ts +2 -2
- package/dist/ai/aiGenerator.js +21 -72
- package/dist/ai/aiGenerator.js.map +1 -1
- package/dist/ai/prompts.js +51 -51
- package/dist/ai/prompts.js.map +1 -1
- package/dist/approvals/approvalInbox.d.ts +150 -0
- package/dist/approvals/approvalInbox.js +412 -0
- package/dist/approvals/approvalInbox.js.map +1 -0
- package/dist/approvals/index.d.ts +2 -0
- package/dist/approvals/index.js +2 -0
- package/dist/approvals/index.js.map +1 -0
- package/dist/behavior/behaviorStore.d.ts +2 -0
- package/dist/behavior/behaviorStore.js +50 -46
- package/dist/behavior/behaviorStore.js.map +1 -1
- package/dist/cache/index.d.ts +2 -0
- package/dist/cache/index.js +58 -14
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/index.js +438 -58
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/openUrl.d.ts +10 -0
- package/dist/cli/openUrl.js +31 -0
- package/dist/cli/openUrl.js.map +1 -0
- package/dist/cli/parser.d.ts +155 -0
- package/dist/cli/parser.js +504 -2
- package/dist/cli/parser.js.map +1 -1
- package/dist/config/dataPaths.d.ts +31 -0
- package/dist/config/dataPaths.js +45 -0
- package/dist/config/dataPaths.js.map +1 -0
- package/dist/config/localConfig.d.ts +1 -2
- package/dist/config/localConfig.js +27 -21
- package/dist/config/localConfig.js.map +1 -1
- package/dist/config/serverConfig.d.ts +2 -0
- package/dist/config/serverConfig.js +8 -1
- package/dist/config/serverConfig.js.map +1 -1
- package/dist/contracts/contract.d.ts +76 -0
- package/dist/contracts/contract.js +603 -0
- package/dist/contracts/contract.js.map +1 -0
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/credentials/credentialBroker.d.ts +207 -0
- package/dist/credentials/credentialBroker.js +588 -0
- package/dist/credentials/credentialBroker.js.map +1 -0
- package/dist/credentials/index.d.ts +2 -0
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/dashboard/app.js +685 -615
- package/dist/dashboard/index.html +13 -7
- package/dist/dashboard/styles.css +349 -349
- package/dist/egress/capabilities.d.ts +32 -0
- package/dist/egress/capabilities.js +165 -0
- package/dist/egress/capabilities.js.map +1 -0
- package/dist/egress/linuxBootstrap.d.ts +1 -0
- package/dist/egress/linuxBootstrap.js +121 -0
- package/dist/egress/linuxBootstrap.js.map +1 -0
- package/dist/egress/run.d.ts +18 -0
- package/dist/egress/run.js +305 -0
- package/dist/egress/run.js.map +1 -0
- package/dist/errors/errorEngine.d.ts +2 -2
- package/dist/errors/errorEngine.js +3 -40
- package/dist/errors/errorEngine.js.map +1 -1
- package/dist/evals/evals.d.ts +126 -0
- package/dist/evals/evals.js +466 -0
- package/dist/evals/evals.js.map +1 -0
- package/dist/evals/index.d.ts +2 -0
- package/dist/evals/index.js +2 -0
- package/dist/evals/index.js.map +1 -0
- package/dist/evidence/index.d.ts +2 -0
- package/dist/evidence/index.js +2 -0
- package/dist/evidence/index.js.map +1 -0
- package/dist/evidence/report.d.ts +143 -0
- package/dist/evidence/report.js +556 -0
- package/dist/evidence/report.js.map +1 -0
- package/dist/fault/faultLab.d.ts +5 -4
- package/dist/fault/faultLab.js +66 -24
- package/dist/fault/faultLab.js.map +1 -1
- package/dist/index.d.ts +46 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/inventory/analysis.d.ts +12 -0
- package/dist/inventory/analysis.js +457 -0
- package/dist/inventory/analysis.js.map +1 -0
- package/dist/inventory/index.d.ts +5 -0
- package/dist/inventory/index.js +4 -0
- package/dist/inventory/index.js.map +1 -0
- package/dist/inventory/inventoryController.d.ts +53 -0
- package/dist/inventory/inventoryController.js +1044 -0
- package/dist/inventory/inventoryController.js.map +1 -0
- package/dist/inventory/types.d.ts +579 -0
- package/dist/inventory/types.js +787 -0
- package/dist/inventory/types.js.map +1 -0
- package/dist/landing/assets/index-CxWsopzP.css +1 -0
- package/dist/landing/assets/index-DZlpyhqd.js +55 -0
- package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
- package/dist/landing/index.html +15 -15
- package/dist/ledger/actionLedger.d.ts +125 -0
- package/dist/ledger/actionLedger.js +594 -0
- package/dist/ledger/actionLedger.js.map +1 -0
- package/dist/ledger/index.d.ts +2 -0
- package/dist/ledger/index.js +2 -0
- package/dist/ledger/index.js.map +1 -0
- package/dist/mcp/server.js +4 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/index.d.ts +3 -0
- package/dist/policy/index.js +2 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/policy.d.ts +14 -0
- package/dist/policy/policy.js +282 -0
- package/dist/policy/policy.js.map +1 -0
- package/dist/policy/types.d.ts +52 -0
- package/dist/policy/types.js +2 -0
- package/dist/policy/types.js.map +1 -0
- package/dist/productTelemetry/index.d.ts +16 -0
- package/dist/productTelemetry/index.js +142 -0
- package/dist/productTelemetry/index.js.map +1 -0
- package/dist/providers/conformance.d.ts +7 -0
- package/dist/providers/conformance.js +48 -0
- package/dist/providers/conformance.js.map +1 -0
- package/dist/providers/index.d.ts +4 -2
- package/dist/providers/index.js +3 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/packs/resendPack.d.ts +2 -0
- package/dist/providers/packs/resendPack.js +147 -0
- package/dist/providers/packs/resendPack.js.map +1 -0
- package/dist/providers/packs/stripePack.d.ts +2 -0
- package/dist/providers/packs/stripePack.js +852 -0
- package/dist/providers/packs/stripePack.js.map +1 -0
- package/dist/providers/registry.d.ts +7 -3
- package/dist/providers/registry.js +55 -4
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/resend.js +2 -11
- package/dist/providers/resend.js.map +1 -1
- package/dist/providers/runtime.d.ts +8 -0
- package/dist/providers/runtime.js +48 -0
- package/dist/providers/runtime.js.map +1 -0
- package/dist/providers/stripeWebhook.d.ts +13 -0
- package/dist/providers/stripeWebhook.js +50 -0
- package/dist/providers/stripeWebhook.js.map +1 -0
- package/dist/providers/types.d.ts +153 -0
- package/dist/proxy/cacheKey.js +3 -3
- package/dist/proxy/cacheKey.js.map +1 -1
- package/dist/proxy/providerDetector.d.ts +2 -7
- package/dist/proxy/providerDetector.js +4 -4
- package/dist/proxy/providerDetector.js.map +1 -1
- package/dist/proxy/proxyHandler.js +116 -12
- package/dist/proxy/proxyHandler.js.map +1 -1
- package/dist/proxy/requestNormalizer.d.ts +0 -1
- package/dist/proxy/requestNormalizer.js +2 -5
- package/dist/proxy/requestNormalizer.js.map +1 -1
- package/dist/reliability/costGovernance.d.ts +175 -0
- package/dist/reliability/costGovernance.js +515 -0
- package/dist/reliability/costGovernance.js.map +1 -0
- package/dist/reliability/index.d.ts +8 -0
- package/dist/reliability/index.js +5 -0
- package/dist/reliability/index.js.map +1 -0
- package/dist/reliability/reconciliation.d.ts +157 -0
- package/dist/reliability/reconciliation.js +522 -0
- package/dist/reliability/reconciliation.js.map +1 -0
- package/dist/reliability/runtimeHealth.d.ts +60 -0
- package/dist/reliability/runtimeHealth.js +356 -0
- package/dist/reliability/runtimeHealth.js.map +1 -0
- package/dist/reliability/slo.d.ts +131 -0
- package/dist/reliability/slo.js +375 -0
- package/dist/reliability/slo.js.map +1 -0
- package/dist/report/safetyReport.js +7 -3
- package/dist/report/safetyReport.js.map +1 -1
- package/dist/safety/index.d.ts +2 -0
- package/dist/safety/index.js +2 -0
- package/dist/safety/index.js.map +1 -0
- package/dist/safety/safetyController.d.ts +187 -0
- package/dist/safety/safetyController.js +477 -0
- package/dist/safety/safetyController.js.map +1 -0
- package/dist/scenarios/scenarioBundle.d.ts +75 -0
- package/dist/scenarios/scenarioBundle.js +750 -0
- package/dist/scenarios/scenarioBundle.js.map +1 -0
- package/dist/scenarios/scenarioStore.d.ts +1 -0
- package/dist/scenarios/scenarioStore.js +47 -32
- package/dist/scenarios/scenarioStore.js.map +1 -1
- package/dist/security/headerSanitizer.d.ts +2 -0
- package/dist/security/headerSanitizer.js +36 -1
- package/dist/security/headerSanitizer.js.map +1 -1
- package/dist/security/secrets.js +24 -4
- package/dist/security/secrets.js.map +1 -1
- package/dist/server/accessControl.d.ts +5 -0
- package/dist/server/accessControl.js +100 -0
- package/dist/server/accessControl.js.map +1 -0
- package/dist/server/createServer.d.ts +0 -5
- package/dist/server/createServer.js +9 -29
- package/dist/server/createServer.js.map +1 -1
- package/dist/server/eventsStore.d.ts +6 -1
- package/dist/server/eventsStore.js +83 -15
- package/dist/server/eventsStore.js.map +1 -1
- package/dist/server/routes.js +36 -10
- package/dist/server/routes.js.map +1 -1
- package/dist/server/sse.d.ts +2 -1
- package/dist/server/sse.js +15 -3
- package/dist/server/sse.js.map +1 -1
- package/dist/setup/setupGenerator.js +53 -6
- package/dist/setup/setupGenerator.js.map +1 -1
- package/dist/state/stateStore.d.ts +5 -0
- package/dist/state/stateStore.js +48 -62
- package/dist/state/stateStore.js.map +1 -1
- package/dist/storage/fileStore.d.ts +10 -0
- package/dist/storage/fileStore.js +131 -0
- package/dist/storage/fileStore.js.map +1 -0
- package/dist/teamControl/controlPlane.d.ts +255 -0
- package/dist/teamControl/controlPlane.js +918 -0
- package/dist/teamControl/controlPlane.js.map +1 -0
- package/dist/teamControl/deployment.d.ts +33 -0
- package/dist/teamControl/deployment.js +67 -0
- package/dist/teamControl/deployment.js.map +1 -0
- package/dist/teamControl/index.d.ts +4 -0
- package/dist/teamControl/index.js +3 -0
- package/dist/teamControl/index.js.map +1 -0
- package/dist/tests/testGenerator.js +4 -1
- package/dist/tests/testGenerator.js.map +1 -1
- package/dist/trust/index.d.ts +2 -0
- package/dist/trust/index.js +2 -0
- package/dist/trust/index.js.map +1 -0
- package/dist/trust/trustLadder.d.ts +182 -0
- package/dist/trust/trustLadder.js +560 -0
- package/dist/trust/trustLadder.js.map +1 -0
- package/dist/worlds/index.d.ts +2 -0
- package/dist/worlds/index.js +2 -0
- package/dist/worlds/index.js.map +1 -0
- package/dist/worlds/worlds.d.ts +160 -0
- package/dist/worlds/worlds.js +491 -0
- package/dist/worlds/worlds.js.map +1 -0
- package/docs/assets/dashboard.png +0 -0
- package/docs/ci.md +49 -0
- package/docs/commercial/README.md +29 -0
- package/docs/commercial/data-inventory.md +36 -0
- package/docs/commercial/manual-invoicing.md +44 -0
- package/docs/commercial/metrics.md +51 -0
- package/docs/commercial/pricing.md +103 -0
- package/docs/commercial/privacy-policy-draft.md +41 -0
- package/docs/commercial/terms-topics-for-counsel.md +51 -0
- package/docs/design-partners/README.md +52 -0
- package/docs/design-partners/demo-narratives.md +33 -0
- package/docs/design-partners/design-partner-offer.md +39 -0
- package/docs/design-partners/discovery-questions.md +28 -0
- package/docs/design-partners/feedback-capture-template.md +48 -0
- package/docs/design-partners/icp-one-pager.md +43 -0
- package/docs/design-partners/onboarding-checklist.md +33 -0
- package/docs/design-partners/pilot-success-criteria.md +27 -0
- package/docs/design-partners/pricing-interview-script.md +18 -0
- package/docs/design-partners/telemetry-plan.md +48 -0
- package/docs/development/baseline.md +114 -0
- package/docs/development/onboarding-smoke.md +37 -0
- package/docs/development/verification-0.1.8.md +25 -0
- package/docs/enterprise-product-roadmap-ru.md +1879 -0
- package/docs/fundraising/README.md +87 -0
- package/docs/fundraising/data-room-checklist.md +72 -0
- package/docs/fundraising/demo-script.md +52 -0
- package/docs/fundraising/design-partners-50.md +74 -0
- package/docs/fundraising/launch-posts.md +37 -0
- package/docs/fundraising/metrics-and-evidence.md +30 -0
- package/docs/fundraising/roadmap-12-month.md +25 -0
- package/docs/fundraising/technical-due-diligence-index.md +37 -0
- package/docs/fundraising/yc-application.md +25 -0
- package/docs/github-actions.md +84 -0
- package/docs/hosted-pilot.md +111 -0
- package/docs/mcp.md +52 -50
- package/docs/operations/disaster-recovery-runbook.md +81 -0
- package/docs/operations/kill-switch-runbook.md +32 -0
- package/docs/policy.md +61 -0
- package/docs/providers/authoring-packs.md +175 -0
- package/docs/providers/stripe-core-pack.md +82 -0
- package/docs/release-checklist.md +83 -71
- package/docs/release-migration-and-rollback.md +63 -0
- package/docs/release-readiness.md +39 -0
- package/docs/security/action-gateway-threat-model.md +34 -0
- package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
- package/docs/security/approval-inbox-threat-model.md +35 -0
- package/docs/security/credential-broker-threat-model.md +52 -0
- package/docs/security/egress-threat-model.md +126 -0
- package/docs/security/inventory-threat-model.md +104 -0
- package/docs/security/kill-switch-budgets-threat-model.md +37 -0
- package/docs/security/reliability-threat-model.md +111 -0
- package/docs/security/trust-ladder-threat-model.md +33 -0
- package/docs/team-control-plane.md +72 -0
- package/docs/usage.md +571 -57
- package/examples/README.md +10 -0
- package/examples/agent-instructions/README.md +39 -39
- package/examples/ci-smoke/README.md +21 -0
- package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
- package/examples/ci-smoke/package.json +9 -0
- package/examples/ci-smoke/production-egress.mjs +2 -0
- package/examples/ci-smoke/safe.mjs +17 -0
- package/examples/evals/README.md +24 -0
- package/examples/evals/retry-after.eval.json +52 -0
- package/examples/generic-rest/README.md +27 -27
- package/examples/github-fetch/README.md +35 -35
- package/examples/openai-streaming/README.md +16 -0
- package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
- package/examples/policy/ghostapi.policy.yaml +27 -0
- package/examples/record-replay/README.md +19 -0
- package/examples/record-replay/replay-requests.json +22 -0
- package/examples/record-replay/stripe-checkout.har.json +30 -0
- package/examples/resend-node/README.md +22 -22
- package/examples/stripe-node/README.md +35 -29
- package/examples/stripe-node/checkout-flow.mjs +61 -0
- package/examples/twilio-node/README.md +21 -21
- package/examples/worlds/README.md +19 -0
- package/examples/worlds/subscription-recovery.mjs +12 -0
- package/package.json +26 -11
- package/dist/providers/stripe.d.ts +0 -2
- package/dist/providers/stripe.js +0 -15
- package/dist/providers/stripe.js.map +0 -1
- package/docs/assets/ghostapi-avatar.png +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -2,22 +2,35 @@
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import https from "node:https";
|
|
4
4
|
import net from "node:net";
|
|
5
|
-
import { access, mkdir } from "node:fs/promises";
|
|
5
|
+
import { access, lstat, mkdir, readFile, realpath } from "node:fs/promises";
|
|
6
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
6
7
|
import { constants } from "node:fs";
|
|
7
|
-
import { spawn } from "node:child_process";
|
|
8
8
|
import { generate } from "selfsigned";
|
|
9
9
|
import { createServer } from "../server/createServer.js";
|
|
10
10
|
import { clearCache } from "../cache/index.js";
|
|
11
11
|
import { clearState } from "../state/stateStore.js";
|
|
12
12
|
import { clearEvents } from "../server/eventsStore.js";
|
|
13
13
|
import { DEFAULT_MODEL, loadServerConfig } from "../config/serverConfig.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { initializeLocalConfig, readLocalConfig, writeLocalConfig } from "../config/localConfig.js";
|
|
15
|
+
import { getDataPaths } from "../config/dataPaths.js";
|
|
16
|
+
import { detectEgressCapabilities, formatEgressCapabilityReport } from "../egress/capabilities.js";
|
|
17
|
+
import { EgressRunError, runEgressCommand } from "../egress/run.js";
|
|
18
|
+
import { PolicyValidationError, evaluatePolicy, formatPolicyDecision, loadPolicyFile } from "../policy/index.js";
|
|
19
|
+
import { isLoopbackHost } from "../server/accessControl.js";
|
|
20
|
+
import { getProviderManifests, isRegisteredProvider, providerRegistry } from "../providers/registry.js";
|
|
16
21
|
import { parseCliArgs } from "./parser.js";
|
|
17
22
|
import { CliError } from "./errors.js";
|
|
18
23
|
import { startMcpServer } from "../mcp/server.js";
|
|
19
24
|
import { generateRepoSetup, writeRepoSetup } from "../setup/setupGenerator.js";
|
|
20
25
|
import { generateSafetyReport } from "../report/safetyReport.js";
|
|
26
|
+
import { compareEvidenceReports, formatEvidenceCompare, formatEvidenceReport, generateEvidenceReport, loadEvidenceReport, EvidenceReportError } from "../evidence/index.js";
|
|
27
|
+
import { createScenarioReplayer, formatScenarioSanitizationSummary, loadScenarioBundle, prepareScenarioRecordingFromFile, ScenarioBundleError, writeScenarioBundle } from "../scenarios/scenarioBundle.js";
|
|
28
|
+
import { ContractError, diffContracts, formatContractDiff, importHarContractFromFile, importOpenApiContractFromFile, loadContract, writeContract } from "../contracts/index.js";
|
|
29
|
+
import { EvalError, formatEvalReport, runEval } from "../evals/index.js";
|
|
30
|
+
import { SyntheticWorldError, createWorld, forkWorld, formatWorld, inspectWorld, resetWorld } from "../worlds/index.js";
|
|
31
|
+
import { formatProductTelemetry, readProductTelemetry, recordProductTelemetry, setProductTelemetryEnabled } from "../productTelemetry/index.js";
|
|
32
|
+
import { ActionGatewayError, createLocalActionGateway } from "../actions/index.js";
|
|
33
|
+
import { openUrl } from "./openUrl.js";
|
|
21
34
|
async function main() {
|
|
22
35
|
const command = parseCliArgs(process.argv.slice(2));
|
|
23
36
|
switch (command.name) {
|
|
@@ -42,7 +55,64 @@ async function main() {
|
|
|
42
55
|
inspectProvider(command.provider);
|
|
43
56
|
return;
|
|
44
57
|
case "doctor":
|
|
45
|
-
await runDoctor(command.options
|
|
58
|
+
await runDoctor(command.options);
|
|
59
|
+
return;
|
|
60
|
+
case "run": {
|
|
61
|
+
const result = await runEgressCommand(command.options);
|
|
62
|
+
await recordLocalProductTelemetry("enforced_run_completed");
|
|
63
|
+
process.exitCode = result.exitCode;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case "policy-validate":
|
|
67
|
+
await validatePolicy(command.file);
|
|
68
|
+
return;
|
|
69
|
+
case "policy-explain":
|
|
70
|
+
await explainPolicy(command.file, command.event);
|
|
71
|
+
return;
|
|
72
|
+
case "evidence-generate":
|
|
73
|
+
await generateEvidence(command.options);
|
|
74
|
+
return;
|
|
75
|
+
case "evidence-view":
|
|
76
|
+
await viewEvidence(command.options);
|
|
77
|
+
return;
|
|
78
|
+
case "evidence-compare":
|
|
79
|
+
await compareEvidence(command.options);
|
|
80
|
+
return;
|
|
81
|
+
case "record":
|
|
82
|
+
await recordScenario(command.options);
|
|
83
|
+
return;
|
|
84
|
+
case "replay":
|
|
85
|
+
await replayBundle(command.options);
|
|
86
|
+
return;
|
|
87
|
+
case "contract-import-openapi":
|
|
88
|
+
await importOpenApi(command.options);
|
|
89
|
+
return;
|
|
90
|
+
case "contract-import-har":
|
|
91
|
+
await importHar(command.options);
|
|
92
|
+
return;
|
|
93
|
+
case "contract-diff":
|
|
94
|
+
await diffContractFiles(command.options);
|
|
95
|
+
return;
|
|
96
|
+
case "eval":
|
|
97
|
+
await runEvalCommand(command.options);
|
|
98
|
+
return;
|
|
99
|
+
case "world-create":
|
|
100
|
+
await createWorldCommand(command.options);
|
|
101
|
+
return;
|
|
102
|
+
case "world-inspect":
|
|
103
|
+
await inspectWorldCommand(command.id, command.json === true);
|
|
104
|
+
return;
|
|
105
|
+
case "world-reset":
|
|
106
|
+
await resetWorldCommand(command.id, command.json === true);
|
|
107
|
+
return;
|
|
108
|
+
case "world-fork":
|
|
109
|
+
await forkWorldCommand(command.sourceId, command.options);
|
|
110
|
+
return;
|
|
111
|
+
case "action-inspect":
|
|
112
|
+
await inspectActionCommand(command.actionId, command.json === true);
|
|
113
|
+
return;
|
|
114
|
+
case "telemetry":
|
|
115
|
+
await runTelemetryCommand(command.action, command.json === true);
|
|
46
116
|
return;
|
|
47
117
|
case "init":
|
|
48
118
|
await initProject();
|
|
@@ -73,7 +143,9 @@ async function startServer(config, open = false) {
|
|
|
73
143
|
server.on("error", (error) => {
|
|
74
144
|
if (error.code === "EADDRINUSE") {
|
|
75
145
|
console.error(`Error: Port ${config.port} is already in use.`);
|
|
76
|
-
console.error(`
|
|
146
|
+
console.error(`Reason: Another process is already listening on ${config.host}:${config.port}.`);
|
|
147
|
+
console.error(`Remediation: Run ghostapi start --port <free-port> or stop the process using ${config.port}.`);
|
|
148
|
+
console.error("Docs: README.md#quickstart and docs/usage.md");
|
|
77
149
|
process.exit(1);
|
|
78
150
|
}
|
|
79
151
|
console.error(`Error: ${error.message}`);
|
|
@@ -81,11 +153,15 @@ async function startServer(config, open = false) {
|
|
|
81
153
|
});
|
|
82
154
|
server.listen(config.port, config.host, () => {
|
|
83
155
|
const dashboardUrl = `${protocol}://${config.host}:${config.port}/dashboard`;
|
|
156
|
+
const remote = !isLoopbackHost(config.host);
|
|
84
157
|
console.log(`GhostAPI listening on ${protocol}://${config.host}:${config.port}`);
|
|
85
158
|
console.log(`Dashboard: ${dashboardUrl}`);
|
|
86
|
-
console.log(`Model: ${config.model}${config.offline ? " (offline)" : ""}`);
|
|
159
|
+
console.log(`Model: ${config.model}${config.offline ? " (offline)" : config.allowExternalLlm ? " (external LLM opt-in enabled)" : " (local only)"}`);
|
|
160
|
+
if (remote) {
|
|
161
|
+
console.warn("Warning: GhostAPI is bound beyond loopback. Use HTTPS or a secure tunnel because the dashboard token does not encrypt traffic.");
|
|
162
|
+
}
|
|
87
163
|
if (open)
|
|
88
|
-
openUrl(dashboardUrl);
|
|
164
|
+
openUrl(remote && config.authToken ? `${dashboardUrl}?token=${encodeURIComponent(config.authToken)}` : dashboardUrl);
|
|
89
165
|
});
|
|
90
166
|
}
|
|
91
167
|
async function createDevTlsOptions(host) {
|
|
@@ -112,8 +188,16 @@ async function clearTarget(target) {
|
|
|
112
188
|
}
|
|
113
189
|
async function initProject() {
|
|
114
190
|
const result = await initializeLocalConfig();
|
|
115
|
-
|
|
191
|
+
const setup = await writeRepoSetup();
|
|
192
|
+
const configPath = getDataPaths().config;
|
|
193
|
+
console.log(result.created ? `Created ${configPath}.` : `${configPath} already exists.`);
|
|
116
194
|
console.log(`Model: ${result.config.model ?? DEFAULT_MODEL}`);
|
|
195
|
+
console.log(`Created setup files: ${setup.result.created.length > 0 ? setup.result.created.join(", ") : "none"}.`);
|
|
196
|
+
console.log(`Skipped existing setup files: ${setup.result.skipped.length > 0 ? setup.result.skipped.join(", ") : "none"}.`);
|
|
197
|
+
console.log("Next: ghostapi doctor");
|
|
198
|
+
console.log("Then on supported Linux hosts: ghostapi run -- npm test");
|
|
199
|
+
console.log("Otherwise: ghostapi start --open and point SDKs at http://127.0.0.1:8080.");
|
|
200
|
+
await recordLocalProductTelemetry("init_completed");
|
|
117
201
|
}
|
|
118
202
|
async function printRepoSetup(write) {
|
|
119
203
|
const written = write ? await writeRepoSetup() : null;
|
|
@@ -146,12 +230,6 @@ function openDashboard(options) {
|
|
|
146
230
|
openUrl(url);
|
|
147
231
|
console.log(`Opened ${url}`);
|
|
148
232
|
}
|
|
149
|
-
function openUrl(url) {
|
|
150
|
-
const command = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
151
|
-
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
152
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
153
|
-
child.unref();
|
|
154
|
-
}
|
|
155
233
|
async function printSafetyReport() {
|
|
156
234
|
const report = await generateSafetyReport();
|
|
157
235
|
console.log(`GhostAPI safety report for ${report.projectRoot}`);
|
|
@@ -181,8 +259,9 @@ async function setModel(model) {
|
|
|
181
259
|
console.log(`Model set to ${model}.`);
|
|
182
260
|
}
|
|
183
261
|
function listProviders() {
|
|
184
|
-
for (const
|
|
185
|
-
|
|
262
|
+
for (const manifest of getProviderManifests()) {
|
|
263
|
+
const version = manifest.packVersion === null ? manifest.implementation : `pack ${manifest.packVersion}`;
|
|
264
|
+
console.log(`${manifest.name.padEnd(8)} ${manifest.displayName} (${version})`);
|
|
186
265
|
}
|
|
187
266
|
}
|
|
188
267
|
function inspectProvider(provider) {
|
|
@@ -190,47 +269,301 @@ function inspectProvider(provider) {
|
|
|
190
269
|
throw new CliError(`Unknown provider: ${provider}`, `Run ghostapi providers list to see supported providers.`);
|
|
191
270
|
}
|
|
192
271
|
const adapter = providerRegistry[provider];
|
|
272
|
+
const manifest = getProviderManifests().find((candidate) => candidate.name === provider);
|
|
273
|
+
if (manifest === undefined)
|
|
274
|
+
throw new CliError(`Provider manifest missing: ${provider}`);
|
|
193
275
|
console.log(`${adapter.displayName}`);
|
|
194
276
|
console.log(`Name: ${adapter.name}`);
|
|
195
|
-
console.log(`Mode: ${
|
|
277
|
+
console.log(`Mode: ${manifest.implementation}`);
|
|
278
|
+
console.log(`Pack version: ${manifest.packVersion ?? "n/a"}`);
|
|
279
|
+
console.log(`API versions: ${manifest.apiVersions?.supported.join(", ") ?? "legacy"}`);
|
|
280
|
+
console.log(`Capabilities: ${Object.entries(manifest.capabilities).filter(([, enabled]) => enabled).map(([name]) => name).join(", ")}`);
|
|
196
281
|
console.log("Error formatting: supported");
|
|
197
282
|
}
|
|
198
|
-
async function runDoctor(
|
|
199
|
-
|
|
283
|
+
async function runDoctor(options) {
|
|
284
|
+
if (options.egress) {
|
|
285
|
+
const report = detectEgressCapabilities();
|
|
286
|
+
console.log(options.json ? JSON.stringify(report, null, 2) : formatEgressCapabilityReport(report));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
200
289
|
const checks = [];
|
|
290
|
+
let config = null;
|
|
291
|
+
try {
|
|
292
|
+
config = loadServerConfig(process.env, [], options.port ? { port: options.port } : {});
|
|
293
|
+
checks.push({ label: "Config", status: "ok", detail: "loaded", docs: "docs/usage.md#configuration" });
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
checks.push({ label: "Config", status: "fail", detail: error instanceof Error ? error.message : "invalid configuration", remediation: "Fix GHOSTAPI_* environment values or .ghostapi/config.json, then re-run ghostapi doctor.", docs: "docs/usage.md#configuration" });
|
|
297
|
+
}
|
|
201
298
|
const major = Number(process.versions.node.split(".")[0]);
|
|
202
|
-
checks.push({ label: "Node version",
|
|
299
|
+
checks.push({ label: "Node version", status: major >= 20 ? "ok" : "fail", detail: process.versions.node, remediation: "Install Node.js 20 or newer.", docs: "README.md#quickstart" });
|
|
203
300
|
const writeAccess = await canWriteGhostApiDir();
|
|
204
|
-
checks.push({ label: "
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
301
|
+
checks.push({ label: "GhostAPI data write access", status: writeAccess ? "ok" : "fail", detail: getDataPaths().root, remediation: "Check GHOSTAPI_DATA_DIR and directory permissions.", docs: "README.md#local-files" });
|
|
302
|
+
if (config !== null) {
|
|
303
|
+
const portAvailable = await isPortAvailable(config.host, config.port);
|
|
304
|
+
checks.push({ label: "Port availability", status: portAvailable ? "ok" : "fail", detail: `${config.host}:${config.port}`, remediation: `Run ghostapi start --port <free-port> or stop the process using ${config.port}.`, docs: "README.md#quickstart" });
|
|
305
|
+
checks.push({ label: "Model config", status: config.model.trim() !== "" ? "ok" : "fail", detail: config.model, remediation: "Run ghostapi model set <model>.", docs: "docs/usage.md#configuration" });
|
|
306
|
+
const hasApiKey = Boolean(config.apiKey);
|
|
307
|
+
checks.push({
|
|
308
|
+
label: "LLM API key",
|
|
309
|
+
status: config.allowExternalLlm && !hasApiKey ? "fail" : "ok",
|
|
310
|
+
detail: config.offline ? "offline mode" : config.allowExternalLlm ? hasApiKey ? "explicitly enabled" : "enabled without GHOSTAPI_LLM_API_KEY" : "disabled; deterministic provider mocks enabled",
|
|
311
|
+
remediation: config.allowExternalLlm && !hasApiKey ? "Set GHOSTAPI_LLM_API_KEY or remove the external LLM opt-in." : undefined,
|
|
312
|
+
docs: "README.md#safety-model"
|
|
313
|
+
});
|
|
314
|
+
const remote = !isLoopbackHost(config.host);
|
|
315
|
+
checks.push({
|
|
316
|
+
label: "Remote bind auth",
|
|
317
|
+
status: !remote || (config.authToken !== undefined && config.authToken.length >= 24) ? "ok" : "fail",
|
|
318
|
+
detail: remote ? "non-loopback host configured" : "loopback only",
|
|
319
|
+
remediation: remote ? "Set a strong GHOSTAPI_AUTH_TOKEN and use HTTPS or a secure tunnel." : undefined,
|
|
320
|
+
docs: "README.md#safety-model"
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const tlsBypass = process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0";
|
|
324
|
+
checks.push({ label: "TLS safety", status: !tlsBypass ? "ok" : "fail", detail: tlsBypass ? "NODE_TLS_REJECT_UNAUTHORIZED=0" : "safe", remediation: "Unset NODE_TLS_REJECT_UNAUTHORIZED.", docs: "SECURITY.md" });
|
|
325
|
+
const egress = detectEgressCapabilities();
|
|
326
|
+
const linuxNamespace = egress.capabilities.find((capability) => capability.id === "linux-network-namespace");
|
|
327
|
+
const enforcementAvailable = process.platform === "linux" && linuxNamespace !== undefined;
|
|
209
328
|
checks.push({
|
|
210
|
-
label: "
|
|
211
|
-
|
|
212
|
-
detail:
|
|
213
|
-
|
|
329
|
+
label: "Enforcement capability",
|
|
330
|
+
status: enforcementAvailable ? "warn" : "warn",
|
|
331
|
+
detail: enforcementAvailable ? linuxNamespace.detail : `ghostapi run process isolation is unsupported/experimental on ${process.platform}`,
|
|
332
|
+
remediation: enforcementAvailable ? "Run ghostapi run to execute the real preflight; doctor does not launch the target." : "Use ghostapi start --open for local provider simulation, or run enforced tests on Linux.",
|
|
333
|
+
docs: "docs/security/egress-threat-model.md"
|
|
214
334
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (check.
|
|
224
|
-
console.log(`
|
|
335
|
+
if (options.json) {
|
|
336
|
+
console.log(JSON.stringify({ schemaVersion: 1, checks, egress }, null, 2));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
for (const check of checks) {
|
|
340
|
+
console.log(`${check.status.padEnd(4)} ${check.label}: ${check.detail}`);
|
|
341
|
+
if (check.status !== "ok" && check.remediation)
|
|
342
|
+
console.log(` Remediation: ${check.remediation}`);
|
|
343
|
+
if (check.status !== "ok" && check.docs)
|
|
344
|
+
console.log(` Docs: ${check.docs}`);
|
|
225
345
|
}
|
|
226
346
|
}
|
|
347
|
+
const failed = checks.filter((check) => check.status === "fail").length;
|
|
227
348
|
if (failed > 0)
|
|
228
|
-
throw new CliError(`Doctor found ${failed} issue${failed === 1 ? "" : "s"}
|
|
349
|
+
throw new CliError(`Doctor found ${failed} issue${failed === 1 ? "" : "s"}.`, "Fix the failed checks above, then re-run ghostapi doctor.");
|
|
350
|
+
}
|
|
351
|
+
async function validatePolicy(file) {
|
|
352
|
+
const loaded = await loadPolicyFile(file, process.cwd(), true);
|
|
353
|
+
if (loaded === null)
|
|
354
|
+
throw new CliError("Policy file was not found.");
|
|
355
|
+
console.log(`Policy valid: ${loaded.path}`);
|
|
356
|
+
console.log(`Schema version: ${loaded.policy.version}`);
|
|
357
|
+
console.log(`SHA-256: ${loaded.hash}`);
|
|
358
|
+
}
|
|
359
|
+
async function explainPolicy(file, event) {
|
|
360
|
+
const loaded = await loadPolicyFile(file, process.cwd(), true);
|
|
361
|
+
if (loaded === null)
|
|
362
|
+
throw new CliError("Policy file was not found.");
|
|
363
|
+
console.log(formatPolicyDecision(evaluatePolicy(loaded.policy, event)));
|
|
364
|
+
}
|
|
365
|
+
async function generateEvidence(options) {
|
|
366
|
+
const { report, path } = await generateEvidenceReport({ policyPath: options.policyPath, runPath: options.runPath, outPath: options.outPath, contractBaselinePath: options.contractBaselinePath, contractCandidatePath: options.contractCandidatePath });
|
|
367
|
+
if (options.json)
|
|
368
|
+
console.log(JSON.stringify({ path, report }, null, 2));
|
|
369
|
+
else {
|
|
370
|
+
console.log(formatEvidenceReport(report));
|
|
371
|
+
console.log(`Artifact: ${path}`);
|
|
372
|
+
}
|
|
373
|
+
if (options.ci && !report.summary.passed)
|
|
374
|
+
process.exitCode = 2;
|
|
375
|
+
await recordLocalProductTelemetry("evidence_generated");
|
|
376
|
+
}
|
|
377
|
+
async function viewEvidence(options) {
|
|
378
|
+
const report = await loadEvidenceReport(options.path);
|
|
379
|
+
console.log(options.json ? JSON.stringify(report, null, 2) : formatEvidenceReport(report));
|
|
380
|
+
}
|
|
381
|
+
async function compareEvidence(options) {
|
|
382
|
+
const left = await loadEvidenceReport(options.leftPath);
|
|
383
|
+
const right = await loadEvidenceReport(options.rightPath);
|
|
384
|
+
const result = compareEvidenceReports(left, right);
|
|
385
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatEvidenceCompare(result));
|
|
386
|
+
if (!result.equal)
|
|
387
|
+
process.exitCode = 1;
|
|
388
|
+
}
|
|
389
|
+
async function recordScenario(options) {
|
|
390
|
+
const bundle = await prepareScenarioRecordingFromFile(options.inputPath, {
|
|
391
|
+
title: options.title,
|
|
392
|
+
allowedSandboxHosts: options.allowedSandboxHosts,
|
|
393
|
+
pii: parsePiiRules(options.pii)
|
|
394
|
+
});
|
|
395
|
+
console.log(formatScenarioSanitizationSummary(bundle.sanitization));
|
|
396
|
+
if (bundle.sanitization.requiresApproval && !options.approve) {
|
|
397
|
+
throw new CliError("Recording was not saved because sanitization found potentially sensitive traffic.", "Review the summary, then re-run the exact command with --approve to save the sanitized bundle.");
|
|
398
|
+
}
|
|
399
|
+
const path = await writeScenarioBundle(bundle, options.outPath);
|
|
400
|
+
console.log(`Bundle: ${path}`);
|
|
401
|
+
}
|
|
402
|
+
async function replayBundle(options) {
|
|
403
|
+
const bundle = await loadScenarioBundle(options.bundlePath);
|
|
404
|
+
const requests = await loadReplayRequests(options.requestsPath);
|
|
405
|
+
const replayer = createScenarioReplayer(bundle);
|
|
406
|
+
const responses = requests.map((request) => replayer.replay(request));
|
|
407
|
+
if (replayer.remaining !== 0)
|
|
408
|
+
throw new CliError(`Replay ended with ${replayer.remaining} interaction${replayer.remaining === 1 ? "" : "s"} remaining.`, "Provide the complete ordered request sequence; GhostAPI never guesses a later match.");
|
|
409
|
+
if (options.json)
|
|
410
|
+
console.log(JSON.stringify({ responses }, null, 2));
|
|
411
|
+
else {
|
|
412
|
+
console.log(`Replay matched ${responses.length} interaction${responses.length === 1 ? "" : "s"}.`);
|
|
413
|
+
for (const response of responses)
|
|
414
|
+
console.log(` ${response.index}: ${response.status}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function importOpenApi(options) {
|
|
418
|
+
const contract = await importOpenApiContractFromFile(options.inputPath, { title: options.title });
|
|
419
|
+
const path = await writeContract(contract, options.outPath);
|
|
420
|
+
console.log(`Contract: ${path}`);
|
|
421
|
+
console.log(`Operations: ${contract.operations.length}`);
|
|
422
|
+
}
|
|
423
|
+
async function importHar(options) {
|
|
424
|
+
const { bundle, contract } = await importHarContractFromFile(options.inputPath, {
|
|
425
|
+
title: options.title,
|
|
426
|
+
allowedSandboxHosts: options.allowedSandboxHosts,
|
|
427
|
+
pii: parsePiiRules(options.pii)
|
|
428
|
+
});
|
|
429
|
+
console.log(formatScenarioSanitizationSummary(bundle.sanitization));
|
|
430
|
+
if (bundle.sanitization.requiresApproval && !options.approve) {
|
|
431
|
+
throw new CliError("HAR import was not saved because sanitization found potentially sensitive traffic.", "Review the summary, then re-run the exact command with --approve to save the sanitized bundle and contract.");
|
|
432
|
+
}
|
|
433
|
+
const bundlePath = await writeScenarioBundle(bundle, options.outPath);
|
|
434
|
+
const contractPath = await writeContract(contract, options.contractOutPath);
|
|
435
|
+
console.log(`Bundle: ${bundlePath}`);
|
|
436
|
+
console.log(`Contract: ${contractPath}`);
|
|
437
|
+
}
|
|
438
|
+
async function diffContractFiles(options) {
|
|
439
|
+
const [baseline, candidate, loadedPolicy] = await Promise.all([
|
|
440
|
+
loadContract(options.baselinePath),
|
|
441
|
+
loadContract(options.candidatePath),
|
|
442
|
+
options.policyPath === undefined ? Promise.resolve(null) : loadPolicyFile(options.policyPath, process.cwd(), true)
|
|
443
|
+
]);
|
|
444
|
+
const diff = diffContracts(baseline, candidate);
|
|
445
|
+
const decision = loadedPolicy === null ? null : evaluatePolicy(loadedPolicy.policy, { type: "report", productionEgressAttempts: 0, forbiddenCredentialMatches: 0, breakingContractChanges: diff.summary.breaking });
|
|
446
|
+
if (options.json)
|
|
447
|
+
console.log(JSON.stringify({ diff, policy: decision }, null, 2));
|
|
448
|
+
else {
|
|
449
|
+
console.log(formatContractDiff(diff));
|
|
450
|
+
if (decision !== null)
|
|
451
|
+
console.log(`Policy: ${decision.allowed ? "PASS" : "FAIL"} - ${decision.reason}`);
|
|
452
|
+
}
|
|
453
|
+
if (options.ci && (decision === null ? diff.summary.breaking > 0 : !decision.allowed))
|
|
454
|
+
process.exitCode = 2;
|
|
455
|
+
}
|
|
456
|
+
async function runEvalCommand(options) {
|
|
457
|
+
const { report, path } = await runEval({ specPath: options.specPath, template: options.template, evidencePath: options.evidencePath, outPath: options.outPath });
|
|
458
|
+
if (options.json)
|
|
459
|
+
console.log(JSON.stringify({ path, report }, null, 2));
|
|
460
|
+
else {
|
|
461
|
+
console.log(formatEvalReport(report));
|
|
462
|
+
console.log(`Artifact: ${path}`);
|
|
463
|
+
}
|
|
464
|
+
if (options.ci && !report.score.passed)
|
|
465
|
+
process.exitCode = 2;
|
|
466
|
+
await recordLocalProductTelemetry("eval_completed");
|
|
467
|
+
}
|
|
468
|
+
async function createWorldCommand(options) {
|
|
469
|
+
const world = await createWorld(options);
|
|
470
|
+
console.log(options.json ? JSON.stringify(world, null, 2) : `${formatWorld(world)}\nPath: ${getDataPaths().worlds}`);
|
|
471
|
+
}
|
|
472
|
+
async function inspectWorldCommand(id, json) {
|
|
473
|
+
const world = await inspectWorld(id);
|
|
474
|
+
console.log(json ? JSON.stringify(world, null, 2) : formatWorld(world));
|
|
475
|
+
}
|
|
476
|
+
async function resetWorldCommand(id, json) {
|
|
477
|
+
const world = await resetWorld(id);
|
|
478
|
+
console.log(json ? JSON.stringify(world, null, 2) : `${formatWorld(world)}\nReset: restored baseline state.`);
|
|
479
|
+
}
|
|
480
|
+
async function forkWorldCommand(sourceId, options) {
|
|
481
|
+
const world = await forkWorld(sourceId, options);
|
|
482
|
+
console.log(options.json ? JSON.stringify(world, null, 2) : `${formatWorld(world)}\nForked from: ${sourceId}`);
|
|
483
|
+
}
|
|
484
|
+
async function inspectActionCommand(actionId, json) {
|
|
485
|
+
const record = await createLocalActionGateway().inspect(actionId);
|
|
486
|
+
console.log(json ? JSON.stringify(record, null, 2) : `Action: ${record.envelope.actionId}\nHash: ${record.actionHash}\nReceipts: ${record.receipts.map((receipt) => receipt.status).join(", ")}`);
|
|
487
|
+
}
|
|
488
|
+
async function runTelemetryCommand(action, json) {
|
|
489
|
+
const snapshot = action === "enable"
|
|
490
|
+
? await setProductTelemetryEnabled(true)
|
|
491
|
+
: action === "disable"
|
|
492
|
+
? await setProductTelemetryEnabled(false)
|
|
493
|
+
: await readProductTelemetry();
|
|
494
|
+
if (json) {
|
|
495
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
console.log(formatProductTelemetry(snapshot));
|
|
499
|
+
if (action === "disable")
|
|
500
|
+
console.log("Existing local counters were deleted.");
|
|
501
|
+
if (action === "enable")
|
|
502
|
+
console.log("Only aggregate event counts and up to eight ISO weeks are recorded locally after this command; no network export exists.");
|
|
503
|
+
}
|
|
504
|
+
async function recordLocalProductTelemetry(event) {
|
|
505
|
+
try {
|
|
506
|
+
await recordProductTelemetry(event);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
console.error("Warning: local opt-in product telemetry was not recorded; no telemetry was sent.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function parsePiiRules(value) {
|
|
513
|
+
if (value === undefined)
|
|
514
|
+
return undefined;
|
|
515
|
+
const selected = new Set(value === "none" ? [] : value.split(","));
|
|
516
|
+
return { emails: selected.has("emails"), phones: selected.has("phones"), addresses: selected.has("addresses") };
|
|
517
|
+
}
|
|
518
|
+
async function loadReplayRequests(path) {
|
|
519
|
+
const target = isAbsolute(path) ? resolve(path) : resolve(process.cwd(), path);
|
|
520
|
+
if (!isInsideAllowedRoots(target))
|
|
521
|
+
throw new CliError("Replay requests path traversal outside the project root or GHOSTAPI_DATA_DIR is not allowed.");
|
|
522
|
+
const info = await lstat(target);
|
|
523
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
524
|
+
throw new CliError("Replay requests input must be a regular non-symlink file.");
|
|
525
|
+
if (!await isRealPathInsideAllowedRoots(target))
|
|
526
|
+
throw new CliError("Replay requests input resolves outside the project root or GHOSTAPI_DATA_DIR through a symlink.");
|
|
527
|
+
const source = await readFile(target, "utf8");
|
|
528
|
+
if (Buffer.byteLength(source, "utf8") > 1024 * 1024)
|
|
529
|
+
throw new CliError("Replay requests input exceeds 1048576 bytes.");
|
|
530
|
+
let parsed;
|
|
531
|
+
try {
|
|
532
|
+
parsed = JSON.parse(source);
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
throw new CliError("Replay requests input is not valid JSON.");
|
|
536
|
+
}
|
|
537
|
+
const requests = Array.isArray(parsed) ? parsed : parsed !== null && typeof parsed === "object" && Array.isArray(parsed.requests) ? parsed.requests : null;
|
|
538
|
+
if (requests === null || requests.length === 0 || requests.length > 100)
|
|
539
|
+
throw new CliError("Replay requests input must contain 1-100 requests.");
|
|
540
|
+
return requests.map((request) => {
|
|
541
|
+
if (request === null || typeof request !== "object" || Array.isArray(request))
|
|
542
|
+
throw new CliError("Replay request must be an object.");
|
|
543
|
+
const candidate = request;
|
|
544
|
+
if (typeof candidate.method !== "string" || typeof candidate.path !== "string")
|
|
545
|
+
throw new CliError("Replay request requires string method and path.");
|
|
546
|
+
return { method: candidate.method, path: candidate.path, headers: candidate.headers, body: candidate.body };
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function isInsideAllowedRoots(target) {
|
|
550
|
+
return isInside(process.cwd(), target) || isInside(getDataPaths().root, target);
|
|
551
|
+
}
|
|
552
|
+
function isInside(root, target) {
|
|
553
|
+
const relativePath = relative(resolve(root), target);
|
|
554
|
+
return relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
555
|
+
}
|
|
556
|
+
async function isRealPathInsideAllowedRoots(target) {
|
|
557
|
+
const realTarget = await realpath(target);
|
|
558
|
+
const realProjectRoot = await realpath(process.cwd());
|
|
559
|
+
const dataRoot = await realpath(getDataPaths().root).catch(() => null);
|
|
560
|
+
return isInside(realProjectRoot, realTarget) || (dataRoot !== null && isInside(dataRoot, realTarget));
|
|
229
561
|
}
|
|
230
562
|
async function canWriteGhostApiDir() {
|
|
563
|
+
const dataDir = getDataPaths().root;
|
|
231
564
|
try {
|
|
232
|
-
await mkdir(
|
|
233
|
-
await access(
|
|
565
|
+
await mkdir(dataDir, { recursive: true });
|
|
566
|
+
await access(dataDir, constants.W_OK);
|
|
234
567
|
return true;
|
|
235
568
|
}
|
|
236
569
|
catch {
|
|
@@ -246,29 +579,76 @@ function isPortAvailable(host, port) {
|
|
|
246
579
|
});
|
|
247
580
|
}
|
|
248
581
|
function printHelp() {
|
|
249
|
-
console.log(`GhostAPI CLI
|
|
250
|
-
|
|
251
|
-
Usage:
|
|
252
|
-
ghostapi start [--host 127.0.0.1] [--port 8080] [--model gpt-4o-mini] [--offline] [--https] [--open]
|
|
253
|
-
ghostapi open [--host 127.0.0.1] [--port 8080] [--https]
|
|
254
|
-
ghostapi clear cache|state|events|all
|
|
255
|
-
ghostapi model get
|
|
256
|
-
ghostapi model set <model>
|
|
257
|
-
ghostapi providers list
|
|
258
|
-
ghostapi providers inspect <provider>
|
|
259
|
-
ghostapi setup [--write]
|
|
260
|
-
ghostapi report
|
|
261
|
-
ghostapi mcp
|
|
262
|
-
ghostapi doctor [--port 8080]
|
|
582
|
+
console.log(`GhostAPI CLI
|
|
583
|
+
|
|
584
|
+
Usage:
|
|
585
|
+
ghostapi start [--host 127.0.0.1] [--port 8080] [--model gpt-4o-mini] [--offline] [--https] [--allow-external-llm] [--open]
|
|
586
|
+
ghostapi open [--host 127.0.0.1] [--port 8080] [--https]
|
|
587
|
+
ghostapi clear cache|state|events|all
|
|
588
|
+
ghostapi model get
|
|
589
|
+
ghostapi model set <model>
|
|
590
|
+
ghostapi providers list
|
|
591
|
+
ghostapi providers inspect <provider>
|
|
592
|
+
ghostapi setup [--write]
|
|
593
|
+
ghostapi report
|
|
594
|
+
ghostapi mcp
|
|
595
|
+
ghostapi doctor [--port 8080] [--json]
|
|
596
|
+
ghostapi doctor --egress [--json]
|
|
597
|
+
ghostapi run [--port 8080] [--allow-host localhost] [--policy ghostapi.policy.yaml] -- <command> [args...]
|
|
598
|
+
ghostapi policy validate [--file ghostapi.policy.yaml]
|
|
599
|
+
ghostapi policy explain <scenario-id>|network <host>|credential <value>|enforcement <mode>|report <production-attempts> <credential-matches> [--file ghostapi.policy.yaml]
|
|
600
|
+
ghostapi evidence generate [--policy ghostapi.policy.yaml] [--run .ghostapi/runs/<id>/run.json] [--out .ghostapi/reports/report.json] [--contract-baseline base.contract.json --contract-candidate head.contract.json] [--ci] [--json]
|
|
601
|
+
ghostapi evidence view <report.json> [--json]
|
|
602
|
+
ghostapi evidence compare <left.json> <right.json> [--json]
|
|
603
|
+
ghostapi record --input <capture.json|har.json> --allow-sandbox-host <host> [--out bundle.json] [--title title] [--pii emails,phones,addresses] [--approve]
|
|
604
|
+
ghostapi replay <bundle.json> --requests <requests.json> [--json]
|
|
605
|
+
ghostapi contract import-openapi --input <openapi.json> [--out contract.json] [--title title]
|
|
606
|
+
ghostapi contract import-har --input <capture.har> --allow-sandbox-host <host> [--out bundle.json] [--contract-out contract.json] [--title title] [--pii emails,phones,addresses] [--approve]
|
|
607
|
+
ghostapi contract diff --baseline <contract.json> --candidate <contract.json> [--policy ghostapi.policy.yaml] [--ci] [--json]
|
|
608
|
+
ghostapi eval --spec <eval.json>|--template <name> [--evidence report.json] [--out report.eval.json] [--ci] [--json]
|
|
609
|
+
ghostapi world create --id <world-id> --seed <seed> [--title title] [--json]
|
|
610
|
+
ghostapi world inspect <world-id> [--json]
|
|
611
|
+
ghostapi world reset <world-id> [--json]
|
|
612
|
+
ghostapi world fork <source-world-id> --id <fork-world-id> [--title title] [--json]
|
|
613
|
+
ghostapi action inspect <action-id> [--json]
|
|
614
|
+
ghostapi telemetry status|enable|disable|export [--json]
|
|
263
615
|
ghostapi init`);
|
|
264
616
|
}
|
|
265
617
|
main().catch((error) => {
|
|
266
|
-
if (error instanceof CliError) {
|
|
618
|
+
if (error instanceof CliError || error instanceof EgressRunError) {
|
|
267
619
|
console.error(`Error: ${error.message}`);
|
|
268
620
|
if (error.hint)
|
|
269
621
|
console.error(`Hint: ${error.hint}`);
|
|
270
622
|
process.exit(1);
|
|
271
623
|
}
|
|
624
|
+
if (error instanceof PolicyValidationError) {
|
|
625
|
+
console.error(`Error: ${error.message}`);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
if (error instanceof EvidenceReportError) {
|
|
629
|
+
console.error(`Error: ${error.message}`);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
if (error instanceof ScenarioBundleError) {
|
|
633
|
+
console.error(`Error: ${error.message}`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
if (error instanceof ContractError) {
|
|
637
|
+
console.error(`Error: ${error.message}`);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
if (error instanceof EvalError) {
|
|
641
|
+
console.error(`Error: ${error.message}`);
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
if (error instanceof SyntheticWorldError) {
|
|
645
|
+
console.error(`Error: ${error.message}`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
if (error instanceof ActionGatewayError) {
|
|
649
|
+
console.error(`Error: ${error.message}`);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
272
652
|
const message = error instanceof Error ? error.message : "Unknown CLI error";
|
|
273
653
|
console.error(`Error: ${message}`);
|
|
274
654
|
console.error("Hint: Run ghostapi doctor for environment diagnostics.");
|