open-multi-agent-kit 0.95.2 → 0.96.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +37 -4
  3. package/dist/approvals/approval-receipt-store.d.ts.map +1 -1
  4. package/dist/approvals/approval-receipt-store.js +25 -0
  5. package/dist/approvals/approval-receipt-store.js.map +1 -1
  6. package/dist/commands/doctor-provider.d.ts.map +1 -1
  7. package/dist/commands/doctor-provider.js +15 -20
  8. package/dist/commands/doctor-provider.js.map +1 -1
  9. package/dist/commands/sdk-session-cli.d.ts +24 -0
  10. package/dist/commands/sdk-session-cli.d.ts.map +1 -0
  11. package/dist/commands/sdk-session-cli.js +225 -0
  12. package/dist/commands/sdk-session-cli.js.map +1 -0
  13. package/dist/core/advisory-judge-model.d.ts +18 -0
  14. package/dist/core/advisory-judge-model.d.ts.map +1 -0
  15. package/dist/core/advisory-judge-model.js +168 -0
  16. package/dist/core/advisory-judge-model.js.map +1 -0
  17. package/dist/core/advisory-judge-response.d.ts +8 -0
  18. package/dist/core/advisory-judge-response.d.ts.map +1 -0
  19. package/dist/core/advisory-judge-response.js +61 -0
  20. package/dist/core/advisory-judge-response.js.map +1 -0
  21. package/dist/core/advisory-judge-types.d.ts +53 -0
  22. package/dist/core/advisory-judge-types.d.ts.map +1 -0
  23. package/dist/core/advisory-judge-types.js +2 -0
  24. package/dist/core/advisory-judge-types.js.map +1 -0
  25. package/dist/core/advisory-judge.d.ts +8 -0
  26. package/dist/core/advisory-judge.d.ts.map +1 -0
  27. package/dist/core/advisory-judge.js +178 -0
  28. package/dist/core/advisory-judge.js.map +1 -0
  29. package/dist/core/agent-session-services.d.ts +1 -0
  30. package/dist/core/agent-session-services.d.ts.map +1 -1
  31. package/dist/core/agent-session-services.js +1 -0
  32. package/dist/core/agent-session-services.js.map +1 -1
  33. package/dist/core/agent-session.d.ts +6 -3
  34. package/dist/core/agent-session.d.ts.map +1 -1
  35. package/dist/core/agent-session.js +141 -184
  36. package/dist/core/agent-session.js.map +1 -1
  37. package/dist/core/auth-storage.d.ts.map +1 -1
  38. package/dist/core/auth-storage.js +28 -4
  39. package/dist/core/auth-storage.js.map +1 -1
  40. package/dist/core/compaction-gate.d.ts +28 -0
  41. package/dist/core/compaction-gate.d.ts.map +1 -0
  42. package/dist/core/compaction-gate.js +34 -0
  43. package/dist/core/compaction-gate.js.map +1 -0
  44. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  45. package/dist/core/context-budget-system-prompt-items.js +5 -3
  46. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  47. package/dist/core/domain-loadouts.d.ts.map +1 -1
  48. package/dist/core/domain-loadouts.js +2 -2
  49. package/dist/core/domain-loadouts.js.map +1 -1
  50. package/dist/core/durable-goal-checkpoint.d.ts +6 -0
  51. package/dist/core/durable-goal-checkpoint.d.ts.map +1 -0
  52. package/dist/core/durable-goal-checkpoint.js +195 -0
  53. package/dist/core/durable-goal-checkpoint.js.map +1 -0
  54. package/dist/core/durable-goal-parse.d.ts +3 -0
  55. package/dist/core/durable-goal-parse.d.ts.map +1 -0
  56. package/dist/core/durable-goal-parse.js +86 -0
  57. package/dist/core/durable-goal-parse.js.map +1 -0
  58. package/dist/core/durable-goal-reducer.d.ts +3 -0
  59. package/dist/core/durable-goal-reducer.d.ts.map +1 -0
  60. package/dist/core/durable-goal-reducer.js +175 -0
  61. package/dist/core/durable-goal-reducer.js.map +1 -0
  62. package/dist/core/durable-goal-store.d.ts +19 -0
  63. package/dist/core/durable-goal-store.d.ts.map +1 -0
  64. package/dist/core/durable-goal-store.js +126 -0
  65. package/dist/core/durable-goal-store.js.map +1 -0
  66. package/dist/core/durable-goal.d.ts +92 -0
  67. package/dist/core/durable-goal.d.ts.map +1 -0
  68. package/dist/core/durable-goal.js +62 -0
  69. package/dist/core/durable-goal.js.map +1 -0
  70. package/dist/core/extensions/builtin/goal-controller.d.ts +3 -0
  71. package/dist/core/extensions/builtin/goal-controller.d.ts.map +1 -0
  72. package/dist/core/extensions/builtin/goal-controller.js +125 -0
  73. package/dist/core/extensions/builtin/goal-controller.js.map +1 -0
  74. package/dist/core/extensions/builtin/identical-loop.d.ts +3 -0
  75. package/dist/core/extensions/builtin/identical-loop.d.ts.map +1 -0
  76. package/dist/core/extensions/builtin/identical-loop.js +31 -0
  77. package/dist/core/extensions/builtin/identical-loop.js.map +1 -0
  78. package/dist/core/extensions/builtin/prompt-preset.d.ts +3 -0
  79. package/dist/core/extensions/builtin/prompt-preset.d.ts.map +1 -0
  80. package/dist/core/extensions/builtin/prompt-preset.js +13 -0
  81. package/dist/core/extensions/builtin/prompt-preset.js.map +1 -0
  82. package/dist/core/extensions/builtin/tool-pair-repair.d.ts +3 -0
  83. package/dist/core/extensions/builtin/tool-pair-repair.d.ts.map +1 -0
  84. package/dist/core/extensions/builtin/tool-pair-repair.js +12 -0
  85. package/dist/core/extensions/builtin/tool-pair-repair.js.map +1 -0
  86. package/dist/core/extensions/types.d.ts +6 -2
  87. package/dist/core/extensions/types.d.ts.map +1 -1
  88. package/dist/core/extensions/types.js.map +1 -1
  89. package/dist/core/goal-continuation.d.ts +13 -0
  90. package/dist/core/goal-continuation.d.ts.map +1 -0
  91. package/dist/core/goal-continuation.js +10 -0
  92. package/dist/core/goal-continuation.js.map +1 -0
  93. package/dist/core/grok-harness.d.ts +1 -1
  94. package/dist/core/grok-harness.d.ts.map +1 -1
  95. package/dist/core/grok-harness.js +1 -1
  96. package/dist/core/grok-harness.js.map +1 -1
  97. package/dist/core/grok-playbook.d.ts +1 -1
  98. package/dist/core/grok-playbook.d.ts.map +1 -1
  99. package/dist/core/grok-playbook.js +1 -1
  100. package/dist/core/grok-playbook.js.map +1 -1
  101. package/dist/core/identical-loop.d.ts +15 -0
  102. package/dist/core/identical-loop.d.ts.map +1 -0
  103. package/dist/core/identical-loop.js +34 -0
  104. package/dist/core/identical-loop.js.map +1 -0
  105. package/dist/core/index.d.ts +1 -1
  106. package/dist/core/index.d.ts.map +1 -1
  107. package/dist/core/index.js +1 -1
  108. package/dist/core/index.js.map +1 -1
  109. package/dist/core/model-prompt-policy.d.ts +11 -0
  110. package/dist/core/model-prompt-policy.d.ts.map +1 -0
  111. package/dist/core/model-prompt-policy.js +17 -0
  112. package/dist/core/model-prompt-policy.js.map +1 -0
  113. package/dist/core/model-registry.d.ts.map +1 -1
  114. package/dist/core/model-registry.js +16 -4
  115. package/dist/core/model-registry.js.map +1 -1
  116. package/dist/core/model-resolver.d.ts.map +1 -1
  117. package/dist/core/model-resolver.js +2 -2
  118. package/dist/core/model-resolver.js.map +1 -1
  119. package/dist/core/prompt-budget.d.ts +21 -0
  120. package/dist/core/prompt-budget.d.ts.map +1 -0
  121. package/dist/core/prompt-budget.js +51 -0
  122. package/dist/core/prompt-budget.js.map +1 -0
  123. package/dist/core/prompt-cache.d.ts +14 -0
  124. package/dist/core/prompt-cache.d.ts.map +1 -0
  125. package/dist/core/prompt-cache.js +16 -0
  126. package/dist/core/prompt-cache.js.map +1 -0
  127. package/dist/core/prompt-preset.d.ts +8 -0
  128. package/dist/core/prompt-preset.d.ts.map +1 -0
  129. package/dist/core/prompt-preset.js +56 -0
  130. package/dist/core/prompt-preset.js.map +1 -0
  131. package/dist/core/provider-resilience.d.ts +23 -0
  132. package/dist/core/provider-resilience.d.ts.map +1 -1
  133. package/dist/core/provider-resilience.js +34 -1
  134. package/dist/core/provider-resilience.js.map +1 -1
  135. package/dist/core/provider-retry.d.ts +29 -0
  136. package/dist/core/provider-retry.d.ts.map +1 -0
  137. package/dist/core/provider-retry.js +49 -0
  138. package/dist/core/provider-retry.js.map +1 -0
  139. package/dist/core/provider-usage.d.ts +2 -1
  140. package/dist/core/provider-usage.d.ts.map +1 -1
  141. package/dist/core/provider-usage.js +40 -6
  142. package/dist/core/provider-usage.js.map +1 -1
  143. package/dist/core/resource-loader.d.ts.map +1 -1
  144. package/dist/core/resource-loader.js +32 -0
  145. package/dist/core/resource-loader.js.map +1 -1
  146. package/dist/core/sandbox/backend.d.ts +12 -0
  147. package/dist/core/sandbox/backend.d.ts.map +1 -1
  148. package/dist/core/sandbox/backend.js +52 -42
  149. package/dist/core/sandbox/backend.js.map +1 -1
  150. package/dist/core/sandbox/policy.d.ts +2 -0
  151. package/dist/core/sandbox/policy.d.ts.map +1 -1
  152. package/dist/core/sandbox/policy.js +2 -1
  153. package/dist/core/sandbox/policy.js.map +1 -1
  154. package/dist/core/sdk.d.ts +2 -0
  155. package/dist/core/sdk.d.ts.map +1 -1
  156. package/dist/core/sdk.js +1 -0
  157. package/dist/core/sdk.js.map +1 -1
  158. package/dist/core/session-system-prompt.d.ts +28 -0
  159. package/dist/core/session-system-prompt.d.ts.map +1 -0
  160. package/dist/core/session-system-prompt.js +37 -0
  161. package/dist/core/session-system-prompt.js.map +1 -0
  162. package/dist/core/strict-record.d.ts +2 -0
  163. package/dist/core/strict-record.d.ts.map +1 -0
  164. package/dist/core/strict-record.js +9 -0
  165. package/dist/core/strict-record.js.map +1 -0
  166. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  167. package/dist/core/subagent-orchestration.js +4 -2
  168. package/dist/core/subagent-orchestration.js.map +1 -1
  169. package/dist/core/tool-pair-repair.d.ts +11 -0
  170. package/dist/core/tool-pair-repair.d.ts.map +1 -0
  171. package/dist/core/tool-pair-repair.js +38 -0
  172. package/dist/core/tool-pair-repair.js.map +1 -0
  173. package/dist/core/tools/artifact-spill.d.ts +13 -0
  174. package/dist/core/tools/artifact-spill.d.ts.map +1 -0
  175. package/dist/core/tools/artifact-spill.js +26 -0
  176. package/dist/core/tools/artifact-spill.js.map +1 -0
  177. package/dist/core/tools/bash.d.ts +7 -4
  178. package/dist/core/tools/bash.d.ts.map +1 -1
  179. package/dist/core/tools/bash.js +8 -7
  180. package/dist/core/tools/bash.js.map +1 -1
  181. package/dist/core/tools/diagnostics.d.ts.map +1 -1
  182. package/dist/core/tools/diagnostics.js +8 -2
  183. package/dist/core/tools/diagnostics.js.map +1 -1
  184. package/dist/core/tools/index.d.ts +1 -1
  185. package/dist/core/tools/index.d.ts.map +1 -1
  186. package/dist/core/tools/index.js.map +1 -1
  187. package/dist/core/tools/read.d.ts +1 -0
  188. package/dist/core/tools/read.d.ts.map +1 -1
  189. package/dist/core/tools/read.js +10 -1
  190. package/dist/core/tools/read.js.map +1 -1
  191. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  192. package/dist/core/tools/tool-definition-wrapper.js +2 -0
  193. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  194. package/dist/guardrails/evidence-protocol.d.ts +10 -0
  195. package/dist/guardrails/evidence-protocol.d.ts.map +1 -0
  196. package/dist/guardrails/evidence-protocol.js +34 -0
  197. package/dist/guardrails/evidence-protocol.js.map +1 -0
  198. package/dist/guardrails/evidence-system.d.ts +3 -1
  199. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  200. package/dist/guardrails/evidence-system.js +22 -14
  201. package/dist/guardrails/evidence-system.js.map +1 -1
  202. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
  203. package/dist/guardrails/replay-ledger-lock-owner.js +3 -1
  204. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
  205. package/dist/guardrails/replay-payload-hash.d.ts +6 -0
  206. package/dist/guardrails/replay-payload-hash.d.ts.map +1 -0
  207. package/dist/guardrails/replay-payload-hash.js +24 -0
  208. package/dist/guardrails/replay-payload-hash.js.map +1 -0
  209. package/dist/index.d.ts +20 -2
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +19 -0
  212. package/dist/index.js.map +1 -1
  213. package/dist/main.d.ts.map +1 -1
  214. package/dist/main.js +8 -0
  215. package/dist/main.js.map +1 -1
  216. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  217. package/dist/modes/interactive/theme/theme.js +14 -3
  218. package/dist/modes/interactive/theme/theme.js.map +1 -1
  219. package/dist/types/evidence.d.ts +11 -3
  220. package/dist/types/evidence.d.ts.map +1 -1
  221. package/dist/types/evidence.js.map +1 -1
  222. package/docs/adaptorch-preview-spec.md +1 -1
  223. package/docs/adaptorch-preview.md +2 -2
  224. package/docs/book-to-skill.md +79 -0
  225. package/docs/compaction.md +10 -0
  226. package/docs/containerization.md +9 -0
  227. package/docs/docs.json +18 -2
  228. package/docs/environment-variables.md +11 -4
  229. package/docs/extensions.md +14 -1
  230. package/docs/grok-harness.md +35 -12
  231. package/docs/index.md +9 -5
  232. package/docs/loadout-domains/grok-harness.md +2 -2
  233. package/docs/models.md +21 -8
  234. package/docs/provider-resilience.md +42 -55
  235. package/docs/providers.md +13 -4
  236. package/docs/run-protocol.md +104 -0
  237. package/docs/sdk.md +111 -5
  238. package/docs/security.md +22 -32
  239. package/docs/sessions.md +20 -0
  240. package/docs/settings.md +2 -0
  241. package/docs/skills.md +9 -1
  242. package/docs/usage.md +38 -16
  243. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  244. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  245. package/examples/extensions/custom-provider-gitlab-duo/index.ts +6 -1
  246. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  247. package/examples/extensions/gondolin/package-lock.json +2 -2
  248. package/examples/extensions/gondolin/package.json +1 -1
  249. package/examples/extensions/sandbox/package-lock.json +2 -2
  250. package/examples/extensions/sandbox/package.json +1 -1
  251. package/examples/extensions/with-deps/package-lock.json +2 -2
  252. package/examples/extensions/with-deps/package.json +1 -1
  253. package/npm-shrinkwrap.json +37 -15
  254. package/package.json +8 -5
  255. package/dist/core/context-budget-compressors.d.ts +0 -50
  256. package/dist/core/context-budget-compressors.d.ts.map +0 -1
  257. package/dist/core/context-budget-compressors.js +0 -166
  258. package/dist/core/context-budget-compressors.js.map +0 -1
  259. package/dist/core/extensions/loader.js.bak-sharedjiti-20260812-111246 +0 -553
