open-multi-agent-kit 0.95.1 → 0.96.0

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 (306) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +16 -1
  3. package/dist/approvals/approval-receipt-store.d.ts +18 -0
  4. package/dist/approvals/approval-receipt-store.d.ts.map +1 -0
  5. package/dist/approvals/approval-receipt-store.js +194 -0
  6. package/dist/approvals/approval-receipt-store.js.map +1 -0
  7. package/dist/approvals/approval-receipt.d.ts +43 -0
  8. package/dist/approvals/approval-receipt.d.ts.map +1 -0
  9. package/dist/approvals/approval-receipt.js +208 -0
  10. package/dist/approvals/approval-receipt.js.map +1 -0
  11. package/dist/approvals/approval-verifier.d.ts +17 -0
  12. package/dist/approvals/approval-verifier.d.ts.map +1 -0
  13. package/dist/approvals/approval-verifier.js +23 -0
  14. package/dist/approvals/approval-verifier.js.map +1 -0
  15. package/dist/approvals/plannotator-approval-bridge.d.ts +58 -0
  16. package/dist/approvals/plannotator-approval-bridge.d.ts.map +1 -0
  17. package/dist/approvals/plannotator-approval-bridge.js +169 -0
  18. package/dist/approvals/plannotator-approval-bridge.js.map +1 -0
  19. package/dist/cli/args.d.ts.map +1 -1
  20. package/dist/cli/args.js +2 -0
  21. package/dist/cli/args.js.map +1 -1
  22. package/dist/commands/package-doctor-cli.d.ts +13 -0
  23. package/dist/commands/package-doctor-cli.d.ts.map +1 -0
  24. package/dist/commands/package-doctor-cli.js +80 -0
  25. package/dist/commands/package-doctor-cli.js.map +1 -0
  26. package/dist/commands/router-feedback-cli.d.ts +11 -0
  27. package/dist/commands/router-feedback-cli.d.ts.map +1 -0
  28. package/dist/commands/router-feedback-cli.js +99 -0
  29. package/dist/commands/router-feedback-cli.js.map +1 -0
  30. package/dist/commands/stats-cli.d.ts +20 -0
  31. package/dist/commands/stats-cli.d.ts.map +1 -0
  32. package/dist/commands/stats-cli.js +99 -0
  33. package/dist/commands/stats-cli.js.map +1 -0
  34. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  35. package/dist/core/agent-session-runtime.js +4 -1
  36. package/dist/core/agent-session-runtime.js.map +1 -1
  37. package/dist/core/agent-session.d.ts +99 -31
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +624 -216
  40. package/dist/core/agent-session.js.map +1 -1
  41. package/dist/core/bash-executor.d.ts.map +1 -1
  42. package/dist/core/bash-executor.js +8 -2
  43. package/dist/core/bash-executor.js.map +1 -1
  44. package/dist/core/compaction/transaction.d.ts +9 -0
  45. package/dist/core/compaction/transaction.d.ts.map +1 -1
  46. package/dist/core/compaction/transaction.js +17 -2
  47. package/dist/core/compaction/transaction.js.map +1 -1
  48. package/dist/core/context-budget-governor-v2.d.ts +1 -1
  49. package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
  50. package/dist/core/context-budget-governor-v2.js +1 -1
  51. package/dist/core/context-budget-governor-v2.js.map +1 -1
  52. package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
  53. package/dist/core/context-budget-system-prompt.js +18 -2
  54. package/dist/core/context-budget-system-prompt.js.map +1 -1
  55. package/dist/core/context-budget-v2-cache-disk.d.ts +109 -0
  56. package/dist/core/context-budget-v2-cache-disk.d.ts.map +1 -0
  57. package/dist/core/context-budget-v2-cache-disk.js +343 -0
  58. package/dist/core/context-budget-v2-cache-disk.js.map +1 -0
  59. package/dist/core/context-budget-v2-cache-keys.d.ts +22 -0
  60. package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
  61. package/dist/core/context-budget-v2-cache-keys.js +4 -4
  62. package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
  63. package/dist/core/context-budget-v2-cache.d.ts +1 -0
  64. package/dist/core/context-budget-v2-cache.d.ts.map +1 -1
  65. package/dist/core/context-budget-v2-cache.js +1 -0
  66. package/dist/core/context-budget-v2-cache.js.map +1 -1
  67. package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
  68. package/dist/core/context-budget-v2-planner.js +17 -14
  69. package/dist/core/context-budget-v2-planner.js.map +1 -1
  70. package/dist/core/context-budget-v2-types.d.ts +1 -0
  71. package/dist/core/context-budget-v2-types.d.ts.map +1 -1
  72. package/dist/core/context-budget-v2-types.js.map +1 -1
  73. package/dist/core/extensions/loader.d.ts.map +1 -1
  74. package/dist/core/extensions/loader.js +45 -22
  75. package/dist/core/extensions/loader.js.map +1 -1
  76. package/dist/core/extensions/types.d.ts +4 -0
  77. package/dist/core/extensions/types.d.ts.map +1 -1
  78. package/dist/core/extensions/types.js.map +1 -1
  79. package/dist/core/mcp/client.d.ts +105 -0
  80. package/dist/core/mcp/client.d.ts.map +1 -0
  81. package/dist/core/mcp/client.js +198 -0
  82. package/dist/core/mcp/client.js.map +1 -0
  83. package/dist/core/mcp/config.d.ts +23 -0
  84. package/dist/core/mcp/config.d.ts.map +1 -0
  85. package/dist/core/mcp/config.js +89 -0
  86. package/dist/core/mcp/config.js.map +1 -0
  87. package/dist/core/mcp/index.d.ts +15 -0
  88. package/dist/core/mcp/index.d.ts.map +1 -0
  89. package/dist/core/mcp/index.js +15 -0
  90. package/dist/core/mcp/index.js.map +1 -0
  91. package/dist/core/mcp/manager.d.ts +84 -0
  92. package/dist/core/mcp/manager.d.ts.map +1 -0
  93. package/dist/core/mcp/manager.js +173 -0
  94. package/dist/core/mcp/manager.js.map +1 -0
  95. package/dist/core/mcp/protocol.d.ts +59 -0
  96. package/dist/core/mcp/protocol.d.ts.map +1 -0
  97. package/dist/core/mcp/protocol.js +103 -0
  98. package/dist/core/mcp/protocol.js.map +1 -0
  99. package/dist/core/mcp/stdio-transport.d.ts +63 -0
  100. package/dist/core/mcp/stdio-transport.d.ts.map +1 -0
  101. package/dist/core/mcp/stdio-transport.js +132 -0
  102. package/dist/core/mcp/stdio-transport.js.map +1 -0
  103. package/dist/core/mcp/tools.d.ts +47 -0
  104. package/dist/core/mcp/tools.d.ts.map +1 -0
  105. package/dist/core/mcp/tools.js +102 -0
  106. package/dist/core/mcp/tools.js.map +1 -0
  107. package/dist/core/package-archive.d.ts +3 -0
  108. package/dist/core/package-archive.d.ts.map +1 -0
  109. package/dist/core/package-archive.js +161 -0
  110. package/dist/core/package-archive.js.map +1 -0
  111. package/dist/core/package-doctor-source-scan.d.ts +13 -0
  112. package/dist/core/package-doctor-source-scan.d.ts.map +1 -0
  113. package/dist/core/package-doctor-source-scan.js +124 -0
  114. package/dist/core/package-doctor-source-scan.js.map +1 -0
  115. package/dist/core/package-doctor.d.ts +41 -0
  116. package/dist/core/package-doctor.d.ts.map +1 -0
  117. package/dist/core/package-doctor.js +145 -0
  118. package/dist/core/package-doctor.js.map +1 -0
  119. package/dist/core/package-manager.d.ts +10 -1
  120. package/dist/core/package-manager.d.ts.map +1 -1
  121. package/dist/core/package-manager.js +175 -42
  122. package/dist/core/package-manager.js.map +1 -1
  123. package/dist/core/package-manifest.d.ts +26 -0
  124. package/dist/core/package-manifest.d.ts.map +1 -0
  125. package/dist/core/package-manifest.js +85 -0
  126. package/dist/core/package-manifest.js.map +1 -0
  127. package/dist/core/pi-compat.d.ts +5 -0
  128. package/dist/core/pi-compat.d.ts.map +1 -0
  129. package/dist/core/pi-compat.js +18 -0
  130. package/dist/core/pi-compat.js.map +1 -0
  131. package/dist/core/provider-resilience.d.ts +7 -0
  132. package/dist/core/provider-resilience.d.ts.map +1 -1
  133. package/dist/core/provider-resilience.js +11 -0
  134. package/dist/core/provider-resilience.js.map +1 -1
  135. package/dist/core/reasoning-router-bias.d.ts +10 -14
  136. package/dist/core/reasoning-router-bias.d.ts.map +1 -1
  137. package/dist/core/reasoning-router-bias.js +22 -15
  138. package/dist/core/reasoning-router-bias.js.map +1 -1
  139. package/dist/core/reasoning-router-v4.d.ts +9 -1
  140. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  141. package/dist/core/reasoning-router-v4.js +20 -5
  142. package/dist/core/reasoning-router-v4.js.map +1 -1
  143. package/dist/core/redaction.d.ts +2 -5
  144. package/dist/core/redaction.d.ts.map +1 -1
  145. package/dist/core/redaction.js +14 -1
  146. package/dist/core/redaction.js.map +1 -1
  147. package/dist/core/repository-learning-scope.d.ts +10 -0
  148. package/dist/core/repository-learning-scope.d.ts.map +1 -0
  149. package/dist/core/repository-learning-scope.js +42 -0
  150. package/dist/core/repository-learning-scope.js.map +1 -0
  151. package/dist/core/resource-loader.d.ts.map +1 -1
  152. package/dist/core/resource-loader.js +16 -1
  153. package/dist/core/resource-loader.js.map +1 -1
  154. package/dist/core/router-feedback-collector.d.ts +2 -1
  155. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  156. package/dist/core/router-feedback-collector.js +2 -2
  157. package/dist/core/router-feedback-collector.js.map +1 -1
  158. package/dist/core/runtime-provenance.d.ts +54 -0
  159. package/dist/core/runtime-provenance.d.ts.map +1 -0
  160. package/dist/core/runtime-provenance.js +115 -0
  161. package/dist/core/runtime-provenance.js.map +1 -0
  162. package/dist/core/sandbox/backend.d.ts.map +1 -1
  163. package/dist/core/sandbox/backend.js +39 -21
  164. package/dist/core/sandbox/backend.js.map +1 -1
  165. package/dist/core/sandbox/default-policy.d.ts +4 -5
  166. package/dist/core/sandbox/default-policy.d.ts.map +1 -1
  167. package/dist/core/sandbox/default-policy.js +9 -11
  168. package/dist/core/sandbox/default-policy.js.map +1 -1
  169. package/dist/core/session-bash-runtime.d.ts +4 -4
  170. package/dist/core/session-bash-runtime.d.ts.map +1 -1
  171. package/dist/core/session-bash-runtime.js +6 -5
  172. package/dist/core/session-bash-runtime.js.map +1 -1
  173. package/dist/core/session-compaction-service.d.ts.map +1 -1
  174. package/dist/core/session-compaction-service.js +7 -7
  175. package/dist/core/session-compaction-service.js.map +1 -1
  176. package/dist/core/session-manager.d.ts +4 -1
  177. package/dist/core/session-manager.d.ts.map +1 -1
  178. package/dist/core/session-manager.js +47 -7
  179. package/dist/core/session-manager.js.map +1 -1
  180. package/dist/core/settings-manager.d.ts +7 -4
  181. package/dist/core/settings-manager.d.ts.map +1 -1
  182. package/dist/core/settings-manager.js +11 -3
  183. package/dist/core/settings-manager.js.map +1 -1
  184. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  185. package/dist/core/subagent-orchestration.js +4 -2
  186. package/dist/core/subagent-orchestration.js.map +1 -1
  187. package/dist/core/system-prompt.d.ts +9 -1
  188. package/dist/core/system-prompt.d.ts.map +1 -1
  189. package/dist/core/system-prompt.js +10 -4
  190. package/dist/core/system-prompt.js.map +1 -1
  191. package/dist/core/tools/bash.d.ts +2 -2
  192. package/dist/core/tools/bash.d.ts.map +1 -1
  193. package/dist/core/tools/bash.js +2 -1
  194. package/dist/core/tools/bash.js.map +1 -1
  195. package/dist/core/tools/diagnostics.d.ts.map +1 -1
  196. package/dist/core/tools/diagnostics.js +8 -2
  197. package/dist/core/tools/diagnostics.js.map +1 -1
  198. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  199. package/dist/core/tools/tool-definition-wrapper.js +15 -0
  200. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  201. package/dist/core/turn-metrics.d.ts +135 -0
  202. package/dist/core/turn-metrics.d.ts.map +1 -0
  203. package/dist/core/turn-metrics.js +261 -0
  204. package/dist/core/turn-metrics.js.map +1 -0
  205. package/dist/core/verified-bash-runtime.d.ts.map +1 -1
  206. package/dist/core/verified-bash-runtime.js +10 -4
  207. package/dist/core/verified-bash-runtime.js.map +1 -1
  208. package/dist/guardrails/evidence-protocol.d.ts +10 -0
  209. package/dist/guardrails/evidence-protocol.d.ts.map +1 -0
  210. package/dist/guardrails/evidence-protocol.js +34 -0
  211. package/dist/guardrails/evidence-protocol.js.map +1 -0
  212. package/dist/guardrails/evidence-system.d.ts +3 -1
  213. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  214. package/dist/guardrails/evidence-system.js +22 -14
  215. package/dist/guardrails/evidence-system.js.map +1 -1
  216. package/dist/guardrails/replay-ledger-lock-owner.d.ts +7 -0
  217. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
  218. package/dist/guardrails/replay-ledger-lock-owner.js +42 -1
  219. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
  220. package/dist/guardrails/replay-payload-hash.d.ts +6 -0
  221. package/dist/guardrails/replay-payload-hash.d.ts.map +1 -0
  222. package/dist/guardrails/replay-payload-hash.js +24 -0
  223. package/dist/guardrails/replay-payload-hash.js.map +1 -0
  224. package/dist/index.d.ts +8 -2
  225. package/dist/index.d.ts.map +1 -1
  226. package/dist/index.js +7 -1
  227. package/dist/index.js.map +1 -1
  228. package/dist/main.d.ts.map +1 -1
  229. package/dist/main.js +18 -0
  230. package/dist/main.js.map +1 -1
  231. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  232. package/dist/modes/interactive/components/control-panel-layout.js +2 -2
  233. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  234. package/dist/modes/interactive/components/footer.d.ts +3 -0
  235. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  236. package/dist/modes/interactive/components/footer.js +40 -12
  237. package/dist/modes/interactive/components/footer.js.map +1 -1
  238. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  239. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  240. package/dist/modes/interactive/components/settings-selector.js +10 -0
  241. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  242. package/dist/modes/interactive/components/status-sidebar.d.ts +14 -2
  243. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -1
  244. package/dist/modes/interactive/components/status-sidebar.js +106 -19
  245. package/dist/modes/interactive/components/status-sidebar.js.map +1 -1
  246. package/dist/modes/interactive/interactive-mode.d.ts +10 -1
  247. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  248. package/dist/modes/interactive/interactive-mode.js +104 -17
  249. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  250. package/dist/modes/interactive/theme/dark.json +1 -1
  251. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  252. package/dist/modes/interactive/theme/theme.js +1 -0
  253. package/dist/modes/interactive/theme/theme.js.map +1 -1
  254. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  255. package/dist/modes/rpc/rpc-mode.js +1 -1
  256. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  257. package/dist/types/evidence.d.ts +11 -3
  258. package/dist/types/evidence.d.ts.map +1 -1
  259. package/dist/types/evidence.js.map +1 -1
  260. package/docs/book-to-skill.md +79 -0
  261. package/docs/compaction.md +35 -3
  262. package/docs/containerization.md +14 -2
  263. package/docs/docs.json +16 -0
  264. package/docs/environment-variables.md +1 -1
  265. package/docs/extensions.md +75 -16
  266. package/docs/index.md +3 -0
  267. package/docs/mcp.md +97 -0
  268. package/docs/metrics.md +87 -0
  269. package/docs/models.md +12 -0
  270. package/docs/packages.md +25 -4
  271. package/docs/provider-resilience.md +37 -56
  272. package/docs/providers.md +5 -1
  273. package/docs/run-protocol.md +72 -0
  274. package/docs/sdk.md +40 -6
  275. package/docs/sessions.md +6 -0
  276. package/docs/settings.md +10 -7
  277. package/docs/skills.md +9 -1
  278. package/docs/usage.md +14 -4
  279. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  280. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  281. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  282. package/examples/extensions/gondolin/package-lock.json +2 -2
  283. package/examples/extensions/gondolin/package.json +1 -1
  284. package/examples/extensions/plannotator-approval-bridge.ts +83 -0
  285. package/examples/extensions/sandbox/package-lock.json +2 -2
  286. package/examples/extensions/sandbox/package.json +1 -1
  287. package/examples/extensions/subagent/README.md +21 -11
  288. package/examples/extensions/subagent/adaptive-agent-runtime.ts +9 -3
  289. package/examples/extensions/subagent/agents/planner.md +0 -2
  290. package/examples/extensions/subagent/agents/reviewer.md +0 -2
  291. package/examples/extensions/subagent/agents/scout.md +0 -2
  292. package/examples/extensions/subagent/agents/worker.md +0 -1
  293. package/examples/extensions/subagent/agents.test.ts +9 -0
  294. package/examples/extensions/subagent/agents.ts +11 -4
  295. package/examples/extensions/subagent/deadline-budget.test.ts +32 -0
  296. package/examples/extensions/subagent/deadline-budget.ts +25 -1
  297. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +10 -0
  298. package/examples/extensions/subagent/index.ts +234 -55
  299. package/examples/extensions/subagent/subagent-execution-policy.test.ts +27 -0
  300. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +167 -2
  301. package/examples/extensions/subagent/workflow-graph.test.ts +80 -0
  302. package/examples/extensions/subagent/workflow-graph.ts +89 -0
  303. package/examples/extensions/with-deps/package-lock.json +2 -2
  304. package/examples/extensions/with-deps/package.json +1 -1
  305. package/npm-shrinkwrap.json +275 -439
  306. package/package.json +11 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.96.0] - 2026-08-16
