@thebackstoryis/engineering-with-ai 0.2.9

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 (334) hide show
  1. package/Docs/README.md +50 -0
  2. package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
  3. package/Docs/adoption/non-technical-team-guide.md +126 -0
  4. package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
  5. package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
  6. package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
  7. package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
  8. package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
  9. package/Docs/cli-reference.md +113 -0
  10. package/Docs/completed-phase-evidence-amendments.md +74 -0
  11. package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
  12. package/Docs/context-aware-delivery-companion-guide.md +198 -0
  13. package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
  14. package/Docs/context-management-and-token-efficiency.md +113 -0
  15. package/Docs/design-systems/design-system-implementation-guide.md +85 -0
  16. package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
  17. package/Docs/design-systems/design-system-review-guide.md +51 -0
  18. package/Docs/design-systems/design-system-user-guide.md +96 -0
  19. package/Docs/design-systems/product-owner-guide.md +49 -0
  20. package/Docs/designing-organisation-blueprint-packs.md +384 -0
  21. package/Docs/developer-delivery-guide.md +224 -0
  22. package/Docs/error-reporting-guide.md +110 -0
  23. package/Docs/error-reporting-provider-guide.md +49 -0
  24. package/Docs/examples/error-report-adapter.md +70 -0
  25. package/Docs/examples/meeting-review.md +76 -0
  26. package/Docs/examples/minimal-design-system.md +67 -0
  27. package/Docs/examples/prototype-review-inputs.md +175 -0
  28. package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
  29. package/Docs/examples/test-scenario-input.md +68 -0
  30. package/Docs/examples/worked-examples.md +147 -0
  31. package/Docs/existing-project-onboarding-guide.md +214 -0
  32. package/Docs/explanation/core-concepts.md +26 -0
  33. package/Docs/explanation/delivery-workflow.md +48 -0
  34. package/Docs/governance/governance-team-guide.md +139 -0
  35. package/Docs/governed-starter-project-materialisation-guide.md +284 -0
  36. package/Docs/guide-catalogue.md +117 -0
  37. package/Docs/guided-discovery-facilitator-guide.md +172 -0
  38. package/Docs/guided-intent-workspace-guide.md +109 -0
  39. package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
  40. package/Docs/human-approval-and-assurance-guide.md +146 -0
  41. package/Docs/knowledge-proposals-implementer-guide.md +106 -0
  42. package/Docs/knowledge-proposals-user-guide.md +247 -0
  43. package/Docs/maintainers/context-benchmarks.md +29 -0
  44. package/Docs/maintainers/contributing.md +58 -0
  45. package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
  46. package/Docs/maintainers/verification-walkthroughs.md +104 -0
  47. package/Docs/meeting-evidence-implementer-guide.md +132 -0
  48. package/Docs/meeting-evidence-user-guide.md +200 -0
  49. package/Docs/operations/dashboard-and-delivery-state.md +153 -0
  50. package/Docs/operations/dashboard-configuration.md +87 -0
  51. package/Docs/operations/installation-updating-and-entitlements.md +135 -0
  52. package/Docs/operations/premium-personas-setup.md +76 -0
  53. package/Docs/operations/troubleshooting-and-recovery.md +205 -0
  54. package/Docs/organisation-rollout-guide.md +142 -0
  55. package/Docs/persona-entitlement-provider-guide.md +199 -0
  56. package/Docs/persona-guided-prototype-iteration.md +129 -0
  57. package/Docs/personas/organisation-specific-personas.md +103 -0
  58. package/Docs/personas/persona-authoring-cookbook.md +176 -0
  59. package/Docs/personas/persona-engagement-ui.md +133 -0
  60. package/Docs/personas/persona-governance.md +118 -0
  61. package/Docs/platform-export-analysis-guide.md +336 -0
  62. package/Docs/policies/governance-owner-guide.md +36 -0
  63. package/Docs/policies/implementation-guide.md +42 -0
  64. package/Docs/policies/organisation-policy-design-gates.md +58 -0
  65. package/Docs/policies/policy-pack-authoring-guide.md +108 -0
  66. package/Docs/policies/product-owner-guide.md +43 -0
  67. package/Docs/policies/technical-owner-guide.md +37 -0
  68. package/Docs/product-owner-guide.md +327 -0
  69. package/Docs/project-portfolio-orchestration-guide.md +199 -0
  70. package/Docs/quality/manual-qa-and-acceptance.md +162 -0
  71. package/Docs/quality/persona-driven-test-scenarios.md +172 -0
  72. package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
  73. package/Docs/reference/capabilities-and-project-layout.md +678 -0
  74. package/Docs/reference/cli-and-configuration.md +398 -0
  75. package/Docs/reference/contributions-api.md +23 -0
  76. package/Docs/reference/security-adapter-authoring.md +81 -0
  77. package/Docs/reference/starter-adapter-authoring.md +74 -0
  78. package/Docs/repository-source-map-guide.md +381 -0
  79. package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
  80. package/Docs/screen-prototype-creation-guide.md +324 -0
  81. package/Docs/security-validation-guide.md +353 -0
  82. package/Docs/solution-readiness-review-guide.md +123 -0
  83. package/Docs/standards/project-standards-authoring.md +157 -0
  84. package/Docs/team-hub-guide.md +162 -0
  85. package/Docs/team-hub-resource-registry-guide.md +167 -0
  86. package/Docs/tutorials/first-delivery.md +83 -0
  87. package/Docs/tutorials/first-session.md +62 -0
  88. package/Docs/using-lifecycle-hooks.md +381 -0
  89. package/Docs/working-with-personas.md +274 -0
  90. package/LICENSE +165 -0
  91. package/README.md +96 -0
  92. package/agents-src/claude/ewai-security-reviewer.md +15 -0
  93. package/bin/ewai +5 -0
  94. package/config/archaeology-record-families.yaml +59 -0
  95. package/config/delivery-artifacts.yaml +121 -0
  96. package/config/delivery-stages.yaml +77 -0
  97. package/config/design-system.schema.json +46 -0
  98. package/config/error-reporting.schema.json +79 -0
  99. package/config/evidence-depth.schema.json +53 -0
  100. package/config/intent.schema.json +90 -0
  101. package/config/knowledge-proposals-proposal.schema.json +34 -0
  102. package/config/lifecycle-event.schema.json +68 -0
  103. package/config/lifecycle-handler.schema.json +45 -0
  104. package/config/lifecycle-hook-ack.schema.json +19 -0
  105. package/config/meeting-evidence-candidate.schema.json +102 -0
  106. package/config/organisation-policy.schema.json +137 -0
  107. package/config/pack.schema.json +250 -0
  108. package/config/persona-pack.schema.json +21 -0
  109. package/config/persona.schema.json +17 -0
  110. package/config/policy-evaluation.schema.json +77 -0
  111. package/config/policy-facts.schema.json +140 -0
  112. package/config/portfolio.schema.json +68 -0
  113. package/config/project.schema.json +313 -0
  114. package/config/prototype-iteration.schema.json +128 -0
  115. package/config/rollout.schema.json +87 -0
  116. package/config/security-adapter.schema.json +31 -0
  117. package/config/security-scan-request.schema.json +64 -0
  118. package/config/security-scan-response.schema.json +52 -0
  119. package/config/security-validation-policy.schema.json +74 -0
  120. package/config/starter-source-acknowledgement.schema.json +13 -0
  121. package/config/starter-source-adapter.schema.json +38 -0
  122. package/config/starter-source-request.schema.json +61 -0
  123. package/package.json +77 -0
  124. package/packs/core/pack.yaml +7 -0
  125. package/packs/design-systems/default/experience-promise.md +9 -0
  126. package/packs/design-systems/default/intentional-review.md +10 -0
  127. package/packs/design-systems/default/interaction-and-entry.md +9 -0
  128. package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
  129. package/packs/design-systems/default/pack.yaml +44 -0
  130. package/packs/design-systems/default/principles.md +10 -0
  131. package/packs/personas/core/pack.yaml +7 -0
  132. package/packs/personas/core/personas/archaeologist.md +37 -0
  133. package/packs/personas/core/personas/end-user.md +17 -0
  134. package/packs/personas/core/personas/maintainer.md +17 -0
  135. package/packs/personas/core/personas/operator.md +17 -0
  136. package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
  137. package/packs/technologies/laravel/pack.yaml +30 -0
  138. package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
  139. package/packs/technologies/nuxt/pack.yaml +30 -0
  140. package/packs/technologies/power-platform/pack.yaml +31 -0
  141. package/packs/technologies/salesforce/pack.yaml +25 -0
  142. package/public/app.js +4896 -0
  143. package/public/apple-touch-icon.png +0 -0
  144. package/public/assets/backstory-icon.png +0 -0
  145. package/public/dashboard-navigation.js +98 -0
  146. package/public/favicon-16.png +0 -0
  147. package/public/favicon-32.png +0 -0
  148. package/public/favicon.ico +0 -0
  149. package/public/index.html +789 -0
  150. package/public/styles.css +2693 -0
  151. package/public/team-hub/app.js +202 -0
  152. package/public/team-hub/index.html +79 -0
  153. package/public/team-hub/styles.css +90 -0
  154. package/scripts/publication-check.mjs +140 -0
  155. package/scripts/setup.mjs +21 -0
  156. package/skills-src/ewai-archaeology/SKILL.md +334 -0
  157. package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
  158. package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
  159. package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
  160. package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
  161. package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
  162. package/skills-src/ewai-architecture/SKILL.md +108 -0
  163. package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
  164. package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
  165. package/skills-src/ewai-context/SKILL.md +68 -0
  166. package/skills-src/ewai-context/agents/openai.yaml +4 -0
  167. package/skills-src/ewai-context-import/SKILL.md +118 -0
  168. package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
  169. package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
  170. package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
  171. package/skills-src/ewai-deliver/SKILL.md +122 -0
  172. package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
  173. package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
  174. package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
  175. package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
  176. package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
  177. package/skills-src/ewai-design-system-author/SKILL.md +28 -0
  178. package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
  179. package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
  180. package/skills-src/ewai-design-system-review/SKILL.md +26 -0
  181. package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
  182. package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
  183. package/skills-src/ewai-error-reporting/SKILL.md +46 -0
  184. package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
  185. package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
  186. package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
  187. package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
  188. package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
  189. package/skills-src/ewai-intent/SKILL.md +68 -0
  190. package/skills-src/ewai-intent/agents/openai.yaml +4 -0
  191. package/skills-src/ewai-intent/references/intent-contract.md +42 -0
  192. package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
  193. package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
  194. package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
  195. package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
  196. package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
  197. package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
  198. package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
  199. package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
  200. package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
  201. package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
  202. package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
  203. package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
  204. package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
  205. package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
  206. package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
  207. package/skills-src/ewai-pipeline/SKILL.md +130 -0
  208. package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
  209. package/skills-src/ewai-pipeline/references/cli.md +86 -0
  210. package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
  211. package/skills-src/ewai-portfolio/SKILL.md +70 -0
  212. package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
  213. package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
  214. package/skills-src/ewai-project-discovery/SKILL.md +95 -0
  215. package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
  216. package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
  217. package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
  218. package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
  219. package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
  220. package/skills-src/ewai-retro/SKILL.md +48 -0
  221. package/skills-src/ewai-retro/agents/openai.yaml +4 -0
  222. package/skills-src/ewai-retro/references/asset-routing.md +14 -0
  223. package/skills-src/ewai-rollout/SKILL.md +74 -0
  224. package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
  225. package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
  226. package/skills-src/ewai-shape-intents/SKILL.md +84 -0
  227. package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
  228. package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
  229. package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
  230. package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
  231. package/skills-src/ewai-standards-check/SKILL.md +93 -0
  232. package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
  233. package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
  234. package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
  235. package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
  236. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
  237. package/src/afk-worker.mjs +16 -0
  238. package/src/archaeology.mjs +1333 -0
  239. package/src/checkin.mjs +261 -0
  240. package/src/cli.mjs +2427 -0
  241. package/src/companion-guidance.mjs +257 -0
  242. package/src/companion-opening.mjs +62 -0
  243. package/src/companion.mjs +256 -0
  244. package/src/context.mjs +210 -0
  245. package/src/dashboard-preferences.mjs +80 -0
  246. package/src/delivery-artifacts.mjs +204 -0
  247. package/src/delivery-documents.mjs +248 -0
  248. package/src/delivery-gates.mjs +317 -0
  249. package/src/delivery.mjs +1433 -0
  250. package/src/design-system-application.mjs +291 -0
  251. package/src/design-system-authoring.mjs +101 -0
  252. package/src/design-systems.mjs +466 -0
  253. package/src/discovery.mjs +1314 -0
  254. package/src/error-reporting.mjs +323 -0
  255. package/src/evidence-depth.mjs +543 -0
  256. package/src/execution-state.mjs +243 -0
  257. package/src/install.mjs +166 -0
  258. package/src/intent-dependencies.mjs +117 -0
  259. package/src/intent-maps.mjs +402 -0
  260. package/src/intents.mjs +747 -0
  261. package/src/knowledge-proposals.mjs +717 -0
  262. package/src/launcher.mjs +51 -0
  263. package/src/meeting-evidence.mjs +703 -0
  264. package/src/network-rollout.mjs +386 -0
  265. package/src/organisation-blueprints.mjs +438 -0
  266. package/src/organisation-policies.mjs +448 -0
  267. package/src/packs.mjs +44 -0
  268. package/src/paths.mjs +62 -0
  269. package/src/persona-entitlements.mjs +438 -0
  270. package/src/persona-licence-config.mjs +98 -0
  271. package/src/persona-website-provider.mjs +134 -0
  272. package/src/persona-zip.mjs +87 -0
  273. package/src/personas.mjs +159 -0
  274. package/src/platform-metadata-analysis.mjs +314 -0
  275. package/src/policy-design-gates.mjs +623 -0
  276. package/src/policy-gate-integration.mjs +318 -0
  277. package/src/portfolio.mjs +509 -0
  278. package/src/power-platform-source-map.mjs +190 -0
  279. package/src/project.mjs +449 -0
  280. package/src/prototype-iterations.mjs +730 -0
  281. package/src/repository-source-map.mjs +603 -0
  282. package/src/runtime/afk-conductor.mjs +973 -0
  283. package/src/runtime/context-assembly.mjs +457 -0
  284. package/src/runtime/context-benchmarks.mjs +115 -0
  285. package/src/runtime/dashboard-actions.mjs +109 -0
  286. package/src/runtime/dashboard-handoffs.mjs +149 -0
  287. package/src/runtime/dashboard-server.mjs +1272 -0
  288. package/src/runtime/dashboard.mjs +197 -0
  289. package/src/runtime/database.mjs +789 -0
  290. package/src/runtime/error-reporting.mjs +581 -0
  291. package/src/runtime/evidence-depth-workspace.mjs +412 -0
  292. package/src/runtime/execution-leases.mjs +299 -0
  293. package/src/runtime/guided-discovery.mjs +350 -0
  294. package/src/runtime/guided-intents.mjs +517 -0
  295. package/src/runtime/impact-analysis.mjs +535 -0
  296. package/src/runtime/intents.mjs +222 -0
  297. package/src/runtime/knowledge.mjs +86 -0
  298. package/src/runtime/lifecycle-hooks.mjs +1239 -0
  299. package/src/runtime/mcp-config.mjs +110 -0
  300. package/src/runtime/mcp-server.mjs +1885 -0
  301. package/src/runtime/palace.mjs +362 -0
  302. package/src/runtime/paths.mjs +58 -0
  303. package/src/runtime/persona-engagement.mjs +255 -0
  304. package/src/runtime/phase-contributions.mjs +594 -0
  305. package/src/runtime/policy-workspace.mjs +170 -0
  306. package/src/runtime/prototype-iterations.mjs +235 -0
  307. package/src/runtime/provider-adapters.mjs +163 -0
  308. package/src/runtime/repository-index.mjs +838 -0
  309. package/src/runtime/runs.mjs +185 -0
  310. package/src/runtime/security-validation.mjs +1230 -0
  311. package/src/runtime/starter-materialisation.mjs +1155 -0
  312. package/src/runtime/team-hub-client.mjs +479 -0
  313. package/src/runtime/team-hub-database.mjs +288 -0
  314. package/src/runtime/team-hub-server.mjs +191 -0
  315. package/src/runtime/team-hub.mjs +110 -0
  316. package/src/runtime/tree-sitter-index.mjs +390 -0
  317. package/src/runtime/version.mjs +1 -0
  318. package/src/runtime/work.mjs +633 -0
  319. package/src/salesforce-source-map.mjs +212 -0
  320. package/src/security-validation-config.mjs +224 -0
  321. package/src/solution-readiness.mjs +620 -0
  322. package/src/starter-materialisation-contract.mjs +407 -0
  323. package/src/task-graph.mjs +544 -0
  324. package/src/team-hub-resources.mjs +239 -0
  325. package/src/team-hub.mjs +242 -0
  326. package/src/test-scenarios.mjs +622 -0
  327. package/src/validation-config.mjs +289 -0
  328. package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
  329. package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
  330. package/templates/SPECS/6.Build/_tracker-template.md +16 -0
  331. package/templates/SPECS/pipeline.yaml +62 -0
  332. package/templates/discovery-answers.yaml +86 -0
  333. package/templates/intent-body.md +21 -0
  334. package/tests/fixtures/context-benchmarks.json +9 -0
