@yiaany/ghostapi 0.1.7 → 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.
Files changed (323) hide show
  1. package/CHANGELOG.md +42 -2
  2. package/CONTRIBUTING.md +119 -117
  3. package/LICENSE +21 -21
  4. package/README.md +208 -185
  5. package/SECURITY.md +53 -37
  6. package/dist/actions/gateway.d.ts +179 -0
  7. package/dist/actions/gateway.js +514 -0
  8. package/dist/actions/gateway.js.map +1 -0
  9. package/dist/actions/index.d.ts +2 -0
  10. package/dist/actions/index.js +2 -0
  11. package/dist/actions/index.js.map +1 -0
  12. package/dist/ai/aiClient.js +4 -4
  13. package/dist/ai/aiClient.js.map +1 -1
  14. package/dist/ai/aiGenerator.d.ts +2 -2
  15. package/dist/ai/aiGenerator.js +21 -72
  16. package/dist/ai/aiGenerator.js.map +1 -1
  17. package/dist/ai/prompts.js +51 -51
  18. package/dist/ai/prompts.js.map +1 -1
  19. package/dist/approvals/approvalInbox.d.ts +150 -0
  20. package/dist/approvals/approvalInbox.js +412 -0
  21. package/dist/approvals/approvalInbox.js.map +1 -0
  22. package/dist/approvals/index.d.ts +2 -0
  23. package/dist/approvals/index.js +2 -0
  24. package/dist/approvals/index.js.map +1 -0
  25. package/dist/behavior/behaviorStore.d.ts +2 -0
  26. package/dist/behavior/behaviorStore.js +50 -46
  27. package/dist/behavior/behaviorStore.js.map +1 -1
  28. package/dist/cache/index.d.ts +2 -0
  29. package/dist/cache/index.js +58 -14
  30. package/dist/cache/index.js.map +1 -1
  31. package/dist/cli/index.js +438 -58
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/cli/openUrl.d.ts +10 -0
  34. package/dist/cli/openUrl.js +31 -0
  35. package/dist/cli/openUrl.js.map +1 -0
  36. package/dist/cli/parser.d.ts +155 -0
  37. package/dist/cli/parser.js +504 -2
  38. package/dist/cli/parser.js.map +1 -1
  39. package/dist/config/dataPaths.d.ts +31 -0
  40. package/dist/config/dataPaths.js +45 -0
  41. package/dist/config/dataPaths.js.map +1 -0
  42. package/dist/config/localConfig.d.ts +1 -2
  43. package/dist/config/localConfig.js +27 -21
  44. package/dist/config/localConfig.js.map +1 -1
  45. package/dist/config/serverConfig.d.ts +2 -0
  46. package/dist/config/serverConfig.js +8 -1
  47. package/dist/config/serverConfig.js.map +1 -1
  48. package/dist/contracts/contract.d.ts +76 -0
  49. package/dist/contracts/contract.js +603 -0
  50. package/dist/contracts/contract.js.map +1 -0
  51. package/dist/contracts/index.d.ts +2 -0
  52. package/dist/contracts/index.js +2 -0
  53. package/dist/contracts/index.js.map +1 -0
  54. package/dist/credentials/credentialBroker.d.ts +207 -0
  55. package/dist/credentials/credentialBroker.js +588 -0
  56. package/dist/credentials/credentialBroker.js.map +1 -0
  57. package/dist/credentials/index.d.ts +2 -0
  58. package/dist/credentials/index.js +2 -0
  59. package/dist/credentials/index.js.map +1 -0
  60. package/dist/dashboard/app.js +685 -615
  61. package/dist/dashboard/index.html +13 -7
  62. package/dist/dashboard/styles.css +349 -349
  63. package/dist/egress/capabilities.d.ts +32 -0
  64. package/dist/egress/capabilities.js +165 -0
  65. package/dist/egress/capabilities.js.map +1 -0
  66. package/dist/egress/linuxBootstrap.d.ts +1 -0
  67. package/dist/egress/linuxBootstrap.js +121 -0
  68. package/dist/egress/linuxBootstrap.js.map +1 -0
  69. package/dist/egress/run.d.ts +18 -0
  70. package/dist/egress/run.js +305 -0
  71. package/dist/egress/run.js.map +1 -0
  72. package/dist/errors/errorEngine.d.ts +2 -2
  73. package/dist/errors/errorEngine.js +3 -40
  74. package/dist/errors/errorEngine.js.map +1 -1
  75. package/dist/evals/evals.d.ts +126 -0
  76. package/dist/evals/evals.js +466 -0
  77. package/dist/evals/evals.js.map +1 -0
  78. package/dist/evals/index.d.ts +2 -0
  79. package/dist/evals/index.js +2 -0
  80. package/dist/evals/index.js.map +1 -0
  81. package/dist/evidence/index.d.ts +2 -0
  82. package/dist/evidence/index.js +2 -0
  83. package/dist/evidence/index.js.map +1 -0
  84. package/dist/evidence/report.d.ts +143 -0
  85. package/dist/evidence/report.js +556 -0
  86. package/dist/evidence/report.js.map +1 -0
  87. package/dist/fault/faultLab.d.ts +5 -4
  88. package/dist/fault/faultLab.js +66 -24
  89. package/dist/fault/faultLab.js.map +1 -1
  90. package/dist/index.d.ts +46 -0
  91. package/dist/index.js +23 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/inventory/analysis.d.ts +12 -0
  94. package/dist/inventory/analysis.js +457 -0
  95. package/dist/inventory/analysis.js.map +1 -0
  96. package/dist/inventory/index.d.ts +5 -0
  97. package/dist/inventory/index.js +4 -0
  98. package/dist/inventory/index.js.map +1 -0
  99. package/dist/inventory/inventoryController.d.ts +53 -0
  100. package/dist/inventory/inventoryController.js +1044 -0
  101. package/dist/inventory/inventoryController.js.map +1 -0
  102. package/dist/inventory/types.d.ts +579 -0
  103. package/dist/inventory/types.js +787 -0
  104. package/dist/inventory/types.js.map +1 -0
  105. package/dist/landing/assets/index-CxWsopzP.css +1 -0
  106. package/dist/landing/assets/index-DZlpyhqd.js +55 -0
  107. package/dist/landing/assets/index-Dk-Xezs3.js +55 -0
  108. package/dist/landing/index.html +15 -15
  109. package/dist/ledger/actionLedger.d.ts +125 -0
  110. package/dist/ledger/actionLedger.js +594 -0
  111. package/dist/ledger/actionLedger.js.map +1 -0
  112. package/dist/ledger/index.d.ts +2 -0
  113. package/dist/ledger/index.js +2 -0
  114. package/dist/ledger/index.js.map +1 -0
  115. package/dist/mcp/server.js +4 -3
  116. package/dist/mcp/server.js.map +1 -1
  117. package/dist/policy/index.d.ts +3 -0
  118. package/dist/policy/index.js +2 -0
  119. package/dist/policy/index.js.map +1 -0
  120. package/dist/policy/policy.d.ts +14 -0
  121. package/dist/policy/policy.js +282 -0
  122. package/dist/policy/policy.js.map +1 -0
  123. package/dist/policy/types.d.ts +52 -0
  124. package/dist/policy/types.js +2 -0
  125. package/dist/policy/types.js.map +1 -0
  126. package/dist/productTelemetry/index.d.ts +16 -0
  127. package/dist/productTelemetry/index.js +142 -0
  128. package/dist/productTelemetry/index.js.map +1 -0
  129. package/dist/providers/conformance.d.ts +7 -0
  130. package/dist/providers/conformance.js +48 -0
  131. package/dist/providers/conformance.js.map +1 -0
  132. package/dist/providers/index.d.ts +4 -2
  133. package/dist/providers/index.js +3 -1
  134. package/dist/providers/index.js.map +1 -1
  135. package/dist/providers/packs/resendPack.d.ts +2 -0
  136. package/dist/providers/packs/resendPack.js +147 -0
  137. package/dist/providers/packs/resendPack.js.map +1 -0
  138. package/dist/providers/packs/stripePack.d.ts +2 -0
  139. package/dist/providers/packs/stripePack.js +852 -0
  140. package/dist/providers/packs/stripePack.js.map +1 -0
  141. package/dist/providers/registry.d.ts +7 -3
  142. package/dist/providers/registry.js +55 -4
  143. package/dist/providers/registry.js.map +1 -1
  144. package/dist/providers/resend.js +2 -11
  145. package/dist/providers/resend.js.map +1 -1
  146. package/dist/providers/runtime.d.ts +8 -0
  147. package/dist/providers/runtime.js +48 -0
  148. package/dist/providers/runtime.js.map +1 -0
  149. package/dist/providers/stripeWebhook.d.ts +13 -0
  150. package/dist/providers/stripeWebhook.js +50 -0
  151. package/dist/providers/stripeWebhook.js.map +1 -0
  152. package/dist/providers/types.d.ts +153 -0
  153. package/dist/proxy/cacheKey.js +3 -3
  154. package/dist/proxy/cacheKey.js.map +1 -1
  155. package/dist/proxy/providerDetector.d.ts +2 -7
  156. package/dist/proxy/providerDetector.js +4 -4
  157. package/dist/proxy/providerDetector.js.map +1 -1
  158. package/dist/proxy/proxyHandler.js +116 -12
  159. package/dist/proxy/proxyHandler.js.map +1 -1
  160. package/dist/proxy/requestNormalizer.d.ts +0 -1
  161. package/dist/proxy/requestNormalizer.js +2 -5
  162. package/dist/proxy/requestNormalizer.js.map +1 -1
  163. package/dist/reliability/costGovernance.d.ts +175 -0
  164. package/dist/reliability/costGovernance.js +515 -0
  165. package/dist/reliability/costGovernance.js.map +1 -0
  166. package/dist/reliability/index.d.ts +8 -0
  167. package/dist/reliability/index.js +5 -0
  168. package/dist/reliability/index.js.map +1 -0
  169. package/dist/reliability/reconciliation.d.ts +157 -0
  170. package/dist/reliability/reconciliation.js +522 -0
  171. package/dist/reliability/reconciliation.js.map +1 -0
  172. package/dist/reliability/runtimeHealth.d.ts +60 -0
  173. package/dist/reliability/runtimeHealth.js +356 -0
  174. package/dist/reliability/runtimeHealth.js.map +1 -0
  175. package/dist/reliability/slo.d.ts +131 -0
  176. package/dist/reliability/slo.js +375 -0
  177. package/dist/reliability/slo.js.map +1 -0
  178. package/dist/report/safetyReport.js +7 -3
  179. package/dist/report/safetyReport.js.map +1 -1
  180. package/dist/safety/index.d.ts +2 -0
  181. package/dist/safety/index.js +2 -0
  182. package/dist/safety/index.js.map +1 -0
  183. package/dist/safety/safetyController.d.ts +187 -0
  184. package/dist/safety/safetyController.js +477 -0
  185. package/dist/safety/safetyController.js.map +1 -0
  186. package/dist/scenarios/scenarioBundle.d.ts +75 -0
  187. package/dist/scenarios/scenarioBundle.js +750 -0
  188. package/dist/scenarios/scenarioBundle.js.map +1 -0
  189. package/dist/scenarios/scenarioStore.d.ts +1 -0
  190. package/dist/scenarios/scenarioStore.js +47 -32
  191. package/dist/scenarios/scenarioStore.js.map +1 -1
  192. package/dist/security/headerSanitizer.d.ts +2 -0
  193. package/dist/security/headerSanitizer.js +36 -1
  194. package/dist/security/headerSanitizer.js.map +1 -1
  195. package/dist/security/secrets.js +24 -4
  196. package/dist/security/secrets.js.map +1 -1
  197. package/dist/server/accessControl.d.ts +5 -0
  198. package/dist/server/accessControl.js +100 -0
  199. package/dist/server/accessControl.js.map +1 -0
  200. package/dist/server/createServer.d.ts +0 -5
  201. package/dist/server/createServer.js +9 -29
  202. package/dist/server/createServer.js.map +1 -1
  203. package/dist/server/eventsStore.d.ts +6 -1
  204. package/dist/server/eventsStore.js +83 -15
  205. package/dist/server/eventsStore.js.map +1 -1
  206. package/dist/server/routes.js +36 -10
  207. package/dist/server/routes.js.map +1 -1
  208. package/dist/server/sse.d.ts +2 -1
  209. package/dist/server/sse.js +15 -3
  210. package/dist/server/sse.js.map +1 -1
  211. package/dist/setup/setupGenerator.js +53 -6
  212. package/dist/setup/setupGenerator.js.map +1 -1
  213. package/dist/state/stateStore.d.ts +5 -0
  214. package/dist/state/stateStore.js +48 -62
  215. package/dist/state/stateStore.js.map +1 -1
  216. package/dist/storage/fileStore.d.ts +10 -0
  217. package/dist/storage/fileStore.js +131 -0
  218. package/dist/storage/fileStore.js.map +1 -0
  219. package/dist/teamControl/controlPlane.d.ts +255 -0
  220. package/dist/teamControl/controlPlane.js +918 -0
  221. package/dist/teamControl/controlPlane.js.map +1 -0
  222. package/dist/teamControl/deployment.d.ts +33 -0
  223. package/dist/teamControl/deployment.js +67 -0
  224. package/dist/teamControl/deployment.js.map +1 -0
  225. package/dist/teamControl/index.d.ts +4 -0
  226. package/dist/teamControl/index.js +3 -0
  227. package/dist/teamControl/index.js.map +1 -0
  228. package/dist/tests/testGenerator.js +4 -1
  229. package/dist/tests/testGenerator.js.map +1 -1
  230. package/dist/trust/index.d.ts +2 -0
  231. package/dist/trust/index.js +2 -0
  232. package/dist/trust/index.js.map +1 -0
  233. package/dist/trust/trustLadder.d.ts +182 -0
  234. package/dist/trust/trustLadder.js +560 -0
  235. package/dist/trust/trustLadder.js.map +1 -0
  236. package/dist/worlds/index.d.ts +2 -0
  237. package/dist/worlds/index.js +2 -0
  238. package/dist/worlds/index.js.map +1 -0
  239. package/dist/worlds/worlds.d.ts +160 -0
  240. package/dist/worlds/worlds.js +491 -0
  241. package/dist/worlds/worlds.js.map +1 -0
  242. package/docs/assets/dashboard.png +0 -0
  243. package/docs/ci.md +49 -0
  244. package/docs/commercial/README.md +29 -0
  245. package/docs/commercial/data-inventory.md +36 -0
  246. package/docs/commercial/manual-invoicing.md +44 -0
  247. package/docs/commercial/metrics.md +51 -0
  248. package/docs/commercial/pricing.md +103 -0
  249. package/docs/commercial/privacy-policy-draft.md +41 -0
  250. package/docs/commercial/terms-topics-for-counsel.md +51 -0
  251. package/docs/design-partners/README.md +52 -0
  252. package/docs/design-partners/demo-narratives.md +33 -0
  253. package/docs/design-partners/design-partner-offer.md +39 -0
  254. package/docs/design-partners/discovery-questions.md +28 -0
  255. package/docs/design-partners/feedback-capture-template.md +48 -0
  256. package/docs/design-partners/icp-one-pager.md +43 -0
  257. package/docs/design-partners/onboarding-checklist.md +33 -0
  258. package/docs/design-partners/pilot-success-criteria.md +27 -0
  259. package/docs/design-partners/pricing-interview-script.md +18 -0
  260. package/docs/design-partners/telemetry-plan.md +48 -0
  261. package/docs/development/baseline.md +114 -0
  262. package/docs/development/onboarding-smoke.md +37 -0
  263. package/docs/development/verification-0.1.8.md +25 -0
  264. package/docs/enterprise-product-roadmap-ru.md +1879 -0
  265. package/docs/fundraising/README.md +87 -0
  266. package/docs/fundraising/data-room-checklist.md +72 -0
  267. package/docs/fundraising/demo-script.md +52 -0
  268. package/docs/fundraising/design-partners-50.md +74 -0
  269. package/docs/fundraising/launch-posts.md +37 -0
  270. package/docs/fundraising/metrics-and-evidence.md +30 -0
  271. package/docs/fundraising/roadmap-12-month.md +25 -0
  272. package/docs/fundraising/technical-due-diligence-index.md +37 -0
  273. package/docs/fundraising/yc-application.md +25 -0
  274. package/docs/github-actions.md +84 -0
  275. package/docs/hosted-pilot.md +111 -0
  276. package/docs/mcp.md +52 -50
  277. package/docs/operations/disaster-recovery-runbook.md +81 -0
  278. package/docs/operations/kill-switch-runbook.md +32 -0
  279. package/docs/policy.md +61 -0
  280. package/docs/providers/authoring-packs.md +175 -0
  281. package/docs/providers/stripe-core-pack.md +82 -0
  282. package/docs/release-checklist.md +83 -71
  283. package/docs/release-migration-and-rollback.md +63 -0
  284. package/docs/release-readiness.md +39 -0
  285. package/docs/security/action-gateway-threat-model.md +34 -0
  286. package/docs/security/action-ledger-incident-replay-threat-model.md +42 -0
  287. package/docs/security/approval-inbox-threat-model.md +35 -0
  288. package/docs/security/credential-broker-threat-model.md +52 -0
  289. package/docs/security/egress-threat-model.md +126 -0
  290. package/docs/security/inventory-threat-model.md +104 -0
  291. package/docs/security/kill-switch-budgets-threat-model.md +37 -0
  292. package/docs/security/reliability-threat-model.md +111 -0
  293. package/docs/security/trust-ladder-threat-model.md +33 -0
  294. package/docs/team-control-plane.md +72 -0
  295. package/docs/usage.md +571 -57
  296. package/examples/README.md +10 -0
  297. package/examples/agent-instructions/README.md +39 -39
  298. package/examples/ci-smoke/README.md +21 -0
  299. package/examples/ci-smoke/ghostapi.policy.yaml +22 -0
  300. package/examples/ci-smoke/package.json +9 -0
  301. package/examples/ci-smoke/production-egress.mjs +2 -0
  302. package/examples/ci-smoke/safe.mjs +17 -0
  303. package/examples/evals/README.md +24 -0
  304. package/examples/evals/retry-after.eval.json +52 -0
  305. package/examples/generic-rest/README.md +27 -27
  306. package/examples/github-fetch/README.md +35 -35
  307. package/examples/openai-streaming/README.md +16 -0
  308. package/examples/openai-streaming/streaming-tool-call.mjs +31 -0
  309. package/examples/policy/ghostapi.policy.yaml +27 -0
  310. package/examples/record-replay/README.md +19 -0
  311. package/examples/record-replay/replay-requests.json +22 -0
  312. package/examples/record-replay/stripe-checkout.har.json +30 -0
  313. package/examples/resend-node/README.md +22 -22
  314. package/examples/stripe-node/README.md +35 -29
  315. package/examples/stripe-node/checkout-flow.mjs +61 -0
  316. package/examples/twilio-node/README.md +21 -21
  317. package/examples/worlds/README.md +19 -0
  318. package/examples/worlds/subscription-recovery.mjs +12 -0
  319. package/package.json +27 -12
  320. package/dist/providers/stripe.d.ts +0 -2
  321. package/dist/providers/stripe.js +0 -15
  322. package/dist/providers/stripe.js.map +0 -1
  323. package/docs/assets/ghostapi-avatar.png +0 -0