6
+
7
+ ### Added
8
+
9
+ - Added `omk-protocol`, the versioned `TaskSpec -> ExecutionAttempt -> Observation -> EvaluationResult -> RuntimeDecision` contract package, with runtime validators, explicit waivers, and pure semantic and runtime-decision reducers.
10
+ - Added `evidenceReceiptToObservation()` to project integrity-checked EvidenceReceipt v3 cores into immutable protocol facts. Legacy mutable `EvidenceStatus` and `TaskContract` verdict APIs remain compatible but are deprecated.
11
+ - Added the optional `omk-book-to-skill` package with compile/update commands, a pinned upstream workflow, advisory generated-skill scanning, and SHA-256 source/artifact provenance checks. Python extractors remain outside OMK core.
12
+ - Added deterministically seeded, bounded `fast-check` model and property suites for WPL transitions, replay migration and CAS, evidence freshness, subagent topology, run-journal CAS, and timeout/abort settlement ordering.
13
+
14
+ ### Changed
15
+
16
+ - New replay events declare `jcs-rfc8785-v2` and hash RFC 8785-canonical payloads. Events without an algorithm remain verified as `json-stringify-v1`; mixed ledgers and exports preserve legacy records without rewriting them.
17
+
18
+ ### Fixed
19
+
20
+ - NVIDIA NIM's `z-ai/glm-5.2` entry now transmits `reasoning_effort`, including the generated `max` thinking level; other NVIDIA models keep conservative compatibility defaults.
21
+ - Billing-cycle and quota exhaustion, including provider 403 usage-limit responses, now classify as `provider.rate_limit` and can switch to the first configured, authenticated resilience candidate before retry. Each attempt remains journaled, and a recovered retry ends with a later `completed` termination.
22
+ - Subagent DAG scheduling now sorts simultaneously ready lanes by lane ID, so topology aggregation does not depend on input insertion order.
23
+ - Local release bundles now include `omk-adaptorch-wpl`, allowing isolated installs of the full packed workspace without resolving that dependency from the registry.
24
+
25
+ ## [0.95.2] - 2026-08-15
26
+
27
+ ### Changed
28
+
29
+ - macOS sessions now derive replay-ledger process identity from bounded BSD `ps -o lstart=` output, so replay-lock acquisition no longer fails closed during AgentSession startup on Darwin while Linux `/proc` behavior remains unchanged.
30
+ - The status rail now verifies connected MCP servers with protocol pings, marks dead processes as failed, and retries failed servers on a bounded slower cadence without spawning idle servers.
31
+ - Interactive UX now includes an empty-editor affordance hint, elapsed-time and interrupt details on the working indicator, transient information notices, width-safe editor scroll borders, and opt-in footer CPU/memory metrics.
32
+ - Extension tools can resolve timeouts from current context. The subagent extension uses this to remove task-count, concurrency, execution-budget, attempt, and outer tool-timeout caps in Ultra while preserving explicit cancellation and non-Ultra limits.
33
+ - The startup control panel now identifies the product with `WELCOME TO OMK` instead of legacy Pi agent branding.
34
+ - Context-budget prompts use compact metadata for valid zero-resource plans while retaining cache-state and legacy optimizer compatibility telemetry; diagnostic and non-empty plans keep full observability, and invalid budgets bypass the plan cache so diagnostics cannot be hidden by a prior hit.
35
+ - The workspace context-budget cache scans every persisted key/value, keeps credential-shaped entries in memory only, and removes unsafe legacy snapshots instead of persisting secret-shaped text.
36
+ - Exact context representations now reuse content-addressed cache entries across different queries and budget sizes instead of forcing avoidable misses.
37
+ - Opt-in reasoning-router learning now isolates default ledgers and compiled bias snapshots per repository or git worktree, captures explicit manual thinking-level overrides as bounded feedback, and keeps updates behind the shipped, deterministic `omk router-feedback compile-bias` between-session command. Snapshot loading rejects nonzero biases below the strong-evidence threshold, unsafe or inconsistent counts, and duplicate cells; compilation uses an exclusive randomized temporary file before atomic rename.
38
+ - Session bash now defaults to OS sandbox enforcement instead of ledger-only audit mode. macOS `sandbox-exec` and Linux `bwrap` restrict writes to the workspace/temp directories and disable network access; missing backends fail closed. Unwrapped `audit` and disabled `off` modes now require an explicit `OMK_BASH_SANDBOX` value, and unknown values resolve to `enforce`.
39
+
3
40
  ## [0.95.1] - 2026-08-01