@@ -0,0 +1,1230 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { execFileSync, spawn } from 'node:child_process';
3
+ import {
4
+ accessSync,
5
+ closeSync,
6
+ constants,
7
+ existsSync,
8
+ fstatSync,
9
+ lstatSync,
10
+ openSync,
11
+ readFileSync,
12
+ readdirSync,
13
+ readlinkSync,
14
+ realpathSync,
15
+ statSync,
16
+ } from 'node:fs';
17
+ import { isAbsolute, relative, resolve } from 'node:path';
18
+ import {
19
+ ASSURANCE_NOTICE,
20
+ SECURITY_CAPABILITIES,
21
+ SECURITY_PROVIDER_CATALOGUE,
22
+ normaliseSecurityValidationConfig,
23
+ securityProvider,
24
+ } from '../security-validation-config.mjs';
25
+ import { isWithin, now, sha256 } from '../delivery-documents.mjs';
26
+ import { loadProjectConfig } from '../project.mjs';
27
+ import { openRuntimeDatabase } from './database.mjs';
28
+ import { selectContextualPersonas } from './persona-engagement.mjs';
29
+
30
+ const manifestFields = new Set([
31
+ 'schema', 'id', 'name', 'publisher', 'version', 'protocolVersion', 'capabilities', 'entrypoint',
32
+ ]);
33
+ const publisherFields = new Set(['id', 'name']);
34
+ const adapterIdPattern = /^[a-z][a-z0-9.-]{1,79}$/;
35
+ const versionPattern = /^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/;
36
+ const maxManifestBytes = 64 * 1024;
37
+ const maxAdapterOutputBytes = 1024 * 1024;
38
+ const maxProviderArtefactBytes = 10 * 1024 * 1024;
39
+ const maxAdapterPackageBytes = 64 * 1024 * 1024;
40
+ const maxAdapterPackageFiles = 1024;
41
+ const maxRevisionDiffBytes = 64 * 1024 * 1024;
42
+ const activeSecurityProcesses = new Map();
43
+ const responseFields = new Set(['schema', 'runId', 'status', 'complete', 'capabilities', 'tool', 'findings', 'warnings']);
44
+ const findingFields = new Set(['id', 'capability', 'severity', 'confidence', 'summary', 'locations', 'remediation', 'evidence']);
45
+ const toolFields = new Set(['name', 'version']);
46
+
47
+ function strictFields(value, fields, label) {
48
+ for (const key of Object.keys(value)) {
49
+ if (!fields.has(key)) throw new Error(`${label} contains unknown field: ${key}`);
50
+ }
51
+ }
52
+
53
+ function safeProviderProjection(provider, state, signals = []) {
54
+ return {
55
+ id: provider.id,
56
+ name: provider.name,
57
+ publisher: provider.publisher,
58
+ state,
59
+ official_sources: [...provider.official_sources],
60
+ capabilities: [...provider.capabilities],
61
+ artifact_import_supported: provider.artifact_import_supported === true,
62
+ summary: provider.summary,
63
+ cautions: [...provider.cautions],
64
+ detected_signals: [...signals],
65
+ };
66
+ }
67
+
68
+ function safeAdapterProjection(row) {
69
+ return {
70
+ id: row.id,
71
+ name: row.name,
72
+ publisher: { id: row.publisher_id, name: row.publisher_name },
73
+ version: row.version,
74
+ protocolVersion: row.protocol_version,
75
+ capabilities: JSON.parse(row.capabilities_json),
76
+ manifestDigest: row.manifest_digest,
77
+ packageDigest: row.package_digest,
78
+ registeredAt: row.registered_at,
79
+ updatedAt: row.updated_at,
80
+ };
81
+ }
82
+
83
+ function safeRunProjection(row) {
84
+ return {
85
+ id: row.id,
86
+ profileId: row.profile_id,
87
+ providerId: row.provider_id ?? null,
88
+ adapterId: row.adapter_id ?? null,
89
+ interactionMode: row.interaction_mode,
90
+ capability: row.capability,
91
+ status: row.status,
92
+ revision: row.revision,
93
+ scope: JSON.parse(row.scope_json),
94
+ targetClass: row.target_class ?? null,
95
+ targetRef: row.target_ref ?? null,
96
+ complete: Boolean(row.complete),
97
+ outcome: row.outcome,
98
+ reportDigest: row.report_digest,
99
+ policyDigest: row.policy_digest,
100
+ tool: JSON.parse(row.tool_json || '{}'),
101
+ redactionStatus: row.redaction_status,
102
+ warnings: JSON.parse(row.warnings_json),
103
+ createdAt: row.created_at,
104
+ startedAt: row.started_at,
105
+ completedAt: row.completed_at,
106
+ };
107
+ }
108
+
109
+ function profileDigest(profile) {
110
+ return sha256(JSON.stringify(profile));
111
+ }
112
+
113
+ function safeFindingProjection(row) {
114
+ return {
115
+ id: row.finding_key,
116
+ recordId: row.id,
117
+ runId: row.run_id,
118
+ capability: row.capability,
119
+ providerSeverity: row.provider_severity,
120
+ providerConfidence: row.provider_confidence,
121
+ summary: row.summary,
122
+ locations: JSON.parse(row.locations_json),
123
+ remediation: row.remediation,
124
+ evidence: JSON.parse(row.evidence_json),
125
+ policyConsequence: row.policy_consequence,
126
+ createdAt: row.created_at,
127
+ };
128
+ }
129
+
130
+ function securityPolicy(projectRoot) {
131
+ return normaliseSecurityValidationConfig(loadProjectConfig(projectRoot).config);
132
+ }
133
+
134
+ function policyDigest(policy) {
135
+ return sha256(JSON.stringify(policy));
136
+ }
137
+
138
+ export function captureSecurityPolicy(projectRoot, options = {}) {
139
+ const policy = securityPolicy(projectRoot);
140
+ return {
141
+ schema: 'ewai.security-policy-snapshot/v1',
142
+ capturedAt: String(options.capturedAt ?? now()),
143
+ policyDigest: policyDigest(policy),
144
+ policy,
145
+ };
146
+ }
147
+
148
+ function profileFor(projectRoot, profileId) {
149
+ const policy = securityPolicy(projectRoot);
150
+ if (!policy.enabled) throw new Error('Security validation is not configured for this project.');
151
+ const profile = policy.profiles.find((candidate) => candidate.id === profileId);
152
+ if (!profile) throw new Error(`Unknown security profile: ${profileId}`);
153
+ return { policy, profile };
154
+ }
155
+
156
+ function credentialShaped(value) {
157
+ return /(?:Bearer\s+[A-Za-z0-9._~+\/-]{12,}|sk-[A-Za-z0-9_-]{16,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)/i.test(value);
158
+ }
159
+
160
+ function safeRelativeReference(projectRoot, value, label, options = {}) {
161
+ const text = String(value ?? '').trim();
162
+ if (!text || text.length > 500 || credentialShaped(text)) throw new Error(`${label} is unsafe.`);
163
+ const pathPart = text.replace(/:\d+(?::\d+)?$/, '');
164
+ if (isAbsolute(pathPart) || pathPart === '..' || pathPart.startsWith('../') || pathPart.includes('/../')) {
165
+ throw new Error(`${label} must be project-relative.`);
166
+ }
167
+ const absolute = resolve(projectRoot, pathPart);
168
+ if (!isWithin(resolve(projectRoot), absolute)) throw new Error(`${label} escapes the project.`);
169
+ if (options.requireExists) {
170
+ if (!existsSync(absolute) || lstatSync(absolute).isSymbolicLink() || (!lstatSync(absolute).isFile() && !lstatSync(absolute).isDirectory())) {
171
+ throw new Error(`${label} must reference existing non-symbolic project evidence.`);
172
+ }
173
+ }
174
+ return text;
175
+ }
176
+
177
+ function validateScanResponse(projectRoot, response, expected) {
178
+ if (!response || typeof response !== 'object' || Array.isArray(response)) throw new Error('Security adapter response must be an object.');
179
+ strictFields(response, responseFields, 'Security adapter response');
180
+ if (response.schema !== 'ewai.security-scan-response/v1') throw new Error('Unsupported security scan response schema.');
181
+ if (response.runId !== expected.runId) throw new Error('Security adapter response run identity does not match.');
182
+ if (!['complete', 'incomplete', 'failed'].includes(response.status)) throw new Error('Security adapter response status is invalid.');
183
+ if (typeof response.complete !== 'boolean') throw new Error('Security adapter response completeness is required.');
184
+ if ((response.status === 'complete') !== response.complete) throw new Error('Security adapter response status and completeness disagree.');
185
+ if (!Array.isArray(response.capabilities) || !response.capabilities.length || response.capabilities.length > SECURITY_CAPABILITIES.length || !response.capabilities.includes(expected.capability)) {
186
+ throw new Error('Security adapter response does not evidence the requested capability.');
187
+ }
188
+ if (new Set(response.capabilities).size !== response.capabilities.length) throw new Error('Security adapter response capabilities must be unique.');
189
+ for (const capability of response.capabilities) {
190
+ if (!SECURITY_CAPABILITIES.includes(capability)) throw new Error(`Unsupported response capability: ${capability}`);
191
+ }
192
+ let tool = null;
193
+ if (response.tool !== undefined) {
194
+ if (!response.tool || typeof response.tool !== 'object' || Array.isArray(response.tool)) throw new Error('Security adapter response tool must be an object.');
195
+ strictFields(response.tool, toolFields, 'Security adapter response tool');
196
+ const toolName = String(response.tool.name ?? '').trim();
197
+ const toolVersion = String(response.tool.version ?? '').trim();
198
+ if (!toolName || toolName.length > 120 || !toolVersion || toolVersion.length > 80 || credentialShaped(`${toolName} ${toolVersion}`)) {
199
+ throw new Error('Security adapter response tool identity is unsafe.');
200
+ }
201
+ tool = { name: toolName, version: toolVersion };
202
+ }
203
+ if (!Array.isArray(response.findings) || response.findings.length > 10000) throw new Error('Security adapter response findings must be a bounded array.');
204
+ const seen = new Set();
205
+ const findings = response.findings.map((finding) => {
206
+ if (!finding || typeof finding !== 'object' || Array.isArray(finding)) throw new Error('Security finding must be an object.');
207
+ strictFields(finding, findingFields, 'Security finding');
208
+ const id = String(finding.id ?? '').trim();
209
+ if (!id || id.length > 160 || credentialShaped(id) || seen.has(id)) throw new Error('Security finding identity is missing, unsafe or duplicated.');
210
+ seen.add(id);
211
+ const capability = String(finding.capability ?? '').trim();
212
+ if (!response.capabilities.includes(capability)) throw new Error('Security finding capability is not evidenced by this response.');
213
+ const severity = String(finding.severity ?? 'unknown').trim().toLowerCase();
214
+ if (!['critical', 'high', 'medium', 'low', 'info', 'unknown'].includes(severity)) throw new Error('Security finding severity is invalid.');
215
+ const confidence = finding.confidence === undefined ? null : Number(finding.confidence);
216
+ if (confidence !== null && (!Number.isFinite(confidence) || confidence < 0 || confidence > 1)) throw new Error('Security finding confidence is invalid.');
217
+ const summary = String(finding.summary ?? '').trim();
218
+ const remediation = String(finding.remediation ?? '').trim();
219
+ if (!summary || summary.length > 1000 || remediation.length > 2000 || credentialShaped(summary) || credentialShaped(remediation)) {
220
+ throw new Error('Security finding contains unsafe or excessive text.');
221
+ }
222
+ if (finding.locations !== undefined && (!Array.isArray(finding.locations) || finding.locations.length > 100)) throw new Error('Security finding locations must be a bounded array.');
223
+ if (finding.evidence !== undefined && (!Array.isArray(finding.evidence) || finding.evidence.length > 100)) throw new Error('Security finding evidence must be a bounded array.');
224
+ const locations = Array.isArray(finding.locations) ? finding.locations.map((value) => safeRelativeReference(projectRoot, value, 'Security finding location')) : [];
225
+ const evidence = Array.isArray(finding.evidence) ? finding.evidence.map((value) => safeRelativeReference(projectRoot, value, 'Security evidence reference')) : [];
226
+ return { id, capability, severity, confidence, summary, locations, remediation, evidence };
227
+ });
228
+ if (response.warnings !== undefined && (!Array.isArray(response.warnings) || response.warnings.length > 1000)) throw new Error('Security response warnings must be a bounded array.');
229
+ const warnings = Array.isArray(response.warnings)
230
+ ? response.warnings.map((warning) => String(warning).trim()).filter(Boolean).map((warning) => {
231
+ if (warning.length > 500 || credentialShaped(warning)) throw new Error('Security response warning is unsafe.');
232
+ return warning;
233
+ })
234
+ : [];
235
+ return { status: response.status, complete: response.complete, capabilities: [...response.capabilities], tool, findings, warnings };
236
+ }
237
+
238
+ function persistValidatedResponse(database, run, response, reportDigest) {
239
+ const completedAt = now();
240
+ const profileSnapshot = JSON.parse(run.profile_snapshot_json || '{}');
241
+ const blockingSeverities = new Set(profileSnapshot.thresholds?.blocking_severities ?? ['critical', 'high']);
242
+ database.exec('BEGIN IMMEDIATE');
243
+ try {
244
+ for (const finding of response.findings) {
245
+ database.prepare(`
246
+ INSERT INTO security_findings (
247
+ id, run_id, finding_key, capability, provider_severity, provider_confidence,
248
+ summary, locations_json, remediation, evidence_json, policy_consequence, created_at
249
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
250
+ `).run(
251
+ randomUUID(), run.id, finding.id, finding.capability, finding.severity, finding.confidence,
252
+ finding.summary, JSON.stringify(finding.locations), finding.remediation, JSON.stringify(finding.evidence),
253
+ blockingSeverities.has(finding.severity) ? 'blocking' : 'review', completedAt,
254
+ );
255
+ }
256
+ database.prepare(`
257
+ UPDATE security_runs
258
+ SET status = ?, complete = ?, outcome = ?, report_digest = ?, tool_json = ?, redaction_status = ?, warnings_json = ?, completed_at = ?
259
+ WHERE id = ?
260
+ `).run(
261
+ response.status,
262
+ response.complete ? 1 : 0,
263
+ response.status,
264
+ reportDigest,
265
+ JSON.stringify(response.tool ?? {}),
266
+ 'safe-fields-only',
267
+ JSON.stringify(response.warnings),
268
+ completedAt,
269
+ run.id,
270
+ );
271
+ database.exec('COMMIT');
272
+ } catch (error) {
273
+ database.exec('ROLLBACK');
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ function digestAdapterPackage(trustedRoot) {
279
+ const files = [];
280
+ let totalBytes = 0;
281
+ const visit = (folder) => {
282
+ for (const entry of readdirSync(folder, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
283
+ const path = resolve(folder, entry.name);
284
+ const stats = lstatSync(path);
285
+ if (stats.isSymbolicLink()) throw new Error('Security adapter packages cannot contain symbolic links.');
286
+ if (stats.isDirectory()) {
287
+ visit(path);
288
+ continue;
289
+ }
290
+ if (!stats.isFile()) throw new Error('Security adapter packages can contain only regular files and directories.');
291
+ totalBytes += stats.size;
292
+ if (files.length + 1 > maxAdapterPackageFiles || totalBytes > maxAdapterPackageBytes) {
293
+ throw new Error('Security adapter package exceeds the safe file or byte limit.');
294
+ }
295
+ files.push({ path, relativePath: relative(trustedRoot, path).replaceAll('\\', '/'), stats });
296
+ }
297
+ };
298
+ visit(trustedRoot);
299
+ const hash = createHash('sha256');
300
+ for (const file of files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))) {
301
+ const bytes = readFileSync(file.path);
302
+ hash.update(`${Buffer.byteLength(file.relativePath)}:${file.relativePath}:${file.stats.mode & 0o777}:${bytes.length}:`);
303
+ hash.update(bytes);
304
+ }
305
+ return hash.digest('hex');
306
+ }
307
+
308
+ function markRunFailed(projectRoot, runId, status, warning = '') {
309
+ const database = openRuntimeDatabase(projectRoot);
310
+ try {
311
+ database.prepare(`UPDATE security_runs SET status = ?, complete = 0, outcome = ?, warnings_json = ?, completed_at = ? WHERE id = ?`)
312
+ .run(status, status, JSON.stringify(warning ? [warning] : []), now(), runId);
313
+ return safeRunProjection(database.prepare('SELECT * FROM security_runs WHERE id = ?').get(runId));
314
+ } finally {
315
+ database.close();
316
+ }
317
+ }
318
+
319
+ function adapterInvocation(executable, request, projectRoot, timeoutMs) {
320
+ return new Promise((resolvePromise) => {
321
+ const child = spawn(executable, [], {
322
+ cwd: resolve(projectRoot),
323
+ env: { PATH: process.env.PATH ?? '', LANG: 'C', EWAI_SECURITY_PROTOCOL: '1' },
324
+ shell: false,
325
+ stdio: ['pipe', 'pipe', 'pipe'],
326
+ });
327
+ let stdout = Buffer.alloc(0);
328
+ let stderrBytes = 0;
329
+ let oversized = false;
330
+ let timedOut = false;
331
+ child.stdout.on('data', (chunk) => {
332
+ if (stdout.length + chunk.length > maxAdapterOutputBytes) {
333
+ oversized = true;
334
+ child.kill('SIGTERM');
335
+ return;
336
+ }
337
+ stdout = Buffer.concat([stdout, chunk]);
338
+ });
339
+ child.stderr.on('data', (chunk) => {
340
+ stderrBytes += chunk.length;
341
+ if (stderrBytes > maxAdapterOutputBytes) {
342
+ oversized = true;
343
+ child.kill('SIGTERM');
344
+ }
345
+ });
346
+ const timeout = setTimeout(() => {
347
+ timedOut = true;
348
+ child.kill('SIGTERM');
349
+ setTimeout(() => child.kill('SIGKILL'), 1_000).unref();
350
+ }, timeoutMs);
351
+ timeout.unref();
352
+ activeSecurityProcesses.set(request.runId, child);
353
+ child.once('error', (error) => {
354
+ clearTimeout(timeout);
355
+ activeSecurityProcesses.delete(request.runId);
356
+ resolvePromise({ exitCode: -1, signal: '', stdout, oversized, timedOut, error });
357
+ });
358
+ child.once('close', (exitCode, signal) => {
359
+ clearTimeout(timeout);
360
+ activeSecurityProcesses.delete(request.runId);
361
+ resolvePromise({ exitCode: exitCode ?? -1, signal: signal ?? '', stdout, oversized, timedOut, error: null });
362
+ });
363
+ child.stdin.end(`${JSON.stringify(request)}\n`);
364
+ });
365
+ }
366
+
367
+ function readManifest(adapterRoot) {
368
+ const root = resolve(adapterRoot);
369
+ if (!existsSync(root) || !lstatSync(root).isDirectory() || lstatSync(root).isSymbolicLink()) {
370
+ throw new Error('Security adapter root must be an existing non-symbolic-link directory.');
371
+ }
372
+ const trustedRoot = realpathSync(root);
373
+ const manifestPath = resolve(trustedRoot, 'security-adapter.json');
374
+ if (!existsSync(manifestPath) || !lstatSync(manifestPath).isFile() || lstatSync(manifestPath).isSymbolicLink()) {
375
+ throw new Error('Security adapter requires a regular security-adapter.json manifest.');
376
+ }
377
+ if (statSync(manifestPath).size > maxManifestBytes) throw new Error('Security adapter manifest is too large.');
378
+ let manifest;
379
+ const manifestBytes = readFileSync(manifestPath);
380
+ try {
381
+ manifest = JSON.parse(manifestBytes.toString('utf8'));
382
+ } catch {
383
+ throw new Error('Security adapter manifest must be valid JSON.');
384
+ }
385
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) throw new Error('Security adapter manifest must be an object.');
386
+ strictFields(manifest, manifestFields, 'Security adapter manifest');
387
+ if (manifest.schema !== 'ewai.security-adapter/v1') throw new Error('Unsupported security adapter manifest schema.');
388
+ if (!adapterIdPattern.test(String(manifest.id ?? ''))) throw new Error('Security adapter id is invalid.');
389
+ if (!String(manifest.name ?? '').trim() || String(manifest.name).length > 120) throw new Error('Security adapter name is invalid.');
390
+ if (!manifest.publisher || typeof manifest.publisher !== 'object' || Array.isArray(manifest.publisher)) throw new Error('Security adapter publisher is required.');
391
+ strictFields(manifest.publisher, publisherFields, 'Security adapter publisher');
392
+ if (!adapterIdPattern.test(String(manifest.publisher.id ?? '')) || !String(manifest.publisher.name ?? '').trim()) {
393
+ throw new Error('Security adapter publisher identity is invalid.');
394
+ }
395
+ if (!versionPattern.test(String(manifest.version ?? ''))) throw new Error('Security adapter version must be semantic version text.');
396
+ if (manifest.protocolVersion !== '1') throw new Error('Unsupported security adapter protocol version.');
397
+ if (!Array.isArray(manifest.capabilities) || !manifest.capabilities.length) throw new Error('Security adapter capabilities are required.');
398
+ if (new Set(manifest.capabilities).size !== manifest.capabilities.length) throw new Error('Security adapter capabilities must be unique.');
399
+ for (const capability of manifest.capabilities) {
400
+ if (!SECURITY_CAPABILITIES.includes(capability)) throw new Error(`Unsupported security adapter capability: ${capability}`);
401
+ }
402
+ const entrypointValue = String(manifest.entrypoint ?? '').trim();
403
+ if (!entrypointValue || isAbsolute(entrypointValue)) throw new Error('Security adapter entrypoint must be relative to its trusted root.');
404
+ const entrypoint = resolve(trustedRoot, entrypointValue);
405
+ if (!isWithin(trustedRoot, entrypoint) || !existsSync(entrypoint) || lstatSync(entrypoint).isSymbolicLink() || !lstatSync(entrypoint).isFile()) {
406
+ throw new Error('Security adapter entrypoint must be a regular file inside its trusted root.');
407
+ }
408
+ if (!isWithin(trustedRoot, realpathSync(entrypoint))) throw new Error('Security adapter entrypoint escapes its trusted root.');
409
+ try {
410
+ accessSync(entrypoint, constants.X_OK);
411
+ } catch {
412
+ throw new Error('Security adapter entrypoint must be executable.');
413
+ }
414
+ const manifestDigest = sha256(manifestBytes);
415
+ return {
416
+ manifest: {
417
+ schema: manifest.schema,
418
+ id: manifest.id,
419
+ name: String(manifest.name).trim(),
420
+ publisher: { id: manifest.publisher.id, name: String(manifest.publisher.name).trim() },
421
+ version: manifest.version,
422
+ protocolVersion: manifest.protocolVersion,
423
+ capabilities: [...manifest.capabilities],
424
+ entrypoint: entrypointValue,
425
+ },
426
+ trustedRoot,
427
+ entrypoint: realpathSync(entrypoint),
428
+ manifestDigest,
429
+ packageDigest: digestAdapterPackage(trustedRoot),
430
+ };
431
+ }
432
+
433
+ export function discoverSecurityProviders(projectRoot) {
434
+ const root = realpathSync(resolve(projectRoot));
435
+ const providers = SECURITY_PROVIDER_CATALOGUE.map((provider) => {
436
+ const providerRoot = resolve(root, provider.discovery.root);
437
+ if (!isWithin(root, providerRoot) || !existsSync(providerRoot)) return safeProviderProjection(provider, 'not-detected');
438
+ const rootStat = lstatSync(providerRoot);
439
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || realpathSync(providerRoot) !== providerRoot) {
440
+ return safeProviderProjection(provider, 'not-detected');
441
+ }
442
+ const signals = [];
443
+ for (const signal of provider.discovery.signals) {
444
+ const candidate = resolve(providerRoot, signal);
445
+ if (!isWithin(providerRoot, candidate) || !existsSync(candidate)) continue;
446
+ const candidateStat = lstatSync(candidate);
447
+ if (candidateStat.isSymbolicLink() || (!candidateStat.isFile() && !candidateStat.isDirectory())) continue;
448
+ signals.push(`${provider.discovery.root}/${signal}`);
449
+ }
450
+ return safeProviderProjection(provider, signals.length ? 'artefacts-detected' : 'not-detected', signals);
451
+ });
452
+ return { schema: 'ewai.security-provider-discovery/v1', providers, assurance_notice: ASSURANCE_NOTICE };
453
+ }
454
+
455
+ export function validateSecurityAdapter(projectRoot, adapterRoot) {
456
+ const validated = readManifest(adapterRoot);
457
+ return {
458
+ schema: 'ewai.security-adapter-validation/v1',
459
+ valid: true,
460
+ adapter: {
461
+ id: validated.manifest.id,
462
+ name: validated.manifest.name,
463
+ publisher: validated.manifest.publisher,
464
+ version: validated.manifest.version,
465
+ protocolVersion: validated.manifest.protocolVersion,
466
+ capabilities: [...validated.manifest.capabilities],
467
+ manifestDigest: validated.manifestDigest,
468
+ packageDigest: validated.packageDigest,
469
+ },
470
+ assurance_notice: ASSURANCE_NOTICE,
471
+ };
472
+ }
473
+
474
+ export function registerSecurityAdapter(projectRoot, adapterRoot, options = {}) {
475
+ if (!options.confirmed) throw new Error('Security adapter registration requires explicit confirmation.');
476
+ const validated = readManifest(adapterRoot);
477
+ const database = openRuntimeDatabase(projectRoot);
478
+ try {
479
+ const existing = database.prepare('SELECT * FROM security_adapters WHERE id = ?').get(validated.manifest.id);
480
+ if (existing && existing.package_digest !== validated.packageDigest && existing.version === validated.manifest.version) {
481
+ throw new Error(`Security adapter ${validated.manifest.id} digest drift requires a new reviewed version or identity.`);
482
+ }
483
+ const timestamp = now();
484
+ database.prepare(`
485
+ INSERT INTO security_adapters (
486
+ id, name, publisher_id, publisher_name, version, protocol_version,
487
+ trusted_root, entrypoint, manifest_digest, package_digest, capabilities_json,
488
+ manifest_json, registered_at, updated_at
489
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
490
+ ON CONFLICT(id) DO UPDATE SET
491
+ name = excluded.name,
492
+ publisher_id = excluded.publisher_id,
493
+ publisher_name = excluded.publisher_name,
494
+ version = excluded.version,
495
+ protocol_version = excluded.protocol_version,
496
+ trusted_root = excluded.trusted_root,
497
+ entrypoint = excluded.entrypoint,
498
+ manifest_digest = excluded.manifest_digest,
499
+ package_digest = excluded.package_digest,
500
+ capabilities_json = excluded.capabilities_json,
501
+ manifest_json = excluded.manifest_json,
502
+ updated_at = excluded.updated_at
503
+ `).run(
504
+ validated.manifest.id,
505
+ validated.manifest.name,
506
+ validated.manifest.publisher.id,
507
+ validated.manifest.publisher.name,
508
+ validated.manifest.version,
509
+ validated.manifest.protocolVersion,
510
+ validated.trustedRoot,
511
+ validated.entrypoint,
512
+ validated.manifestDigest,
513
+ validated.packageDigest,
514
+ JSON.stringify(validated.manifest.capabilities),
515
+ JSON.stringify(validated.manifest),
516
+ existing?.registered_at ?? timestamp,
517
+ timestamp,
518
+ );
519
+ const row = database.prepare('SELECT * FROM security_adapters WHERE id = ?').get(validated.manifest.id);
520
+ return { schema: 'ewai.security-adapter-registration/v1', adapter: safeAdapterProjection(row), assurance_notice: ASSURANCE_NOTICE };
521
+ } finally {
522
+ database.close();
523
+ }
524
+ }
525
+
526
+ export function listSecurityAdapters(projectRoot) {
527
+ const database = openRuntimeDatabase(projectRoot);
528
+ try {
529
+ const adapters = database.prepare('SELECT * FROM security_adapters ORDER BY name, id').all().map(safeAdapterProjection);
530
+ return { schema: 'ewai.security-adapters/v1', adapters, assurance_notice: ASSURANCE_NOTICE };
531
+ } finally {
532
+ database.close();
533
+ }
534
+ }
535
+
536
+ export function prepareSecuritySkillHandoff(projectRoot, input = {}) {
537
+ realpathSync(resolve(projectRoot));
538
+ const provider = securityProvider(String(input.providerId ?? ''));
539
+ if (!provider) throw new Error(`Unknown security provider: ${input.providerId || '(empty)'}`);
540
+ const profileId = String(input.profileId ?? '').trim();
541
+ if (!profileId) throw new Error('Security skill handoff requires a profile ID.');
542
+ const capability = String(input.capability ?? '').trim();
543
+ if (!provider.capabilities.includes(capability)) throw new Error(`${provider.name} does not declare capability ${capability || '(empty)'}.`);
544
+ const revision = String(input.revision ?? '').trim();
545
+ if (!revision || revision.length > 240 || credentialShaped(revision)) throw new Error('Security skill handoff requires an exact safe repository or target revision.');
546
+ const scope = Array.isArray(input.scope) ? input.scope.map((item) => String(item).trim()).filter(Boolean) : [];
547
+ for (const path of scope) {
548
+ if (isAbsolute(path) || path === '..' || path.startsWith('../') || path.includes('/../')) throw new Error('Security handoff scope must remain project-relative.');
549
+ }
550
+ return {
551
+ schema: 'ewai.security-skill-handoff/v1',
552
+ status: 'awaiting-evidence',
553
+ evidence: null,
554
+ handoff: {
555
+ id: randomUUID(),
556
+ provider: safeProviderProjection(provider, 'available'),
557
+ profileId,
558
+ capability,
559
+ revision,
560
+ scope,
561
+ responseContract: 'ewai.security-scan-response/v1',
562
+ preparedAt: now(),
563
+ },
564
+ assurance_notice: ASSURANCE_NOTICE,
565
+ };
566
+ }
567
+
568
+ export function prepareSecurityRun(projectRoot, profileId, input = {}) {
569
+ const { profile } = profileFor(projectRoot, profileId);
570
+ if (!input.confirmed) throw new Error('Security run preparation requires explicit confirmation.');
571
+ const mode = String(input.mode ?? (profile.adapter ? 'command' : profile.provider ? 'skill' : '')).trim();
572
+ if (!profile.modes.includes(mode)) throw new Error(`Security profile ${profile.id} does not allow ${mode || 'the requested'} mode.`);
573
+ const revision = String(input.trustedRevision ?? '').trim();
574
+ if (!revision || revision.length > 240 || credentialShaped(revision)) throw new Error('Security run preparation requires a safe server-resolved repository or target revision.');
575
+ if (profile.capability === 'llm-runtime-red-team') {
576
+ if (!profile.target_ref || !profile.target_class) throw new Error('Runtime security profiles require a preconfigured safe target reference and class.');
577
+ if (!input.confirmedRuntimeTarget) throw new Error('Runtime security profiles require explicit target confirmation.');
578
+ if (profile.target_class === 'production') throw new Error('Production runtime security targets are denied by default.');
579
+ }
580
+ if (mode === 'skill') {
581
+ return prepareSecuritySkillHandoff(projectRoot, {
582
+ providerId: profile.provider,
583
+ profileId: profile.id,
584
+ capability: profile.capability,
585
+ revision,
586
+ scope: Array.isArray(profile.scope?.include) ? profile.scope.include : [],
587
+ });
588
+ }
589
+ if (mode !== 'command') throw new Error('Artefact imports must be prepared through the server discovery-token operation.');
590
+ if (!profile.adapter) throw new Error(`Security profile ${profile.id} has no registered adapter identity.`);
591
+ const database = openRuntimeDatabase(projectRoot);
592
+ try {
593
+ const adapter = database.prepare('SELECT * FROM security_adapters WHERE id = ?').get(profile.adapter);
594
+ if (!adapter) throw new Error(`Registered security adapter not found: ${profile.adapter}`);
595
+ const capabilities = JSON.parse(adapter.capabilities_json);
596
+ if (!capabilities.includes(profile.capability)) throw new Error(`Security adapter ${profile.adapter} does not support ${profile.capability}.`);
597
+ const id = randomUUID();
598
+ const timestamp = now();
599
+ database.prepare(`
600
+ INSERT INTO security_runs (
601
+ id, profile_id, provider_id, adapter_id, interaction_mode, capability, status,
602
+ revision, scope_json, target_class, target_ref, policy_digest, profile_snapshot_json, created_at
603
+ ) VALUES (?, ?, ?, ?, 'command', ?, 'prepared', ?, ?, ?, ?, ?, ?, ?)
604
+ `).run(
605
+ id, profile.id, profile.provider, profile.adapter, profile.capability, revision,
606
+ JSON.stringify(profile.scope), profile.target_class, profile.target_ref,
607
+ profileDigest(profile), JSON.stringify(profile), timestamp,
608
+ );
609
+ const run = safeRunProjection(database.prepare('SELECT * FROM security_runs WHERE id = ?').get(id));
610
+ return { schema: 'ewai.security-run-preparation/v1', run, assurance_notice: ASSURANCE_NOTICE };
611
+ } finally {
612
+ database.close();
613
+ }
614
+ }
615
+
616
+ export async function executeSecurityRun(projectRoot, runId) {
617
+ let run;
618
+ let adapter;
619
+ let attemptId;
620
+ let attemptOrdinal;
621
+ const startedAt = now();
622
+ const database = openRuntimeDatabase(projectRoot);
623
+ try {
624
+ run = database.prepare('SELECT * FROM security_runs WHERE id = ?').get(runId);
625
+ if (!run) throw new Error(`Security run not found: ${runId}`);
626
+ if (run.status !== 'prepared') throw new Error(`Security run ${runId} is not prepared.`);
627
+ if (run.interaction_mode !== 'command' || !run.adapter_id) throw new Error('Only prepared command-adapter runs can execute locally.');
628
+ adapter = database.prepare('SELECT * FROM security_adapters WHERE id = ?').get(run.adapter_id);
629
+ if (!adapter) throw new Error(`Registered security adapter not found: ${run.adapter_id}`);
630
+ const current = readManifest(adapter.trusted_root);
631
+ if (current.packageDigest !== adapter.package_digest || current.entrypoint !== adapter.entrypoint) {
632
+ throw new Error(`Security adapter ${adapter.id} digest drift prevents execution.`);
633
+ }
634
+ attemptOrdinal = Number(database.prepare('SELECT COALESCE(MAX(ordinal), 0) AS ordinal FROM security_attempts WHERE run_id = ?').get(runId).ordinal) + 1;
635
+ attemptId = randomUUID();
636
+ database.exec('BEGIN IMMEDIATE');
637
+ database.prepare('UPDATE security_runs SET status = ?, started_at = ? WHERE id = ?').run('running', startedAt, runId);
638
+ database.prepare(`INSERT INTO security_attempts (id, run_id, ordinal, status, started_at) VALUES (?, ?, ?, 'running', ?)`)
639
+ .run(attemptId, runId, attemptOrdinal, startedAt);
640
+ database.exec('COMMIT');
641
+ } catch (error) {
642
+ try { database.exec('ROLLBACK'); } catch {}
643
+ database.close();
644
+ throw error;
645
+ }
646
+ database.close();
647
+
648
+ const request = {
649
+ schema: 'ewai.security-scan-request/v1',
650
+ runId,
651
+ profile: { id: run.profile_id, capability: run.capability },
652
+ project: { revision: run.revision },
653
+ scope: JSON.parse(run.scope_json),
654
+ target: run.target_ref ? { class: run.target_class, ref: run.target_ref } : null,
655
+ };
656
+ const timeoutMs = Math.max(1_000, Math.min(3_600_000, Number(profileFor(projectRoot, run.profile_id).profile.timeout_seconds) * 1000));
657
+ const invocation = await adapterInvocation(adapter.entrypoint, request, projectRoot, timeoutMs);
658
+ const completedAt = now();
659
+ const after = openRuntimeDatabase(projectRoot);
660
+ let cancellationRequested = false;
661
+ try {
662
+ cancellationRequested = after.prepare('SELECT status FROM security_runs WHERE id = ?').get(runId)?.status === 'cancelling';
663
+ after.prepare(`
664
+ UPDATE security_attempts SET status = ?, exit_code = ?, signal = ?, timed_out = ?,
665
+ duration_ms = ?, completed_at = ? WHERE id = ?
666
+ `).run(
667
+ cancellationRequested ? 'cancelled' : invocation.timedOut ? 'timed-out' : invocation.oversized ? 'failed' : invocation.exitCode === 0 ? 'complete' : 'failed',
668
+ invocation.exitCode, invocation.signal, invocation.timedOut ? 1 : 0,
669
+ Math.max(0, new Date(completedAt).getTime() - new Date(startedAt).getTime()), completedAt, attemptId,
670
+ );
671
+ } finally {
672
+ after.close();
673
+ }
674
+
675
+ if (cancellationRequested) {
676
+ return { schema: 'ewai.security-run-result/v1', run: markRunFailed(projectRoot, runId, 'cancelled', 'Adapter execution was cancelled.'), assurance_notice: ASSURANCE_NOTICE };
677
+ }
678
+ if (invocation.error || invocation.exitCode !== 0 || invocation.timedOut || invocation.oversized) {
679
+ const warning = invocation.timedOut ? 'Adapter timed out.' : invocation.oversized ? 'Adapter output exceeded the safe limit.' : 'Adapter execution failed.';
680
+ return { schema: 'ewai.security-run-result/v1', run: markRunFailed(projectRoot, runId, 'failed', warning), assurance_notice: ASSURANCE_NOTICE };
681
+ }
682
+ const reportDigest = sha256(invocation.stdout);
683
+ try {
684
+ const parsed = JSON.parse(invocation.stdout.toString('utf8'));
685
+ const validated = validateScanResponse(projectRoot, parsed, { runId, capability: run.capability });
686
+ const resultDatabase = openRuntimeDatabase(projectRoot);
687
+ try {
688
+ persistValidatedResponse(resultDatabase, run, validated, reportDigest);
689
+ const completed = safeRunProjection(resultDatabase.prepare('SELECT * FROM security_runs WHERE id = ?').get(runId));
690
+ return { schema: 'ewai.security-run-result/v1', run: completed, assurance_notice: ASSURANCE_NOTICE };
691
+ } finally {
692
+ resultDatabase.close();
693
+ }
694
+ } catch {
695
+ return { schema: 'ewai.security-run-result/v1', run: markRunFailed(projectRoot, runId, 'failed', 'Adapter response failed validation.'), assurance_notice: ASSURANCE_NOTICE };
696
+ }
697
+ }
698
+
699
+ function safeArtefactPath(projectRoot, provider, signal) {
700
+ const root = realpathSync(resolve(projectRoot));
701
+ const providerRoot = resolve(root, provider.discovery.root);
702
+ if (!existsSync(providerRoot) || lstatSync(providerRoot).isSymbolicLink() || !lstatSync(providerRoot).isDirectory()) return null;
703
+ if (!isWithin(root, providerRoot) || realpathSync(providerRoot) !== providerRoot) return null;
704
+ const candidate = resolve(providerRoot, signal);
705
+ if (!existsSync(candidate)) return null;
706
+ const stats = lstatSync(candidate);
707
+ if (stats.isSymbolicLink() || !stats.isFile() || stats.size > maxProviderArtefactBytes) return null;
708
+ if (!isWithin(providerRoot, candidate) || realpathSync(candidate) !== candidate) return null;
709
+ return relative(root, candidate).replaceAll('\\', '/');
710
+ }
711
+
712
+ function supportedArtefacts(projectRoot, provider) {
713
+ const expected = provider.id === 'agentic-security'
714
+ ? ['findings.json', 'last-scan.json']
715
+ : provider.id === 'deepsec'
716
+ ? ['report.json']
717
+ : [];
718
+ if (!expected.length) return [];
719
+ const artefacts = expected.map((signal) => safeArtefactPath(projectRoot, provider, signal)).filter(Boolean);
720
+ return artefacts.length === expected.length ? artefacts : [];
721
+ }
722
+
723
+ function stableJsonRead(projectRoot, projectRelative) {
724
+ const root = realpathSync(resolve(projectRoot));
725
+ const path = resolve(root, projectRelative);
726
+ if (!isWithin(root, path) || !existsSync(path) || lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile()) {
727
+ throw new Error('Security provider artefact is missing, unsafe or symbolic.');
728
+ }
729
+ if (realpathSync(path) !== path) throw new Error('Security provider artefact escapes the project.');
730
+ const noFollow = constants.O_NOFOLLOW ?? 0;
731
+ const descriptor = openSync(path, constants.O_RDONLY | noFollow);
732
+ try {
733
+ const before = fstatSync(descriptor);
734
+ if (!before.isFile()) throw new Error('Security provider artefact must be a regular file.');
735
+ if (before.size > maxProviderArtefactBytes) throw new Error('Security provider artefact is too large.');
736
+ const bytes = readFileSync(descriptor);
737
+ const after = fstatSync(descriptor);
738
+ if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
739
+ throw new Error('Security provider artefact changed while being read.');
740
+ }
741
+ return { value: JSON.parse(bytes.toString('utf8')), bytes };
742
+ } finally {
743
+ closeSync(descriptor);
744
+ }
745
+ }
746
+
747
+ export function prepareSecurityArtifactImport(projectRoot, profileId, providerId, input = {}) {
748
+ const { profile } = profileFor(projectRoot, profileId);
749
+ if (!profile.modes.includes('artifact-import')) throw new Error(`Security profile ${profile.id} does not allow artefact import.`);
750
+ if (profile.provider && profile.provider !== providerId) throw new Error(`Security profile ${profile.id} is configured for ${profile.provider}.`);
751
+ const provider = securityProvider(providerId);
752
+ if (!provider) throw new Error(`Unknown security provider: ${providerId}`);
753
+ if (!provider.artifact_import_supported) throw new Error(`${provider.name} does not support native artefact import; use a skill handoff.`);
754
+ if (!provider.capabilities.includes(profile.capability)) throw new Error(`${provider.name} does not declare ${profile.capability}.`);
755
+ const revision = String(input.trustedRevision ?? '').trim();
756
+ if (!revision || revision.length > 240 || credentialShaped(revision)) throw new Error('Security artefact import requires a safe server-resolved revision.');
757
+ const artefacts = supportedArtefacts(projectRoot, provider);
758
+ if (!artefacts.length) throw new Error(`${provider.name} has no complete supported artefacts to import; symbolic or partial artefacts are rejected.`);
759
+ const token = randomUUID();
760
+ const createdAt = now();
761
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString();
762
+ const database = openRuntimeDatabase(projectRoot);
763
+ try {
764
+ database.prepare(`INSERT INTO security_discovery_tokens (id, provider_id, profile_id, artefacts_json, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?)`)
765
+ .run(token, provider.id, profile.id, JSON.stringify({ artefacts, revision }), createdAt, expiresAt);
766
+ } finally {
767
+ database.close();
768
+ }
769
+ return {
770
+ schema: 'ewai.security-artifact-import-preparation/v1',
771
+ status: 'awaiting-import',
772
+ token,
773
+ provider: safeProviderProjection(provider, 'artefacts-detected', artefacts),
774
+ profileId: profile.id,
775
+ expiresAt,
776
+ assurance_notice: ASSURANCE_NOTICE,
777
+ };
778
+ }
779
+
780
+ function providerResponse(providerId, artefacts, runId, capability, expectedRevision) {
781
+ if (providerId === 'agentic-security') {
782
+ const findings = artefacts.find((item) => item.path.endsWith('/findings.json'))?.value;
783
+ const scan = artefacts.find((item) => item.path.endsWith('/last-scan.json'))?.value;
784
+ if (!Array.isArray(findings) || !scan || typeof scan !== 'object') throw new Error('Agentic Security artefacts are malformed.');
785
+ if (String(scan.revision ?? '') !== expectedRevision) throw new Error('Agentic Security artefact revision does not match the prepared project revision.');
786
+ return {
787
+ schema: 'ewai.security-scan-response/v1', runId,
788
+ status: scan.complete === true && scan.status === 'complete' ? 'complete' : 'incomplete',
789
+ complete: scan.complete === true && scan.status === 'complete',
790
+ capabilities: Array.isArray(scan.capabilities) ? scan.capabilities : [capability],
791
+ findings: findings.map((finding) => ({
792
+ id: finding.id,
793
+ capability: finding.capability ?? capability,
794
+ severity: finding.severity ?? 'unknown',
795
+ confidence: finding.confidence,
796
+ summary: finding.summary,
797
+ locations: finding.locations ?? (finding.path ? [finding.path] : []),
798
+ remediation: finding.remediation ?? '',
799
+ evidence: finding.evidence ?? (finding.path ? [finding.path] : []),
800
+ })),
801
+ warnings: [],
802
+ };
803
+ }
804
+ const report = artefacts[0]?.value;
805
+ if (!report || typeof report !== 'object' || !Array.isArray(report.findings)) throw new Error('DeepSec artefact is malformed.');
806
+ if (String(report.revision ?? '') !== expectedRevision) throw new Error('DeepSec artefact revision does not match the prepared project revision.');
807
+ return {
808
+ schema: 'ewai.security-scan-response/v1', runId,
809
+ status: report.complete === true && report.status === 'complete' ? 'complete' : 'incomplete',
810
+ complete: report.complete === true && report.status === 'complete',
811
+ capabilities: Array.isArray(report.capabilities) ? report.capabilities : [capability],
812
+ findings: report.findings,
813
+ warnings: Array.isArray(report.warnings) ? report.warnings : [],
814
+ };
815
+ }
816
+
817
+ export function importSecurityArtifacts(projectRoot, token, options = {}) {
818
+ if (!options.confirmed) throw new Error('Security artefact import requires explicit confirmation.');
819
+ const database = openRuntimeDatabase(projectRoot);
820
+ let discovered;
821
+ try {
822
+ discovered = database.prepare('SELECT * FROM security_discovery_tokens WHERE id = ?').get(String(token ?? ''));
823
+ if (!discovered) throw new Error('Unknown security discovery token.');
824
+ if (discovered.consumed_at) throw new Error('Security discovery token was already consumed.');
825
+ if (Date.parse(discovered.expires_at) <= Date.now()) throw new Error('Security discovery token has expired.');
826
+ } finally {
827
+ database.close();
828
+ }
829
+ const { profile } = profileFor(projectRoot, discovered.profile_id);
830
+ const descriptor = JSON.parse(discovered.artefacts_json);
831
+ const artefacts = descriptor.artefacts.map((path) => ({ path, ...stableJsonRead(projectRoot, path) }));
832
+ const runId = randomUUID();
833
+ const response = validateScanResponse(
834
+ projectRoot,
835
+ providerResponse(discovered.provider_id, artefacts, runId, profile.capability, descriptor.revision),
836
+ { runId, capability: profile.capability },
837
+ );
838
+ const reportDigest = sha256(Buffer.concat(artefacts.map((artefact) => artefact.bytes)));
839
+ const timestamp = now();
840
+ const writeDatabase = openRuntimeDatabase(projectRoot);
841
+ try {
842
+ writeDatabase.exec('BEGIN IMMEDIATE');
843
+ const fresh = writeDatabase.prepare('SELECT * FROM security_discovery_tokens WHERE id = ?').get(discovered.id);
844
+ if (!fresh || fresh.consumed_at) throw new Error('Security discovery token was already consumed.');
845
+ writeDatabase.prepare(`
846
+ INSERT INTO security_runs (
847
+ id, profile_id, provider_id, interaction_mode, capability, status, revision,
848
+ scope_json, target_class, target_ref, policy_digest, profile_snapshot_json, created_at, started_at
849
+ ) VALUES (?, ?, ?, 'artifact-import', ?, 'importing', ?, ?, ?, ?, ?, ?, ?, ?)
850
+ `).run(
851
+ runId, profile.id, discovered.provider_id, profile.capability, descriptor.revision,
852
+ JSON.stringify(profile.scope), profile.target_class, profile.target_ref,
853
+ profileDigest(profile), JSON.stringify(profile), timestamp, timestamp,
854
+ );
855
+ writeDatabase.prepare('UPDATE security_discovery_tokens SET consumed_at = ? WHERE id = ?').run(timestamp, discovered.id);
856
+ writeDatabase.exec('COMMIT');
857
+ persistValidatedResponse(writeDatabase, { id: runId }, response, reportDigest);
858
+ const run = safeRunProjection(writeDatabase.prepare('SELECT * FROM security_runs WHERE id = ?').get(runId));
859
+ return { schema: 'ewai.security-artifact-import/v1', run, assurance_notice: ASSURANCE_NOTICE };
860
+ } catch (error) {
861
+ try { writeDatabase.exec('ROLLBACK'); } catch {}
862
+ throw error;
863
+ } finally {
864
+ writeDatabase.close();
865
+ }
866
+ }
867
+
868
+ export function cancelSecurityRun(projectRoot, runId, options = {}) {
869
+ if (!options.confirmed) throw new Error('Security run cancellation requires explicit confirmation.');
870
+ const database = openRuntimeDatabase(projectRoot);
871
+ try {
872
+ const run = database.prepare('SELECT * FROM security_runs WHERE id = ?').get(runId);
873
+ if (!run) throw new Error(`Security run not found: ${runId}`);
874
+ if (!['prepared', 'running', 'cancelling'].includes(run.status)) throw new Error(`Security run ${runId} is already terminal.`);
875
+ const child = activeSecurityProcesses.get(runId);
876
+ if (child) {
877
+ database.prepare('UPDATE security_runs SET status = ? WHERE id = ?').run('cancelling', runId);
878
+ child.kill('SIGTERM');
879
+ } else {
880
+ database.prepare(`UPDATE security_runs SET status = 'cancelled', complete = 0, outcome = 'cancelled', completed_at = ? WHERE id = ?`).run(now(), runId);
881
+ }
882
+ return { schema: 'ewai.security-run-cancellation/v1', run: safeRunProjection(database.prepare('SELECT * FROM security_runs WHERE id = ?').get(runId)), assurance_notice: ASSURANCE_NOTICE };
883
+ } finally {
884
+ database.close();
885
+ }
886
+ }
887
+
888
+ export function listSecurityRuns(projectRoot) {
889
+ const database = openRuntimeDatabase(projectRoot);
890
+ try {
891
+ const runs = database.prepare('SELECT * FROM security_runs ORDER BY created_at DESC, id').all().map(safeRunProjection);
892
+ return { schema: 'ewai.security-runs/v1', runs, assurance_notice: ASSURANCE_NOTICE };
893
+ } finally {
894
+ database.close();
895
+ }
896
+ }
897
+
898
+ export function listSecurityFindings(projectRoot) {
899
+ const database = openRuntimeDatabase(projectRoot);
900
+ try {
901
+ const findings = database.prepare('SELECT * FROM security_findings ORDER BY created_at DESC, finding_key').all().map(safeFindingProjection);
902
+ return { schema: 'ewai.security-findings/v1', findings, assurance_notice: ASSURANCE_NOTICE };
903
+ } finally {
904
+ database.close();
905
+ }
906
+ }
907
+
908
+ const dispositionFields = new Set([
909
+ 'decision', 'reviewer', 'reason', 'evidence', 'riskOwner', 'expiresAt', 'nextRole',
910
+ ]);
911
+ const dispositionDecisions = new Set(['remediate', 'false-positive', 'accept-risk', 'escalate']);
912
+
913
+ function safeDispositionProjection(row) {
914
+ return {
915
+ id: row.id,
916
+ findingId: row.finding_id,
917
+ decision: row.decision,
918
+ reviewer: row.reviewer,
919
+ reason: row.reason,
920
+ evidence: JSON.parse(row.evidence_json),
921
+ riskOwner: row.risk_owner ?? null,
922
+ expiresAt: row.expires_at ?? null,
923
+ nextRole: row.next_role ?? null,
924
+ createdAt: row.created_at,
925
+ };
926
+ }
927
+
928
+ export function recordSecurityDisposition(projectRoot, findingId, input = {}) {
929
+ if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Security disposition must be an object.');
930
+ strictFields(input, dispositionFields, 'Security disposition');
931
+ const decision = String(input.decision ?? '').trim();
932
+ if (!dispositionDecisions.has(decision)) throw new Error('Security disposition decision is invalid.');
933
+ const reviewer = String(input.reviewer ?? '').trim();
934
+ const reason = String(input.reason ?? '').trim();
935
+ if (!reviewer || reviewer.length > 160 || credentialShaped(reviewer)) throw new Error('Security disposition requires a safe named reviewer.');
936
+ if (!reason || reason.length > 2000 || credentialShaped(reason)) throw new Error('Security disposition requires a safe reason.');
937
+ if (input.evidence !== undefined && (!Array.isArray(input.evidence) || input.evidence.length > 100)) {
938
+ throw new Error('Security disposition evidence must be a bounded array.');
939
+ }
940
+ const evidence = Array.isArray(input.evidence)
941
+ ? input.evidence.map((value) => safeRelativeReference(projectRoot, value, 'Security disposition evidence', { requireExists: true }))
942
+ : [];
943
+ if (['false-positive', 'accept-risk'].includes(decision) && !evidence.length) {
944
+ throw new Error(`${decision} disposition requires project evidence.`);
945
+ }
946
+ const riskOwner = String(input.riskOwner ?? '').trim();
947
+ const expiresAt = String(input.expiresAt ?? '').trim();
948
+ if (decision === 'accept-risk') {
949
+ if (!riskOwner || riskOwner.length > 160 || credentialShaped(riskOwner)) throw new Error('Accepted risk requires a safe named risk owner.');
950
+ if (!expiresAt || !Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.now()) {
951
+ throw new Error('Accepted risk requires a future expiry or review date.');
952
+ }
953
+ }
954
+ if (decision !== 'accept-risk' && (riskOwner || expiresAt)) throw new Error('Risk owner and expiry apply only to accepted risk.');
955
+ const nextRole = String(input.nextRole ?? '').trim();
956
+ if (decision === 'escalate' && (!nextRole || nextRole.length > 160 || credentialShaped(nextRole))) throw new Error('Escalation requires a safe next accountable role.');
957
+ if (decision !== 'escalate' && nextRole) throw new Error('Next accountable role applies only to escalation.');
958
+ const database = openRuntimeDatabase(projectRoot);
959
+ try {
960
+ const finding = database.prepare('SELECT * FROM security_findings WHERE id = ? OR finding_key = ? ORDER BY created_at DESC LIMIT 1')
961
+ .get(findingId, findingId);
962
+ if (!finding) throw new Error(`Security finding not found: ${findingId}`);
963
+ const id = randomUUID();
964
+ const createdAt = now();
965
+ database.prepare(`
966
+ INSERT INTO security_dispositions (
967
+ id, finding_id, decision, reviewer, reason, evidence_json, risk_owner, expires_at, next_role, created_at
968
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
969
+ `).run(
970
+ id, finding.id, decision, reviewer, reason, JSON.stringify(evidence),
971
+ riskOwner || null, expiresAt || null, nextRole || null, createdAt,
972
+ );
973
+ const row = database.prepare('SELECT * FROM security_dispositions WHERE id = ?').get(id);
974
+ return { schema: 'ewai.security-disposition/v1', disposition: safeDispositionProjection(row), assurance_notice: ASSURANCE_NOTICE };
975
+ } finally {
976
+ database.close();
977
+ }
978
+ }
979
+
980
+ export function listSecurityDispositions(projectRoot, findingId = '') {
981
+ const database = openRuntimeDatabase(projectRoot);
982
+ try {
983
+ let rows;
984
+ if (findingId) {
985
+ const finding = database.prepare('SELECT id FROM security_findings WHERE id = ? OR finding_key = ? ORDER BY created_at DESC LIMIT 1')
986
+ .get(findingId, findingId);
987
+ rows = finding
988
+ ? database.prepare('SELECT * FROM security_dispositions WHERE finding_id = ? ORDER BY created_at, id').all(finding.id)
989
+ : [];
990
+ } else {
991
+ rows = database.prepare('SELECT * FROM security_dispositions ORDER BY created_at, id').all();
992
+ }
993
+ return { schema: 'ewai.security-dispositions/v1', dispositions: rows.map(safeDispositionProjection), assurance_notice: ASSURANCE_NOTICE };
994
+ } finally {
995
+ database.close();
996
+ }
997
+ }
998
+
999
+ export function assessSecurityReadiness(projectRoot, options = {}) {
1000
+ const currentPolicy = securityPolicy(projectRoot);
1001
+ let policy = currentPolicy;
1002
+ if (options.policySnapshot) {
1003
+ const snapshot = options.policySnapshot;
1004
+ const snapshotValid = snapshot
1005
+ && snapshot.schema === 'ewai.security-policy-snapshot/v1'
1006
+ && snapshot.policy
1007
+ && snapshot.policyDigest === policyDigest(snapshot.policy);
1008
+ if (!snapshotValid) {
1009
+ return {
1010
+ schema: 'ewai.security-readiness/v1', status: 'blocked', ready: false, profiles: [],
1011
+ reasons: [{ code: 'policy-snapshot-invalid' }], assurance_notice: ASSURANCE_NOTICE,
1012
+ };
1013
+ }
1014
+ policy = snapshot.policy;
1015
+ const currentPolicyDigest = policyDigest(currentPolicy);
1016
+ if (currentPolicyDigest !== snapshot.policyDigest) {
1017
+ return {
1018
+ schema: 'ewai.security-readiness/v1',
1019
+ status: 'blocked',
1020
+ ready: false,
1021
+ profiles: policy.profiles.map((profile) => ({
1022
+ id: profile.id,
1023
+ capability: profile.capability,
1024
+ required: profile.required,
1025
+ accountableRole: profile.accountable_role,
1026
+ status: 'stale',
1027
+ run: null,
1028
+ })),
1029
+ reasons: [{ code: 'security-policy-changed', approvedPolicyDigest: snapshot.policyDigest, currentPolicyDigest }],
1030
+ assurance_notice: ASSURANCE_NOTICE,
1031
+ };
1032
+ }
1033
+ }
1034
+ if (!policy.enabled) {
1035
+ return {
1036
+ schema: 'ewai.security-readiness/v1',
1037
+ status: 'not-configured',
1038
+ ready: true,
1039
+ profiles: [],
1040
+ reasons: [],
1041
+ assurance_notice: ASSURANCE_NOTICE,
1042
+ };
1043
+ }
1044
+ const revision = String(options.trustedRevision ?? resolveSecurityRevision(projectRoot)).trim();
1045
+ const assessedAt = String(options.now ?? now());
1046
+ const assessedTime = Date.parse(assessedAt);
1047
+ if (!Number.isFinite(assessedTime)) throw new Error('Security readiness assessment time is invalid.');
1048
+ const database = openRuntimeDatabase(projectRoot);
1049
+ try {
1050
+ const profiles = [];
1051
+ const reasons = [];
1052
+ for (const profile of policy.profiles) {
1053
+ const run = database.prepare(`
1054
+ SELECT * FROM security_runs WHERE profile_id = ? ORDER BY created_at DESC, id DESC LIMIT 1
1055
+ `).get(profile.id);
1056
+ const projection = {
1057
+ id: profile.id,
1058
+ capability: profile.capability,
1059
+ required: profile.required,
1060
+ accountableRole: profile.accountable_role,
1061
+ status: 'missing',
1062
+ run: run ? safeRunProjection(run) : null,
1063
+ };
1064
+ if (!run) {
1065
+ if (profile.required) reasons.push({ code: 'missing-evidence', profileId: profile.id, capability: profile.capability });
1066
+ profiles.push(projection);
1067
+ continue;
1068
+ }
1069
+ if (run.status !== 'complete' || !run.complete) {
1070
+ projection.status = run.status === 'failed' ? 'failed' : run.status === 'cancelled' ? 'cancelled' : 'incomplete';
1071
+ if (profile.required) reasons.push({ code: 'incomplete-evidence', profileId: profile.id, status: run.status });
1072
+ profiles.push(projection);
1073
+ continue;
1074
+ }
1075
+ const currentPolicyDigest = profileDigest(profile);
1076
+ if (!run.policy_digest || run.policy_digest !== currentPolicyDigest) {
1077
+ projection.status = 'stale';
1078
+ if (profile.required) reasons.push({ code: 'policy-mismatch', profileId: profile.id, evidencePolicyDigest: run.policy_digest || null, currentPolicyDigest });
1079
+ profiles.push(projection);
1080
+ continue;
1081
+ }
1082
+ if (!revision || run.revision !== revision) {
1083
+ projection.status = 'stale';
1084
+ if (profile.required) reasons.push({ code: 'revision-mismatch', profileId: profile.id, evidenceRevision: run.revision, currentRevision: revision || null });
1085
+ }
1086
+ const completedTime = Date.parse(run.completed_at);
1087
+ if (!Number.isFinite(completedTime) || assessedTime - completedTime > profile.freshness_hours * 60 * 60 * 1000) {
1088
+ projection.status = 'stale';
1089
+ if (profile.required) reasons.push({ code: 'stale-evidence', profileId: profile.id, completedAt: run.completed_at, freshnessHours: profile.freshness_hours });
1090
+ }
1091
+ const blockingFindings = database.prepare(`
1092
+ SELECT * FROM security_findings WHERE run_id = ? AND policy_consequence = 'blocking' ORDER BY created_at, id
1093
+ `).all(run.id);
1094
+ for (const finding of blockingFindings) {
1095
+ const disposition = database.prepare(`
1096
+ SELECT * FROM security_dispositions WHERE finding_id = ? ORDER BY created_at DESC, id DESC LIMIT 1
1097
+ `).get(finding.id);
1098
+ if (!disposition) {
1099
+ reasons.push({ code: 'blocking-finding', profileId: profile.id, findingId: finding.finding_key });
1100
+ continue;
1101
+ }
1102
+ if (disposition.decision === 'accept-risk') {
1103
+ if (!disposition.expires_at || Date.parse(disposition.expires_at) <= assessedTime) {
1104
+ reasons.push({ code: 'expired-risk-acceptance', profileId: profile.id, findingId: finding.finding_key, expiresAt: disposition.expires_at });
1105
+ }
1106
+ continue;
1107
+ }
1108
+ if (disposition.decision === 'false-positive') continue;
1109
+ reasons.push({
1110
+ code: disposition.decision === 'escalate' ? 'finding-escalated' : 'remediation-pending',
1111
+ profileId: profile.id,
1112
+ findingId: finding.finding_key,
1113
+ });
1114
+ }
1115
+ const findingBlocked = reasons.some((reason) => (
1116
+ reason.profileId === profile.id
1117
+ && ['blocking-finding', 'expired-risk-acceptance', 'finding-escalated', 'remediation-pending'].includes(reason.code)
1118
+ ));
1119
+ projection.status = findingBlocked ? 'blocked' : projection.status === 'stale' ? 'stale' : 'current';
1120
+ profiles.push(projection);
1121
+ }
1122
+ const blockingReasons = reasons.filter((reason) => policy.profiles.find((profile) => profile.id === reason.profileId)?.required !== false);
1123
+ return {
1124
+ schema: 'ewai.security-readiness/v1',
1125
+ status: blockingReasons.length ? 'blocked' : 'ready',
1126
+ ready: blockingReasons.length === 0,
1127
+ assessedAt,
1128
+ revision: revision || null,
1129
+ profiles,
1130
+ reasons,
1131
+ assurance_notice: ASSURANCE_NOTICE,
1132
+ };
1133
+ } finally {
1134
+ database.close();
1135
+ }
1136
+ }
1137
+
1138
+ export function resolveSecurityRevision(projectRoot) {
1139
+ try {
1140
+ const root = resolve(projectRoot);
1141
+ const head = execFileSync('git', ['rev-parse', 'HEAD'], {
1142
+ cwd: root,
1143
+ encoding: 'utf8',
1144
+ stdio: ['ignore', 'pipe', 'ignore'],
1145
+ timeout: 5_000,
1146
+ }).trim();
1147
+ const status = execFileSync('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all'], {
1148
+ cwd: root,
1149
+ encoding: 'buffer',
1150
+ maxBuffer: maxRevisionDiffBytes,
1151
+ stdio: ['ignore', 'pipe', 'ignore'],
1152
+ timeout: 10_000,
1153
+ });
1154
+ if (!status.length) return head;
1155
+ const diff = execFileSync('git', ['diff', '--binary', '--no-ext-diff', 'HEAD', '--'], {
1156
+ cwd: root,
1157
+ encoding: 'buffer',
1158
+ maxBuffer: maxRevisionDiffBytes,
1159
+ stdio: ['ignore', 'pipe', 'ignore'],
1160
+ timeout: 15_000,
1161
+ });
1162
+ const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard', '-z'], {
1163
+ cwd: root,
1164
+ encoding: 'buffer',
1165
+ maxBuffer: maxRevisionDiffBytes,
1166
+ stdio: ['ignore', 'pipe', 'ignore'],
1167
+ timeout: 10_000,
1168
+ }).toString('utf8').split('\0').filter(Boolean);
1169
+ const hash = createHash('sha256');
1170
+ hash.update(head);
1171
+ hash.update(status);
1172
+ hash.update(diff);
1173
+ let untrackedBytes = 0;
1174
+ for (const projectRelative of untracked.sort()) {
1175
+ const path = resolve(root, projectRelative);
1176
+ if (!isWithin(root, path)) throw new Error('Untracked revision path escapes the project.');
1177
+ const stats = lstatSync(path);
1178
+ hash.update(`${Buffer.byteLength(projectRelative)}:${projectRelative}:${stats.mode & 0o777}:`);
1179
+ if (stats.isSymbolicLink()) {
1180
+ hash.update(`link:${readlinkSync(path)}`);
1181
+ } else if (stats.isFile()) {
1182
+ untrackedBytes += stats.size;
1183
+ if (untrackedBytes > maxRevisionDiffBytes) throw new Error('Untracked revision content exceeds the safe limit.');
1184
+ hash.update(readFileSync(path));
1185
+ }
1186
+ }
1187
+ return `${head}:worktree-sha256:${hash.digest('hex')}`;
1188
+ } catch {
1189
+ return '';
1190
+ }
1191
+ }
1192
+
1193
+ export function readSecurityWorkspace(projectRoot, options = {}) {
1194
+ const policy = securityPolicy(projectRoot);
1195
+ const providers = discoverSecurityProviders(projectRoot).providers;
1196
+ const adapters = listSecurityAdapters(projectRoot).adapters;
1197
+ const runs = listSecurityRuns(projectRoot).runs;
1198
+ const findings = listSecurityFindings(projectRoot).findings;
1199
+ const dispositions = listSecurityDispositions(projectRoot).dispositions;
1200
+ const readiness = assessSecurityReadiness(projectRoot, {
1201
+ trustedRevision: options.trustedRevision ?? resolveSecurityRevision(projectRoot),
1202
+ now: options.now,
1203
+ });
1204
+ const signals = [
1205
+ 'security', 'risk', 'privacy', 'source code', 'release',
1206
+ ...readiness.profiles.filter((profile) => profile.status !== 'current').map((profile) => profile.capability),
1207
+ ...findings.filter((finding) => finding.policyConsequence === 'blocking').map((finding) => finding.summary),
1208
+ ];
1209
+ const activePersonas = selectContextualPersonas({
1210
+ signals,
1211
+ context: { readiness: readiness.status, capabilities: policy.profiles.map((profile) => profile.capability) },
1212
+ contextLabel: 'security evidence, findings and release consequence',
1213
+ personaCatalogue: options.personas ?? [],
1214
+ limit: 5,
1215
+ });
1216
+ return {
1217
+ schema: 'ewai.security-workspace/v1',
1218
+ assurance_notice: ASSURANCE_NOTICE,
1219
+ policy,
1220
+ readiness,
1221
+ providers,
1222
+ adapters,
1223
+ runs,
1224
+ findings: findings.map((finding) => ({
1225
+ ...finding,
1226
+ dispositions: dispositions.filter((disposition) => disposition.findingId === finding.recordId),
1227
+ })),
1228
+ active_personas: activePersonas,
1229
+ };
1230
+ }