@caupulican/pi-adaptative 0.81.41 → 0.81.42

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 (237) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bundled-resources/runtimes/pi-shell-engine/commands/text.py +5 -1
  3. package/dist/bundled-resources/runtimes/pi-shell-engine/context.py +3 -3
  4. package/dist/bundled-resources/runtimes/pi-shell-engine/errors.py +8 -0
  5. package/dist/bundled-resources/runtimes/pi-shell-engine/exec.py +26 -3
  6. package/dist/bundled-resources/runtimes/pi-shell-engine/expand.py +3 -0
  7. package/dist/bundled-resources/runtimes/pi-shell-engine/main.py +4 -1
  8. package/dist/bundled-resources/runtimes/pi-shell-engine/state.py +2 -1
  9. package/dist/bundled-resources/runtimes/pi-shell-engine/tokens.py +2 -0
  10. package/dist/core/agent-paths.d.ts +32 -0
  11. package/dist/core/agent-paths.d.ts.map +1 -1
  12. package/dist/core/agent-paths.js +65 -0
  13. package/dist/core/agent-paths.js.map +1 -1
  14. package/dist/core/agent-session-contracts.d.ts +19 -1
  15. package/dist/core/agent-session-contracts.d.ts.map +1 -1
  16. package/dist/core/agent-session-contracts.js.map +1 -1
  17. package/dist/core/agent-session.d.ts.map +1 -1
  18. package/dist/core/agent-session.js +4 -9
  19. package/dist/core/agent-session.js.map +1 -1
  20. package/dist/core/autonomy/contracts.d.ts +2 -0
  21. package/dist/core/autonomy/contracts.d.ts.map +1 -1
  22. package/dist/core/autonomy/contracts.js.map +1 -1
  23. package/dist/core/autonomy/envelope-enforcement.d.ts.map +1 -1
  24. package/dist/core/autonomy/envelope-enforcement.js +4 -9
  25. package/dist/core/autonomy/envelope-enforcement.js.map +1 -1
  26. package/dist/core/autonomy/lane-tool-surface.d.ts +3 -1
  27. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  28. package/dist/core/autonomy/lane-tool-surface.js +17 -4
  29. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  30. package/dist/core/autonomy/path-scope.d.ts +14 -0
  31. package/dist/core/autonomy/path-scope.d.ts.map +1 -1
  32. package/dist/core/autonomy/path-scope.js +37 -6
  33. package/dist/core/autonomy/path-scope.js.map +1 -1
  34. package/dist/core/background-lane-controller.d.ts +27 -1
  35. package/dist/core/background-lane-controller.d.ts.map +1 -1
  36. package/dist/core/background-lane-controller.js +32 -1
  37. package/dist/core/background-lane-controller.js.map +1 -1
  38. package/dist/core/delegation/managed-lane-controller.d.ts +11 -1
  39. package/dist/core/delegation/managed-lane-controller.d.ts.map +1 -1
  40. package/dist/core/delegation/managed-lane-controller.js +84 -41
  41. package/dist/core/delegation/managed-lane-controller.js.map +1 -1
  42. package/dist/core/delegation/session-worker-claim.d.ts.map +1 -1
  43. package/dist/core/delegation/session-worker-claim.js +4 -3
  44. package/dist/core/delegation/session-worker-claim.js.map +1 -1
  45. package/dist/core/delegation/worker-action-journal.d.ts +73 -0
  46. package/dist/core/delegation/worker-action-journal.d.ts.map +1 -0
  47. package/dist/core/delegation/worker-action-journal.js +297 -0
  48. package/dist/core/delegation/worker-action-journal.js.map +1 -0
  49. package/dist/core/delegation/worker-actions.d.ts +36 -1
  50. package/dist/core/delegation/worker-actions.d.ts.map +1 -1
  51. package/dist/core/delegation/worker-actions.js +171 -46
  52. package/dist/core/delegation/worker-actions.js.map +1 -1
  53. package/dist/core/delegation/worker-agent-control-coordinator.d.ts +75 -0
  54. package/dist/core/delegation/worker-agent-control-coordinator.d.ts.map +1 -0
  55. package/dist/core/delegation/worker-agent-control-coordinator.js +218 -0
  56. package/dist/core/delegation/worker-agent-control-coordinator.js.map +1 -0
  57. package/dist/core/delegation/worker-agent-control.d.ts +66 -0
  58. package/dist/core/delegation/worker-agent-control.d.ts.map +1 -0
  59. package/dist/core/delegation/worker-agent-control.js +163 -0
  60. package/dist/core/delegation/worker-agent-control.js.map +1 -0
  61. package/dist/core/delegation/worker-attempt-executor.d.ts +80 -0
  62. package/dist/core/delegation/worker-attempt-executor.d.ts.map +1 -0
  63. package/dist/core/delegation/worker-attempt-executor.js +343 -0
  64. package/dist/core/delegation/worker-attempt-executor.js.map +1 -0
  65. package/dist/core/delegation/worker-claim.d.ts +30 -0
  66. package/dist/core/delegation/worker-claim.d.ts.map +1 -1
  67. package/dist/core/delegation/worker-claim.js +205 -53
  68. package/dist/core/delegation/worker-claim.js.map +1 -1
  69. package/dist/core/delegation/worker-conversation-store.d.ts +116 -0
  70. package/dist/core/delegation/worker-conversation-store.d.ts.map +1 -0
  71. package/dist/core/delegation/worker-conversation-store.js +442 -0
  72. package/dist/core/delegation/worker-conversation-store.js.map +1 -0
  73. package/dist/core/delegation/worker-delegation-controller.d.ts +21 -5
  74. package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
  75. package/dist/core/delegation/worker-delegation-controller.js +386 -251
  76. package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
  77. package/dist/core/delegation/worker-delegation-request.d.ts +14 -0
  78. package/dist/core/delegation/worker-delegation-request.d.ts.map +1 -1
  79. package/dist/core/delegation/worker-delegation-request.js.map +1 -1
  80. package/dist/core/delegation/worker-dispatch-scheduler.d.ts +4 -2
  81. package/dist/core/delegation/worker-dispatch-scheduler.d.ts.map +1 -1
  82. package/dist/core/delegation/worker-dispatch-scheduler.js +16 -6
  83. package/dist/core/delegation/worker-dispatch-scheduler.js.map +1 -1
  84. package/dist/core/delegation/worker-execution-policy.d.ts +2 -1
  85. package/dist/core/delegation/worker-execution-policy.d.ts.map +1 -1
  86. package/dist/core/delegation/worker-execution-policy.js +5 -7
  87. package/dist/core/delegation/worker-execution-policy.js.map +1 -1
  88. package/dist/core/delegation/worker-lifecycle.d.ts +44 -1
  89. package/dist/core/delegation/worker-lifecycle.d.ts.map +1 -1
  90. package/dist/core/delegation/worker-lifecycle.js +120 -0
  91. package/dist/core/delegation/worker-lifecycle.js.map +1 -1
  92. package/dist/core/delegation/worker-process-owner.d.ts +18 -0
  93. package/dist/core/delegation/worker-process-owner.d.ts.map +1 -0
  94. package/dist/core/delegation/worker-process-owner.js +54 -0
  95. package/dist/core/delegation/worker-process-owner.js.map +1 -0
  96. package/dist/core/delegation/worker-profile-resolver.d.ts +5 -1
  97. package/dist/core/delegation/worker-profile-resolver.d.ts.map +1 -1
  98. package/dist/core/delegation/worker-profile-resolver.js +8 -0
  99. package/dist/core/delegation/worker-profile-resolver.js.map +1 -1
  100. package/dist/core/delegation/worker-recovery-coordinator.d.ts +54 -0
  101. package/dist/core/delegation/worker-recovery-coordinator.d.ts.map +1 -0
  102. package/dist/core/delegation/worker-recovery-coordinator.js +181 -0
  103. package/dist/core/delegation/worker-recovery-coordinator.js.map +1 -0
  104. package/dist/core/delegation/worker-resource-catalog.d.ts +32 -0
  105. package/dist/core/delegation/worker-resource-catalog.d.ts.map +1 -0
  106. package/dist/core/delegation/worker-resource-catalog.js +116 -0
  107. package/dist/core/delegation/worker-resource-catalog.js.map +1 -0
  108. package/dist/core/delegation/worker-resource-materializer.d.ts +47 -0
  109. package/dist/core/delegation/worker-resource-materializer.d.ts.map +1 -0
  110. package/dist/core/delegation/worker-resource-materializer.js +137 -0
  111. package/dist/core/delegation/worker-resource-materializer.js.map +1 -0
  112. package/dist/core/delegation/worker-runner.d.ts +3 -1
  113. package/dist/core/delegation/worker-runner.d.ts.map +1 -1
  114. package/dist/core/delegation/worker-runner.js +189 -54
  115. package/dist/core/delegation/worker-runner.js.map +1 -1
  116. package/dist/core/delegation/worker-terminal-finalizer.d.ts +23 -0
  117. package/dist/core/delegation/worker-terminal-finalizer.d.ts.map +1 -0
  118. package/dist/core/delegation/worker-terminal-finalizer.js +12 -0
  119. package/dist/core/delegation/worker-terminal-finalizer.js.map +1 -0
  120. package/dist/core/delegation/worker-write-reservation-coordinator.d.ts +38 -0
  121. package/dist/core/delegation/worker-write-reservation-coordinator.d.ts.map +1 -0
  122. package/dist/core/delegation/worker-write-reservation-coordinator.js +107 -0
  123. package/dist/core/delegation/worker-write-reservation-coordinator.js.map +1 -0
  124. package/dist/core/delegation/worker-write-reservation.d.ts +90 -0
  125. package/dist/core/delegation/worker-write-reservation.d.ts.map +1 -0
  126. package/dist/core/delegation/worker-write-reservation.js +348 -0
  127. package/dist/core/delegation/worker-write-reservation.js.map +1 -0
  128. package/dist/core/models/model-endpoint.d.ts +3 -0
  129. package/dist/core/models/model-endpoint.d.ts.map +1 -0
  130. package/dist/core/models/model-endpoint.js +11 -0
  131. package/dist/core/models/model-endpoint.js.map +1 -0
  132. package/dist/core/orchestration/agent-resume.d.ts +2 -0
  133. package/dist/core/orchestration/agent-resume.d.ts.map +1 -1
  134. package/dist/core/orchestration/agent-resume.js +7 -0
  135. package/dist/core/orchestration/agent-resume.js.map +1 -1
  136. package/dist/core/orchestration/attempt-usage.d.ts +28 -0
  137. package/dist/core/orchestration/attempt-usage.d.ts.map +1 -0
  138. package/dist/core/orchestration/attempt-usage.js +168 -0
  139. package/dist/core/orchestration/attempt-usage.js.map +1 -0
  140. package/dist/core/orchestration/capability-gateway.d.ts +19 -1
  141. package/dist/core/orchestration/capability-gateway.d.ts.map +1 -1
  142. package/dist/core/orchestration/capability-gateway.js +128 -28
  143. package/dist/core/orchestration/capability-gateway.js.map +1 -1
  144. package/dist/core/orchestration/contracts.d.ts +35 -0
  145. package/dist/core/orchestration/contracts.d.ts.map +1 -1
  146. package/dist/core/orchestration/contracts.js +17 -0
  147. package/dist/core/orchestration/contracts.js.map +1 -1
  148. package/dist/core/orchestration/delegation-ledger.d.ts +17 -2
  149. package/dist/core/orchestration/delegation-ledger.d.ts.map +1 -1
  150. package/dist/core/orchestration/delegation-ledger.js +78 -3
  151. package/dist/core/orchestration/delegation-ledger.js.map +1 -1
  152. package/dist/core/orchestration/event-store.d.ts +1 -0
  153. package/dist/core/orchestration/event-store.d.ts.map +1 -1
  154. package/dist/core/orchestration/event-store.js +78 -38
  155. package/dist/core/orchestration/event-store.js.map +1 -1
  156. package/dist/core/orchestration/profile-registry.d.ts.map +1 -1
  157. package/dist/core/orchestration/profile-registry.js +81 -41
  158. package/dist/core/orchestration/profile-registry.js.map +1 -1
  159. package/dist/core/orchestration/profile-store.d.ts.map +1 -1
  160. package/dist/core/orchestration/profile-store.js +18 -4
  161. package/dist/core/orchestration/profile-store.js.map +1 -1
  162. package/dist/core/orchestration/risk-budget.d.ts +2 -0
  163. package/dist/core/orchestration/risk-budget.d.ts.map +1 -1
  164. package/dist/core/orchestration/risk-budget.js +26 -0
  165. package/dist/core/orchestration/risk-budget.js.map +1 -1
  166. package/dist/core/orchestration/task-runtime.d.ts +13 -1
  167. package/dist/core/orchestration/task-runtime.d.ts.map +1 -1
  168. package/dist/core/orchestration/task-runtime.js +127 -16
  169. package/dist/core/orchestration/task-runtime.js.map +1 -1
  170. package/dist/core/orchestration/worker-execution-contract.d.ts +3 -1
  171. package/dist/core/orchestration/worker-execution-contract.d.ts.map +1 -1
  172. package/dist/core/orchestration/worker-execution-contract.js +75 -23
  173. package/dist/core/orchestration/worker-execution-contract.js.map +1 -1
  174. package/dist/core/orchestration/worker-result-adapter.d.ts +9 -3
  175. package/dist/core/orchestration/worker-result-adapter.d.ts.map +1 -1
  176. package/dist/core/orchestration/worker-result-adapter.js +167 -14
  177. package/dist/core/orchestration/worker-result-adapter.js.map +1 -1
  178. package/dist/core/reflection-controller.d.ts +1 -1
  179. package/dist/core/reflection-controller.d.ts.map +1 -1
  180. package/dist/core/reflection-controller.js +90 -54
  181. package/dist/core/reflection-controller.js.map +1 -1
  182. package/dist/core/research/evidence-bundle.d.ts +20 -0
  183. package/dist/core/research/evidence-bundle.d.ts.map +1 -1
  184. package/dist/core/research/evidence-bundle.js +173 -69
  185. package/dist/core/research/evidence-bundle.js.map +1 -1
  186. package/dist/core/runtime-builder.d.ts +7 -0
  187. package/dist/core/runtime-builder.d.ts.map +1 -1
  188. package/dist/core/runtime-builder.js +15 -1
  189. package/dist/core/runtime-builder.js.map +1 -1
  190. package/dist/core/session-artifact-bundle.d.ts +35 -0
  191. package/dist/core/session-artifact-bundle.d.ts.map +1 -0
  192. package/dist/core/session-artifact-bundle.js +61 -0
  193. package/dist/core/session-artifact-bundle.js.map +1 -0
  194. package/dist/core/tools/delegate-status.d.ts +6 -1
  195. package/dist/core/tools/delegate-status.d.ts.map +1 -1
  196. package/dist/core/tools/delegate-status.js +100 -18
  197. package/dist/core/tools/delegate-status.js.map +1 -1
  198. package/dist/core/tools/delegate.d.ts +9 -1
  199. package/dist/core/tools/delegate.d.ts.map +1 -1
  200. package/dist/core/tools/delegate.js +323 -61
  201. package/dist/core/tools/delegate.js.map +1 -1
  202. package/dist/core/tools/run-process.d.ts +1 -1
  203. package/dist/core/tools/run-process.d.ts.map +1 -1
  204. package/dist/core/tools/run-process.js +69 -9
  205. package/dist/core/tools/run-process.js.map +1 -1
  206. package/dist/core/tools/shell-contract-router.d.ts.map +1 -1
  207. package/dist/core/tools/shell-contract-router.js +12 -5
  208. package/dist/core/tools/shell-contract-router.js.map +1 -1
  209. package/dist/core/util/bounded-file.d.ts +15 -0
  210. package/dist/core/util/bounded-file.d.ts.map +1 -0
  211. package/dist/core/util/bounded-file.js +120 -0
  212. package/dist/core/util/bounded-file.js.map +1 -0
  213. package/dist/core/util/bounded-jsonl.d.ts.map +1 -1
  214. package/dist/core/util/bounded-jsonl.js +119 -9
  215. package/dist/core/util/bounded-jsonl.js.map +1 -1
  216. package/dist/core/util/bounded-value.d.ts +3 -0
  217. package/dist/core/util/bounded-value.d.ts.map +1 -0
  218. package/dist/core/util/bounded-value.js +12 -0
  219. package/dist/core/util/bounded-value.js.map +1 -0
  220. package/dist/core/util/value-guards.d.ts +2 -0
  221. package/dist/core/util/value-guards.d.ts.map +1 -1
  222. package/dist/core/util/value-guards.js +5 -0
  223. package/dist/core/util/value-guards.js.map +1 -1
  224. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  225. package/dist/modes/interactive/components/session-selector.js +27 -44
  226. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  227. package/docs/tool-repair.md +3 -0
  228. package/docs/windows.md +4 -2
  229. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  230. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  231. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  232. package/examples/extensions/sandbox/package-lock.json +2 -2
  233. package/examples/extensions/sandbox/package.json +1 -1
  234. package/examples/extensions/with-deps/package-lock.json +2 -2
  235. package/examples/extensions/with-deps/package.json +1 -1
  236. package/npm-shrinkwrap.json +12 -12
  237. package/package.json +4 -4