4
41
 
5
42
  ### Added
package/README.md CHANGED
@@ -40,6 +40,16 @@ Then:
40
40
 
41
41
  OMK stores user configuration under `~/.omk/agent/` and project configuration under `.omk/`.
42
42
 
43
+ ## Optional document compiler
44
+
45
+ Install `omk-book-to-skill` to compile documents into reusable skills without adding Python extractors to OMK core:
46
+
47
+ ```bash
48
+ omk install npm:omk-book-to-skill@0.95.2
49
+ ```
50
+
51
+ It provides compile, update, and verification commands plus a local SHA-256 provenance manifest. See [Book to Skill](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/book-to-skill.md).
52
+
43
53
  ## Providers & Models
44
54
 
45
55
  OMK includes provider adapters for Anthropic, OpenAI Codex, Google, OpenCode, Kimi, Qwen, ZAI/GLM, Grok, and other compatible services. Availability depends on the credentials and endpoints configured on the current machine.
@@ -47,11 +57,14 @@ OMK includes provider adapters for Anthropic, OpenAI Codex, Google, OpenCode, Ki
47
57
  - `/login` adds or selects credentials.
48
58
  - `/logout` removes credentials for a selected provider.
49
59
  - `/model` lists models available to the current credential set.
60
+ - NVIDIA NIM's `z-ai/glm-5.2` entry transmits reasoning effort, including `/thinking max`.
50
61
  - The optional status rail shows independent provider quota windows when an official API or passive response signal is available.
51
62
  - Missing quota values are shown as unavailable rather than estimated.
52
63
 
53
64
  OAuth credentials are stored locally in `~/.omk/agent/auth.json`. Multi-account providers keep accounts separate and send requests only through the explicitly selected account.
54
65
 
66
+ Quota and billing-cycle errors are classified as rate limits and can switch to the first authenticated resilience candidate before retry. See [Provider resilience](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/provider-resilience.md).
67
+
55
68
  See [Provider setup](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/providers.md) for provider-specific authentication, quota behavior, and environment variables.
56
69
 
57
70
  ### Kimi For Coding