package/CHANGELOG.md CHANGED
@@ -2,6 +2,66 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.96.1] - 2026-08-20
6
+
7
+ ### Added
8
+
9
+ - Added built-in harness loop extensions: identical-loop detection, compaction tool-pair repair, Kimi/K3/GLM/Grok/Claude prompt presets, and `/goal` with automatic continuation plus explicit `pause`, `resume`, evidence-gated `complete`, and `clear` lifecycle commands.
10
+ - Added pass-gated advisory best-of-N selection with strict weighted judge responses, forced-redacted candidate material, deterministic fallback, evaluation-bound request digests, and an explicit `ModelRegistry`-backed LLM adapter.
11
+ - Added digest-bound `Goal / Core / Verified / Open / Next` seam checkpoints to the existing durable-goal journal, including `/goal checkpoint <json>` and checkpoint-aware continuation.
12
+ - Added `omk sdk session status|tail|inspect|send` for external session controllers. Ambiguous selectors now fail closed, writes require exact IDs, active owners block concurrent access, and credential-shaped transcript text is redacted from output.
13
+ - Legacy `read` mode (`OMK_OMP_SEAMS=0`) now reports a private `0700` temporary spill directory with an exclusive `0600` file for recoverable line or byte truncation; it never writes beside or through the source path. A first line that alone exceeds the byte cap remains preview-only.
14
+
15
+ ### Changed
16
+
17
+ - Sandbox backend probing is now cached per local bash operations instance. All enforce-mode fallback verdicts share the concrete missing-backend diagnosis (`bwrap`, user namespaces, `sandbox-exec`, or unsupported platform). Policy semantics are unchanged.
18
+ - Extracted the session system-prompt assembly from `AgentSession._rebuildSystemPrompt` into the pure `assembleSessionSystemPrompt` module (`core/session-system-prompt.ts`). Provider playbook resolution stays at the call site; the assembly is now directly testable.
19
+ - Extracted the retry/failover decisions from `AgentSession._isRetryableError` and `_prepareRetry` into the pure `core/provider-retry.ts` module (`isRetryableAssistantError`, `nextRetryAttempt`, `computeRetryDelayMs`). Retry ordering, backoff, and failover semantics are unchanged.
20
+ - Extracted the compaction gates from `AgentSession._checkCompaction` into the pure `core/compaction-gate.ts` module (`shouldSkipCompactionCheck`, `isSessionModelOverflow`). Gate ordering and staleness semantics are unchanged.
21
+ - Extracted the failover trigger and refused-model bookkeeping from `AgentSession._maybeFailoverFromSafetyStop` into `core/provider-retry.ts` (`isFailoverTriggerError`, `failoverModelKey`). Chain ordering and blacklist semantics are unchanged.
22
+ - Extracted the context-budget arithmetic from `AgentSession._getContextBudgetOptions` into the pure `core/prompt-budget.ts` module (`computePromptTokenBudget`, `computeResponseReserveTokens`). Env parsing stays at the call site; budget values are unchanged.
23
+ - Extracted the prompt-cache key transition classification from `AgentSession._recordPromptCachePlan` into the pure `core/prompt-cache.ts` module (`classifyPromptCacheTransition`). Counter and break-reason semantics are unchanged.
24
+ - Grok 4.5 / 4.3 now expose `/think max` and `ultra` in the selector. Those aliases still send xAI `reasoning_effort: "high"` because those models have no upstream `xhigh`/`max` tier.
25
+ - Native xAI SuperGrok usage now polls `GET https://cli-chat-proxy.grok.com/v1/billing?format=credits` and shows the weekly SuperGrok pool from `creditUsagePercent`. Stale `grok-oauth-proxy` credentials are dropped from `/login` and `/logout`.
26
+
27
+ ### Fixed
28
+
29
+ - Durable-goal continuation now records and skips an unavailable WSL/project workspace instead of emitting an `ENODEV` extension stack after every failed provider attempt.
30
+ - Content/safety refusals are capped at one same-model retry when failover is unavailable, preventing the default transport retry budget from replaying the same refusal three times.
31
+ - Fable models remain visible in the model catalog, and a saved Fable default is honored when sticky-safety blocking is disabled.
32
+ - Claude models now omit discovered context files by default, avoiding provider false positives from unrelated instruction text; `OMK_CLAUDE_CONTEXT_FILES=1` restores the full context.
33
+ - Extension `resourceClaims` now survive both tool-definition adapters, allowing `dag-v2` to schedule non-conflicting custom tool calls concurrently instead of treating them as unclaimed exclusive work.
34
+ - Built-in tool-pair repair now uses the real `AgentMessage` contract (`toolCall` blocks and top-level `role: "toolResult"` messages), removing orphan pairs without unsafe message-shape casts.
35
+ - Pinned the transitive development dependency `nanoid` to 3.3.18, clearing GHSA-2v37-7h3g-55p8 from both full and production npm audits.
36
+
37
+ ### Docs
38
+
39
+ - Redesigned the root README around the Scope → Route → Verify → Replay control loop with a WCAG-aware cyberpunk OMK Girl hero and slow feature GIF generated through GPT Image 2. The root `DESIGN.md` now defines public brand tokens, media budgets, and reduced-motion guidance.
40
+
41
+ ### Removed
42
+
43
+ - Removed the `grok-oauth-proxy` provider path. Grok harness dispatch, failover, usage, and presets now use native `xai`. Stale `models.json` entries for `grok-oauth-proxy` are ignored instead of reappearing in `/login` and `/model`.
44
+
45
+ ## [0.96.0] - 2026-08-16
46
+
47
+ ### Added
48
+
49
+ - Added `omk-protocol`, the versioned `TaskSpec -> ExecutionAttempt -> Observation -> EvaluationResult -> RuntimeDecision` contract package, with runtime validators, explicit waivers, and pure semantic and runtime-decision reducers.
50
+ - 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.
51
+ - 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.
52
+ - 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.
53
+
54
+ ### Changed
55
+
56
+ - 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.
57
+
58
+ ### Fixed
59
+
60
+ - 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.
61
+ - 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.
62
+ - Subagent DAG scheduling now sorts simultaneously ready lanes by lane ID, so topology aggregation does not depend on input insertion order.
63
+ - Local release bundles now include `omk-adaptorch-wpl`, allowing isolated installs of the full packed workspace without resolving that dependency from the registry.
64
+
5
65
  ## [0.95.2] - 2026-08-15
