open-multi-agent-kit 0.95.2 → 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 (70) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +15 -0
  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/core/agent-session.d.ts +2 -2
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +41 -17
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/provider-resilience.d.ts +7 -0
  11. package/dist/core/provider-resilience.d.ts.map +1 -1
  12. package/dist/core/provider-resilience.js +11 -0
  13. package/dist/core/provider-resilience.js.map +1 -1
  14. package/dist/core/subagent-orchestration.d.ts.map +1 -1
  15. package/dist/core/subagent-orchestration.js +4 -2
  16. package/dist/core/subagent-orchestration.js.map +1 -1
  17. package/dist/core/tools/diagnostics.d.ts.map +1 -1
  18. package/dist/core/tools/diagnostics.js +8 -2
  19. package/dist/core/tools/diagnostics.js.map +1 -1
  20. package/dist/guardrails/evidence-protocol.d.ts +10 -0
  21. package/dist/guardrails/evidence-protocol.d.ts.map +1 -0
  22. package/dist/guardrails/evidence-protocol.js +34 -0
  23. package/dist/guardrails/evidence-protocol.js.map +1 -0
  24. package/dist/guardrails/evidence-system.d.ts +3 -1
  25. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  26. package/dist/guardrails/evidence-system.js +22 -14
  27. package/dist/guardrails/evidence-system.js.map +1 -1
  28. package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -1
  29. package/dist/guardrails/replay-ledger-lock-owner.js +3 -1
  30. package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -1
  31. package/dist/guardrails/replay-payload-hash.d.ts +6 -0
  32. package/dist/guardrails/replay-payload-hash.d.ts.map +1 -0
  33. package/dist/guardrails/replay-payload-hash.js +24 -0
  34. package/dist/guardrails/replay-payload-hash.js.map +1 -0
  35. package/dist/index.d.ts +2 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/types/evidence.d.ts +11 -3
  40. package/dist/types/evidence.d.ts.map +1 -1
  41. package/dist/types/evidence.js.map +1 -1
  42. package/docs/book-to-skill.md +79 -0
  43. package/docs/compaction.md +10 -0
  44. package/docs/docs.json +8 -0
  45. package/docs/index.md +3 -0
  46. package/docs/models.md +12 -0
  47. package/docs/provider-resilience.md +37 -56
  48. package/docs/providers.md +4 -0
  49. package/docs/run-protocol.md +72 -0
  50. package/docs/sdk.md +15 -2
  51. package/docs/sessions.md +6 -0
  52. package/docs/settings.md +2 -0
  53. package/docs/skills.md +9 -1
  54. package/docs/usage.md +1 -1
  55. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  56. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  57. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  58. package/examples/extensions/gondolin/package-lock.json +2 -2
  59. package/examples/extensions/gondolin/package.json +1 -1
  60. package/examples/extensions/sandbox/package-lock.json +2 -2
  61. package/examples/extensions/sandbox/package.json +1 -1
  62. package/examples/extensions/with-deps/package-lock.json +2 -2
  63. package/examples/extensions/with-deps/package.json +1 -1
  64. package/npm-shrinkwrap.json +37 -15
  65. package/package.json +8 -5
  66. package/dist/core/context-budget-compressors.d.ts +0 -50
  67. package/dist/core/context-budget-compressors.d.ts.map +0 -1
  68. package/dist/core/context-budget-compressors.js +0 -166
  69. package/dist/core/context-budget-compressors.js.map +0 -1
  70. package/dist/core/extensions/loader.js.bak-sharedjiti-20260812-111246 +0 -553
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
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
+
5
25
  ## [0.95.2] - 2026-08-15
6
26
 