@@ -142,6 +155,8 @@ See [RPC](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/rpc
142
155
 
143
156
  The TypeScript SDK exposes session creation, provider configuration, tools, and event streams. Extensions can add commands, tools, UI components, themes, and resource loaders without patching OMK core.
144
157
 
158
+ `omk-protocol` provides the versioned `TaskSpec -> ExecutionAttempt -> Observation -> EvaluationResult -> RuntimeDecision` contracts and pure semantic reducers. See [Run Protocol v1](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/run-protocol.md) for the implemented scope and migration boundaries.
159
+
145
160
  - [SDK](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/sdk.md)
146
161
  - [Extensions](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/extensions.md)
147
162
  - [Skills](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/skills.md)
@@ -170,7 +185,7 @@ npm test
170
185
 
171
186
  See [Development](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/development.md) and [CONTRIBUTING.md](https://github.com/dmae97/omk/blob/main/CONTRIBUTING.md).
172
187
 
173
- Release notes: [v0.95.1](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v0.95.1.md).
188
+ Release notes: [v0.95.2](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v0.95.2.md).
174
189
 
175
190
  ## License
176
191
 
@@ -0,0 +1,18 @@
1
+ import { type ApprovalReceipt } from "./approval-receipt.ts";
2
+ export interface ApprovalReceiptWriteResult {
3
+ path: string;
4
+ created: boolean;
5
+ }
6
+ export declare class ApprovalReceiptStore {
7
+ readonly rootDir: string;
8
+ constructor(rootDir: string);
9
+ private ensurePrivateDirectory;
10
+ private assertRootUnchanged;
11
+ private pathFor;
12
+ private openReceiptForRead;
13
+ write(receipt: ApprovalReceipt): ApprovalReceiptWriteResult;
14
+ has(receiptId: string): boolean;
15
+ read(receiptId: string): ApprovalReceipt;
16
+ listReceiptIds(): string[];
17
+ }
18
+ //# sourceMappingURL=approval-receipt-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-receipt-store.d.ts","sourceRoot":"","sources":["../../src/approvals/approval-receipt-store.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,eAAe,EAAqD,MAAM,uBAAuB,CAAC;AAKhH,MAAM,WAAW,0BAA0B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CACjB;AAsDD,qBAAa,oBAAoB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,YAAY,OAAO,EAAE,MAAM,EAG1B;IAED,OAAO,CAAC,sBAAsB;IAa9B,OAAO,CAAC,mBAAmB;IAO3B,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,kBAAkB;IAW1B,KAAK,CAAC,OAAO,EAAE,eAAe,GAAG,0BAA0B,CA4D1D;IAED,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAe9B;IAED,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,CAUvC;IAED,cAAc,IAAI,MAAM,EAAE,CASzB;CACD","sourcesContent":["import {\n\tcloseSync,\n\tconstants,\n\tfchmodSync,\n\tfstatSync,\n\tfsyncSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { type ApprovalReceipt, serializeApprovalReceipt, validateApprovalReceipt } from \"./approval-receipt.ts\";\n\nconst MAX_RECEIPT_BYTES = 1024 * 1024;\nconst NOFOLLOW = constants.O_NOFOLLOW ?? 0;\n\nexport interface ApprovalReceiptWriteResult {\n\tpath: string;\n\tcreated: boolean;\n}\n\ninterface RootIdentity {\n\tdev: number;\n\tino: number;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n\treturn error instanceof Error && \"code\" in error && error.code === code;\n}\n\nfunction assertOwnedByCurrentUser(uid: number, label: string): void {\n\tconst getuid = process.getuid;\n\tif (typeof getuid === \"function\" && uid !== getuid()) {\n\t\tthrow new Error(`${label} is not owned by the current user`);\n\t}\n}\n\nfunction receiptFileName(receiptId: string): string {\n\tif (!/^approval-[a-f0-9]{32}$/u.test(receiptId)) throw new Error(\"Approval receipt id is invalid\");\n\treturn `${receiptId}.json`;\n}\n\nfunction assertPrivateReceiptFd(fd: number, filePath: string): void {\n\tconst stat = fstatSync(fd);\n\tif (!stat.isFile()) throw new Error(`Approval receipt is not a regular file: ${filePath}`);\n\tif (stat.nlink !== 1) throw new Error(`Approval receipt must not be hard-linked: ${filePath}`);\n\tassertOwnedByCurrentUser(stat.uid, `Approval receipt ${filePath}`);\n\tif ((stat.mode & 0o777) !== 0o600) {\n\t\tthrow new Error(`Approval receipt permissions must be 0600: ${filePath}`);\n\t}\n\tif (stat.size > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n}\n\nfunction readReceiptFromFd(fd: number, filePath: string): ApprovalReceipt {\n\tassertPrivateReceiptFd(fd, filePath);\n\tconst buffer = Buffer.alloc(MAX_RECEIPT_BYTES + 1);\n\tlet offset = 0;\n\twhile (offset < buffer.length) {\n\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, offset);\n\t\tif (read === 0) break;\n\t\toffset += read;\n\t}\n\tif (offset > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\tassertPrivateReceiptFd(fd, filePath);\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(buffer.subarray(0, offset).toString(\"utf8\")) as unknown;\n\t} catch {\n\t\tthrow new Error(`Approval receipt is not valid JSON: ${filePath}`);\n\t}\n\treturn validateApprovalReceipt(value);\n}\n\nexport class ApprovalReceiptStore {\n\treadonly rootDir: string;\n\n\tconstructor(rootDir: string) {\n\t\tthis.rootDir = resolve(rootDir);\n\t\tthis.ensurePrivateDirectory();\n\t}\n\n\tprivate ensurePrivateDirectory(): RootIdentity {\n\t\tmkdirSync(this.rootDir, { recursive: true, mode: 0o700 });\n\t\tconst stat = lstatSync(this.rootDir);\n\t\tif (stat.isSymbolicLink() || !stat.isDirectory()) {\n\t\t\tthrow new Error(`Approval receipt root must be a real directory: ${this.rootDir}`);\n\t\t}\n\t\tassertOwnedByCurrentUser(stat.uid, `Approval receipt root ${this.rootDir}`);\n\t\tif ((stat.mode & 0o777) !== 0o700) {\n\t\t\tthrow new Error(`Approval receipt root permissions must be 0700: ${this.rootDir}`);\n\t\t}\n\t\treturn { dev: stat.dev, ino: stat.ino };\n\t}\n\n\tprivate assertRootUnchanged(expected: RootIdentity): void {\n\t\tconst current = this.ensurePrivateDirectory();\n\t\tif (current.dev !== expected.dev || current.ino !== expected.ino) {\n\t\t\tthrow new Error(\"Approval receipt root changed during the operation\");\n\t\t}\n\t}\n\n\tprivate pathFor(receiptId: string): string {\n\t\treturn join(this.rootDir, receiptFileName(receiptId));\n\t}\n\n\tprivate openReceiptForRead(filePath: string): number {\n\t\tconst fd = openSync(filePath, constants.O_RDONLY | NOFOLLOW);\n\t\ttry {\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\treturn fd;\n\t\t} catch (error) {\n\t\t\tcloseSync(fd);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\twrite(receipt: ApprovalReceipt): ApprovalReceiptWriteResult {\n\t\tconst validated = validateApprovalReceipt(receipt);\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(validated.core.receiptId);\n\t\tconst canonical = serializeApprovalReceipt(validated);\n\t\tconst bytes = Buffer.from(canonical, \"utf8\");\n\t\tif (bytes.byteLength > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\n\t\t// Explicit existence/symlink pre-check. O_EXCL/O_NOFOLLOW are honored on\n\t\t// proper Linux but silently ignored on some platforms (e.g. WSL2), which\n\t\t// would silently void the never-overwrite and symlink-rejection invariants\n\t\t// (hard rule: no silent degraded operation). Enforce them via lstat so the\n\t\t// guarantees hold on every platform; O_EXCL below stays as the atomic\n\t\t// backstop for the create path on capable platforms.\n\t\tlet existingStat: ReturnType<typeof lstatSync> | null = null;\n\t\ttry {\n\t\t\texistingStat = lstatSync(filePath);\n\t\t} catch (error) {\n\t\t\tif (!isErrno(error, \"ENOENT\")) throw error;\n\t\t}\n\t\tif (existingStat !== null) {\n\t\t\tif (existingStat.isSymbolicLink()) {\n\t\t\t\tthrow new Error(`Approval receipt must not be a symbolic link: ${filePath}`);\n\t\t\t}\n\t\t\tconst prior = this.read(validated.core.receiptId);\n\t\t\tif (serializeApprovalReceipt(prior) !== canonical) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\treturn { path: filePath, created: false };\n\t\t}\n\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);\n\t\t} catch (error) {\n\t\t\tif (!isErrno(error, \"EEXIST\")) throw error;\n\t\t\tconst existing = this.read(validated.core.receiptId);\n\t\t\tif (serializeApprovalReceipt(existing) !== canonical) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\treturn { path: filePath, created: false };\n\t\t}\n\n\t\ttry {\n\t\t\tfchmodSync(fd, 0o600);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\twriteFileSync(fd, bytes);\n\t\t\tfsyncSync(fd);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn { path: filePath, created: true };\n\t}\n\n\thas(receiptId: string): boolean {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = this.openReceiptForRead(this.pathFor(receiptId));\n\t\t} catch (error) {\n\t\t\tif (isErrno(error, \"ENOENT\")) {\n\t\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tcloseSync(fd);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn true;\n\t}\n\n\tread(receiptId: string): ApprovalReceipt {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(receiptId);\n\t\tconst fd = this.openReceiptForRead(filePath);\n\t\ttry {\n\t\t\treturn readReceiptFromFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t}\n\t}\n\n\tlistReceiptIds(): string[] {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst ids = readdirSync(this.rootDir)\n\t\t\t.map((name) => /^(approval-[a-f0-9]{32})\\.json$/u.exec(name)?.[1])\n\t\t\t.filter((id): id is string => id !== undefined)\n\t\t\t.sort((left, right) => left.localeCompare(right));\n\t\tfor (const id of ids) this.has(id);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn ids;\n\t}\n}\n"]}
@@ -0,0 +1,194 @@
1
+ import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, writeFileSync, } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { serializeApprovalReceipt, validateApprovalReceipt } from "./approval-receipt.js";
4
+ const MAX_RECEIPT_BYTES = 1024 * 1024;
5
+ const NOFOLLOW = constants.O_NOFOLLOW ?? 0;
6
+ function isErrno(error, code) {
7
+ return error instanceof Error && "code" in error && error.code === code;
8
+ }
9
+ function assertOwnedByCurrentUser(uid, label) {
10
+ const getuid = process.getuid;
11
+ if (typeof getuid === "function" && uid !== getuid()) {
12
+ throw new Error(`${label} is not owned by the current user`);
13
+ }
14
+ }
15
+ function receiptFileName(receiptId) {
16
+ if (!/^approval-[a-f0-9]{32}$/u.test(receiptId))
17
+ throw new Error("Approval receipt id is invalid");
18
+ return `${receiptId}.json`;
19
+ }
20
+ function assertPrivateReceiptFd(fd, filePath) {
21
+ const stat = fstatSync(fd);
22
+ if (!stat.isFile())
23
+ throw new Error(`Approval receipt is not a regular file: ${filePath}`);
24
+ if (stat.nlink !== 1)
25
+ throw new Error(`Approval receipt must not be hard-linked: ${filePath}`);
26
+ assertOwnedByCurrentUser(stat.uid, `Approval receipt ${filePath}`);
27
+ if ((stat.mode & 0o777) !== 0o600) {
28
+ throw new Error(`Approval receipt permissions must be 0600: ${filePath}`);
29
+ }
30
+ if (stat.size > MAX_RECEIPT_BYTES)
31
+ throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);
32
+ }
33
+ function readReceiptFromFd(fd, filePath) {
34
+ assertPrivateReceiptFd(fd, filePath);
35
+ const buffer = Buffer.alloc(MAX_RECEIPT_BYTES + 1);
36
+ let offset = 0;
37
+ while (offset < buffer.length) {
38
+ const read = readSync(fd, buffer, offset, buffer.length - offset, offset);
39
+ if (read === 0)
40
+ break;
41
+ offset += read;
42
+ }
43
+ if (offset > MAX_RECEIPT_BYTES)
44
+ throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);
45
+ assertPrivateReceiptFd(fd, filePath);
46
+ let value;
47
+ try {
48
+ value = JSON.parse(buffer.subarray(0, offset).toString("utf8"));
49
+ }
50
+ catch {
51
+ throw new Error(`Approval receipt is not valid JSON: ${filePath}`);
52
+ }
53
+ return validateApprovalReceipt(value);
54
+ }
55
+ export class ApprovalReceiptStore {
56
+ rootDir;
57
+ constructor(rootDir) {
58
+ this.rootDir = resolve(rootDir);
59
+ this.ensurePrivateDirectory();
60
+ }
61
+ ensurePrivateDirectory() {
62
+ mkdirSync(this.rootDir, { recursive: true, mode: 0o700 });
63
+ const stat = lstatSync(this.rootDir);
64
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
65
+ throw new Error(`Approval receipt root must be a real directory: ${this.rootDir}`);
66
+ }
67
+ assertOwnedByCurrentUser(stat.uid, `Approval receipt root ${this.rootDir}`);
68
+ if ((stat.mode & 0o777) !== 0o700) {
69
+ throw new Error(`Approval receipt root permissions must be 0700: ${this.rootDir}`);
70
+ }
71
+ return { dev: stat.dev, ino: stat.ino };
72
+ }
73
+ assertRootUnchanged(expected) {
74
+ const current = this.ensurePrivateDirectory();
75
+ if (current.dev !== expected.dev || current.ino !== expected.ino) {
76
+ throw new Error("Approval receipt root changed during the operation");
77
+ }
78
+ }
79
+ pathFor(receiptId) {
80
+ return join(this.rootDir, receiptFileName(receiptId));
81
+ }
82
+ openReceiptForRead(filePath) {
83
+ const fd = openSync(filePath, constants.O_RDONLY | NOFOLLOW);
84
+ try {
85
+ assertPrivateReceiptFd(fd, filePath);
86
+ return fd;
87
+ }
88
+ catch (error) {
89
+ closeSync(fd);
90
+ throw error;
91
+ }
92
+ }
93
+ write(receipt) {
94
+ const validated = validateApprovalReceipt(receipt);
95
+ const rootIdentity = this.ensurePrivateDirectory();
96
+ const filePath = this.pathFor(validated.core.receiptId);
97
+ const canonical = serializeApprovalReceipt(validated);
98
+ const bytes = Buffer.from(canonical, "utf8");
99
+ if (bytes.byteLength > MAX_RECEIPT_BYTES)
100
+ throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);
101
+ // Explicit existence/symlink pre-check. O_EXCL/O_NOFOLLOW are honored on
102
+ // proper Linux but silently ignored on some platforms (e.g. WSL2), which
103
+ // would silently void the never-overwrite and symlink-rejection invariants
104
+ // (hard rule: no silent degraded operation). Enforce them via lstat so the
105
+ // guarantees hold on every platform; O_EXCL below stays as the atomic
106
+ // backstop for the create path on capable platforms.
107
+ let existingStat = null;
108
+ try {
109
+ existingStat = lstatSync(filePath);
110
+ }
111
+ catch (error) {
112
+ if (!isErrno(error, "ENOENT"))
113
+ throw error;
114
+ }
115
+ if (existingStat !== null) {
116
+ if (existingStat.isSymbolicLink()) {
117
+ throw new Error(`Approval receipt must not be a symbolic link: ${filePath}`);
118
+ }
119
+ const prior = this.read(validated.core.receiptId);
120
+ if (serializeApprovalReceipt(prior) !== canonical) {
121
+ throw new Error(`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`);
122
+ }
123
+ this.assertRootUnchanged(rootIdentity);
124
+ return { path: filePath, created: false };
125
+ }
126
+ let fd;
127
+ try {
128
+ fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);
129
+ }
130
+ catch (error) {
131
+ if (!isErrno(error, "EEXIST"))
132
+ throw error;
133
+ const existing = this.read(validated.core.receiptId);
134
+ if (serializeApprovalReceipt(existing) !== canonical) {
135
+ throw new Error(`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`);
136
+ }
137
+ this.assertRootUnchanged(rootIdentity);
138
+ return { path: filePath, created: false };
139
+ }
140
+ try {
141
+ fchmodSync(fd, 0o600);
142
+ assertPrivateReceiptFd(fd, filePath);
143
+ writeFileSync(fd, bytes);
144
+ fsyncSync(fd);
145
+ assertPrivateReceiptFd(fd, filePath);
146
+ }
147
+ finally {
148
+ closeSync(fd);
149
+ }
150
+ this.assertRootUnchanged(rootIdentity);
151
+ return { path: filePath, created: true };
152
+ }
153
+ has(receiptId) {
154
+ const rootIdentity = this.ensurePrivateDirectory();
155
+ let fd;
156
+ try {
157
+ fd = this.openReceiptForRead(this.pathFor(receiptId));
158
+ }
159
+ catch (error) {
160
+ if (isErrno(error, "ENOENT")) {
161
+ this.assertRootUnchanged(rootIdentity);
162
+ return false;
163
+ }
164
+ throw error;
165
+ }
166
+ closeSync(fd);
167
+ this.assertRootUnchanged(rootIdentity);
168
+ return true;
169
+ }
170
+ read(receiptId) {
171
+ const rootIdentity = this.ensurePrivateDirectory();
172
+ const filePath = this.pathFor(receiptId);
173
+ const fd = this.openReceiptForRead(filePath);
174
+ try {
175
+ return readReceiptFromFd(fd, filePath);
176
+ }
177
+ finally {
178
+ closeSync(fd);
179
+ this.assertRootUnchanged(rootIdentity);
180
+ }
181
+ }
182
+ listReceiptIds() {
183
+ const rootIdentity = this.ensurePrivateDirectory();
184
+ const ids = readdirSync(this.rootDir)
185
+ .map((name) => /^(approval-[a-f0-9]{32})\.json$/u.exec(name)?.[1])
186
+ .filter((id) => id !== undefined)
187
+ .sort((left, right) => left.localeCompare(right));
188
+ for (const id of ids)
189
+ this.has(id);
190
+ this.assertRootUnchanged(rootIdentity);
191
+ return ids;
192
+ }
193
+ }
194
+ //# sourceMappingURL=approval-receipt-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-receipt-store.js","sourceRoot":"","sources":["../../src/approvals/approval-receipt-store.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tcloseSync,\n\tconstants,\n\tfchmodSync,\n\tfstatSync,\n\tfsyncSync,\n\tlstatSync,\n\tmkdirSync,\n\topenSync,\n\treaddirSync,\n\treadSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { type ApprovalReceipt, serializeApprovalReceipt, validateApprovalReceipt } from \"./approval-receipt.ts\";\n\nconst MAX_RECEIPT_BYTES = 1024 * 1024;\nconst NOFOLLOW = constants.O_NOFOLLOW ?? 0;\n\nexport interface ApprovalReceiptWriteResult {\n\tpath: string;\n\tcreated: boolean;\n}\n\ninterface RootIdentity {\n\tdev: number;\n\tino: number;\n}\n\nfunction isErrno(error: unknown, code: string): boolean {\n\treturn error instanceof Error && \"code\" in error && error.code === code;\n}\n\nfunction assertOwnedByCurrentUser(uid: number, label: string): void {\n\tconst getuid = process.getuid;\n\tif (typeof getuid === \"function\" && uid !== getuid()) {\n\t\tthrow new Error(`${label} is not owned by the current user`);\n\t}\n}\n\nfunction receiptFileName(receiptId: string): string {\n\tif (!/^approval-[a-f0-9]{32}$/u.test(receiptId)) throw new Error(\"Approval receipt id is invalid\");\n\treturn `${receiptId}.json`;\n}\n\nfunction assertPrivateReceiptFd(fd: number, filePath: string): void {\n\tconst stat = fstatSync(fd);\n\tif (!stat.isFile()) throw new Error(`Approval receipt is not a regular file: ${filePath}`);\n\tif (stat.nlink !== 1) throw new Error(`Approval receipt must not be hard-linked: ${filePath}`);\n\tassertOwnedByCurrentUser(stat.uid, `Approval receipt ${filePath}`);\n\tif ((stat.mode & 0o777) !== 0o600) {\n\t\tthrow new Error(`Approval receipt permissions must be 0600: ${filePath}`);\n\t}\n\tif (stat.size > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n}\n\nfunction readReceiptFromFd(fd: number, filePath: string): ApprovalReceipt {\n\tassertPrivateReceiptFd(fd, filePath);\n\tconst buffer = Buffer.alloc(MAX_RECEIPT_BYTES + 1);\n\tlet offset = 0;\n\twhile (offset < buffer.length) {\n\t\tconst read = readSync(fd, buffer, offset, buffer.length - offset, offset);\n\t\tif (read === 0) break;\n\t\toffset += read;\n\t}\n\tif (offset > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\tassertPrivateReceiptFd(fd, filePath);\n\tlet value: unknown;\n\ttry {\n\t\tvalue = JSON.parse(buffer.subarray(0, offset).toString(\"utf8\")) as unknown;\n\t} catch {\n\t\tthrow new Error(`Approval receipt is not valid JSON: ${filePath}`);\n\t}\n\treturn validateApprovalReceipt(value);\n}\n\nexport class ApprovalReceiptStore {\n\treadonly rootDir: string;\n\n\tconstructor(rootDir: string) {\n\t\tthis.rootDir = resolve(rootDir);\n\t\tthis.ensurePrivateDirectory();\n\t}\n\n\tprivate ensurePrivateDirectory(): RootIdentity {\n\t\tmkdirSync(this.rootDir, { recursive: true, mode: 0o700 });\n\t\tconst stat = lstatSync(this.rootDir);\n\t\tif (stat.isSymbolicLink() || !stat.isDirectory()) {\n\t\t\tthrow new Error(`Approval receipt root must be a real directory: ${this.rootDir}`);\n\t\t}\n\t\tassertOwnedByCurrentUser(stat.uid, `Approval receipt root ${this.rootDir}`);\n\t\tif ((stat.mode & 0o777) !== 0o700) {\n\t\t\tthrow new Error(`Approval receipt root permissions must be 0700: ${this.rootDir}`);\n\t\t}\n\t\treturn { dev: stat.dev, ino: stat.ino };\n\t}\n\n\tprivate assertRootUnchanged(expected: RootIdentity): void {\n\t\tconst current = this.ensurePrivateDirectory();\n\t\tif (current.dev !== expected.dev || current.ino !== expected.ino) {\n\t\t\tthrow new Error(\"Approval receipt root changed during the operation\");\n\t\t}\n\t}\n\n\tprivate pathFor(receiptId: string): string {\n\t\treturn join(this.rootDir, receiptFileName(receiptId));\n\t}\n\n\tprivate openReceiptForRead(filePath: string): number {\n\t\tconst fd = openSync(filePath, constants.O_RDONLY | NOFOLLOW);\n\t\ttry {\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\treturn fd;\n\t\t} catch (error) {\n\t\t\tcloseSync(fd);\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\twrite(receipt: ApprovalReceipt): ApprovalReceiptWriteResult {\n\t\tconst validated = validateApprovalReceipt(receipt);\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(validated.core.receiptId);\n\t\tconst canonical = serializeApprovalReceipt(validated);\n\t\tconst bytes = Buffer.from(canonical, \"utf8\");\n\t\tif (bytes.byteLength > MAX_RECEIPT_BYTES) throw new Error(`Approval receipt exceeds ${MAX_RECEIPT_BYTES} bytes`);\n\n\t\t// Explicit existence/symlink pre-check. O_EXCL/O_NOFOLLOW are honored on\n\t\t// proper Linux but silently ignored on some platforms (e.g. WSL2), which\n\t\t// would silently void the never-overwrite and symlink-rejection invariants\n\t\t// (hard rule: no silent degraded operation). Enforce them via lstat so the\n\t\t// guarantees hold on every platform; O_EXCL below stays as the atomic\n\t\t// backstop for the create path on capable platforms.\n\t\tlet existingStat: ReturnType<typeof lstatSync> | null = null;\n\t\ttry {\n\t\t\texistingStat = lstatSync(filePath);\n\t\t} catch (error) {\n\t\t\tif (!isErrno(error, \"ENOENT\")) throw error;\n\t\t}\n\t\tif (existingStat !== null) {\n\t\t\tif (existingStat.isSymbolicLink()) {\n\t\t\t\tthrow new Error(`Approval receipt must not be a symbolic link: ${filePath}`);\n\t\t\t}\n\t\t\tconst prior = this.read(validated.core.receiptId);\n\t\t\tif (serializeApprovalReceipt(prior) !== canonical) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\treturn { path: filePath, created: false };\n\t\t}\n\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);\n\t\t} catch (error) {\n\t\t\tif (!isErrno(error, \"EEXIST\")) throw error;\n\t\t\tconst existing = this.read(validated.core.receiptId);\n\t\t\tif (serializeApprovalReceipt(existing) !== canonical) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Approval receipt already exists with different content and is never overwritten: ${validated.core.receiptId}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\treturn { path: filePath, created: false };\n\t\t}\n\n\t\ttry {\n\t\t\tfchmodSync(fd, 0o600);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t\twriteFileSync(fd, bytes);\n\t\t\tfsyncSync(fd);\n\t\t\tassertPrivateReceiptFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t}\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn { path: filePath, created: true };\n\t}\n\n\thas(receiptId: string): boolean {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tlet fd: number;\n\t\ttry {\n\t\t\tfd = this.openReceiptForRead(this.pathFor(receiptId));\n\t\t} catch (error) {\n\t\t\tif (isErrno(error, \"ENOENT\")) {\n\t\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t\tcloseSync(fd);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn true;\n\t}\n\n\tread(receiptId: string): ApprovalReceipt {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst filePath = this.pathFor(receiptId);\n\t\tconst fd = this.openReceiptForRead(filePath);\n\t\ttry {\n\t\t\treturn readReceiptFromFd(fd, filePath);\n\t\t} finally {\n\t\t\tcloseSync(fd);\n\t\t\tthis.assertRootUnchanged(rootIdentity);\n\t\t}\n\t}\n\n\tlistReceiptIds(): string[] {\n\t\tconst rootIdentity = this.ensurePrivateDirectory();\n\t\tconst ids = readdirSync(this.rootDir)\n\t\t\t.map((name) => /^(approval-[a-f0-9]{32})\\.json$/u.exec(name)?.[1])\n\t\t\t.filter((id): id is string => id !== undefined)\n\t\t\t.sort((left, right) => left.localeCompare(right));\n\t\tfor (const id of ids) this.has(id);\n\t\tthis.assertRootUnchanged(rootIdentity);\n\t\treturn ids;\n\t}\n}\n"]}
@@ -0,0 +1,43 @@
1
+ export type ApprovalDecision = "approved" | "rejected" | "invalidated";
2
+ export interface ApprovalContentDigest {
3
+ sha256: string;
4
+ bytes: number;
5
+ }
6
+ export interface ApprovalPlanDigest extends ApprovalContentDigest {
7
+ path: string;
8
+ }
9
+ export interface ApprovalReceiptCore {
10
+ schemaVersion: 1;
11
+ receiptId: string;
12
+ provider: "plannotator";
13
+ trust: "external-extension-event";
14
+ requestId: string;
15
+ reviewId: string;
16
+ decision: ApprovalDecision;
17
+ decidedAt: string;
18
+ sessionId: string;
19
+ plan: ApprovalPlanDigest;
20
+ feedback?: ApprovalContentDigest;
21
+ }
22
+ export interface ApprovalReceipt {
23
+ core: ApprovalReceiptCore;
24
+ coreSha256: string;
25
+ }
26
+ export interface CreateApprovalReceiptInput {
27
+ provider: "plannotator";
28
+ requestId: string;
29
+ reviewId: string;
30
+ decision: ApprovalDecision;
31
+ decidedAt: string;
32
+ sessionId: string;
33
+ planPath: string;
34
+ planContent: string;
35
+ feedback?: string;
36
+ }
37
+ export declare function computeApprovalContentDigest(value: string): ApprovalContentDigest;
38
+ export declare function computeApprovalReceiptCoreSha256(core: ApprovalReceiptCore): string;
39
+ export declare function validateApprovalReceipt(value: unknown): ApprovalReceipt;
40
+ export declare function createApprovalReceipt(input: CreateApprovalReceiptInput): ApprovalReceipt;
41
+ export declare function parseApprovalReceipt(serialized: string): ApprovalReceipt;
42
+ export declare function serializeApprovalReceipt(receipt: ApprovalReceipt): string;
43
+ //# sourceMappingURL=approval-receipt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"approval-receipt.d.ts","sourceRoot":"","sources":["../../src/approvals/approval-receipt.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;AAEvE,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,qBAAqB;IAChE,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,mBAAmB;IACnC,aAAa,EAAE,CAAC,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,aAAa,CAAC;IACxB,KAAK,EAAE,0BAA0B,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,CAAC,EAAE,qBAAqB,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,EAAE,aAAa,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAMD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,CAEjF;AAyFD,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAElF;AAOD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CA8CvE;AAiBD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,0BAA0B,GAAG,eAAe,CAwBxF;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAQxE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAEzE","sourcesContent":["import { createHash, timingSafeEqual } from \"node:crypto\";\nimport { isAbsolute } from \"node:path\";\n\nconst SHA256_PATTERN = /^[a-f0-9]{64}$/u;\nconst SAFE_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u;\nconst CORE_DOMAIN = \"omk.approval-receipt.core.v1\\0\";\nconst ID_DOMAIN = \"omk.approval-receipt.id.v1\\0\";\n\nexport type ApprovalDecision = \"approved\" | \"rejected\" | \"invalidated\";\n\nexport interface ApprovalContentDigest {\n\tsha256: string;\n\tbytes: number;\n}\n\nexport interface ApprovalPlanDigest extends ApprovalContentDigest {\n\tpath: string;\n}\n\nexport interface ApprovalReceiptCore {\n\tschemaVersion: 1;\n\treceiptId: string;\n\tprovider: \"plannotator\";\n\ttrust: \"external-extension-event\";\n\trequestId: string;\n\treviewId: string;\n\tdecision: ApprovalDecision;\n\tdecidedAt: string;\n\tsessionId: string;\n\tplan: ApprovalPlanDigest;\n\tfeedback?: ApprovalContentDigest;\n}\n\nexport interface ApprovalReceipt {\n\tcore: ApprovalReceiptCore;\n\tcoreSha256: string;\n}\n\nexport interface CreateApprovalReceiptInput {\n\tprovider: \"plannotator\";\n\trequestId: string;\n\treviewId: string;\n\tdecision: ApprovalDecision;\n\tdecidedAt: string;\n\tsessionId: string;\n\tplanPath: string;\n\tplanContent: string;\n\tfeedback?: string;\n}\n\nfunction sha256(domain: string, value: string): string {\n\treturn createHash(\"sha256\").update(domain).update(value, \"utf8\").digest(\"hex\");\n}\n\nexport function computeApprovalContentDigest(value: string): ApprovalContentDigest {\n\treturn { sha256: sha256(\"omk.approval-content.v1\\0\", value), bytes: Buffer.byteLength(value, \"utf8\") };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction hasOwn(value: Record<string, unknown>, key: string): boolean {\n\treturn Object.getOwnPropertyDescriptor(value, key) !== undefined;\n}\n\nfunction exactKeys(value: Record<string, unknown>, required: string[], optional: string[] = []): void {\n\tconst allowed = new Set([...required, ...optional]);\n\tfor (const key of Object.keys(value)) {\n\t\tif (!allowed.has(key)) throw new Error(`Unexpected approval receipt field: ${key}`);\n\t}\n\tfor (const key of required) {\n\t\tif (!hasOwn(value, key)) throw new Error(`Missing approval receipt field: ${key}`);\n\t}\n}\n\nfunction safeId(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !SAFE_ID_PATTERN.test(value)) throw new Error(`${label} is not a safe identifier`);\n\treturn value;\n}\n\nfunction sha256Hex(value: unknown, label: string): string {\n\tif (typeof value !== \"string\" || !SHA256_PATTERN.test(value)) throw new Error(`${label} must be lowercase SHA-256`);\n\treturn value;\n}\n\nfunction nonNegativeInteger(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 0)\n\t\tthrow new Error(`${label} must be a non-negative safe integer`);\n\treturn value as number;\n}\n\nfunction workspaceRelativePath(value: unknown): string {\n\tif (typeof value !== \"string\" || !value || value.includes(\"\\\\\") || value.includes(\"\\0\") || isAbsolute(value)) {\n\t\tthrow new Error(\"plan.path must be workspace-relative\");\n\t}\n\tconst segments = value.split(\"/\");\n\tif (segments.some((segment) => segment === \"\" || segment === \".\" || segment === \"..\")) {\n\t\tthrow new Error(\"plan.path must be workspace-relative\");\n\t}\n\treturn value;\n}\n\nfunction timestamp(value: unknown): string {\n\tif (typeof value !== \"string\") throw new Error(\"decidedAt must be an ISO timestamp\");\n\tconst parsed = new Date(value);\n\tif (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {\n\t\tthrow new Error(\"decidedAt must be a canonical ISO timestamp\");\n\t}\n\treturn value;\n}\n\nfunction parseContentDigest(value: unknown, label: string): ApprovalContentDigest {\n\tif (!isRecord(value)) throw new Error(`${label} must be an object`);\n\texactKeys(value, [\"sha256\", \"bytes\"]);\n\treturn {\n\t\tsha256: sha256Hex(value.sha256, `${label}.sha256`),\n\t\tbytes: nonNegativeInteger(value.bytes, `${label}.bytes`),\n\t};\n}\n\nfunction parsePlan(value: unknown): ApprovalPlanDigest {\n\tif (!isRecord(value)) throw new Error(\"plan must be an object\");\n\texactKeys(value, [\"path\", \"sha256\", \"bytes\"]);\n\treturn {\n\t\tpath: workspaceRelativePath(value.path),\n\t\tsha256: sha256Hex(value.sha256, \"plan.sha256\"),\n\t\tbytes: nonNegativeInteger(value.bytes, \"plan.bytes\"),\n\t};\n}\n\nfunction canonicalJson(value: unknown): string {\n\tif (value === null || typeof value === \"boolean\" || typeof value === \"string\") return JSON.stringify(value);\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) throw new Error(\"Approval receipt contains a non-finite number\");\n\t\treturn JSON.stringify(value);\n\t}\n\tif (Array.isArray(value)) return `[${value.map(canonicalJson).join(\",\")}]`;\n\tif (!isRecord(value)) throw new Error(\"Approval receipt contains an unsupported value\");\n\treturn `{${Object.keys(value)\n\t\t.sort((left, right) => left.localeCompare(right))\n\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)\n\t\t.join(\",\")}}`;\n}\n\nexport function computeApprovalReceiptCoreSha256(core: ApprovalReceiptCore): string {\n\treturn sha256(CORE_DOMAIN, canonicalJson(core));\n}\n\nfunction constantTimeEqual(left: string, right: string): boolean {\n\tif (!SHA256_PATTERN.test(left) || !SHA256_PATTERN.test(right)) return false;\n\treturn timingSafeEqual(Buffer.from(left, \"hex\"), Buffer.from(right, \"hex\"));\n}\n\nexport function validateApprovalReceipt(value: unknown): ApprovalReceipt {\n\tif (!isRecord(value)) throw new Error(\"Approval receipt must be an object\");\n\texactKeys(value, [\"core\", \"coreSha256\"]);\n\tif (!isRecord(value.core)) throw new Error(\"Approval receipt core must be an object\");\n\tconst coreValue = value.core;\n\texactKeys(\n\t\tcoreValue,\n\t\t[\n\t\t\t\"schemaVersion\",\n\t\t\t\"receiptId\",\n\t\t\t\"provider\",\n\t\t\t\"trust\",\n\t\t\t\"requestId\",\n\t\t\t\"reviewId\",\n\t\t\t\"decision\",\n\t\t\t\"decidedAt\",\n\t\t\t\"sessionId\",\n\t\t\t\"plan\",\n\t\t],\n\t\t[\"feedback\"],\n\t);\n\tif (coreValue.schemaVersion !== 1) throw new Error(\"Unsupported approval receipt schemaVersion\");\n\tif (coreValue.provider !== \"plannotator\") throw new Error(\"Unsupported approval provider\");\n\tif (coreValue.trust !== \"external-extension-event\") throw new Error(\"Invalid approval trust classification\");\n\tif (coreValue.decision !== \"approved\" && coreValue.decision !== \"rejected\" && coreValue.decision !== \"invalidated\") {\n\t\tthrow new Error(\"Invalid approval decision\");\n\t}\n\tconst core: ApprovalReceiptCore = {\n\t\tschemaVersion: 1,\n\t\treceiptId: safeId(coreValue.receiptId, \"receiptId\"),\n\t\tprovider: \"plannotator\",\n\t\ttrust: \"external-extension-event\",\n\t\trequestId: safeId(coreValue.requestId, \"requestId\"),\n\t\treviewId: safeId(coreValue.reviewId, \"reviewId\"),\n\t\tdecision: coreValue.decision,\n\t\tdecidedAt: timestamp(coreValue.decidedAt),\n\t\tsessionId: safeId(coreValue.sessionId, \"sessionId\"),\n\t\tplan: parsePlan(coreValue.plan),\n\t\t...(hasOwn(coreValue, \"feedback\") ? { feedback: parseContentDigest(coreValue.feedback, \"feedback\") } : {}),\n\t};\n\tconst expectedReceiptId = computeApprovalReceiptId(core);\n\tif (core.receiptId !== expectedReceiptId) throw new Error(\"Approval receipt id mismatch\");\n\tconst coreSha256 = sha256Hex(value.coreSha256, \"coreSha256\");\n\tconst expectedDigest = computeApprovalReceiptCoreSha256(core);\n\tif (!constantTimeEqual(coreSha256, expectedDigest)) throw new Error(\"Approval receipt digest mismatch\");\n\treturn { core, coreSha256 };\n}\n\nfunction computeApprovalReceiptId(\n\tcore: Omit<ApprovalReceiptCore, \"receiptId\" | \"decidedAt\" | \"decision\" | \"feedback\">,\n): string {\n\tconst identity = {\n\t\tschemaVersion: core.schemaVersion,\n\t\tprovider: core.provider,\n\t\ttrust: core.trust,\n\t\trequestId: core.requestId,\n\t\treviewId: core.reviewId,\n\t\tsessionId: core.sessionId,\n\t\tplan: core.plan,\n\t};\n\treturn `approval-${sha256(ID_DOMAIN, canonicalJson(identity)).slice(0, 32)}`;\n}\n\nexport function createApprovalReceipt(input: CreateApprovalReceiptInput): ApprovalReceipt {\n\tconst plan: ApprovalPlanDigest = {\n\t\tpath: workspaceRelativePath(input.planPath),\n\t\t...computeApprovalContentDigest(input.planContent),\n\t};\n\tconst coreWithoutId = {\n\t\tschemaVersion: 1 as const,\n\t\tprovider: input.provider,\n\t\ttrust: \"external-extension-event\" as const,\n\t\trequestId: safeId(input.requestId, \"requestId\"),\n\t\treviewId: safeId(input.reviewId, \"reviewId\"),\n\t\tdecision: input.decision,\n\t\tdecidedAt: timestamp(input.decidedAt),\n\t\tsessionId: safeId(input.sessionId, \"sessionId\"),\n\t\tplan,\n\t\t...(input.feedback !== undefined && input.feedback.length > 0\n\t\t\t? { feedback: computeApprovalContentDigest(input.feedback) }\n\t\t\t: {}),\n\t};\n\tconst core: ApprovalReceiptCore = {\n\t\t...coreWithoutId,\n\t\treceiptId: computeApprovalReceiptId(coreWithoutId),\n\t};\n\treturn validateApprovalReceipt({ core, coreSha256: computeApprovalReceiptCoreSha256(core) });\n}\n\nexport function parseApprovalReceipt(serialized: string): ApprovalReceipt {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(serialized) as unknown;\n\t} catch {\n\t\tthrow new Error(\"Approval receipt is not valid JSON\");\n\t}\n\treturn validateApprovalReceipt(parsed);\n}\n\nexport function serializeApprovalReceipt(receipt: ApprovalReceipt): string {\n\treturn `${JSON.stringify(validateApprovalReceipt(receipt), null, 2)}\\n`;\n}\n"]}
@@ -0,0 +1,208 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ import { isAbsolute } from "node:path";
3
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
4
+ const SAFE_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$/u;
5
+ const CORE_DOMAIN = "omk.approval-receipt.core.v1\0";
6
+ const ID_DOMAIN = "omk.approval-receipt.id.v1\0";
7
+ function sha256(domain, value) {
8
+ return createHash("sha256").update(domain).update(value, "utf8").digest("hex");
9
+ }
10
+ export function computeApprovalContentDigest(value) {
11
+ return { sha256: sha256("omk.approval-content.v1\0", value), bytes: Buffer.byteLength(value, "utf8") };
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function hasOwn(value, key) {
17
+ return Object.getOwnPropertyDescriptor(value, key) !== undefined;
18
+ }
19
+ function exactKeys(value, required, optional = []) {
20
+ const allowed = new Set([...required, ...optional]);
21
+ for (const key of Object.keys(value)) {
22
+ if (!allowed.has(key))
23
+ throw new Error(`Unexpected approval receipt field: ${key}`);
24
+ }
25
+ for (const key of required) {
26
+ if (!hasOwn(value, key))
27
+ throw new Error(`Missing approval receipt field: ${key}`);
28
+ }
29
+ }
30
+ function safeId(value, label) {
31
+ if (typeof value !== "string" || !SAFE_ID_PATTERN.test(value))
32
+ throw new Error(`${label} is not a safe identifier`);
33
+ return value;
34
+ }
35
+ function sha256Hex(value, label) {
36
+ if (typeof value !== "string" || !SHA256_PATTERN.test(value))
37
+ throw new Error(`${label} must be lowercase SHA-256`);
38
+ return value;
39
+ }
40
+ function nonNegativeInteger(value, label) {
41
+ if (!Number.isSafeInteger(value) || value < 0)
42
+ throw new Error(`${label} must be a non-negative safe integer`);
43
+ return value;
44
+ }
45
+ function workspaceRelativePath(value) {
46
+ if (typeof value !== "string" || !value || value.includes("\\") || value.includes("\0") || isAbsolute(value)) {
47
+ throw new Error("plan.path must be workspace-relative");
48
+ }
49
+ const segments = value.split("/");
50
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
51
+ throw new Error("plan.path must be workspace-relative");
52
+ }
53
+ return value;
54
+ }
55
+ function timestamp(value) {
56
+ if (typeof value !== "string")
57
+ throw new Error("decidedAt must be an ISO timestamp");
58
+ const parsed = new Date(value);
59
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString() !== value) {
60
+ throw new Error("decidedAt must be a canonical ISO timestamp");
61
+ }
62
+ return value;
63
+ }
64
+ function parseContentDigest(value, label) {
65
+ if (!isRecord(value))
66
+ throw new Error(`${label} must be an object`);
67
+ exactKeys(value, ["sha256", "bytes"]);
68
+ return {
69
+ sha256: sha256Hex(value.sha256, `${label}.sha256`),
70
+ bytes: nonNegativeInteger(value.bytes, `${label}.bytes`),
71
+ };
72
+ }
73
+ function parsePlan(value) {
74
+ if (!isRecord(value))
75
+ throw new Error("plan must be an object");
76
+ exactKeys(value, ["path", "sha256", "bytes"]);
77
+ return {
78
+ path: workspaceRelativePath(value.path),
79
+ sha256: sha256Hex(value.sha256, "plan.sha256"),
80
+ bytes: nonNegativeInteger(value.bytes, "plan.bytes"),
81
+ };
82
+ }
83
+ function canonicalJson(value) {
84
+ if (value === null || typeof value === "boolean" || typeof value === "string")
85
+ return JSON.stringify(value);
86
+ if (typeof value === "number") {
87
+ if (!Number.isFinite(value))
88
+ throw new Error("Approval receipt contains a non-finite number");
89
+ return JSON.stringify(value);
90
+ }
91
+ if (Array.isArray(value))
92
+ return `[${value.map(canonicalJson).join(",")}]`;
93
+ if (!isRecord(value))
94
+ throw new Error("Approval receipt contains an unsupported value");
95
+ return `{${Object.keys(value)
96
+ .sort((left, right) => left.localeCompare(right))
97
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
98
+ .join(",")}}`;
99
+ }
100
+ export function computeApprovalReceiptCoreSha256(core) {
101
+ return sha256(CORE_DOMAIN, canonicalJson(core));
102
+ }
103
+ function constantTimeEqual(left, right) {
104
+ if (!SHA256_PATTERN.test(left) || !SHA256_PATTERN.test(right))
105
+ return false;
106
+ return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
107
+ }
108
+ export function validateApprovalReceipt(value) {
109
+ if (!isRecord(value))
110
+ throw new Error("Approval receipt must be an object");
111
+ exactKeys(value, ["core", "coreSha256"]);
112
+ if (!isRecord(value.core))
113
+ throw new Error("Approval receipt core must be an object");
114
+ const coreValue = value.core;
115
+ exactKeys(coreValue, [
116
+ "schemaVersion",
117
+ "receiptId",
118
+ "provider",
119
+ "trust",
120
+ "requestId",
121
+ "reviewId",
122
+ "decision",
123
+ "decidedAt",
124
+ "sessionId",
125
+ "plan",
126
+ ], ["feedback"]);
127
+ if (coreValue.schemaVersion !== 1)
128
+ throw new Error("Unsupported approval receipt schemaVersion");
129
+ if (coreValue.provider !== "plannotator")
130
+ throw new Error("Unsupported approval provider");
131
+ if (coreValue.trust !== "external-extension-event")
132
+ throw new Error("Invalid approval trust classification");
133
+ if (coreValue.decision !== "approved" && coreValue.decision !== "rejected" && coreValue.decision !== "invalidated") {
134
+ throw new Error("Invalid approval decision");
135
+ }
136
+ const core = {
137
+ schemaVersion: 1,
138
+ receiptId: safeId(coreValue.receiptId, "receiptId"),
139
+ provider: "plannotator",
140
+ trust: "external-extension-event",
141
+ requestId: safeId(coreValue.requestId, "requestId"),
142
+ reviewId: safeId(coreValue.reviewId, "reviewId"),
143
+ decision: coreValue.decision,
144
+ decidedAt: timestamp(coreValue.decidedAt),
145
+ sessionId: safeId(coreValue.sessionId, "sessionId"),
146
+ plan: parsePlan(coreValue.plan),
147
+ ...(hasOwn(coreValue, "feedback") ? { feedback: parseContentDigest(coreValue.feedback, "feedback") } : {}),
148
+ };
149
+ const expectedReceiptId = computeApprovalReceiptId(core);
150
+ if (core.receiptId !== expectedReceiptId)
151
+ throw new Error("Approval receipt id mismatch");
152
+ const coreSha256 = sha256Hex(value.coreSha256, "coreSha256");
153
+ const expectedDigest = computeApprovalReceiptCoreSha256(core);
154
+ if (!constantTimeEqual(coreSha256, expectedDigest))
155
+ throw new Error("Approval receipt digest mismatch");
156
+ return { core, coreSha256 };
157
+ }
158
+ function computeApprovalReceiptId(core) {
159
+ const identity = {
160
+ schemaVersion: core.schemaVersion,
161
+ provider: core.provider,
162
+ trust: core.trust,
163
+ requestId: core.requestId,
164
+ reviewId: core.reviewId,
165
+ sessionId: core.sessionId,
166
+ plan: core.plan,
167
+ };
168
+ return `approval-${sha256(ID_DOMAIN, canonicalJson(identity)).slice(0, 32)}`;
169
+ }
170
+ export function createApprovalReceipt(input) {
171
+ const plan = {
172
+ path: workspaceRelativePath(input.planPath),
173
+ ...computeApprovalContentDigest(input.planContent),
174
+ };
175
+ const coreWithoutId = {
176
+ schemaVersion: 1,
177
+ provider: input.provider,
178
+ trust: "external-extension-event",
179
+ requestId: safeId(input.requestId, "requestId"),
180
+ reviewId: safeId(input.reviewId, "reviewId"),
181
+ decision: input.decision,
182
+ decidedAt: timestamp(input.decidedAt),
183
+ sessionId: safeId(input.sessionId, "sessionId"),
184
+ plan,
185
+ ...(input.feedback !== undefined && input.feedback.length > 0
186
+ ? { feedback: computeApprovalContentDigest(input.feedback) }
187
+ : {}),
188
+ };
189
+ const core = {
190
+ ...coreWithoutId,
191
+ receiptId: computeApprovalReceiptId(coreWithoutId),
192
+ };
193
+ return validateApprovalReceipt({ core, coreSha256: computeApprovalReceiptCoreSha256(core) });
194
+ }
195
+ export function parseApprovalReceipt(serialized) {
196
+ let parsed;
197
+ try {
198
+ parsed = JSON.parse(serialized);
199
+ }
200
+ catch {
201
+ throw new Error("Approval receipt is not valid JSON");
202
+ }
203
+ return validateApprovalReceipt(parsed);
204
+ }
205
+ export function serializeApprovalReceipt(receipt) {
206
+ return `${JSON.stringify(validateApprovalReceipt(receipt), null, 2)}\n`;
207
+ }
208
+ //# sourceMappingURL=approval-receipt.js.map