6
66
 
7
67
  ### Changed
package/README.md CHANGED
@@ -6,7 +6,7 @@ OMK supports interactive terminal use, non-interactive output, RPC integration,
6
6
 
7
7
  ## Install
8
8
 
9
- Requires Node.js 22.19 or newer.
9
+ Requires Node.js 22.19 or newer. Built-in local bash also requires `sandbox-exec` on macOS or `bwrap` plus unprivileged user namespaces on Linux. Enforcement is enabled by default and fails closed with `sandbox.backend_missing`; see [Containerization](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/containerization.md).
10
10
 
11
11
  ```bash
12
12
  npm install -g open-multi-agent-kit --ignore-scripts
@@ -40,18 +40,32 @@ 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.96.1
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
- 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.
55
+ OMK includes provider adapters for Anthropic, OpenAI Codex, Google, OpenCode, Kimi, Qwen, ZAI/GLM, native xAI/Grok, and other compatible services. Availability depends on the credentials and endpoints configured on the current machine.
46
56
 
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 `/think max`.
61
+ - Native `xai` accepts subscription OAuth from `/login` or `XAI_API_KEY` for API billing. Only OAuth exposes weekly SuperGrok usage/reset. See [Grok harness](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/grok-harness.md).
50
62
  - The optional status rail shows independent provider quota windows when an official API or passive response signal is available.
51
63
  - Missing quota values are shown as unavailable rather than estimated.
52
64
 
53
65
  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
66
 
67
+ 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).
68
+
55
69
  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
70
 
57
71
  ### Kimi For Coding
@@ -75,6 +89,7 @@ Type `/` in the editor to open command completion.
75
89
  | `/resume` | Open a previous session |
76
90
  | `/new` | Start a new session |
77
91
  | `/session` | Show session path, messages, tokens, and cost |
92
+ | `/goal [objective]` | Show or set the durable goal; supports `checkpoint <json>`, `pause`, `resume`, evidence-gated `complete`, and `clear` |
78
93
  | `/tree` | Navigate the current session tree |
79
94
  | `/compact [prompt]` | Compact context, optionally with custom instructions |
80
95
  | `/copy` | Copy the last assistant message |
@@ -84,6 +99,10 @@ Type `/` in the editor to open command completion.
84
99
  | `/star` | Open the OMK GitHub repository |
85
100
  | `/quit` | Exit OMK |
86
101
 
102
+ ### Default Harness Safeguards
103
+
104
+ OMK warns from the third repeated identical tool call and blocks the sixth. It also repairs unmatched tool pairs before provider requests and adds guidance for supported Kimi, GLM, and Grok models. `OMK_IDENTICAL_LOOP`, `OMK_TOOL_PAIR_REPAIR`, and `OMK_PROMPT_PRESET` opt out; `OMK_GOAL_CONTROLLER` controls `/goal` and continuation.
105
+
87
106
  See [Usage](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/usage.md) and [Keybindings](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/keybindings.md) for the complete interactive reference.
88
107
 
89
108
  ## Sessions
@@ -92,6 +111,17 @@ Sessions are append-only JSONL transcripts stored under `~/.omk/agent/sessions/`
92
111
 
93
112
  Do not publish session files until you have reviewed them for source code, credentials, personal information, and private tool output.
94
113
 
114
+ ### Scripted Session Inspection
115
+
116
+ ```bash
117
+ omk sdk session status --json
118
+ omk sdk session tail [id] --limit 20
119
+ omk sdk session inspect [id]
120
+ omk sdk session send <id> "message"
121
+ ```
122
+
123
+ These commands inspect or append to stored transcripts. `send` does not wake or execute an agent.
124
+
95
125
  ### Context Compaction
96
126
 
97
127
  Automatic and manual compaction reduce older context while retaining recent conversation state. Cancellation restores queued user input without committing a partial summary.
@@ -140,7 +170,9 @@ See [RPC](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/rpc
140
170
 
141
171
  ## SDK and Extensions
142
172
 
143
- 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.
173
+ The TypeScript SDK exposes session creation, provider configuration, tools, event streams, durable-goal reducers, Goal/Core/Verified/Open/Next seam checkpoints, an explicit pass-gated advisory best-of-N judge, and policy helpers for compaction, retries, prompt budgets, cache transitions, and system-prompt assembly. Extensions can add commands, tools, UI components, themes, and resource loaders without patching OMK core.
174
+
175
+ `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.
144
176
 