@@ -0,0 +1,168 @@
1
+ import { isPlainRecord } from "../util/value-guards.js";
2
+ export const EMPTY_ATTEMPT_USAGE = {
3
+ toolCalls: 0,
4
+ inputTokens: 0,
5
+ outputTokens: 0,
6
+ cacheReadTokens: 0,
7
+ cacheWriteTokens: 0,
8
+ totalTokens: 0,
9
+ costUsd: 0,
10
+ activeWallClockMs: 0,
11
+ };
12
+ /**
13
+ * Validate an untrusted provider-neutral usage report before it reaches durable accounting.
14
+ * Provider totals are authoritative and therefore need not equal the detail field sum.
15
+ */
16
+ const PROVIDER_USAGE_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "cost"];
17
+ const PROVIDER_USAGE_COST_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "total"];
18
+ function parseExactDataRecord(value, fields, label) {
19
+ if (!isPlainRecord(value))
20
+ throw new TypeError(`${label} must be a plain object.`);
21
+ for (const key of Reflect.ownKeys(value)) {
22
+ if (typeof key !== "string" || !fields.includes(key))
23
+ throw new TypeError(`${label} contains an unsupported field.`);
24
+ }
25
+ const parsed = {};
26
+ for (const field of fields) {
27
+ const descriptor = Object.getOwnPropertyDescriptor(value, field);
28
+ if (!descriptor || !("value" in descriptor))
29
+ throw new TypeError(`${label}.${field} must be a data field.`);
30
+ parsed[field] = descriptor.value;
31
+ }
32
+ return parsed;
33
+ }
34
+ /** Parse an untrusted provider-neutral usage report into a closed, known-field value. */
35
+ export function validateProviderUsage(value, label = "provider usage") {
36
+ const usage = parseExactDataRecord(value, PROVIDER_USAGE_FIELDS, label);
37
+ const cost = parseExactDataRecord(usage.cost, PROVIDER_USAGE_COST_FIELDS, `${label}.cost`);
38
+ const tokenFields = [
39
+ ["input", usage.input],
40
+ ["output", usage.output],
41
+ ["cacheRead", usage.cacheRead],
42
+ ["cacheWrite", usage.cacheWrite],
43
+ ["totalTokens", usage.totalTokens],
44
+ ];
45
+ for (const [field, value] of tokenFields) {
46
+ if (!Number.isSafeInteger(value) || value < 0) {
47
+ throw new Error(`${label}.${field} must be a non-negative safe integer.`);
48
+ }
49
+ }
50
+ const costFields = [
51
+ ["input", cost.input],
52
+ ["output", cost.output],
53
+ ["cacheRead", cost.cacheRead],
54
+ ["cacheWrite", cost.cacheWrite],
55
+ ["total", cost.total],
56
+ ];
57
+ for (const [field, value] of costFields) {
58
+ if (!Number.isFinite(value) || value < 0) {
59
+ throw new Error(`${label}.cost.${field} must be finite and non-negative.`);
60
+ }
61
+ }
62
+ return {
63
+ input: usage.input,
64
+ output: usage.output,
65
+ cacheRead: usage.cacheRead,
66
+ cacheWrite: usage.cacheWrite,
67
+ totalTokens: usage.totalTokens,
68
+ cost: {
69
+ input: cost.input,
70
+ output: cost.output,
71
+ cacheRead: cost.cacheRead,
72
+ cacheWrite: cost.cacheWrite,
73
+ total: cost.total,
74
+ },
75
+ };
76
+ }
77
+ /** Validate the one durable cumulative usage shape shared by checkpoints, recovery, and results. */
78
+ export function validateAttemptUsageSnapshot(usage, label = "attempt usage") {
79
+ if (!Number.isSafeInteger(usage.toolCalls) || usage.toolCalls < 0) {
80
+ throw new Error(`${label}.toolCalls must be a non-negative safe integer.`);
81
+ }
82
+ if (!Number.isSafeInteger(usage.inputTokens) || usage.inputTokens < 0) {
83
+ throw new Error(`${label}.inputTokens must be a non-negative safe integer.`);
84
+ }
85
+ if (!Number.isSafeInteger(usage.outputTokens) || usage.outputTokens < 0) {
86
+ throw new Error(`${label}.outputTokens must be a non-negative safe integer.`);
87
+ }
88
+ if (!Number.isSafeInteger(usage.cacheReadTokens) || usage.cacheReadTokens < 0) {
89
+ throw new Error(`${label}.cacheReadTokens must be a non-negative safe integer.`);
90
+ }
91
+ if (!Number.isSafeInteger(usage.cacheWriteTokens) || usage.cacheWriteTokens < 0) {
92
+ throw new Error(`${label}.cacheWriteTokens must be a non-negative safe integer.`);
93
+ }
94
+ if (!Number.isSafeInteger(usage.totalTokens) || usage.totalTokens < 0) {
95
+ throw new Error(`${label}.totalTokens must be a non-negative safe integer.`);
96
+ }
97
+ if (!Number.isFinite(usage.costUsd) || usage.costUsd < 0) {
98
+ throw new Error(`${label}.costUsd must be finite and non-negative.`);
99
+ }
100
+ if (!Number.isFinite(usage.activeWallClockMs) || usage.activeWallClockMs < 0) {
101
+ throw new Error(`${label}.activeWallClockMs must be finite and non-negative.`);
102
+ }
103
+ return structuredClone(usage);
104
+ }
105
+ export function attemptUsageFromGatewayUsage(usage) {
106
+ return validateAttemptUsageSnapshot({
107
+ toolCalls: usage.toolCalls,
108
+ inputTokens: usage.inputTokens,
109
+ outputTokens: usage.outputTokens,
110
+ cacheReadTokens: usage.cacheReadTokens,
111
+ cacheWriteTokens: usage.cacheWriteTokens,
112
+ totalTokens: usage.totalTokens,
113
+ costUsd: usage.costUsd,
114
+ activeWallClockMs: usage.wallClockMs,
115
+ }, "gateway usage");
116
+ }
117
+ export function addAttemptUsage(base, delta, label = "attempt usage") {
118
+ validateAttemptUsageSnapshot(base, `${label} base`);
119
+ validateAttemptUsageSnapshot(delta, `${label} delta`);
120
+ return validateAttemptUsageSnapshot({
121
+ toolCalls: base.toolCalls + delta.toolCalls,
122
+ inputTokens: base.inputTokens + delta.inputTokens,
123
+ outputTokens: base.outputTokens + delta.outputTokens,
124
+ cacheReadTokens: base.cacheReadTokens + delta.cacheReadTokens,
125
+ cacheWriteTokens: base.cacheWriteTokens + delta.cacheWriteTokens,
126
+ totalTokens: base.totalTokens + delta.totalTokens,
127
+ costUsd: base.costUsd + delta.costUsd,
128
+ activeWallClockMs: base.activeWallClockMs + delta.activeWallClockMs,
129
+ }, label);
130
+ }
131
+ /**
132
+ * Recover the strongest cumulative evidence across an atomic checkpoint and its durable transcript.
133
+ * Both inputs are cumulative views of the same attempt, so summing would double-count; component
134
+ * maxima close append-before-checkpoint crash windows conservatively.
135
+ */
136
+ export function reconcileAttemptUsage(checkpoint, transcript) {
137
+ validateAttemptUsageSnapshot(checkpoint, "attempt checkpoint usage");
138
+ validateAttemptUsageSnapshot(transcript, "attempt transcript usage");
139
+ return validateAttemptUsageSnapshot({
140
+ toolCalls: Math.max(checkpoint.toolCalls, transcript.toolCalls),
141
+ inputTokens: Math.max(checkpoint.inputTokens, transcript.inputTokens),
142
+ outputTokens: Math.max(checkpoint.outputTokens, transcript.outputTokens),
143
+ cacheReadTokens: Math.max(checkpoint.cacheReadTokens, transcript.cacheReadTokens),
144
+ cacheWriteTokens: Math.max(checkpoint.cacheWriteTokens, transcript.cacheWriteTokens),
145
+ totalTokens: Math.max(checkpoint.totalTokens, transcript.totalTokens),
146
+ costUsd: Math.max(checkpoint.costUsd, transcript.costUsd),
147
+ activeWallClockMs: Math.max(checkpoint.activeWallClockMs, transcript.activeWallClockMs),
148
+ });
149
+ }
150
+ export function remainingTokenBudget(maxTokens, usage) {
151
+ if (maxTokens === undefined)
152
+ return undefined;
153
+ const remaining = maxTokens - usage.totalTokens;
154
+ return Math.max(0, remaining);
155
+ }
156
+ /** Convert durable cumulative accounting into the provider-neutral report shape exactly once. */
157
+ export function providerUsageFromAttemptUsage(usage) {
158
+ const validated = validateAttemptUsageSnapshot(usage);
159
+ return {
160
+ input: validated.inputTokens,
161
+ output: validated.outputTokens,
162
+ cacheRead: validated.cacheReadTokens,
163
+ cacheWrite: validated.cacheWriteTokens,
164
+ totalTokens: validated.totalTokens,
165
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: validated.costUsd },
166
+ };
167
+ }
168
+ //# sourceMappingURL=attempt-usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attempt-usage.js","sourceRoot":"","sources":["../../../src/core/orchestration/attempt-usage.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Usage } from \"@caupulican/pi-ai\";\nimport { isPlainRecord } from \"../util/value-guards.ts\";\nimport type { AttemptUsageSnapshot } from \"./contracts.ts\";\n\nexport const EMPTY_ATTEMPT_USAGE: AttemptUsageSnapshot = {\n\ttoolCalls: 0,\n\tinputTokens: 0,\n\toutputTokens: 0,\n\tcacheReadTokens: 0,\n\tcacheWriteTokens: 0,\n\ttotalTokens: 0,\n\tcostUsd: 0,\n\tactiveWallClockMs: 0,\n};\n\n/**\n * Validate an untrusted provider-neutral usage report before it reaches durable accounting.\n * Provider totals are authoritative and therefore need not equal the detail field sum.\n */\nconst PROVIDER_USAGE_FIELDS = [\"input\", \"output\", \"cacheRead\", \"cacheWrite\", \"totalTokens\", \"cost\"] as const;\nconst PROVIDER_USAGE_COST_FIELDS = [\"input\", \"output\", \"cacheRead\", \"cacheWrite\", \"total\"] as const;\n\nfunction parseExactDataRecord(value: unknown, fields: readonly string[], label: string): Record<string, unknown> {\n\tif (!isPlainRecord(value)) throw new TypeError(`${label} must be a plain object.`);\n\tfor (const key of Reflect.ownKeys(value)) {\n\t\tif (typeof key !== \"string\" || !fields.includes(key))\n\t\t\tthrow new TypeError(`${label} contains an unsupported field.`);\n\t}\n\tconst parsed: Record<string, unknown> = {};\n\tfor (const field of fields) {\n\t\tconst descriptor = Object.getOwnPropertyDescriptor(value, field);\n\t\tif (!descriptor || !(\"value\" in descriptor)) throw new TypeError(`${label}.${field} must be a data field.`);\n\t\tparsed[field] = descriptor.value;\n\t}\n\treturn parsed;\n}\n\n/** Parse an untrusted provider-neutral usage report into a closed, known-field value. */\nexport function validateProviderUsage(value: unknown, label = \"provider usage\"): Usage {\n\tconst usage = parseExactDataRecord(value, PROVIDER_USAGE_FIELDS, label);\n\tconst cost = parseExactDataRecord(usage.cost, PROVIDER_USAGE_COST_FIELDS, `${label}.cost`);\n\tconst tokenFields: Array<[string, number]> = [\n\t\t[\"input\", usage.input as number],\n\t\t[\"output\", usage.output as number],\n\t\t[\"cacheRead\", usage.cacheRead as number],\n\t\t[\"cacheWrite\", usage.cacheWrite as number],\n\t\t[\"totalTokens\", usage.totalTokens as number],\n\t];\n\tfor (const [field, value] of tokenFields) {\n\t\tif (!Number.isSafeInteger(value) || value < 0) {\n\t\t\tthrow new Error(`${label}.${field} must be a non-negative safe integer.`);\n\t\t}\n\t}\n\tconst costFields: Array<[string, number]> = [\n\t\t[\"input\", cost.input as number],\n\t\t[\"output\", cost.output as number],\n\t\t[\"cacheRead\", cost.cacheRead as number],\n\t\t[\"cacheWrite\", cost.cacheWrite as number],\n\t\t[\"total\", cost.total as number],\n\t];\n\tfor (const [field, value] of costFields) {\n\t\tif (!Number.isFinite(value) || value < 0) {\n\t\t\tthrow new Error(`${label}.cost.${field} must be finite and non-negative.`);\n\t\t}\n\t}\n\treturn {\n\t\tinput: usage.input as number,\n\t\toutput: usage.output as number,\n\t\tcacheRead: usage.cacheRead as number,\n\t\tcacheWrite: usage.cacheWrite as number,\n\t\ttotalTokens: usage.totalTokens as number,\n\t\tcost: {\n\t\t\tinput: cost.input as number,\n\t\t\toutput: cost.output as number,\n\t\t\tcacheRead: cost.cacheRead as number,\n\t\t\tcacheWrite: cost.cacheWrite as number,\n\t\t\ttotal: cost.total as number,\n\t\t},\n\t};\n}\n\n/** Validate the one durable cumulative usage shape shared by checkpoints, recovery, and results. */\nexport function validateAttemptUsageSnapshot(\n\tusage: AttemptUsageSnapshot,\n\tlabel = \"attempt usage\",\n): AttemptUsageSnapshot {\n\tif (!Number.isSafeInteger(usage.toolCalls) || usage.toolCalls < 0) {\n\t\tthrow new Error(`${label}.toolCalls must be a non-negative safe integer.`);\n\t}\n\tif (!Number.isSafeInteger(usage.inputTokens) || usage.inputTokens < 0) {\n\t\tthrow new Error(`${label}.inputTokens must be a non-negative safe integer.`);\n\t}\n\tif (!Number.isSafeInteger(usage.outputTokens) || usage.outputTokens < 0) {\n\t\tthrow new Error(`${label}.outputTokens must be a non-negative safe integer.`);\n\t}\n\tif (!Number.isSafeInteger(usage.cacheReadTokens) || usage.cacheReadTokens < 0) {\n\t\tthrow new Error(`${label}.cacheReadTokens must be a non-negative safe integer.`);\n\t}\n\tif (!Number.isSafeInteger(usage.cacheWriteTokens) || usage.cacheWriteTokens < 0) {\n\t\tthrow new Error(`${label}.cacheWriteTokens must be a non-negative safe integer.`);\n\t}\n\tif (!Number.isSafeInteger(usage.totalTokens) || usage.totalTokens < 0) {\n\t\tthrow new Error(`${label}.totalTokens must be a non-negative safe integer.`);\n\t}\n\tif (!Number.isFinite(usage.costUsd) || usage.costUsd < 0) {\n\t\tthrow new Error(`${label}.costUsd must be finite and non-negative.`);\n\t}\n\tif (!Number.isFinite(usage.activeWallClockMs) || usage.activeWallClockMs < 0) {\n\t\tthrow new Error(`${label}.activeWallClockMs must be finite and non-negative.`);\n\t}\n\treturn structuredClone(usage);\n}\n\nexport function attemptUsageFromGatewayUsage(usage: {\n\ttoolCalls: number;\n\tinputTokens: number;\n\toutputTokens: number;\n\tcacheReadTokens: number;\n\tcacheWriteTokens: number;\n\ttotalTokens: number;\n\tcostUsd: number;\n\twallClockMs: number;\n}): AttemptUsageSnapshot {\n\treturn validateAttemptUsageSnapshot(\n\t\t{\n\t\t\ttoolCalls: usage.toolCalls,\n\t\t\tinputTokens: usage.inputTokens,\n\t\t\toutputTokens: usage.outputTokens,\n\t\t\tcacheReadTokens: usage.cacheReadTokens,\n\t\t\tcacheWriteTokens: usage.cacheWriteTokens,\n\t\t\ttotalTokens: usage.totalTokens,\n\t\t\tcostUsd: usage.costUsd,\n\t\t\tactiveWallClockMs: usage.wallClockMs,\n\t\t},\n\t\t\"gateway usage\",\n\t);\n}\n\nexport function addAttemptUsage(\n\tbase: AttemptUsageSnapshot,\n\tdelta: AttemptUsageSnapshot,\n\tlabel = \"attempt usage\",\n): AttemptUsageSnapshot {\n\tvalidateAttemptUsageSnapshot(base, `${label} base`);\n\tvalidateAttemptUsageSnapshot(delta, `${label} delta`);\n\treturn validateAttemptUsageSnapshot(\n\t\t{\n\t\t\ttoolCalls: base.toolCalls + delta.toolCalls,\n\t\t\tinputTokens: base.inputTokens + delta.inputTokens,\n\t\t\toutputTokens: base.outputTokens + delta.outputTokens,\n\t\t\tcacheReadTokens: base.cacheReadTokens + delta.cacheReadTokens,\n\t\t\tcacheWriteTokens: base.cacheWriteTokens + delta.cacheWriteTokens,\n\t\t\ttotalTokens: base.totalTokens + delta.totalTokens,\n\t\t\tcostUsd: base.costUsd + delta.costUsd,\n\t\t\tactiveWallClockMs: base.activeWallClockMs + delta.activeWallClockMs,\n\t\t},\n\t\tlabel,\n\t);\n}\n\n/**\n * Recover the strongest cumulative evidence across an atomic checkpoint and its durable transcript.\n * Both inputs are cumulative views of the same attempt, so summing would double-count; component\n * maxima close append-before-checkpoint crash windows conservatively.\n */\nexport function reconcileAttemptUsage(\n\tcheckpoint: AttemptUsageSnapshot,\n\ttranscript: AttemptUsageSnapshot,\n): AttemptUsageSnapshot {\n\tvalidateAttemptUsageSnapshot(checkpoint, \"attempt checkpoint usage\");\n\tvalidateAttemptUsageSnapshot(transcript, \"attempt transcript usage\");\n\treturn validateAttemptUsageSnapshot({\n\t\ttoolCalls: Math.max(checkpoint.toolCalls, transcript.toolCalls),\n\t\tinputTokens: Math.max(checkpoint.inputTokens, transcript.inputTokens),\n\t\toutputTokens: Math.max(checkpoint.outputTokens, transcript.outputTokens),\n\t\tcacheReadTokens: Math.max(checkpoint.cacheReadTokens, transcript.cacheReadTokens),\n\t\tcacheWriteTokens: Math.max(checkpoint.cacheWriteTokens, transcript.cacheWriteTokens),\n\t\ttotalTokens: Math.max(checkpoint.totalTokens, transcript.totalTokens),\n\t\tcostUsd: Math.max(checkpoint.costUsd, transcript.costUsd),\n\t\tactiveWallClockMs: Math.max(checkpoint.activeWallClockMs, transcript.activeWallClockMs),\n\t});\n}\n\nexport function remainingTokenBudget(maxTokens: number | undefined, usage: AttemptUsageSnapshot): number | undefined {\n\tif (maxTokens === undefined) return undefined;\n\tconst remaining = maxTokens - usage.totalTokens;\n\treturn Math.max(0, remaining);\n}\n\n/** Convert durable cumulative accounting into the provider-neutral report shape exactly once. */\nexport function providerUsageFromAttemptUsage(usage: AttemptUsageSnapshot): Usage {\n\tconst validated = validateAttemptUsageSnapshot(usage);\n\treturn {\n\t\tinput: validated.inputTokens,\n\t\toutput: validated.outputTokens,\n\t\tcacheRead: validated.cacheReadTokens,\n\t\tcacheWrite: validated.cacheWriteTokens,\n\t\ttotalTokens: validated.totalTokens,\n\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: validated.costUsd },\n\t};\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { ExecutionGrant, ToolCapabilityManifest } from "./contracts.ts";
1
+ import type { AttemptUsageSnapshot, ExecutionGrant, ToolCapabilityManifest } from "./contracts.ts";
2
2
  export type GatewayDecisionCode = "allowed" | "grant_expired" | "tool_not_granted" | "manifest_name_mismatch" | "capability_not_granted" | "path_argument_required" | "path_outside_scope" | "tool_call_budget_exhausted" | "token_budget_exhausted" | "cost_budget_exhausted" | "wall_clock_budget_exhausted";