7
27
  ### Changed
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)
@@ -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":"AAAA,OAAO,EACN,SAAS,EACT,SAAS,EACT,UAAU,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,aAAa,GACb,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAwB,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhH,MAAM,iBAAiB,GAAG,IAAI,GAAG,IAAI,CAAC;AACtC,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,IAAI,CAAC,CAAC;AAY3C,SAAS,OAAO,CAAC,KAAc,EAAE,IAAY,EAAW;IACvD,OAAO,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;AAAA,CACxE;AAED,SAAS,wBAAwB,CAAC,GAAW,EAAE,KAAa,EAAQ;IACnE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC9B,IAAI,OAAO,MAAM,KAAK,UAAU,IAAI,GAAG,KAAK,MAAM,EAAE,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,mCAAmC,CAAC,CAAC;IAC9D,CAAC;AAAA,CACD;AAED,SAAS,eAAe,CAAC,SAAiB,EAAU;IACnD,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACnG,OAAO,GAAG,SAAS,OAAO,CAAC;AAAA,CAC3B;AAED,SAAS,sBAAsB,CAAC,EAAU,EAAE,QAAgB,EAAQ;IACnE,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;IAC3B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,QAAQ,EAAE,CAAC,CAAC;IAC3F,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,6CAA6C,QAAQ,EAAE,CAAC,CAAC;IAC/F,wBAAwB,CAAC,IAAI,CAAC,GAAG,EAAE,oBAAoB,QAAQ,EAAE,CAAC,CAAC;IACnE,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,8CAA8C,QAAQ,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,GAAG,iBAAiB;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,iBAAiB,QAAQ,CAAC,CAAC;AAAA,CAC1G;AAED,SAAS,iBAAiB,CAAC,EAAU,EAAE,QAAgB,EAAmB;IACzE,sBAAsB,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,iBAAiB,GAAG,CAAC,CAAC,CAAC;IACnD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,CAAC,CAAC;QAC1E,IAAI,IAAI,KAAK,CAAC;YAAE,MAAM;QACtB,MAAM,IAAI,IAAI,CAAC;IAChB,CAAC;IACD,IAAI,MAAM,GAAG,iBAAiB;QAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,iBAAiB,QAAQ,CAAC,CAAC;IACvG,sBAAsB,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IACrC,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACJ,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAY,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,uCAAuC,QAAQ,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;AAAA,CACtC;AAED,MAAM,OAAO,oBAAoB;IACvB,OAAO,CAAS;IAEzB,YAAY,OAAe,EAAE;QAC5B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,CAAC,sBAAsB,EAAE,CAAC;IAAA,CAC9B;IAEO,sBAAsB,GAAiB;QAC9C,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1D,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CAAC,mDAAmD,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACpF,CAAC;QACD,wBAAwB,CAAC,IAAI,CAAC,GAAG,EAAE,yBAAyB,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC5E,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,KAAK,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,mDAAmD,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACpF,CAAC;QACD,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC;IAAA,CACxC;IAEO,mBAAmB,CAAC,QAAsB,EAAQ;QACzD,MAAM,OAAO,GAAG,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAC9C,IAAI,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,KAAK,QAAQ,CAAC,GAAG,EAAE,CAAC;YAClE,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACvE,CAAC;IAAA,CACD;IAEO,OAAO,CAAC,SAAiB,EAAU;QAC1C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC;IAAA,CACtD;IAEO,kBAAkB,CAAC,QAAgB,EAAU;QACpD,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,QAAQ,GAAG,QAAQ,CAAC,CAAC;QAC7D,IAAI,CAAC;YACJ,sBAAsB,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YACrC,OAAO,EAAE,CAAC;QACX,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,SAAS,CAAC,EAAE,CAAC,CAAC;YACd,MAAM,KAAK,CAAC;QACb,CAAC;IAAA,CACD;IAED,KAAK,CAAC,OAAwB,EAA8B;QAC3D,MAAM,SAAS,GAAG,uBAAuB,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,YAAY,GAAG,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,wBAAwB,CAAC,SAAS,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QAC7C,IAAI,KAAK,CAAC,UAAU,GAAG,iBAAiB;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,iBAAiB,QAAQ,CAAC,CAAC;QAEjH,IAAI,EAAU,CAAC;QACf,IAAI,CAAC;YACJ,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC;QACtG,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC;gBAAE,MAAM,KAAK,CAAC;YAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACrD,IAAI,wBAAwB,CAAC,QAAQ,CAAC,KAAK,SAAS,EAAE,CAAC;gBACtD,MAAM,IAAI,KAAK,CACd,oFAAoF,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,CAC9G,CAAC;YACH,CAAC;YACD,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;YACvC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAC3C,CAAC;QAED,IAAI,CAAC;YACJ,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YACtB,sBAAsB,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YACrC,aAAa,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;YACzB,SAAS,CAAC,EAAE,CAAC,CAAC;YACd,sBAAsB,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QACtC,CAAC;gBAAS,CAAC;YACV,SAAS,CAAC,EAAE,CAAC,CAAC;QACf,CAAC;QACD,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;QACvC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAAA,CACzC;IAED,GAAG,CAAC,SAAiB,EAAW;QAC/B,MAAM,YAAY,GAAG,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACnD,IAAI,EAAU,CAAC;QACf,IAAI,CAAC;YACJ,EAAE,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;gBAC9B,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;gBACvC,OAAO,KAAK,CAAC;YACd,CAAC;YACD,MAAM,KAAK,CAAC;QACb,CAAC;QACD,SAAS,CAAC,EAAE,CAAC,CAAC;QACd,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC;IAAA,CACZ;IAED,IAAI,CAAC,SAAiB,EAAmB;QACxC,MAAM,YAAY,GAAG,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACzC,MAAM,EAAE,GAAG,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,CAAC;YACJ,OAAO,iBAAiB,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QACxC,CAAC;gBAAS,CAAC;YACV,SAAS,CAAC,EAAE,CAAC,CAAC;YACd,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;QACxC,CAAC;IAAA,CACD;IAED,cAAc,GAAa;QAC1B,MAAM,YAAY,GAAG,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACnD,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC;aACnC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,kCAAkC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aACjE,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,EAAE,KAAK,SAAS,CAAC;aAC9C,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC;QACnD,KAAK,MAAM,EAAE,IAAI,GAAG;YAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACnC,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC;IAAA,CACX;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.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"]}
@@ -293,7 +293,7 @@ export declare class AgentSession {
293
293
  private _pendingNextTurnMessages;
294
294
  private _compactionAbortController;
295
295
  private _autoCompactionAbortController;
296
- private _overflowRecoveryAttempted;
296
+ private _overflowRecoveryAttempts;
297
297
  private _thresholdCompactionEmergency;
298
298
  private _compactionHysteresisState;
299
299
  private _branchSummaryAbortController;
@@ -789,6 +789,7 @@ export declare class AgentSession {
789
789
  private _effectiveTurnContextWindow;
790
790
  private _checkProjectedCompaction;
791
791
  private _checkCompaction;
792
+ private _overflowCompactionSettings;
792
793
  private _runAutoCompaction;
793
794
  /**
794
795
  * Toggle auto-compaction setting.
@@ -832,7 +833,6 @@ export declare class AgentSession {
832
833
  private _refreshToolRegistry;
833
834
  private _buildRuntime;
834
835
  reload(): Promise<void>;
835
- private _isNonRetryableProviderLimitError;
836
836
  /**
837
837
  * Check if an error is retryable (overloaded, rate limit, server errors).
838
838
  * Context overflow errors are NOT retryable (handled by compaction instead).