@@ -0,0 +1,1044 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, readFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { getDataPaths } from "../config/dataPaths.js";
5
+ import { atomicWriteJson, ensurePrivateDirectory, withFileLock } from "../storage/fileStore.js";
6
+ import { sanitizeSecretString } from "../security/secrets.js";
7
+ import { normalizeFreshnessDays } from "./analysis.js";
8
+ import { InventoryError, INVENTORY_LIMITS, buildRecordFreshness, buildRecordProvenance, canonicalPayloadDigest, clone, edgeIdFor, emptyInventoryState, freshnessStatusFor, validateImportPayload, validateOperator, validateState } from "./types.js";
9
+ import { computeBlastRadius, computeFindings, computeRemovalAnalysis, computeRoiReport, findAttackPaths, findingKey, graphEdges } from "./analysis.js";
10
+ export class LocalInventoryController {
11
+ path;
12
+ now;
13
+ operatorAuthorizer;
14
+ freshnessDays;
15
+ evalScenarioExists;
16
+ constructor(options = {}) {
17
+ this.path = options.path ?? getDataPaths().inventoryStore;
18
+ this.now = options.now ?? (() => new Date());
19
+ this.operatorAuthorizer = options.operatorAuthorizer ?? createDisabledInventoryOperatorAuthorizer();
20
+ this.freshnessDays = options.freshnessDays;
21
+ this.evalScenarioExists = options.evalScenarioExists;
22
+ }
23
+ async import(identityValue, payloadValue) {
24
+ const operator = await this.authorize(identityValue, "inventory.import");
25
+ const payload = validateImportPayload(payloadValue);
26
+ const now = this.timestamp();
27
+ const digest = canonicalPayloadDigest(payload);
28
+ const runId = `import-${randomUUID().replace(/-/g, "").slice(0, 32)}`;
29
+ return this.mutate((state) => {
30
+ this.upsertSource(state, operator.tenantId, payload, now);
31
+ const before = new Map();
32
+ for (const edge of state.edges)
33
+ before.set(edge.edgeId, edge);
34
+ const upserted = [];
35
+ const counts = { agents: 0, tools: 0, identities: 0, providers: 0, resources: 0, sideEffects: 0, credentials: 0, policies: 0 };
36
+ const imported = new ImportedRecords(payload);
37
+ for (const input of payload.agents ?? []) {
38
+ upsertAgent(state, operator.tenantId, input, payload, operator.principalId, now);
39
+ upserted.push({ kind: "agent", id: input.agentId });
40
+ counts.agents += 1;
41
+ }
42
+ for (const input of payload.tools ?? []) {
43
+ upsertTool(state, operator.tenantId, input, payload, operator.principalId, now);
44
+ upserted.push({ kind: "tool", id: input.toolId });
45
+ counts.tools += 1;
46
+ }
47
+ for (const input of payload.identities ?? []) {
48
+ upsertIdentity(state, operator.tenantId, input, payload, operator.principalId, now);
49
+ upserted.push({ kind: "identity", id: input.identityId });
50
+ counts.identities += 1;
51
+ }
52
+ for (const input of payload.providers ?? []) {
53
+ upsertProvider(state, operator.tenantId, input, payload, operator.principalId, now);
54
+ upserted.push({ kind: "provider", id: input.providerId });
55
+ counts.providers += 1;
56
+ }
57
+ for (const input of payload.resources ?? []) {
58
+ upsertResource(state, operator.tenantId, input, payload, operator.principalId, now);
59
+ upserted.push({ kind: "resource", id: input.resourceId });
60
+ counts.resources += 1;
61
+ }
62
+ for (const input of payload.sideEffects ?? []) {
63
+ upsertSideEffect(state, operator.tenantId, input, payload, operator.principalId, now);
64
+ upserted.push({ kind: "side_effect", id: input.sideEffectId });
65
+ counts.sideEffects += 1;
66
+ }
67
+ for (const input of payload.credentials ?? []) {
68
+ upsertCredential(state, operator.tenantId, input, payload, operator.principalId, now);
69
+ upserted.push({ kind: "credential", id: input.credentialId });
70
+ counts.credentials += 1;
71
+ }
72
+ for (const input of payload.policies ?? []) {
73
+ upsertPolicy(state, operator.tenantId, input, payload, operator.principalId, now);
74
+ upserted.push({ kind: "policy", id: input.policyId });
75
+ counts.policies += 1;
76
+ }
77
+ validateCrossReferences(state, operator.tenantId, imported);
78
+ const outcome = upsertEdges(state, operator.tenantId, upserted, payload, operator.principalId, now, before);
79
+ this.pruneStaleEdges(state, operator.tenantId, now);
80
+ this.assertStoreBounds(state);
81
+ state.importRuns.push({
82
+ schemaVersion: 1,
83
+ kind: "ghostapi.inventory-import-run",
84
+ tenantId: operator.tenantId,
85
+ runId,
86
+ importedAt: now,
87
+ importedBy: operator.principalId,
88
+ source: { sourceId: payload.source.sourceId, sourceType: payload.source.sourceType, sourceName: payload.source.sourceName, ...(payload.source.sourceVersion === undefined ? {} : { sourceVersion: payload.source.sourceVersion }) },
89
+ digest,
90
+ expectedPolicyHashes: payload.expectedPolicyHashes ?? [],
91
+ ...(payload.counters === undefined ? {} : { counters: payload.counters }),
92
+ recordCounts: counts,
93
+ edgesCreated: outcome.edgesCreated,
94
+ edgesRefreshed: outcome.edgesRefreshed,
95
+ status: "completed"
96
+ });
97
+ this.rotateImportRuns(state, operator.tenantId);
98
+ return { runId, importedAt: now, digest, recordCounts: counts, edgesCreated: outcome.edgesCreated, edgesRefreshed: outcome.edgesRefreshed };
99
+ });
100
+ }
101
+ async inspect(identityValue) {
102
+ const operator = await this.authorize(identityValue, "inventory.inspect");
103
+ const state = await this.read();
104
+ return snapshotFor(state, operator.tenantId, this.timestamp(), this.freshnessDays);
105
+ }
106
+ async analyze(identityValue) {
107
+ const operator = await this.authorize(identityValue, "inventory.analyze");
108
+ const now = this.timestamp();
109
+ return this.mutate((state) => {
110
+ const previous = new Map();
111
+ for (const finding of state.findings) {
112
+ if (finding.tenantId === operator.tenantId)
113
+ previous.set(findingKey(finding), finding);
114
+ }
115
+ const computed = computeFindings(state, operator.tenantId, now, this.freshnessDays);
116
+ let resolved = 0;
117
+ const appliedRemediations = state.remediations.filter((remediation) => remediation.tenantId === operator.tenantId && remediation.status === "applied");
118
+ const nextFindings = computed.map((finding) => {
119
+ const prior = previous.get(findingKey(finding));
120
+ const matchingRemediation = appliedRemediations.find((remediation) => remediation.findingId === finding.findingId && remediation.targetKind === finding.targetKind && remediation.targetId === finding.targetId);
121
+ if (matchingRemediation !== undefined) {
122
+ if (this.isRemediationEffective(state, operator.tenantId, matchingRemediation)) {
123
+ resolved += 1;
124
+ return { ...finding, status: "resolved", resolvedAt: matchingRemediation.appliedAt, remediationId: matchingRemediation.remediationId };
125
+ }
126
+ return finding;
127
+ }
128
+ if (prior !== undefined && prior.status === "resolved") {
129
+ return { ...finding, status: "resolved", resolvedAt: prior.resolvedAt, ...(prior.remediationId === undefined ? {} : { remediationId: prior.remediationId }) };
130
+ }
131
+ return finding;
132
+ });
133
+ state.findings = [...state.findings.filter((finding) => finding.tenantId !== operator.tenantId), ...nextFindings];
134
+ this.assertStoreBounds(state);
135
+ return { findings: nextFindings.length, resolved };
136
+ });
137
+ }
138
+ async graph(identityValue) {
139
+ const operator = await this.authorize(identityValue, "inventory.inspect");
140
+ const state = await this.read();
141
+ return graphEdges(state, operator.tenantId, this.timestamp(), this.freshnessDays);
142
+ }
143
+ async attackPaths(identityValue, agentId) {
144
+ const operator = await this.authorize(identityValue, "inventory.inspect");
145
+ const state = await this.read();
146
+ return findAttackPaths(state, operator.tenantId, safeIdentifier(agentId, "Agent id"), this.timestamp(), this.freshnessDays);
147
+ }
148
+ async blastRadius(identityValue, agentId) {
149
+ const operator = await this.authorize(identityValue, "inventory.inspect");
150
+ const state = await this.read();
151
+ return computeBlastRadius(state, operator.tenantId, safeIdentifier(agentId, "Agent id"), this.timestamp(), this.freshnessDays);
152
+ }
153
+ async removalAnalysis(identityValue) {
154
+ const operator = await this.authorize(identityValue, "inventory.analyze");
155
+ const state = await this.read();
156
+ return computeRemovalAnalysis(state, operator.tenantId, this.timestamp(), this.freshnessDays);
157
+ }
158
+ async roiReport(identityValue) {
159
+ const operator = await this.authorize(identityValue, "inventory.analyze");
160
+ const state = await this.read();
161
+ return computeRoiReport(state, operator.tenantId, this.timestamp(), this.freshnessDays);
162
+ }
163
+ async proposeRemediation(identityValue, input) {
164
+ const operator = await this.authorize(identityValue, "inventory.remediate");
165
+ const proposal = validateRemediationProposal(input);
166
+ const now = this.timestamp();
167
+ return this.mutate((state) => {
168
+ const finding = state.findings.find((candidate) => candidate.tenantId === operator.tenantId && candidate.findingId === proposal.findingId);
169
+ if (finding === undefined)
170
+ throw new InventoryError("Remediation finding was not found for the tenant.");
171
+ if (finding.status === "resolved")
172
+ throw new InventoryError("Cannot propose remediation for a resolved finding.");
173
+ if (proposal.targetKind !== finding.targetKind || proposal.targetId !== finding.targetId)
174
+ throw new InventoryError("Remediation target does not match the finding target.");
175
+ this.assertRemediationTargetExists(state, operator.tenantId, proposal.targetKind, proposal.targetId);
176
+ if (proposal.kind === "reduce_scope") {
177
+ if (proposal.targetKind !== "credential")
178
+ throw new InventoryError("reduce_scope remediation only supports credential targets.");
179
+ const credential = state.credentials.find((candidate) => candidate.tenantId === operator.tenantId && candidate.credentialId === proposal.targetId);
180
+ if (credential === undefined)
181
+ throw new InventoryError("Remediation target credential does not exist.");
182
+ if (credential.status !== "active")
183
+ throw new InventoryError("Cannot reduce scopes on a revoked credential.");
184
+ const reduced = proposal.reducedScopes;
185
+ for (const scope of reduced)
186
+ if (!credential.grantScopes.includes(scope))
187
+ throw new InventoryError("Reduced scopes must be a subset of the current grant scopes; permissions are never expanded.");
188
+ if (reduced.length >= credential.grantScopes.length)
189
+ throw new InventoryError("reduce_scope must remove at least one scope; permissions are never expanded.");
190
+ }
191
+ if (proposal.kind === "create_eval" && this.evalScenarioExists !== undefined && !this.evalScenarioExists(proposal.evalScenarioId))
192
+ throw new InventoryError("Eval scenario was not found for create_eval remediation.");
193
+ if (state.remediations.length >= INVENTORY_LIMITS.maxRemediations)
194
+ throw new InventoryError("Remediation limit was reached.");
195
+ const remediationId = `remediation-${randomUUID().replace(/-/g, "").slice(0, 32)}`;
196
+ const record = {
197
+ schemaVersion: 1,
198
+ tenantId: operator.tenantId,
199
+ remediationId,
200
+ findingId: finding.findingId,
201
+ kind: proposal.kind,
202
+ targetKind: proposal.targetKind,
203
+ targetId: proposal.targetId,
204
+ rationale: proposal.rationale,
205
+ proposedBy: operator.principalId,
206
+ createdAt: now,
207
+ status: "proposed",
208
+ ...(proposal.ownerId === undefined && proposal.reducedScopes === undefined && proposal.evalScenarioId === undefined ? {} : { result: remediationResult(proposal, operator.principalId) })
209
+ };
210
+ state.remediations.push(record);
211
+ this.assertStoreBounds(state);
212
+ return clone(record);
213
+ });
214
+ }
215
+ async applyRemediation(identityValue, remediationId) {
216
+ const operator = await this.authorize(identityValue, "inventory.remediate");
217
+ const now = this.timestamp();
218
+ const safeRemediationId = safeIdentifier(remediationId, "Remediation id");
219
+ return this.mutate((state) => {
220
+ const index = state.remediations.findIndex((candidate) => candidate.tenantId === operator.tenantId && candidate.remediationId === safeRemediationId);
221
+ if (index === -1)
222
+ throw new InventoryError("Remediation was not found for the tenant.");
223
+ const remediation = state.remediations[index];
224
+ if (remediation.status === "applied")
225
+ throw new InventoryError("Remediation was already applied.");
226
+ if (remediation.status === "rejected" || remediation.status === "expired")
227
+ throw new InventoryError(`Remediation is ${remediation.status} and cannot be applied.`);
228
+ applyRemediationEffect(state, operator.tenantId, remediation, now);
229
+ const applied = { ...remediation, status: "applied", appliedAt: now };
230
+ state.remediations[index] = applied;
231
+ const finding = state.findings.find((candidate) => candidate.tenantId === operator.tenantId && candidate.findingId === remediation.findingId);
232
+ if (finding !== undefined) {
233
+ finding.status = "resolved";
234
+ finding.resolvedAt = now;
235
+ finding.remediationId = remediation.remediationId;
236
+ }
237
+ this.assertStoreBounds(state);
238
+ return clone(applied);
239
+ });
240
+ }
241
+ async rejectRemediation(identityValue, remediationId) {
242
+ const operator = await this.authorize(identityValue, "inventory.remediate");
243
+ const now = this.timestamp();
244
+ const safeRemediationId = safeIdentifier(remediationId, "Remediation id");
245
+ return this.mutate((state) => {
246
+ const index = state.remediations.findIndex((candidate) => candidate.tenantId === operator.tenantId && candidate.remediationId === safeRemediationId);
247
+ if (index === -1)
248
+ throw new InventoryError("Remediation was not found for the tenant.");
249
+ const remediation = state.remediations[index];
250
+ if (remediation.status === "applied")
251
+ throw new InventoryError("Remediation is already applied and cannot be rejected.");
252
+ const rejected = { ...remediation, status: "rejected" };
253
+ state.remediations[index] = rejected;
254
+ return clone(rejected);
255
+ });
256
+ }
257
+ async listRemediations(identityValue) {
258
+ const operator = await this.authorize(identityValue, "inventory.inspect");
259
+ const state = await this.read();
260
+ return clone(state.remediations.filter((remediation) => remediation.tenantId === operator.tenantId));
261
+ }
262
+ async export(identityValue) {
263
+ const operator = await this.authorize(identityValue, "inventory.export");
264
+ const state = await this.read();
265
+ const now = this.timestamp();
266
+ const tenantId = operator.tenantId;
267
+ const snapshot = snapshotFor(state, tenantId, now, this.freshnessDays);
268
+ const scenarioRefs = state.remediations
269
+ .filter((remediation) => remediation.tenantId === tenantId && remediation.status === "applied" && remediation.kind === "create_eval" && remediation.result?.evalScenarioId !== undefined)
270
+ .map((remediation) => ({ evalScenarioId: remediation.result.evalScenarioId, forFindingId: remediation.findingId, createdBy: remediation.proposedBy }));
271
+ const normalized = normalizeFreshnessDays(this.freshnessDays);
272
+ const evidenceMetadata = snapshot.agents.map((agent) => ({
273
+ agentId: agent.agentId,
274
+ lastEvidenceAt: agent.lastEvidenceAt,
275
+ status: agent.lastEvidenceAt === null ? "missing" : freshnessStatusFor(agent.lastEvidenceAt, now, normalized.evidenceFreshDays)
276
+ }));
277
+ return {
278
+ schemaVersion: 1,
279
+ kind: "ghostapi.inventory-export",
280
+ tenantId,
281
+ exportedAt: now,
282
+ inventory: snapshot,
283
+ policyRecords: clone(snapshot.policies),
284
+ scenarioRefs,
285
+ evidenceMetadata,
286
+ removalAnalysis: computeRemovalAnalysis(state, tenantId, now, this.freshnessDays),
287
+ roi: computeRoiReport(state, tenantId, now, this.freshnessDays)
288
+ };
289
+ }
290
+ async authorize(identity, permission) {
291
+ const operator = validateOperator(await this.operatorAuthorizer.authenticate(identity));
292
+ if (!operator.permissions.includes(permission))
293
+ throw new InventoryError(`Inventory operator lacks required permission: ${permission}.`);
294
+ return operator;
295
+ }
296
+ timestamp() {
297
+ const value = this.now();
298
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
299
+ throw new InventoryError("Inventory clock is invalid.");
300
+ return value.toISOString();
301
+ }
302
+ upsertSource(state, tenantId, payload, now) {
303
+ const existing = state.sources.find((source) => source.tenantId === tenantId && source.sourceId === payload.source.sourceId);
304
+ if (existing === undefined) {
305
+ if (state.sources.length >= INVENTORY_LIMITS.maxSources)
306
+ throw new InventoryError("Import source limit was reached.");
307
+ state.sources.push({
308
+ tenantId,
309
+ sourceId: payload.source.sourceId,
310
+ sourceType: payload.source.sourceType,
311
+ sourceName: payload.source.sourceName,
312
+ ...(payload.source.sourceVersion === undefined ? {} : { sourceVersion: payload.source.sourceVersion }),
313
+ firstSeenAt: now,
314
+ lastSeenAt: now,
315
+ lastDigest: canonicalPayloadDigest(payload)
316
+ });
317
+ }
318
+ else {
319
+ existing.lastSeenAt = now;
320
+ existing.lastDigest = canonicalPayloadDigest(payload);
321
+ if (payload.source.sourceVersion !== undefined)
322
+ existing.sourceVersion = payload.source.sourceVersion;
323
+ }
324
+ }
325
+ assertRemediationTargetExists(state, tenantId, targetKind, targetId) {
326
+ const exists = targetKind === "agent" ? state.agents.some((record) => record.tenantId === tenantId && record.agentId === targetId)
327
+ : targetKind === "identity" ? state.identities.some((record) => record.tenantId === tenantId && record.identityId === targetId)
328
+ : targetKind === "tool" ? state.tools.some((record) => record.tenantId === tenantId && record.toolId === targetId)
329
+ : targetKind === "provider" ? state.providers.some((record) => record.tenantId === tenantId && record.providerId === targetId)
330
+ : targetKind === "resource" ? state.resources.some((record) => record.tenantId === tenantId && record.resourceId === targetId)
331
+ : targetKind === "side_effect" ? state.sideEffects.some((record) => record.tenantId === tenantId && record.sideEffectId === targetId)
332
+ : targetKind === "credential" ? state.credentials.some((record) => record.tenantId === tenantId && record.credentialId === targetId)
333
+ : targetKind === "environment" ? (state.agents.some((record) => record.tenantId === tenantId && record.environmentIds.includes(targetId)) ||
334
+ state.identities.some((record) => record.tenantId === tenantId && record.environmentIds.includes(targetId)) ||
335
+ state.providers.some((record) => record.tenantId === tenantId && record.environmentIds.includes(targetId)) ||
336
+ state.resources.some((record) => record.tenantId === tenantId && record.environmentIds.includes(targetId)) ||
337
+ state.credentials.some((record) => record.tenantId === tenantId && record.environmentIds.includes(targetId)) ||
338
+ state.policies.some((record) => record.tenantId === tenantId && record.environmentIds.includes(targetId)))
339
+ : targetKind === "policy" ? state.policies.some((record) => record.tenantId === tenantId && record.policyId === targetId)
340
+ : false;
341
+ if (!exists)
342
+ throw new InventoryError(`Remediation target ${targetKind} ${targetId} does not exist for the tenant.`);
343
+ }
344
+ assertStoreBounds(state) {
345
+ if (state.edges.length > INVENTORY_LIMITS.maxEdges)
346
+ throw new InventoryError("Inventory graph edge limit was reached.");
347
+ if (state.findings.length > INVENTORY_LIMITS.maxFindings)
348
+ throw new InventoryError("Inventory finding limit was reached.");
349
+ }
350
+ pruneStaleEdges(state, tenantId, now) {
351
+ const normalized = normalizeFreshnessDays(this.freshnessDays);
352
+ const staleBeforeMs = Date.parse(now) - normalized.edgeStaleDays * 24 * 60 * 60 * 1000;
353
+ state.edges = state.edges.filter((edge) => edge.tenantId !== tenantId || Date.parse(edge.lastSeenAt) >= staleBeforeMs);
354
+ }
355
+ rotateImportRuns(state, tenantId) {
356
+ const tenantRuns = state.importRuns.filter((run) => run.tenantId === tenantId);
357
+ if (tenantRuns.length <= INVENTORY_LIMITS.maxImports)
358
+ return;
359
+ const excess = tenantRuns.length - INVENTORY_LIMITS.maxImports;
360
+ const oldestRunIds = new Set([...tenantRuns].sort((a, b) => Date.parse(a.importedAt) - Date.parse(b.importedAt)).slice(0, excess).map((run) => run.runId));
361
+ state.importRuns = state.importRuns.filter((run) => !oldestRunIds.has(run.runId));
362
+ }
363
+ isRemediationEffective(state, tenantId, remediation) {
364
+ if (remediation.kind === "reduce_scope") {
365
+ if (remediation.targetKind !== "credential")
366
+ return false;
367
+ const credential = state.credentials.find((candidate) => candidate.tenantId === tenantId && candidate.credentialId === remediation.targetId);
368
+ const reduced = remediation.result?.reducedScopes;
369
+ if (credential === undefined || reduced === undefined)
370
+ return false;
371
+ return credential.grantScopes.every((scope) => reduced.includes(scope));
372
+ }
373
+ if (remediation.kind === "revoke") {
374
+ if (remediation.targetKind !== "credential")
375
+ return false;
376
+ const credential = state.credentials.find((candidate) => candidate.tenantId === tenantId && candidate.credentialId === remediation.targetId);
377
+ return credential !== undefined && credential.status === "revoked";
378
+ }
379
+ if (remediation.kind === "assign_owner") {
380
+ const ownerId = remediation.result?.ownerId;
381
+ if (ownerId === undefined)
382
+ return false;
383
+ if (remediation.targetKind === "agent") {
384
+ const record = state.agents.find((candidate) => candidate.tenantId === tenantId && candidate.agentId === remediation.targetId);
385
+ return record !== undefined && record.ownerId === ownerId;
386
+ }
387
+ if (remediation.targetKind === "provider") {
388
+ const record = state.providers.find((candidate) => candidate.tenantId === tenantId && candidate.providerId === remediation.targetId);
389
+ return record !== undefined && record.ownerId === ownerId;
390
+ }
391
+ if (remediation.targetKind === "resource") {
392
+ const record = state.resources.find((candidate) => candidate.tenantId === tenantId && candidate.resourceId === remediation.targetId);
393
+ return record !== undefined && record.ownerId === ownerId;
394
+ }
395
+ if (remediation.targetKind === "credential") {
396
+ const record = state.credentials.find((candidate) => candidate.tenantId === tenantId && candidate.credentialId === remediation.targetId);
397
+ return record !== undefined && record.ownerId === ownerId;
398
+ }
399
+ return false;
400
+ }
401
+ if (remediation.kind === "onboard_through_gateway") {
402
+ if (remediation.targetKind !== "agent")
403
+ return false;
404
+ const record = state.agents.find((candidate) => candidate.tenantId === tenantId && candidate.agentId === remediation.targetId);
405
+ return record !== undefined && record.gatewayManaged === true;
406
+ }
407
+ if (remediation.kind === "create_eval") {
408
+ const scenarioId = remediation.result?.evalScenarioId;
409
+ if (scenarioId === undefined)
410
+ return false;
411
+ return this.evalScenarioExists === undefined ? true : this.evalScenarioExists(scenarioId);
412
+ }
413
+ return false;
414
+ }
415
+ async read() {
416
+ await ensurePrivateDirectory(dirname(this.path));
417
+ const info = await lstat(this.path).catch((error) => isErrorCode(error, "ENOENT") ? null : Promise.reject(error));
418
+ if (info === null)
419
+ return emptyInventoryState();
420
+ if (!info.isFile() || info.isSymbolicLink())
421
+ throw new InventoryError("Inventory store must be a regular non-symlink file.");
422
+ const source = await readFile(this.path, "utf8");
423
+ if (Buffer.byteLength(source, "utf8") > INVENTORY_LIMITS.maxStoreBytes)
424
+ throw new InventoryError("Inventory store exceeds its size limit.");
425
+ try {
426
+ return validateState(JSON.parse(source));
427
+ }
428
+ catch (error) {
429
+ if (error instanceof InventoryError)
430
+ throw error;
431
+ throw new InventoryError("Inventory store is not valid JSON.");
432
+ }
433
+ }
434
+ async mutate(operation) {
435
+ return withFileLock(this.path, async () => {
436
+ const state = await this.read();
437
+ const result = operation(state);
438
+ await atomicWriteJson(this.path, validateState(state));
439
+ return result;
440
+ });
441
+ }
442
+ }
443
+ export function createLocalInventoryController(options = {}) {
444
+ return new LocalInventoryController(options);
445
+ }
446
+ export function createDisabledInventoryOperatorAuthorizer() {
447
+ return { async authenticate() { throw new InventoryError("Inventory operator authorization is not configured."); } };
448
+ }
449
+ export function createTestInventoryOperatorAuthorizer() {
450
+ const issued = new WeakSet();
451
+ return {
452
+ authorizer: {
453
+ async authenticate(identity) {
454
+ if (identity === null || typeof identity !== "object" || !issued.has(identity))
455
+ throw new InventoryError("Inventory operator identity is not authenticated.");
456
+ return validateOperator(identity);
457
+ }
458
+ },
459
+ issue(input) {
460
+ const operator = Object.freeze(validateOperator(input));
461
+ issued.add(operator);
462
+ return operator;
463
+ }
464
+ };
465
+ }
466
+ export function formatImportSummary(summary) {
467
+ return `Inventory import ${summary.runId} at ${summary.importedAt} (digest ${summary.digest.slice(0, 16)}...) added ${summary.recordCounts.agents} agents, ${summary.recordCounts.tools} tools, ${summary.recordCounts.identities} identities, ${summary.recordCounts.providers} providers, ${summary.recordCounts.resources} resources, ${summary.recordCounts.sideEffects} side effects, ${summary.recordCounts.credentials} credentials, ${summary.recordCounts.policies} policies; edges created ${summary.edgesCreated}, refreshed ${summary.edgesRefreshed}.`;
468
+ }
469
+ export function formatAttackPathReport(report) {
470
+ const lines = [`Attack paths for agent ${report.agentId}: ${report.paths.length} path(s), ${report.incompletePaths} incomplete`];
471
+ for (const path of report.paths) {
472
+ const chain = path.steps.map((step) => `${step.kind}:${step.id}`).join(" -> ");
473
+ const edgeNotes = path.edges.length === 0 ? " (no persisted edges)" : ` [edges ${path.edges.map((edge) => `${edge.sourceId}->${edge.targetId} (${edge.relation})`).join(", ")}]`;
474
+ lines.push(` ${path.complete ? "complete" : "INCOMPLETE"} ${chain}${edgeNotes}`);
475
+ }
476
+ return lines.join("\n");
477
+ }
478
+ export function formatBlastRadiusReport(report) {
479
+ const lines = [`Blast radius for agent ${report.agentId}: ${report.resources.length} reachable resource(s), ${report.incompletePaths} incomplete path(s)`];
480
+ for (const resource of report.resources) {
481
+ const effects = resource.sideEffects.map((sideEffect) => `${sideEffect.kind} (${sideEffect.severity})`).join(", ") || "none";
482
+ lines.push(` resource ${resource.resourceId} via provider ${resource.providerId}: ${effects}${resource.sensitive ? " [sensitive]" : ""} (${resource.pathCount} complete path(s))`);
483
+ }
484
+ lines.push(" Rules:");
485
+ for (const rule of report.rules)
486
+ lines.push(` ${rule.ruleId}: ${rule.explanation}`);
487
+ lines.push(` Note: ${report.heuristicNote}`);
488
+ return lines.join("\n");
489
+ }
490
+ export function formatRoiReport(report) {
491
+ const lines = [`ROI report for tenant ${report.tenantId} at ${report.generatedAt} (${report.basis})`];
492
+ lines.push(` Active agents: ${report.activeAgentCount}; production providers: ${report.productionProviderCount}`);
493
+ lines.push(` Incidents: prevented egress attempts ${report.incidents.preventedEgressAttempts}${report.incidents.preventedEgressSources.length === 0 ? " (not measured)" : ""}`);
494
+ lines.push(` Review time: ${report.incidents.reviewTimeMinutes === null ? "not measured" : `${report.incidents.reviewTimeMinutes} minutes`}`);
495
+ lines.push(` Mean time to contain: ${report.incidents.meanTimeToContainMinutes === null ? "not measured" : `${report.incidents.meanTimeToContainMinutes} minutes`}`);
496
+ lines.push(` Remediation outcomes: unused grants removed ${report.remediationOutcomes.unusedGrantsRemoved}, scope reductions ${report.remediationOutcomes.excessiveScopeReductions}, owned grants ${report.remediationOutcomes.ownedGrants}, onboarded agents ${report.remediationOutcomes.onboardedAgents}, evals created ${report.remediationOutcomes.evalsCreated}`);
497
+ lines.push(` Coverage: policy ${report.coverage.policyCoverageBps} bps, evidence ${report.coverage.evidenceCoverageBps} bps, gateway ${report.coverage.gatewayCoverageBps} bps, kill switch ${report.coverage.killSwitchCoverageBps} bps`);
498
+ if (report.notMeasured.length > 0)
499
+ lines.push(` Not measured: ${report.notMeasured.join(", ")}`);
500
+ return lines.join("\n");
501
+ }
502
+ export function formatRemovalAnalysis(report) {
503
+ const lines = [`Removal analysis for tenant ${report.tenantId} at ${report.generatedAt} (${report.basis})`];
504
+ const coverage = report.coverage;
505
+ lines.push(` Coverage: policy ${coverage.policyCoverageBps} bps, evidence ${coverage.evidenceCoverageBps} bps, gateway ${coverage.gatewayCoverageBps} bps, kill switch ${coverage.killSwitchCoverageBps} bps`);
506
+ lines.push(` Direct credentials: ${coverage.directCredentials}; outside-gateway agents: ${coverage.outsideGatewayAgents}; unowned production: ${coverage.unownedProduction}`);
507
+ if (report.lostWithoutGhostApi.length === 0)
508
+ lines.push(" (no verifiable value currently depends on GhostAPI in this inventory)");
509
+ for (const statement of report.lostWithoutGhostApi)
510
+ lines.push(` - ${statement}`);
511
+ return lines.join("\n");
512
+ }
513
+ class ImportedRecords {
514
+ agentIds = new Set();
515
+ toolIds = new Set();
516
+ identityIds = new Set();
517
+ providerIds = new Set();
518
+ resourceIds = new Set();
519
+ sideEffectIds = new Set();
520
+ credentialIds = new Set();
521
+ policyIds = new Set();
522
+ constructor(payload) {
523
+ for (const record of payload.agents ?? [])
524
+ this.agentIds.add(record.agentId);
525
+ for (const record of payload.tools ?? [])
526
+ this.toolIds.add(record.toolId);
527
+ for (const record of payload.identities ?? [])
528
+ this.identityIds.add(record.identityId);
529
+ for (const record of payload.providers ?? [])
530
+ this.providerIds.add(record.providerId);
531
+ for (const record of payload.resources ?? [])
532
+ this.resourceIds.add(record.resourceId);
533
+ for (const record of payload.sideEffects ?? [])
534
+ this.sideEffectIds.add(record.sideEffectId);
535
+ for (const record of payload.credentials ?? [])
536
+ this.credentialIds.add(record.credentialId);
537
+ for (const record of payload.policies ?? [])
538
+ this.policyIds.add(record.policyId);
539
+ }
540
+ }
541
+ function validateCrossReferences(state, tenantId, imported) {
542
+ const has = (kind, id) => {
543
+ if (kind === "agent")
544
+ return imported.agentIds.has(id) || state.agents.some((record) => record.tenantId === tenantId && record.agentId === id);
545
+ if (kind === "tool")
546
+ return imported.toolIds.has(id) || state.tools.some((record) => record.tenantId === tenantId && record.toolId === id);
547
+ if (kind === "identity")
548
+ return imported.identityIds.has(id) || state.identities.some((record) => record.tenantId === tenantId && record.identityId === id);
549
+ if (kind === "provider")
550
+ return imported.providerIds.has(id) || state.providers.some((record) => record.tenantId === tenantId && record.providerId === id);
551
+ if (kind === "resource")
552
+ return imported.resourceIds.has(id) || state.resources.some((record) => record.tenantId === tenantId && record.resourceId === id);
553
+ if (kind === "side_effect")
554
+ return imported.sideEffectIds.has(id) || state.sideEffects.some((record) => record.tenantId === tenantId && record.sideEffectId === id);
555
+ if (kind === "credential")
556
+ return imported.credentialIds.has(id) || state.credentials.some((record) => record.tenantId === tenantId && record.credentialId === id);
557
+ return imported.policyIds.has(id) || state.policies.some((record) => record.tenantId === tenantId && record.policyId === id);
558
+ };
559
+ for (const agent of importedAgents(state, tenantId)) {
560
+ for (const identityId of agent.identityIds)
561
+ if (!has("identity", identityId))
562
+ throw new InventoryError(`Agent ${agent.agentId} references unknown identity ${identityId}.`);
563
+ }
564
+ for (const identity of importedIdentities(state, tenantId)) {
565
+ for (const toolId of identity.toolIds)
566
+ if (!has("tool", toolId))
567
+ throw new InventoryError(`Identity ${identity.identityId} references unknown tool ${toolId}.`);
568
+ }
569
+ for (const tool of importedTools(state, tenantId)) {
570
+ if (tool.providerId !== null && !has("provider", tool.providerId))
571
+ throw new InventoryError(`Tool ${tool.toolId} references unknown provider ${tool.providerId}.`);
572
+ for (const credentialId of tool.credentialIds)
573
+ if (!has("credential", credentialId))
574
+ throw new InventoryError(`Tool ${tool.toolId} references unknown credential ${credentialId}.`);
575
+ }
576
+ for (const provider of importedProviders(state, tenantId)) {
577
+ for (const resourceId of provider.resourceIds)
578
+ if (!has("resource", resourceId))
579
+ throw new InventoryError(`Provider ${provider.providerId} references unknown resource ${resourceId}.`);
580
+ }
581
+ for (const resource of importedResources(state, tenantId)) {
582
+ for (const sideEffectId of resource.sideEffectIds)
583
+ if (!has("side_effect", sideEffectId))
584
+ throw new InventoryError(`Resource ${resource.resourceId} references unknown side effect ${sideEffectId}.`);
585
+ }
586
+ for (const sideEffect of importedSideEffects(state, tenantId)) {
587
+ if (!has("resource", sideEffect.resourceId))
588
+ throw new InventoryError(`Side effect ${sideEffect.sideEffectId} references unknown resource ${sideEffect.resourceId}.`);
589
+ }
590
+ for (const credential of importedCredentials(state, tenantId)) {
591
+ if (!has("provider", credential.providerId))
592
+ throw new InventoryError(`Credential ${credential.credentialId} references unknown provider ${credential.providerId}.`);
593
+ for (const toolId of credential.toolIds)
594
+ if (!has("tool", toolId))
595
+ throw new InventoryError(`Credential ${credential.credentialId} references unknown tool ${toolId}.`);
596
+ }
597
+ }
598
+ function importedAgents(state, tenantId) {
599
+ return state.agents.filter((record) => record.tenantId === tenantId).map((record) => ({ agentId: record.agentId, identityIds: record.identityIds }));
600
+ }
601
+ function importedIdentities(state, tenantId) {
602
+ return state.identities.filter((record) => record.tenantId === tenantId).map((record) => ({ identityId: record.identityId, toolIds: record.toolIds }));
603
+ }
604
+ function importedTools(state, tenantId) {
605
+ return state.tools.filter((record) => record.tenantId === tenantId).map((record) => ({ toolId: record.toolId, providerId: record.providerId, credentialIds: record.credentialIds }));
606
+ }
607
+ function importedProviders(state, tenantId) {
608
+ return state.providers.filter((record) => record.tenantId === tenantId).map((record) => ({ providerId: record.providerId, resourceIds: record.resourceIds }));
609
+ }
610
+ function importedResources(state, tenantId) {
611
+ return state.resources.filter((record) => record.tenantId === tenantId).map((record) => ({ resourceId: record.resourceId, sideEffectIds: record.sideEffectIds }));
612
+ }
613
+ function importedSideEffects(state, tenantId) {
614
+ return state.sideEffects.filter((record) => record.tenantId === tenantId).map((record) => ({ sideEffectId: record.sideEffectId, resourceId: record.resourceId }));
615
+ }
616
+ function importedCredentials(state, tenantId) {
617
+ return state.credentials.filter((record) => record.tenantId === tenantId).map((record) => ({ credentialId: record.credentialId, providerId: record.providerId, toolIds: record.toolIds }));
618
+ }
619
+ function upsertAgent(state, tenantId, input, payload, importedBy, now) {
620
+ const record = {
621
+ schemaVersion: 1,
622
+ kind: "ghostapi.inventory-agent",
623
+ tenantId,
624
+ agentId: input.agentId,
625
+ name: input.name,
626
+ ownerId: input.ownerId === undefined ? null : input.ownerId,
627
+ ...(input.businessPurpose === undefined ? {} : { businessPurpose: input.businessPurpose }),
628
+ ...(input.model === undefined ? {} : { model: input.model }),
629
+ ...(input.runtime === undefined ? {} : { runtime: input.runtime }),
630
+ ...(input.version === undefined ? {} : { version: input.version }),
631
+ identityIds: input.identityIds,
632
+ environmentIds: input.environmentIds,
633
+ gatewayManaged: input.gatewayManaged,
634
+ killSwitchEnabled: input.killSwitchEnabled,
635
+ lastEvidenceAt: input.lastEvidenceAt === undefined ? null : input.lastEvidenceAt
636
+ };
637
+ merge(state, "agent", input.agentId, tenantId, record, payload.source, importedBy, now);
638
+ }
639
+ function upsertTool(state, tenantId, input, payload, importedBy, now) {
640
+ const record = {
641
+ schemaVersion: 1,
642
+ tenantId,
643
+ toolId: input.toolId,
644
+ name: input.name,
645
+ kind: input.kind,
646
+ providerId: input.providerId === undefined ? null : input.providerId,
647
+ requiredScopes: input.requiredScopes,
648
+ credentialIds: input.credentialIds
649
+ };
650
+ merge(state, "tool", input.toolId, tenantId, record, payload.source, importedBy, now);
651
+ }
652
+ function upsertIdentity(state, tenantId, input, payload, importedBy, now) {
653
+ const record = {
654
+ schemaVersion: 1,
655
+ kind: "ghostapi.inventory-identity",
656
+ tenantId,
657
+ identityId: input.identityId,
658
+ principalId: input.principalId,
659
+ role: input.role,
660
+ agentId: input.agentId === undefined ? null : input.agentId,
661
+ toolIds: input.toolIds,
662
+ environmentIds: input.environmentIds,
663
+ scopes: input.scopes
664
+ };
665
+ merge(state, "identity", input.identityId, tenantId, record, payload.source, importedBy, now);
666
+ }
667
+ function upsertProvider(state, tenantId, input, payload, importedBy, now) {
668
+ const record = {
669
+ schemaVersion: 1,
670
+ kind: "ghostapi.inventory-provider",
671
+ tenantId,
672
+ providerId: input.providerId,
673
+ name: input.name,
674
+ providerType: input.providerType,
675
+ ownerId: input.ownerId === undefined ? null : input.ownerId,
676
+ environmentIds: input.environmentIds,
677
+ resourceIds: input.resourceIds,
678
+ gatewayBound: input.gatewayBound,
679
+ killSwitchApplied: input.killSwitchApplied
680
+ };
681
+ merge(state, "provider", input.providerId, tenantId, record, payload.source, importedBy, now);
682
+ }
683
+ function upsertResource(state, tenantId, input, payload, importedBy, now) {
684
+ const record = {
685
+ schemaVersion: 1,
686
+ kind: "ghostapi.inventory-resource",
687
+ tenantId,
688
+ resourceId: input.resourceId,
689
+ providerId: input.providerId,
690
+ resourceType: input.resourceType,
691
+ name: input.name,
692
+ environmentIds: input.environmentIds,
693
+ sensitive: input.sensitive,
694
+ ownerId: input.ownerId === undefined ? null : input.ownerId,
695
+ ...(input.businessPurpose === undefined ? {} : { businessPurpose: input.businessPurpose }),
696
+ sideEffectIds: input.sideEffectIds
697
+ };
698
+ merge(state, "resource", input.resourceId, tenantId, record, payload.source, importedBy, now);
699
+ }
700
+ function upsertSideEffect(state, tenantId, input, payload, importedBy, now) {
701
+ const record = {
702
+ schemaVersion: 1,
703
+ tenantId,
704
+ sideEffectId: input.sideEffectId,
705
+ resourceId: input.resourceId,
706
+ kind: input.kind,
707
+ severity: input.severity
708
+ };
709
+ merge(state, "side_effect", input.sideEffectId, tenantId, record, payload.source, importedBy, now);
710
+ }
711
+ function upsertCredential(state, tenantId, input, payload, importedBy, now) {
712
+ const record = {
713
+ schemaVersion: 1,
714
+ kind: "ghostapi.inventory-credential",
715
+ tenantId,
716
+ credentialId: input.credentialId,
717
+ name: input.name,
718
+ providerId: input.providerId,
719
+ ownerId: input.ownerId === undefined ? null : input.ownerId,
720
+ environmentIds: input.environmentIds,
721
+ grantScopes: input.grantScopes,
722
+ toolIds: input.toolIds,
723
+ status: input.status,
724
+ issuedAt: input.issuedAt,
725
+ lastUsedAt: input.lastUsedAt === undefined ? null : input.lastUsedAt,
726
+ ...(input.rotatedAt === undefined ? {} : { rotatedAt: input.rotatedAt }),
727
+ gatewayBound: input.gatewayBound
728
+ };
729
+ merge(state, "credential", input.credentialId, tenantId, record, payload.source, importedBy, now);
730
+ }
731
+ function upsertPolicy(state, tenantId, input, payload, importedBy, now) {
732
+ const record = {
733
+ schemaVersion: 1,
734
+ kind: "ghostapi.inventory-policy",
735
+ tenantId,
736
+ policyId: input.policyId,
737
+ name: input.name,
738
+ version: input.version,
739
+ hash: input.hash,
740
+ environmentIds: input.environmentIds
741
+ };
742
+ merge(state, "policy", input.policyId, tenantId, record, payload.source, importedBy, now);
743
+ }
744
+ function merge(state, kind, id, tenantId, record, source, importedBy, now) {
745
+ const list = recordList(state, kind);
746
+ const index = list.findIndex((candidate) => candidate.tenantId === tenantId && idForKind(kind, candidate) === id);
747
+ const provenance = buildRecordProvenance(source, now, importedBy);
748
+ const freshness = buildRecordFreshness(now);
749
+ const stored = { ...record, provenance, freshness };
750
+ if (index === -1) {
751
+ if (list.length >= recordLimit(kind))
752
+ throw new InventoryError(`${kind} record limit was reached.`);
753
+ list.push(stored);
754
+ }
755
+ else {
756
+ const prior = list[index];
757
+ const mergedFreshness = { firstSeenAt: prior.freshness?.firstSeenAt ?? now, lastSeenAt: now };
758
+ list[index] = { ...stored, freshness: mergedFreshness, provenance };
759
+ }
760
+ }
761
+ function idForKind(kind, record) {
762
+ switch (kind) {
763
+ case "agent":
764
+ return typeof record.agentId === "string" ? record.agentId : "";
765
+ case "tool":
766
+ return typeof record.toolId === "string" ? record.toolId : "";
767
+ case "identity":
768
+ return typeof record.identityId === "string" ? record.identityId : "";
769
+ case "provider":
770
+ return typeof record.providerId === "string" ? record.providerId : "";
771
+ case "resource":
772
+ return typeof record.resourceId === "string" ? record.resourceId : "";
773
+ case "side_effect":
774
+ return typeof record.sideEffectId === "string" ? record.sideEffectId : "";
775
+ case "credential":
776
+ return typeof record.credentialId === "string" ? record.credentialId : "";
777
+ case "policy":
778
+ return typeof record.policyId === "string" ? record.policyId : "";
779
+ }
780
+ }
781
+ function recordLimit(kind) {
782
+ switch (kind) {
783
+ case "agent":
784
+ return INVENTORY_LIMITS.maxAgents;
785
+ case "tool":
786
+ return INVENTORY_LIMITS.maxTools;
787
+ case "identity":
788
+ return INVENTORY_LIMITS.maxIdentities;
789
+ case "provider":
790
+ return INVENTORY_LIMITS.maxProviders;
791
+ case "resource":
792
+ return INVENTORY_LIMITS.maxResources;
793
+ case "side_effect":
794
+ return INVENTORY_LIMITS.maxSideEffects;
795
+ case "credential":
796
+ return INVENTORY_LIMITS.maxCredentials;
797
+ case "policy":
798
+ return INVENTORY_LIMITS.maxPolicies;
799
+ }
800
+ }
801
+ function recordList(state, kind) {
802
+ if (kind === "agent")
803
+ return state.agents;
804
+ if (kind === "tool")
805
+ return state.tools;
806
+ if (kind === "identity")
807
+ return state.identities;
808
+ if (kind === "provider")
809
+ return state.providers;
810
+ if (kind === "resource")
811
+ return state.resources;
812
+ if (kind === "side_effect")
813
+ return state.sideEffects;
814
+ if (kind === "credential")
815
+ return state.credentials;
816
+ return state.policies;
817
+ }
818
+ function upsertEdges(state, tenantId, upserted, payload, importedBy, now, before) {
819
+ const provenance = buildRecordProvenance(payload.source, now, importedBy);
820
+ let edgesCreated = 0;
821
+ let edgesRefreshed = 0;
822
+ const apply = (sourceKind, sourceId, targetKind, targetId, relation) => {
823
+ const edgeId = edgeIdFor(sourceKind, sourceId, targetKind, targetId, relation);
824
+ const prior = before.get(edgeId);
825
+ if (prior === undefined) {
826
+ state.edges.push({ tenantId, edgeId, sourceKind, sourceId, targetKind, targetId, relation, provenance, firstSeenAt: now, lastSeenAt: now });
827
+ edgesCreated += 1;
828
+ }
829
+ else {
830
+ state.edges = state.edges.map((edge) => (edge.edgeId === edgeId ? { ...edge, lastSeenAt: now } : edge));
831
+ edgesRefreshed += 1;
832
+ }
833
+ };
834
+ for (const node of upserted) {
835
+ if (node.kind === "agent") {
836
+ const agent = state.agents.find((record) => record.tenantId === tenantId && record.agentId === node.id);
837
+ if (agent === undefined)
838
+ continue;
839
+ for (const identityId of agent.identityIds)
840
+ apply("agent", agent.agentId, "identity", identityId, "owns");
841
+ }
842
+ else if (node.kind === "identity") {
843
+ const identity = state.identities.find((record) => record.tenantId === tenantId && record.identityId === node.id);
844
+ if (identity === undefined)
845
+ continue;
846
+ for (const toolId of identity.toolIds)
847
+ apply("identity", identity.identityId, "tool", toolId, "uses");
848
+ }
849
+ else if (node.kind === "tool") {
850
+ const tool = state.tools.find((record) => record.tenantId === tenantId && record.toolId === node.id);
851
+ if (tool === undefined)
852
+ continue;
853
+ if (tool.providerId !== null)
854
+ apply("tool", tool.toolId, "provider", tool.providerId, "connects");
855
+ for (const credentialId of tool.credentialIds)
856
+ apply("tool", tool.toolId, "credential", credentialId, "requires");
857
+ }
858
+ else if (node.kind === "provider") {
859
+ const provider = state.providers.find((record) => record.tenantId === tenantId && record.providerId === node.id);
860
+ if (provider === undefined)
861
+ continue;
862
+ for (const resourceId of provider.resourceIds)
863
+ apply("provider", provider.providerId, "resource", resourceId, "exposes");
864
+ }
865
+ else if (node.kind === "resource") {
866
+ const resource = state.resources.find((record) => record.tenantId === tenantId && record.resourceId === node.id);
867
+ if (resource === undefined)
868
+ continue;
869
+ for (const sideEffectId of resource.sideEffectIds)
870
+ apply("resource", resource.resourceId, "side_effect", sideEffectId, "permits");
871
+ }
872
+ }
873
+ return { recordCounts: { agents: 0, tools: 0, identities: 0, providers: 0, resources: 0, sideEffects: 0, credentials: 0, policies: 0 }, edgesCreated, edgesRefreshed };
874
+ }
875
+ function snapshotFor(state, tenantId, now, freshnessDays) {
876
+ return {
877
+ schemaVersion: 1,
878
+ kind: "ghostapi.inventory-snapshot",
879
+ tenantId,
880
+ agents: clone(state.agents.filter((record) => record.tenantId === tenantId)),
881
+ tools: clone(state.tools.filter((record) => record.tenantId === tenantId)),
882
+ identities: clone(state.identities.filter((record) => record.tenantId === tenantId)),
883
+ providers: clone(state.providers.filter((record) => record.tenantId === tenantId)),
884
+ resources: clone(state.resources.filter((record) => record.tenantId === tenantId)),
885
+ sideEffects: clone(state.sideEffects.filter((record) => record.tenantId === tenantId)),
886
+ credentials: clone(state.credentials.filter((record) => record.tenantId === tenantId)),
887
+ policies: clone(state.policies.filter((record) => record.tenantId === tenantId)),
888
+ edges: graphEdges(state, tenantId, now, freshnessDays),
889
+ findings: clone(state.findings.filter((record) => record.tenantId === tenantId)),
890
+ remediations: clone(state.remediations.filter((record) => record.tenantId === tenantId)),
891
+ sources: clone(state.sources.filter((record) => record.tenantId === tenantId)),
892
+ importRuns: clone(state.importRuns.filter((record) => record.tenantId === tenantId))
893
+ };
894
+ }
895
+ function applyRemediationEffect(state, tenantId, remediation, now) {
896
+ const result = remediation.result;
897
+ if (remediation.kind === "assign_owner") {
898
+ if (result === undefined || result.ownerId === undefined)
899
+ throw new InventoryError("assign_owner remediation requires an owner id.");
900
+ if (remediation.targetKind === "agent") {
901
+ const record = state.agents.find((candidate) => candidate.tenantId === tenantId && candidate.agentId === remediation.targetId);
902
+ if (record === undefined)
903
+ throw new InventoryError("Remediation target agent does not exist.");
904
+ record.ownerId = result.ownerId;
905
+ }
906
+ else if (remediation.targetKind === "provider") {
907
+ const record = state.providers.find((candidate) => candidate.tenantId === tenantId && candidate.providerId === remediation.targetId);
908
+ if (record === undefined)
909
+ throw new InventoryError("Remediation target provider does not exist.");
910
+ record.ownerId = result.ownerId;
911
+ }
912
+ else if (remediation.targetKind === "resource") {
913
+ const record = state.resources.find((candidate) => candidate.tenantId === tenantId && candidate.resourceId === remediation.targetId);
914
+ if (record === undefined)
915
+ throw new InventoryError("Remediation target resource does not exist.");
916
+ record.ownerId = result.ownerId;
917
+ }
918
+ else if (remediation.targetKind === "credential") {
919
+ const record = state.credentials.find((candidate) => candidate.tenantId === tenantId && candidate.credentialId === remediation.targetId);
920
+ if (record === undefined)
921
+ throw new InventoryError("Remediation target credential does not exist.");
922
+ record.ownerId = result.ownerId;
923
+ }
924
+ else {
925
+ throw new InventoryError("assign_owner remediation only supports agent, provider, resource, or credential targets.");
926
+ }
927
+ }
928
+ else if (remediation.kind === "reduce_scope") {
929
+ if (remediation.targetKind !== "credential")
930
+ throw new InventoryError("reduce_scope remediation only supports credential targets.");
931
+ if (result === undefined || result.reducedScopes === undefined)
932
+ throw new InventoryError("reduce_scope remediation requires the reduced scope list.");
933
+ const record = state.credentials.find((candidate) => candidate.tenantId === tenantId && candidate.credentialId === remediation.targetId);
934
+ if (record === undefined)
935
+ throw new InventoryError("Remediation target credential does not exist.");
936
+ if (record.status !== "active")
937
+ throw new InventoryError("Cannot reduce scopes on a revoked credential.");
938
+ const current = record.grantScopes;
939
+ const reduced = result.reducedScopes;
940
+ for (const scope of reduced)
941
+ if (!current.includes(scope))
942
+ throw new InventoryError("Reduced scopes must be a subset of the current grant scopes; permissions are never expanded.");
943
+ if (reduced.length >= current.length)
944
+ throw new InventoryError("reduce_scope must remove at least one scope; permissions are never expanded.");
945
+ record.grantScopes = reduced;
946
+ }
947
+ else if (remediation.kind === "revoke") {
948
+ if (remediation.targetKind !== "credential")
949
+ throw new InventoryError("revoke remediation only supports credential targets.");
950
+ const record = state.credentials.find((candidate) => candidate.tenantId === tenantId && candidate.credentialId === remediation.targetId);
951
+ if (record === undefined)
952
+ throw new InventoryError("Remediation target credential does not exist.");
953
+ record.status = "revoked";
954
+ }
955
+ else if (remediation.kind === "onboard_through_gateway") {
956
+ if (remediation.targetKind !== "agent")
957
+ throw new InventoryError("onboard_through_gateway remediation only supports agent targets.");
958
+ const record = state.agents.find((candidate) => candidate.tenantId === tenantId && candidate.agentId === remediation.targetId);
959
+ if (record === undefined)
960
+ throw new InventoryError("Remediation target agent does not exist.");
961
+ record.gatewayManaged = true;
962
+ record.freshness = { ...record.freshness, lastSeenAt: now };
963
+ }
964
+ else if (remediation.kind === "create_eval") {
965
+ if (remediation.targetKind !== "agent")
966
+ throw new InventoryError("create_eval remediation only supports agent targets.");
967
+ if (result === undefined || result.evalScenarioId === undefined)
968
+ throw new InventoryError("create_eval remediation requires an eval scenario id.");
969
+ if (!state.agents.some((record) => record.tenantId === tenantId && record.agentId === remediation.targetId))
970
+ throw new InventoryError("Remediation target agent does not exist.");
971
+ }
972
+ }
973
+ function remediationResult(proposal, proposedBy) {
974
+ return {
975
+ description: `Proposed by ${proposedBy}`,
976
+ ...(proposal.ownerId === undefined ? {} : { ownerId: proposal.ownerId }),
977
+ ...(proposal.reducedScopes === undefined ? {} : { reducedScopes: proposal.reducedScopes }),
978
+ ...(proposal.evalScenarioId === undefined ? {} : { evalScenarioId: proposal.evalScenarioId })
979
+ };
980
+ }
981
+ function validateRemediationProposal(value) {
982
+ const proposal = object(value, "Remediation proposal must be an object.");
983
+ exactKeys(proposal, ["findingId", "kind", "targetKind", "targetId", "rationale", "ownerId", "reducedScopes", "evalScenarioId"], "remediation proposal", ["ownerId", "reducedScopes", "evalScenarioId"]);
984
+ const kind = proposal.kind;
985
+ if (!["assign_owner", "reduce_scope", "revoke", "onboard_through_gateway", "create_eval"].includes(kind))
986
+ throw new InventoryError("Remediation kind is invalid.");
987
+ const targetKind = proposal.targetKind;
988
+ if (!["agent", "identity", "tool", "provider", "resource", "side_effect", "credential", "environment", "policy"].includes(targetKind))
989
+ throw new InventoryError("Remediation target kind is invalid.");
990
+ const result = {
991
+ findingId: safeIdentifier(proposal.findingId, "Remediation finding id"),
992
+ kind: kind,
993
+ targetKind: targetKind,
994
+ targetId: safeIdentifier(proposal.targetId, "Remediation target id"),
995
+ rationale: safeText(proposal.rationale, "Remediation rationale", 400)
996
+ };
997
+ if (proposal.ownerId !== undefined)
998
+ result.ownerId = safeIdentifier(proposal.ownerId, "Remediation owner id");
999
+ if (proposal.reducedScopes !== undefined)
1000
+ result.reducedScopes = safeScopes(proposal.reducedScopes, "Remediation reduced scopes");
1001
+ if (proposal.evalScenarioId !== undefined)
1002
+ result.evalScenarioId = safeIdentifier(proposal.evalScenarioId, "Remediation eval scenario id");
1003
+ if (result.kind === "assign_owner" && result.ownerId === undefined)
1004
+ throw new InventoryError("assign_owner remediation requires an owner id.");
1005
+ if (result.kind === "reduce_scope" && result.reducedScopes === undefined)
1006
+ throw new InventoryError("reduce_scope remediation requires the reduced scope list.");
1007
+ if (result.kind === "create_eval" && result.evalScenarioId === undefined)
1008
+ throw new InventoryError("create_eval remediation requires an eval scenario id.");
1009
+ return result;
1010
+ }
1011
+ function safeIdentifier(value, label) {
1012
+ if (typeof value !== "string" || !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(value) || sanitizeSecretString(value) !== value)
1013
+ throw new InventoryError(`${label} must be a safe identifier.`);
1014
+ return value;
1015
+ }
1016
+ function safeScopes(value, label) {
1017
+ if (!Array.isArray(value) || value.length > INVENTORY_LIMITS.maxScopesPerRecord)
1018
+ throw new InventoryError(`${label} is invalid.`);
1019
+ return value.map((entry) => {
1020
+ if (typeof entry !== "string" || !/^[a-z0-9][a-z0-9._:\-]{0,127}$/.test(entry) || sanitizeSecretString(entry) !== entry)
1021
+ throw new InventoryError(`${label} must be safe scope identifiers.`);
1022
+ return entry;
1023
+ });
1024
+ }
1025
+ function safeText(value, label, max) {
1026
+ if (typeof value !== "string" || value.trim() === "" || value.length > max || /[\u0000-\u001f]/.test(value) || sanitizeSecretString(value) !== value)
1027
+ throw new InventoryError(`${label} is invalid.`);
1028
+ return value.trim();
1029
+ }
1030
+ function object(value, message) {
1031
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1032
+ throw new InventoryError(message);
1033
+ return value;
1034
+ }
1035
+ function exactKeys(value, keys, label, optional = []) {
1036
+ for (const key of Object.keys(value)) {
1037
+ if (!keys.includes(key) || (value[key] === undefined && !optional.includes(key)))
1038
+ throw new InventoryError(`${label} contains unsupported field: ${key}`);
1039
+ }
1040
+ }
1041
+ function isErrorCode(error, code) {
1042
+ return error instanceof Error && "code" in error && error.code === code;
1043
+ }
1044
+ //# sourceMappingURL=inventoryController.js.map