3
3
  export interface GatewayAuditRecord {
4
4
  grantId: string;
@@ -11,22 +11,32 @@ export interface GatewayAuditRecord {
11
11
  export interface CapabilityGatewayOptions {
12
12
  grant: ExecutionGrant;
13
13
  cwd: string;
14
+ /** Durable active usage carried forward when an attempt resumes in a new process. */
15
+ initialUsage?: GatewayInitialUsage;
14
16
  now?: () => number;
15
17
  onAudit?: (record: GatewayAuditRecord) => void;
16
18
  }
17
19
  export interface GatewayUsageDelta {
18
20
  inputTokens?: number;
19
21
  outputTokens?: number;
22
+ cacheReadTokens?: number;
23
+ cacheWriteTokens?: number;
24
+ /** Provider-authoritative total. Defaults to the supplied detail sum when omitted. */
25
+ totalTokens?: number;
20
26
  costUsd?: number;
21
27
  }
22
28
  export interface GatewayUsageSnapshot {
23
29
  toolCalls: number;
24
30
  inputTokens: number;
25
31
  outputTokens: number;
32
+ cacheReadTokens: number;
33
+ cacheWriteTokens: number;
26
34
  totalTokens: number;
27
35
  costUsd: number;
28
36
  wallClockMs: number;
29
37
  }
38
+ /** Persistable cumulative usage from a prior active segment; excludes restart downtime. */
39
+ export type GatewayInitialUsage = AttemptUsageSnapshot;
30
40
  export declare class CapabilityGatewayDeniedError extends Error {
31
41
  readonly reasonCode: GatewayDecisionCode;
32
42
  constructor(reasonCode: GatewayDecisionCode, message: string);
@@ -42,18 +52,26 @@ export declare class CapabilityGateway {
42
52
  private readonly now;
43
53
  private readonly onAudit?;
44
54
  private readonly startedAt;
55
+ private readonly initialWallClockMs;
45
56
  private toolCalls;
46
57
  private inputTokens;
47
58
  private outputTokens;
59
+ private cacheReadTokens;
60
+ private cacheWriteTokens;
61
+ private totalTokens;
48
62
  private costUsd;
49
63
  constructor(options: CapabilityGatewayOptions);
50
64
  execute<T>(manifest: ToolCapabilityManifest, toolName: string, params: unknown, invoke: () => Promise<T> | T): Promise<T>;
51
65
  /** Immediate pre-execution authorization for runtimes that expose a before-tool-call hook. */
52
66
  authorizeToolCall(manifest: ToolCapabilityManifest, toolName: string, params: unknown): void;
53
67
  recordUsage(delta: GatewayUsageDelta): void;
68
+ /** Enforce resumed cumulative budgets before a provider request that has no tool-call boundary. */
69
+ assertBudgetAvailable(subject?: string): void;
54
70
  getUsage(): GatewayUsageSnapshot;
55
71
  private authorize;
56
72
  private enforceBudget;
73
+ private currentTime;
74
+ private wallClockMsAt;
57
75
  private pathAllowed;
58
76
  private deny;
59
77
  private audit;
@@ -1 +1 @@
1
- {"version":3,"file":"capability-gateway.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/capability-gateway.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAqB,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAEhG,MAAM,MAAM,mBAAmB,GAC5B,SAAS,GACT,eAAe,GACf,kBAAkB,GAClB,wBAAwB,GACxB,wBAAwB,GACxB,wBAAwB,GACxB,oBAAoB,GACpB,4BAA4B,GAC5B,wBAAwB,GACxB,uBAAuB,GACvB,6BAA6B,CAAC;AAEjC,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,EAAE,mBAAmB,CAAC;IAChC,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,wBAAwB;IACxC,KAAK,EAAE,cAAc,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC/C;AAED,MAAM,WAAW,iBAAiB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,4BAA6B,SAAQ,KAAK;IACtD,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IAEzC,YAAY,UAAU,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAI3D;CACD;AA2BD;;;;GAIG;AACH,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,OAAO,CAAK;IAEpB,YAAY,OAAO,EAAE,wBAAwB,EAM5C;IAEK,OAAO,CAAC,CAAC,EACd,QAAQ,EAAE,sBAAsB,EAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAGZ;IAED,8FAA8F;IAC9F,iBAAiB,CAAC,QAAQ,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAI3F;IAED,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAI1C;IAED,QAAQ,IAAI,oBAAoB,CAS/B;IAED,OAAO,CAAC,SAAS;IA+BjB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,KAAK;CAUb","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { extractPathArguments } from \"../autonomy/envelope-enforcement.ts\";\nimport { safeRealpathSync } from \"../autonomy/path-scope.ts\";\nimport type { ExecutionGrant, HarnessCapability, ToolCapabilityManifest } from \"./contracts.ts\";\n\nexport type GatewayDecisionCode =\n\t| \"allowed\"\n\t| \"grant_expired\"\n\t| \"tool_not_granted\"\n\t| \"manifest_name_mismatch\"\n\t| \"capability_not_granted\"\n\t| \"path_argument_required\"\n\t| \"path_outside_scope\"\n\t| \"tool_call_budget_exhausted\"\n\t| \"token_budget_exhausted\"\n\t| \"cost_budget_exhausted\"\n\t| \"wall_clock_budget_exhausted\";\n\nexport interface GatewayAuditRecord {\n\tgrantId: string;\n\tattemptId: string;\n\ttoolName: string;\n\toutcome: \"allow\" | \"deny\";\n\treasonCode: GatewayDecisionCode;\n\tat: string;\n}\n\nexport interface CapabilityGatewayOptions {\n\tgrant: ExecutionGrant;\n\tcwd: string;\n\tnow?: () => number;\n\tonAudit?: (record: GatewayAuditRecord) => void;\n}\n\nexport interface GatewayUsageDelta {\n\tinputTokens?: number;\n\toutputTokens?: number;\n\tcostUsd?: number;\n}\n\nexport interface GatewayUsageSnapshot {\n\ttoolCalls: number;\n\tinputTokens: number;\n\toutputTokens: number;\n\ttotalTokens: number;\n\tcostUsd: number;\n\twallClockMs: number;\n}\n\nexport class CapabilityGatewayDeniedError extends Error {\n\treadonly reasonCode: GatewayDecisionCode;\n\n\tconstructor(reasonCode: GatewayDecisionCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"CapabilityGatewayDeniedError\";\n\t\tthis.reasonCode = reasonCode;\n\t}\n}\n\nconst PATH_CAPABILITIES: ReadonlySet<HarnessCapability> = new Set([\n\t\"filesystem.read\",\n\t\"filesystem.write\",\n\t\"worktree.read\",\n\t\"worktree.mutate\",\n]);\n\nconst WRITE_CAPABILITIES: ReadonlySet<HarnessCapability> = new Set([\"filesystem.write\", \"worktree.mutate\"]);\n\nfunction isWithinRoot(target: string, root: string): boolean {\n\tconst relativePath = relative(root, target);\n\treturn (\n\t\trelativePath === \"\" ||\n\t\t(!relativePath.startsWith(`..${sep}`) && relativePath !== \"..\" && !isAbsolute(relativePath))\n\t);\n}\n\nfunction resolveRealPath(cwd: string, value: string): string | undefined {\n\ttry {\n\t\treturn safeRealpathSync(resolve(cwd, value));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Per-invocation enforcement for an already-compiled grant. The compiler controls what may be\n * loaded; this gateway independently rechecks authority, path scope, expiry, and cumulative budget\n * immediately before execution.\n */\nexport class CapabilityGateway {\n\tprivate readonly grant: ExecutionGrant;\n\tprivate readonly cwd: string;\n\tprivate readonly now: () => number;\n\tprivate readonly onAudit?: (record: GatewayAuditRecord) => void;\n\tprivate readonly startedAt: number;\n\tprivate toolCalls = 0;\n\tprivate inputTokens = 0;\n\tprivate outputTokens = 0;\n\tprivate costUsd = 0;\n\n\tconstructor(options: CapabilityGatewayOptions) {\n\t\tthis.grant = options.grant;\n\t\tthis.cwd = options.cwd;\n\t\tthis.now = options.now ?? Date.now;\n\t\tthis.onAudit = options.onAudit;\n\t\tthis.startedAt = this.now();\n\t}\n\n\tasync execute<T>(\n\t\tmanifest: ToolCapabilityManifest,\n\t\ttoolName: string,\n\t\tparams: unknown,\n\t\tinvoke: () => Promise<T> | T,\n\t): Promise<T> {\n\t\tthis.authorizeToolCall(manifest, toolName, params);\n\t\treturn await invoke();\n\t}\n\n\t/** Immediate pre-execution authorization for runtimes that expose a before-tool-call hook. */\n\tauthorizeToolCall(manifest: ToolCapabilityManifest, toolName: string, params: unknown): void {\n\t\tthis.authorize(manifest, toolName, params);\n\t\tthis.toolCalls++;\n\t\tthis.audit(toolName, \"allow\", \"allowed\");\n\t}\n\n\trecordUsage(delta: GatewayUsageDelta): void {\n\t\tthis.inputTokens += delta.inputTokens ?? 0;\n\t\tthis.outputTokens += delta.outputTokens ?? 0;\n\t\tthis.costUsd += delta.costUsd ?? 0;\n\t}\n\n\tgetUsage(): GatewayUsageSnapshot {\n\t\treturn {\n\t\t\ttoolCalls: this.toolCalls,\n\t\t\tinputTokens: this.inputTokens,\n\t\t\toutputTokens: this.outputTokens,\n\t\t\ttotalTokens: this.inputTokens + this.outputTokens,\n\t\t\tcostUsd: this.costUsd,\n\t\t\twallClockMs: Math.max(0, this.now() - this.startedAt),\n\t\t};\n\t}\n\n\tprivate authorize(manifest: ToolCapabilityManifest, toolName: string, params: unknown): void {\n\t\tif (this.grant.expiresAt && Date.parse(this.grant.expiresAt) <= this.now()) {\n\t\t\tthis.deny(toolName, \"grant_expired\", `Execution grant '${this.grant.grantId}' expired.`);\n\t\t}\n\t\tif (!this.grant.allowedTools.includes(toolName)) {\n\t\t\tthis.deny(toolName, \"tool_not_granted\", `Tool '${toolName}' is not in grant '${this.grant.grantId}'.`);\n\t\t}\n\t\tif (manifest.toolName !== toolName) {\n\t\t\tthis.deny(toolName, \"manifest_name_mismatch\", `Tool '${toolName}' does not match its capability manifest.`);\n\t\t}\n\t\tif (!manifest.capabilities.every((capability) => this.grant.capabilities.includes(capability))) {\n\t\t\tthis.deny(toolName, \"capability_not_granted\", `Tool '${toolName}' requires an ungranted capability.`);\n\t\t}\n\t\tthis.enforceBudget(toolName);\n\n\t\tif (manifest.capabilities.some((capability) => PATH_CAPABILITIES.has(capability))) {\n\t\t\tconst allowedPaths = manifest.capabilities.some((capability) => WRITE_CAPABILITIES.has(capability))\n\t\t\t\t? this.grant.writePaths\n\t\t\t\t: this.grant.readPaths;\n\t\t\tconst paths = extractPathArguments(params);\n\t\t\tif (paths.length === 0) {\n\t\t\t\tthis.deny(toolName, \"path_argument_required\", `Tool '${toolName}' requires an explicit path argument.`);\n\t\t\t}\n\t\t\tfor (const rawPath of paths) {\n\t\t\t\tif (!this.pathAllowed(rawPath, allowedPaths)) {\n\t\t\t\t\tthis.deny(toolName, \"path_outside_scope\", `Path '${rawPath}' is outside grant '${this.grant.grantId}'.`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate enforceBudget(toolName: string): void {\n\t\tconst budget = this.grant.budget;\n\t\tif (budget.maxToolCalls !== undefined && this.toolCalls >= budget.maxToolCalls) {\n\t\t\tthis.deny(toolName, \"tool_call_budget_exhausted\", \"Tool-call budget exhausted.\");\n\t\t}\n\t\tif (budget.maxTokens !== undefined && this.inputTokens + this.outputTokens >= budget.maxTokens) {\n\t\t\tthis.deny(toolName, \"token_budget_exhausted\", \"Token budget exhausted.\");\n\t\t}\n\t\tif (budget.maxCostUsd !== undefined && this.costUsd >= budget.maxCostUsd) {\n\t\t\tthis.deny(toolName, \"cost_budget_exhausted\", \"Cost budget exhausted.\");\n\t\t}\n\t\tif (budget.maxWallClockMs !== undefined && this.now() - this.startedAt >= budget.maxWallClockMs) {\n\t\t\tthis.deny(toolName, \"wall_clock_budget_exhausted\", \"Wall-clock budget exhausted.\");\n\t\t}\n\t}\n\n\tprivate pathAllowed(rawPath: string, allowedPaths: readonly string[]): boolean {\n\t\tconst target = resolveRealPath(this.cwd, rawPath);\n\t\tif (!target || allowedPaths.length === 0) return false;\n\t\tfor (const denied of this.grant.deniedPaths) {\n\t\t\tconst root = resolveRealPath(this.cwd, denied);\n\t\t\tif (root && isWithinRoot(target, root)) return false;\n\t\t}\n\t\treturn allowedPaths.some((allowed) => {\n\t\t\tconst root = resolveRealPath(this.cwd, allowed);\n\t\t\treturn root !== undefined && isWithinRoot(target, root);\n\t\t});\n\t}\n\n\tprivate deny(toolName: string, reasonCode: GatewayDecisionCode, message: string): never {\n\t\tthis.audit(toolName, \"deny\", reasonCode);\n\t\tthrow new CapabilityGatewayDeniedError(reasonCode, message);\n\t}\n\n\tprivate audit(toolName: string, outcome: \"allow\" | \"deny\", reasonCode: GatewayDecisionCode): void {\n\t\tthis.onAudit?.({\n\t\t\tgrantId: this.grant.grantId,\n\t\t\tattemptId: this.grant.attemptId,\n\t\t\ttoolName,\n\t\t\toutcome,\n\t\t\treasonCode,\n\t\t\tat: new Date(this.now()).toISOString(),\n\t\t});\n\t}\n}\n"]}
1
+ {"version":3,"file":"capability-gateway.d.ts","sourceRoot":"","sources":["../../../src/core/orchestration/capability-gateway.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,cAAc,EAAqB,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAEtH,MAAM,MAAM,mBAAmB,GAC5B,SAAS,GACT,eAAe,GACf,kBAAkB,GAClB,wBAAwB,GACxB,wBAAwB,GACxB,wBAAwB,GACxB,oBAAoB,GACpB,4BAA4B,GAC5B,wBAAwB,GACxB,uBAAuB,GACvB,6BAA6B,CAAC;AAEjC,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC;IAC1B,UAAU,EAAE,mBAAmB,CAAC;IAChC,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,wBAAwB;IACxC,KAAK,EAAE,cAAc,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,qFAAqF;IACrF,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAC;CAC/C;AAED,MAAM,WAAW,iBAAiB;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,2FAA2F;AAC3F,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC;AAEvD,qBAAa,4BAA6B,SAAQ,KAAK;IACtD,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IAEzC,YAAY,UAAU,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAI3D;CACD;AA4DD;;;;GAIG;AACH,qBAAa,iBAAiB;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAuC;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,OAAO,CAAS;IAExB,YAAY,OAAO,EAAE,wBAAwB,EAyB5C;IAEK,OAAO,CAAC,CAAC,EACd,QAAQ,EAAE,sBAAsB,EAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAGZ;IAED,8FAA8F;IAC9F,iBAAiB,CAAC,QAAQ,EAAE,sBAAsB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAU3F;IAED,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAwB1C;IAED,mGAAmG;IACnG,qBAAqB,CAAC,OAAO,SAAa,GAAG,IAAI,CAEhD;IAED,QAAQ,IAAI,oBAAoB,CAW/B;IAED,OAAO,CAAC,SAAS;IAqCjB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,aAAa;IAYrB,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,KAAK;CAUb","sourcesContent":["import { resolve } from \"node:path\";\nimport { extractPathArguments } from \"../autonomy/envelope-enforcement.ts\";\nimport { isPathWithinScope, safeRealpathSync } from \"../autonomy/path-scope.ts\";\nimport { validateAttemptUsageSnapshot } from \"./attempt-usage.ts\";\nimport type { AttemptUsageSnapshot, ExecutionGrant, HarnessCapability, ToolCapabilityManifest } from \"./contracts.ts\";\n\nexport type GatewayDecisionCode =\n\t| \"allowed\"\n\t| \"grant_expired\"\n\t| \"tool_not_granted\"\n\t| \"manifest_name_mismatch\"\n\t| \"capability_not_granted\"\n\t| \"path_argument_required\"\n\t| \"path_outside_scope\"\n\t| \"tool_call_budget_exhausted\"\n\t| \"token_budget_exhausted\"\n\t| \"cost_budget_exhausted\"\n\t| \"wall_clock_budget_exhausted\";\n\nexport interface GatewayAuditRecord {\n\tgrantId: string;\n\tattemptId: string;\n\ttoolName: string;\n\toutcome: \"allow\" | \"deny\";\n\treasonCode: GatewayDecisionCode;\n\tat: string;\n}\n\nexport interface CapabilityGatewayOptions {\n\tgrant: ExecutionGrant;\n\tcwd: string;\n\t/** Durable active usage carried forward when an attempt resumes in a new process. */\n\tinitialUsage?: GatewayInitialUsage;\n\tnow?: () => number;\n\tonAudit?: (record: GatewayAuditRecord) => void;\n}\n\nexport interface GatewayUsageDelta {\n\tinputTokens?: number;\n\toutputTokens?: number;\n\tcacheReadTokens?: number;\n\tcacheWriteTokens?: number;\n\t/** Provider-authoritative total. Defaults to the supplied detail sum when omitted. */\n\ttotalTokens?: number;\n\tcostUsd?: number;\n}\n\nexport interface GatewayUsageSnapshot {\n\ttoolCalls: number;\n\tinputTokens: number;\n\toutputTokens: number;\n\tcacheReadTokens: number;\n\tcacheWriteTokens: number;\n\ttotalTokens: number;\n\tcostUsd: number;\n\twallClockMs: number;\n}\n\n/** Persistable cumulative usage from a prior active segment; excludes restart downtime. */\nexport type GatewayInitialUsage = AttemptUsageSnapshot;\n\nexport class CapabilityGatewayDeniedError extends Error {\n\treadonly reasonCode: GatewayDecisionCode;\n\n\tconstructor(reasonCode: GatewayDecisionCode, message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"CapabilityGatewayDeniedError\";\n\t\tthis.reasonCode = reasonCode;\n\t}\n}\n\nconst PATH_CAPABILITIES: ReadonlySet<HarnessCapability> = new Set([\n\t\"filesystem.read\",\n\t\"filesystem.write\",\n\t\"worktree.read\",\n\t\"worktree.mutate\",\n]);\n\nconst WRITE_CAPABILITIES: ReadonlySet<HarnessCapability> = new Set([\"filesystem.write\", \"worktree.mutate\"]);\n\nfunction resolveRealPath(cwd: string, value: string): string | undefined {\n\ttry {\n\t\treturn safeRealpathSync(resolve(cwd, value));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction validateInitialUsage(usage: GatewayInitialUsage): void {\n\ttry {\n\t\tvalidateAttemptUsageSnapshot(usage, \"CapabilityGateway: initial usage\");\n\t} catch {\n\t\tthrow new Error(\n\t\t\t\"CapabilityGateway: initial usage must contain finite non-negative values and safe-integer counts.\",\n\t\t);\n\t}\n}\n\nfunction validatedUsageDelta(delta: GatewayUsageDelta): Required<GatewayUsageDelta> {\n\tconst inputTokens = delta.inputTokens === undefined ? 0 : delta.inputTokens;\n\tconst outputTokens = delta.outputTokens === undefined ? 0 : delta.outputTokens;\n\tconst cacheReadTokens = delta.cacheReadTokens === undefined ? 0 : delta.cacheReadTokens;\n\tconst cacheWriteTokens = delta.cacheWriteTokens === undefined ? 0 : delta.cacheWriteTokens;\n\tconst totalTokens =\n\t\tdelta.totalTokens === undefined\n\t\t\t? inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens\n\t\t\t: delta.totalTokens;\n\tconst costUsd = delta.costUsd === undefined ? 0 : delta.costUsd;\n\tif (\n\t\t!Number.isSafeInteger(inputTokens) ||\n\t\tinputTokens < 0 ||\n\t\t!Number.isSafeInteger(outputTokens) ||\n\t\toutputTokens < 0 ||\n\t\t!Number.isSafeInteger(cacheReadTokens) ||\n\t\tcacheReadTokens < 0 ||\n\t\t!Number.isSafeInteger(cacheWriteTokens) ||\n\t\tcacheWriteTokens < 0 ||\n\t\t!Number.isSafeInteger(totalTokens) ||\n\t\ttotalTokens < 0 ||\n\t\t!Number.isFinite(costUsd) ||\n\t\tcostUsd < 0\n\t) {\n\t\tthrow new Error(\n\t\t\t\"CapabilityGateway: usage delta must contain finite non-negative values and safe-integer token counts.\",\n\t\t);\n\t}\n\treturn { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens, costUsd };\n}\n\n/**\n * Per-invocation enforcement for an already-compiled grant. The compiler controls what may be\n * loaded; this gateway independently rechecks authority, path scope, expiry, and cumulative budget\n * immediately before execution.\n */\nexport class CapabilityGateway {\n\tprivate readonly grant: ExecutionGrant;\n\tprivate readonly cwd: string;\n\tprivate readonly now: () => number;\n\tprivate readonly onAudit?: (record: GatewayAuditRecord) => void;\n\tprivate readonly startedAt: number;\n\tprivate readonly initialWallClockMs: number;\n\tprivate toolCalls: number;\n\tprivate inputTokens: number;\n\tprivate outputTokens: number;\n\tprivate cacheReadTokens: number;\n\tprivate cacheWriteTokens: number;\n\tprivate totalTokens: number;\n\tprivate costUsd: number;\n\n\tconstructor(options: CapabilityGatewayOptions) {\n\t\tconst initialUsage: GatewayInitialUsage = options.initialUsage ?? {\n\t\t\ttoolCalls: 0,\n\t\t\tinputTokens: 0,\n\t\t\toutputTokens: 0,\n\t\t\tcacheReadTokens: 0,\n\t\t\tcacheWriteTokens: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcostUsd: 0,\n\t\t\tactiveWallClockMs: 0,\n\t\t};\n\t\tvalidateInitialUsage(initialUsage);\n\t\tthis.grant = options.grant;\n\t\tthis.cwd = options.cwd;\n\t\tthis.now = options.now ?? Date.now;\n\t\tthis.onAudit = options.onAudit;\n\t\tthis.startedAt = this.currentTime();\n\t\tthis.initialWallClockMs = initialUsage.activeWallClockMs;\n\t\tthis.toolCalls = initialUsage.toolCalls;\n\t\tthis.inputTokens = initialUsage.inputTokens;\n\t\tthis.outputTokens = initialUsage.outputTokens;\n\t\tthis.cacheReadTokens = initialUsage.cacheReadTokens;\n\t\tthis.cacheWriteTokens = initialUsage.cacheWriteTokens;\n\t\tthis.totalTokens = initialUsage.totalTokens;\n\t\tthis.costUsd = initialUsage.costUsd;\n\t}\n\n\tasync execute<T>(\n\t\tmanifest: ToolCapabilityManifest,\n\t\ttoolName: string,\n\t\tparams: unknown,\n\t\tinvoke: () => Promise<T> | T,\n\t): Promise<T> {\n\t\tthis.authorizeToolCall(manifest, toolName, params);\n\t\treturn await invoke();\n\t}\n\n\t/** Immediate pre-execution authorization for runtimes that expose a before-tool-call hook. */\n\tauthorizeToolCall(manifest: ToolCapabilityManifest, toolName: string, params: unknown): void {\n\t\tconst now = this.currentTime();\n\t\tconst wallClockMs = this.wallClockMsAt(now);\n\t\tthis.authorize(manifest, toolName, params, now, wallClockMs);\n\t\tconst toolCalls = this.toolCalls + 1;\n\t\tif (!Number.isSafeInteger(toolCalls)) {\n\t\t\tthrow new Error(\"CapabilityGateway: tool-call count would exceed safe cumulative usage bounds.\");\n\t\t}\n\t\tthis.toolCalls = toolCalls;\n\t\tthis.audit(toolName, \"allow\", \"allowed\");\n\t}\n\n\trecordUsage(delta: GatewayUsageDelta): void {\n\t\tconst validated = validatedUsageDelta(delta);\n\t\tconst inputTokens = this.inputTokens + validated.inputTokens;\n\t\tconst outputTokens = this.outputTokens + validated.outputTokens;\n\t\tconst cacheReadTokens = this.cacheReadTokens + validated.cacheReadTokens;\n\t\tconst cacheWriteTokens = this.cacheWriteTokens + validated.cacheWriteTokens;\n\t\tconst totalTokens = this.totalTokens + validated.totalTokens;\n\t\tconst costUsd = this.costUsd + validated.costUsd;\n\t\tif (\n\t\t\t!Number.isSafeInteger(inputTokens) ||\n\t\t\t!Number.isSafeInteger(outputTokens) ||\n\t\t\t!Number.isSafeInteger(cacheReadTokens) ||\n\t\t\t!Number.isSafeInteger(cacheWriteTokens) ||\n\t\t\t!Number.isSafeInteger(totalTokens) ||\n\t\t\t!Number.isFinite(costUsd)\n\t\t) {\n\t\t\tthrow new Error(\"CapabilityGateway: usage delta would exceed safe cumulative usage bounds.\");\n\t\t}\n\t\tthis.inputTokens = inputTokens;\n\t\tthis.outputTokens = outputTokens;\n\t\tthis.cacheReadTokens = cacheReadTokens;\n\t\tthis.cacheWriteTokens = cacheWriteTokens;\n\t\tthis.totalTokens = totalTokens;\n\t\tthis.costUsd = costUsd;\n\t}\n\n\t/** Enforce resumed cumulative budgets before a provider request that has no tool-call boundary. */\n\tassertBudgetAvailable(subject = \"provider\"): void {\n\t\tthis.enforceBudget(subject, this.wallClockMsAt(this.currentTime()));\n\t}\n\n\tgetUsage(): GatewayUsageSnapshot {\n\t\treturn {\n\t\t\ttoolCalls: this.toolCalls,\n\t\t\tinputTokens: this.inputTokens,\n\t\t\toutputTokens: this.outputTokens,\n\t\t\tcacheReadTokens: this.cacheReadTokens,\n\t\t\tcacheWriteTokens: this.cacheWriteTokens,\n\t\t\ttotalTokens: this.totalTokens,\n\t\t\tcostUsd: this.costUsd,\n\t\t\twallClockMs: this.wallClockMsAt(this.currentTime()),\n\t\t};\n\t}\n\n\tprivate authorize(\n\t\tmanifest: ToolCapabilityManifest,\n\t\ttoolName: string,\n\t\tparams: unknown,\n\t\tnow: number,\n\t\twallClockMs: number,\n\t): void {\n\t\tif (this.grant.expiresAt && Date.parse(this.grant.expiresAt) <= now) {\n\t\t\tthis.deny(toolName, \"grant_expired\", `Execution grant '${this.grant.grantId}' expired.`);\n\t\t}\n\t\tif (!this.grant.allowedTools.includes(toolName)) {\n\t\t\tthis.deny(toolName, \"tool_not_granted\", `Tool '${toolName}' is not in grant '${this.grant.grantId}'.`);\n\t\t}\n\t\tif (manifest.toolName !== toolName) {\n\t\t\tthis.deny(toolName, \"manifest_name_mismatch\", `Tool '${toolName}' does not match its capability manifest.`);\n\t\t}\n\t\tif (!manifest.capabilities.every((capability) => this.grant.capabilities.includes(capability))) {\n\t\t\tthis.deny(toolName, \"capability_not_granted\", `Tool '${toolName}' requires an ungranted capability.`);\n\t\t}\n\t\tthis.enforceBudget(toolName, wallClockMs);\n\n\t\tif (manifest.capabilities.some((capability) => PATH_CAPABILITIES.has(capability))) {\n\t\t\tconst allowedPaths = manifest.capabilities.some((capability) => WRITE_CAPABILITIES.has(capability))\n\t\t\t\t? this.grant.writePaths\n\t\t\t\t: this.grant.readPaths;\n\t\t\tconst paths = extractPathArguments(params);\n\t\t\tif (paths.length === 0) {\n\t\t\t\tthis.deny(toolName, \"path_argument_required\", `Tool '${toolName}' requires an explicit path argument.`);\n\t\t\t}\n\t\t\tfor (const rawPath of paths) {\n\t\t\t\tif (!this.pathAllowed(rawPath, allowedPaths)) {\n\t\t\t\t\tthis.deny(toolName, \"path_outside_scope\", `Path '${rawPath}' is outside grant '${this.grant.grantId}'.`);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate enforceBudget(toolName: string, wallClockMs: number): void {\n\t\tconst budget = this.grant.budget;\n\t\tif (budget.maxToolCalls !== undefined && this.toolCalls >= budget.maxToolCalls) {\n\t\t\tthis.deny(toolName, \"tool_call_budget_exhausted\", \"Tool-call budget exhausted.\");\n\t\t}\n\t\tif (budget.maxTokens !== undefined && this.totalTokens >= budget.maxTokens) {\n\t\t\tthis.deny(toolName, \"token_budget_exhausted\", \"Token budget exhausted.\");\n\t\t}\n\t\tif (budget.maxCostUsd !== undefined && this.costUsd >= budget.maxCostUsd) {\n\t\t\tthis.deny(toolName, \"cost_budget_exhausted\", \"Cost budget exhausted.\");\n\t\t}\n\t\tif (budget.maxWallClockMs !== undefined && wallClockMs >= budget.maxWallClockMs) {\n\t\t\tthis.deny(toolName, \"wall_clock_budget_exhausted\", \"Wall-clock budget exhausted.\");\n\t\t}\n\t}\n\n\tprivate currentTime(): number {\n\t\tconst now = this.now();\n\t\tif (!Number.isFinite(now)) {\n\t\t\tthrow new Error(\"CapabilityGateway: clock source must return a finite time.\");\n\t\t}\n\t\treturn now;\n\t}\n\n\tprivate wallClockMsAt(now: number): number {\n\t\tconst elapsed = now - this.startedAt;\n\t\tif (!Number.isFinite(elapsed)) {\n\t\t\tthrow new Error(\"CapabilityGateway: active wall-clock elapsed time must be finite.\");\n\t\t}\n\t\tconst wallClockMs = this.initialWallClockMs + Math.max(0, elapsed);\n\t\tif (!Number.isFinite(wallClockMs) || wallClockMs < 0) {\n\t\t\tthrow new Error(\"CapabilityGateway: accumulated wall-clock usage must be finite and non-negative.\");\n\t\t}\n\t\treturn wallClockMs;\n\t}\n\n\tprivate pathAllowed(rawPath: string, allowedPaths: readonly string[]): boolean {\n\t\tconst target = resolveRealPath(this.cwd, rawPath);\n\t\tif (!target || allowedPaths.length === 0) return false;\n\t\tfor (const denied of this.grant.deniedPaths) {\n\t\t\tconst root = resolveRealPath(this.cwd, denied);\n\t\t\tif (root && isPathWithinScope(target, root)) return false;\n\t\t}\n\t\treturn allowedPaths.some((allowed) => {\n\t\t\tconst root = resolveRealPath(this.cwd, allowed);\n\t\t\treturn root !== undefined && isPathWithinScope(target, root);\n\t\t});\n\t}\n\n\tprivate deny(toolName: string, reasonCode: GatewayDecisionCode, message: string): never {\n\t\tthis.audit(toolName, \"deny\", reasonCode);\n\t\tthrow new CapabilityGatewayDeniedError(reasonCode, message);\n\t}\n\n\tprivate audit(toolName: string, outcome: \"allow\" | \"deny\", reasonCode: GatewayDecisionCode): void {\n\t\tthis.onAudit?.({\n\t\t\tgrantId: this.grant.grantId,\n\t\t\tattemptId: this.grant.attemptId,\n\t\t\ttoolName,\n\t\t\toutcome,\n\t\t\treasonCode,\n\t\t\tat: new Date(this.currentTime()).toISOString(),\n\t\t});\n\t}\n}\n"]}
@@ -1,6 +1,7 @@
1
- import { isAbsolute, relative, resolve, sep } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { extractPathArguments } from "../autonomy/envelope-enforcement.js";
3
- import { safeRealpathSync } from "../autonomy/path-scope.js";
3
+ import { isPathWithinScope, safeRealpathSync } from "../autonomy/path-scope.js";
4
+ import { validateAttemptUsageSnapshot } from "./attempt-usage.js";
4
5
  export class CapabilityGatewayDeniedError extends Error {
5
6
  reasonCode;
6
7
  constructor(reasonCode, message) {
@@ -16,11 +17,6 @@ const PATH_CAPABILITIES = new Set([
16
17
  "worktree.mutate",
17
18
  ]);
18
19
  const WRITE_CAPABILITIES = new Set(["filesystem.write", "worktree.mutate"]);
19
- function isWithinRoot(target, root) {
20
- const relativePath = relative(root, target);
21
- return (relativePath === "" ||
22
- (!relativePath.startsWith(`..${sep}`) && relativePath !== ".." && !isAbsolute(relativePath)));
23
- }
24
20
  function resolveRealPath(cwd, value) {
25
21
  try {
26
22
  return safeRealpathSync(resolve(cwd, value));
@@ -29,6 +25,39 @@ function resolveRealPath(cwd, value) {
29
25
  return undefined;
30
26
  }
31
27
  }
28
+ function validateInitialUsage(usage) {
29
+ try {
30
+ validateAttemptUsageSnapshot(usage, "CapabilityGateway: initial usage");
31
+ }
32
+ catch {
33
+ throw new Error("CapabilityGateway: initial usage must contain finite non-negative values and safe-integer counts.");
34
+ }
35
+ }
36
+ function validatedUsageDelta(delta) {
37
+ const inputTokens = delta.inputTokens === undefined ? 0 : delta.inputTokens;
38
+ const outputTokens = delta.outputTokens === undefined ? 0 : delta.outputTokens;
39
+ const cacheReadTokens = delta.cacheReadTokens === undefined ? 0 : delta.cacheReadTokens;
40
+ const cacheWriteTokens = delta.cacheWriteTokens === undefined ? 0 : delta.cacheWriteTokens;
41
+ const totalTokens = delta.totalTokens === undefined
42
+ ? inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens
43
+ : delta.totalTokens;
44
+ const costUsd = delta.costUsd === undefined ? 0 : delta.costUsd;
45
+ if (!Number.isSafeInteger(inputTokens) ||
46
+ inputTokens < 0 ||
47
+ !Number.isSafeInteger(outputTokens) ||
48
+ outputTokens < 0 ||
49
+ !Number.isSafeInteger(cacheReadTokens) ||
50
+ cacheReadTokens < 0 ||
51
+ !Number.isSafeInteger(cacheWriteTokens) ||
52
+ cacheWriteTokens < 0 ||
53
+ !Number.isSafeInteger(totalTokens) ||
54
+ totalTokens < 0 ||
55
+ !Number.isFinite(costUsd) ||
56
+ costUsd < 0) {
57
+ throw new Error("CapabilityGateway: usage delta must contain finite non-negative values and safe-integer token counts.");
58
+ }
59
+ return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens, costUsd };
60
+ }
32
61
  /**
33
62
  * Per-invocation enforcement for an already-compiled grant. The compiler controls what may be
34
63
  * loaded; this gateway independently rechecks authority, path scope, expiry, and cumulative budget
@@ -40,16 +69,39 @@ export class CapabilityGateway {
40
69
  now;
41
70
  onAudit;
42
71
  startedAt;
43
- toolCalls = 0;
44
- inputTokens = 0;
45
- outputTokens = 0;
46
- costUsd = 0;
72
+ initialWallClockMs;
73
+ toolCalls;
74
+ inputTokens;
75
+ outputTokens;
76
+ cacheReadTokens;
77
+ cacheWriteTokens;
78
+ totalTokens;
79
+ costUsd;
47
80
  constructor(options) {
81
+ const initialUsage = options.initialUsage ?? {
82
+ toolCalls: 0,
83
+ inputTokens: 0,
84
+ outputTokens: 0,
85
+ cacheReadTokens: 0,
86
+ cacheWriteTokens: 0,
87
+ totalTokens: 0,
88
+ costUsd: 0,
89
+ activeWallClockMs: 0,
90
+ };
91
+ validateInitialUsage(initialUsage);
48
92
  this.grant = options.grant;
49
93
  this.cwd = options.cwd;
50
94
  this.now = options.now ?? Date.now;
51
95
  this.onAudit = options.onAudit;
52
- this.startedAt = this.now();
96
+ this.startedAt = this.currentTime();
97
+ this.initialWallClockMs = initialUsage.activeWallClockMs;
98
+ this.toolCalls = initialUsage.toolCalls;
99
+ this.inputTokens = initialUsage.inputTokens;
100
+ this.outputTokens = initialUsage.outputTokens;
101
+ this.cacheReadTokens = initialUsage.cacheReadTokens;
102
+ this.cacheWriteTokens = initialUsage.cacheWriteTokens;
103
+ this.totalTokens = initialUsage.totalTokens;
104
+ this.costUsd = initialUsage.costUsd;
53
105
  }
54
106
  async execute(manifest, toolName, params, invoke) {
55
107
  this.authorizeToolCall(manifest, toolName, params);
@@ -57,27 +109,57 @@ export class CapabilityGateway {
57
109
  }
58
110
  /** Immediate pre-execution authorization for runtimes that expose a before-tool-call hook. */
59
111
  authorizeToolCall(manifest, toolName, params) {
60
- this.authorize(manifest, toolName, params);
61
- this.toolCalls++;
112
+ const now = this.currentTime();
113
+ const wallClockMs = this.wallClockMsAt(now);
114
+ this.authorize(manifest, toolName, params, now, wallClockMs);
115
+ const toolCalls = this.toolCalls + 1;
116
+ if (!Number.isSafeInteger(toolCalls)) {
117
+ throw new Error("CapabilityGateway: tool-call count would exceed safe cumulative usage bounds.");
118
+ }
119
+ this.toolCalls = toolCalls;
62
120
  this.audit(toolName, "allow", "allowed");
63
121
  }
64
122
  recordUsage(delta) {
65
- this.inputTokens += delta.inputTokens ?? 0;
66
- this.outputTokens += delta.outputTokens ?? 0;
67
- this.costUsd += delta.costUsd ?? 0;
123
+ const validated = validatedUsageDelta(delta);
124
+ const inputTokens = this.inputTokens + validated.inputTokens;
125
+ const outputTokens = this.outputTokens + validated.outputTokens;
126
+ const cacheReadTokens = this.cacheReadTokens + validated.cacheReadTokens;
127
+ const cacheWriteTokens = this.cacheWriteTokens + validated.cacheWriteTokens;
128
+ const totalTokens = this.totalTokens + validated.totalTokens;
129
+ const costUsd = this.costUsd + validated.costUsd;
130
+ if (!Number.isSafeInteger(inputTokens) ||
131
+ !Number.isSafeInteger(outputTokens) ||
132
+ !Number.isSafeInteger(cacheReadTokens) ||
133
+ !Number.isSafeInteger(cacheWriteTokens) ||
134
+ !Number.isSafeInteger(totalTokens) ||
135
+ !Number.isFinite(costUsd)) {
136
+ throw new Error("CapabilityGateway: usage delta would exceed safe cumulative usage bounds.");
137
+ }
138
+ this.inputTokens = inputTokens;
139
+ this.outputTokens = outputTokens;
140
+ this.cacheReadTokens = cacheReadTokens;
141
+ this.cacheWriteTokens = cacheWriteTokens;
142
+ this.totalTokens = totalTokens;
143
+ this.costUsd = costUsd;
144
+ }
145
+ /** Enforce resumed cumulative budgets before a provider request that has no tool-call boundary. */
146
+ assertBudgetAvailable(subject = "provider") {
147
+ this.enforceBudget(subject, this.wallClockMsAt(this.currentTime()));
68
148
  }
69
149
  getUsage() {
70
150
  return {
71
151
  toolCalls: this.toolCalls,
72
152
  inputTokens: this.inputTokens,
73
153
  outputTokens: this.outputTokens,
74
- totalTokens: this.inputTokens + this.outputTokens,
154
+ cacheReadTokens: this.cacheReadTokens,
155
+ cacheWriteTokens: this.cacheWriteTokens,
156
+ totalTokens: this.totalTokens,
75
157
  costUsd: this.costUsd,
76
- wallClockMs: Math.max(0, this.now() - this.startedAt),
158
+ wallClockMs: this.wallClockMsAt(this.currentTime()),
77
159
  };
78
160
  }
79
- authorize(manifest, toolName, params) {
80
- if (this.grant.expiresAt && Date.parse(this.grant.expiresAt) <= this.now()) {
161
+ authorize(manifest, toolName, params, now, wallClockMs) {
162
+ if (this.grant.expiresAt && Date.parse(this.grant.expiresAt) <= now) {
81
163
  this.deny(toolName, "grant_expired", `Execution grant '${this.grant.grantId}' expired.`);
82
164
  }
83
165
  if (!this.grant.allowedTools.includes(toolName)) {
@@ -89,7 +171,7 @@ export class CapabilityGateway {
89
171
  if (!manifest.capabilities.every((capability) => this.grant.capabilities.includes(capability))) {
90
172
  this.deny(toolName, "capability_not_granted", `Tool '${toolName}' requires an ungranted capability.`);
91
173
  }
92
- this.enforceBudget(toolName);
174
+ this.enforceBudget(toolName, wallClockMs);
93
175
  if (manifest.capabilities.some((capability) => PATH_CAPABILITIES.has(capability))) {
94
176
  const allowedPaths = manifest.capabilities.some((capability) => WRITE_CAPABILITIES.has(capability))
95
177
  ? this.grant.writePaths
@@ -105,33 +187,51 @@ export class CapabilityGateway {
105
187
  }
106
188
  }
107
189
  }
108
- enforceBudget(toolName) {
190
+ enforceBudget(toolName, wallClockMs) {
109
191
  const budget = this.grant.budget;
110
192
  if (budget.maxToolCalls !== undefined && this.toolCalls >= budget.maxToolCalls) {
111
193
  this.deny(toolName, "tool_call_budget_exhausted", "Tool-call budget exhausted.");
112
194
  }
113
- if (budget.maxTokens !== undefined && this.inputTokens + this.outputTokens >= budget.maxTokens) {
195
+ if (budget.maxTokens !== undefined && this.totalTokens >= budget.maxTokens) {
114
196
  this.deny(toolName, "token_budget_exhausted", "Token budget exhausted.");
115
197
  }
116
198
  if (budget.maxCostUsd !== undefined && this.costUsd >= budget.maxCostUsd) {
117
199
  this.deny(toolName, "cost_budget_exhausted", "Cost budget exhausted.");
118
200
  }
119
- if (budget.maxWallClockMs !== undefined && this.now() - this.startedAt >= budget.maxWallClockMs) {
201
+ if (budget.maxWallClockMs !== undefined && wallClockMs >= budget.maxWallClockMs) {
120
202
  this.deny(toolName, "wall_clock_budget_exhausted", "Wall-clock budget exhausted.");
121
203
  }
122
204
  }
205
+ currentTime() {
206
+ const now = this.now();
207
+ if (!Number.isFinite(now)) {
208
+ throw new Error("CapabilityGateway: clock source must return a finite time.");
209
+ }
210
+ return now;
211
+ }
212
+ wallClockMsAt(now) {
213
+ const elapsed = now - this.startedAt;
214
+ if (!Number.isFinite(elapsed)) {
215
+ throw new Error("CapabilityGateway: active wall-clock elapsed time must be finite.");
216
+ }
217
+ const wallClockMs = this.initialWallClockMs + Math.max(0, elapsed);
218
+ if (!Number.isFinite(wallClockMs) || wallClockMs < 0) {
219
+ throw new Error("CapabilityGateway: accumulated wall-clock usage must be finite and non-negative.");
220
+ }
221
+ return wallClockMs;
222
+ }
123
223
  pathAllowed(rawPath, allowedPaths) {
124
224
  const target = resolveRealPath(this.cwd, rawPath);
125
225
  if (!target || allowedPaths.length === 0)
126
226
  return false;
127
227
  for (const denied of this.grant.deniedPaths) {
128
228
  const root = resolveRealPath(this.cwd, denied);
129
- if (root && isWithinRoot(target, root))
229
+ if (root && isPathWithinScope(target, root))
130
230
  return false;
131
231
  }
132
232
  return allowedPaths.some((allowed) => {
133
233
  const root = resolveRealPath(this.cwd, allowed);
134
- return root !== undefined && isWithinRoot(target, root);
234
+ return root !== undefined && isPathWithinScope(target, root);
135
235
  });
136
236
  }
137
237
  deny(toolName, reasonCode, message) {
@@ -145,7 +245,7 @@ export class CapabilityGateway {
145
245
  toolName,
146
246
  outcome,
147
247
  reasonCode,
148
- at: new Date(this.now()).toISOString(),
248
+ at: new Date(this.currentTime()).toISOString(),
149
249
  });
150
250
  }
151
251
  }