145
177
  - [SDK](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/sdk.md)
146
178
  - [Extensions](https://github.com/dmae97/omk/blob/main/packages/coding-agent/docs/extensions.md)
@@ -150,6 +182,7 @@ The TypeScript SDK exposes session creation, provider configuration, tools, and
150
182
  ## Security and Privacy
151
183
 
152
184
  - Tool access is explicit and can be scoped by path or runtime policy.
185
+ - Default OS isolation covers AgentSession's built-in local bash only; it does not isolate other tools, extensions, custom code, or the OMK process.
153
186
  - Default tests scrub provider credentials; live provider tests require `LIVE_E2E=1`.
154
187
  - Subscription quota requests use fixed HTTPS provider origins.
155
188
  - Passive quota observers do not block model streams.
@@ -170,7 +203,7 @@ npm test
170
203
 
171
204
  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
205
 
173
- Release notes: [v0.95.2](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v0.95.2.md).
206
+ Release notes: [v0.96.1](https://github.com/dmae97/omk/blob/main/.github/RELEASE_NOTES_v0.96.1.md).
174
207
 
175
208
  ## License
176
209
 
@@ -1 +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,CAkC1D;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\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"]}
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"]}
@@ -98,6 +98,31 @@ export class ApprovalReceiptStore {
98
98
  const bytes = Buffer.from(canonical, "utf8");
99
99
  if (bytes.byteLength > MAX_RECEIPT_BYTES)
100
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
+ }
101
126
  let fd;
102
127
  try {
103
128
  fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW, 0o600);
@@ -1 +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\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"]}
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"]}