@namzu/sdk 44.3.0 → 45.1.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 (271) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/authorization/command-line.d.ts +66 -19
  3. package/dist/authorization/command-line.d.ts.map +1 -1
  4. package/dist/authorization/command-line.js +130 -270
  5. package/dist/authorization/command-line.js.map +1 -1
  6. package/dist/authorization/gate.d.ts +7 -0
  7. package/dist/authorization/gate.d.ts.map +1 -1
  8. package/dist/authorization/gate.js +13 -3
  9. package/dist/authorization/gate.js.map +1 -1
  10. package/dist/authorization/rules.d.ts +10 -1
  11. package/dist/authorization/rules.d.ts.map +1 -1
  12. package/dist/authorization/rules.js +55 -8
  13. package/dist/authorization/rules.js.map +1 -1
  14. package/dist/authorization/shell-lexer.d.ts +152 -0
  15. package/dist/authorization/shell-lexer.d.ts.map +1 -0
  16. package/dist/authorization/shell-lexer.js +2156 -0
  17. package/dist/authorization/shell-lexer.js.map +1 -0
  18. package/dist/authorization/skill-grant.d.ts +182 -0
  19. package/dist/authorization/skill-grant.d.ts.map +1 -0
  20. package/dist/authorization/skill-grant.js +314 -0
  21. package/dist/authorization/skill-grant.js.map +1 -0
  22. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  23. package/dist/bridge/a2a/mapper.js +2 -0
  24. package/dist/bridge/a2a/mapper.js.map +1 -1
  25. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  26. package/dist/bridge/sse/mapper.js +1 -0
  27. package/dist/bridge/sse/mapper.js.map +1 -1
  28. package/dist/directory/types.d.ts +2 -0
  29. package/dist/directory/types.d.ts.map +1 -1
  30. package/dist/directory/types.js.map +1 -1
  31. package/dist/manager/resident/outbox.d.ts +4 -4
  32. package/dist/persona/assembler.d.ts.map +1 -1
  33. package/dist/persona/assembler.js +5 -2
  34. package/dist/persona/assembler.js.map +1 -1
  35. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  36. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  37. package/dist/prompt/coding-agent-doctrine.js +1 -0
  38. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  39. package/dist/public-runtime.d.ts +5 -1
  40. package/dist/public-runtime.d.ts.map +1 -1
  41. package/dist/public-runtime.js +15 -1
  42. package/dist/public-runtime.js.map +1 -1
  43. package/dist/public-tools.d.ts +4 -0
  44. package/dist/public-tools.d.ts.map +1 -1
  45. package/dist/public-tools.js +12 -1
  46. package/dist/public-tools.js.map +1 -1
  47. package/dist/public-types.d.ts +9 -1
  48. package/dist/public-types.d.ts.map +1 -1
  49. package/dist/runtime/jobs/registry.d.ts +2 -2
  50. package/dist/runtime/jobs/registry.d.ts.map +1 -1
  51. package/dist/runtime/jobs/registry.js +6 -2
  52. package/dist/runtime/jobs/registry.js.map +1 -1
  53. package/dist/runtime/query/declined.d.ts +12 -0
  54. package/dist/runtime/query/declined.d.ts.map +1 -0
  55. package/dist/runtime/query/declined.js +12 -0
  56. package/dist/runtime/query/declined.js.map +1 -0
  57. package/dist/runtime/query/executor.d.ts +36 -40
  58. package/dist/runtime/query/executor.d.ts.map +1 -1
  59. package/dist/runtime/query/executor.js +95 -53
  60. package/dist/runtime/query/executor.js.map +1 -1
  61. package/dist/runtime/query/index.d.ts.map +1 -1
  62. package/dist/runtime/query/index.js +8 -0
  63. package/dist/runtime/query/index.js.map +1 -1
  64. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  65. package/dist/runtime/query/iteration/index.js +13 -0
  66. package/dist/runtime/query/iteration/index.js.map +1 -1
  67. package/dist/runtime/query/iteration/phases/context.d.ts +13 -0
  68. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  69. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  70. package/dist/runtime/query/iteration/phases/handoff.d.ts +22 -0
  71. package/dist/runtime/query/iteration/phases/handoff.d.ts.map +1 -0
  72. package/dist/runtime/query/iteration/phases/handoff.js +65 -0
  73. package/dist/runtime/query/iteration/phases/handoff.js.map +1 -0
  74. package/dist/runtime/query/iteration/phases/index.d.ts +1 -0
  75. package/dist/runtime/query/iteration/phases/index.d.ts.map +1 -1
  76. package/dist/runtime/query/iteration/phases/index.js +1 -0
  77. package/dist/runtime/query/iteration/phases/index.js.map +1 -1
  78. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  79. package/dist/runtime/query/iteration/phases/tool-review.js +56 -3
  80. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  81. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  82. package/dist/runtime/query/resume-pending.js +3 -2
  83. package/dist/runtime/query/resume-pending.js.map +1 -1
  84. package/dist/runtime/query/review-policy.d.ts +11 -0
  85. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  86. package/dist/runtime/query/review-policy.js +36 -3
  87. package/dist/runtime/query/review-policy.js.map +1 -1
  88. package/dist/runtime/query/tooling.d.ts +3 -0
  89. package/dist/runtime/query/tooling.d.ts.map +1 -1
  90. package/dist/runtime/query/tooling.js +1 -0
  91. package/dist/runtime/query/tooling.js.map +1 -1
  92. package/dist/schedules/cron.d.ts +21 -0
  93. package/dist/schedules/cron.d.ts.map +1 -0
  94. package/dist/schedules/cron.js +167 -0
  95. package/dist/schedules/cron.js.map +1 -0
  96. package/dist/schedules/describe.d.ts +14 -0
  97. package/dist/schedules/describe.d.ts.map +1 -0
  98. package/dist/schedules/describe.js +133 -0
  99. package/dist/schedules/describe.js.map +1 -0
  100. package/dist/schedules/errors.d.ts +11 -0
  101. package/dist/schedules/errors.d.ts.map +1 -0
  102. package/dist/schedules/errors.js +15 -0
  103. package/dist/schedules/errors.js.map +1 -0
  104. package/dist/schedules/evaluate.d.ts +35 -0
  105. package/dist/schedules/evaluate.d.ts.map +1 -0
  106. package/dist/schedules/evaluate.js +158 -0
  107. package/dist/schedules/evaluate.js.map +1 -0
  108. package/dist/schedules/index.d.ts +11 -0
  109. package/dist/schedules/index.d.ts.map +1 -0
  110. package/dist/schedules/index.js +8 -0
  111. package/dist/schedules/index.js.map +1 -0
  112. package/dist/schedules/next-fire.d.ts +50 -0
  113. package/dist/schedules/next-fire.d.ts.map +1 -0
  114. package/dist/schedules/next-fire.js +250 -0
  115. package/dist/schedules/next-fire.js.map +1 -0
  116. package/dist/schedules/spec.d.ts +30 -0
  117. package/dist/schedules/spec.d.ts.map +1 -0
  118. package/dist/schedules/spec.js +169 -0
  119. package/dist/schedules/spec.js.map +1 -0
  120. package/dist/schedules/types.d.ts +144 -0
  121. package/dist/schedules/types.d.ts.map +1 -0
  122. package/dist/schedules/types.js +11 -0
  123. package/dist/schedules/types.js.map +1 -0
  124. package/dist/schedules/tz.d.ts +44 -0
  125. package/dist/schedules/tz.d.ts.map +1 -0
  126. package/dist/schedules/tz.js +141 -0
  127. package/dist/schedules/tz.js.map +1 -0
  128. package/dist/skills/index.d.ts +1 -1
  129. package/dist/skills/index.d.ts.map +1 -1
  130. package/dist/skills/index.js +1 -1
  131. package/dist/skills/index.js.map +1 -1
  132. package/dist/skills/loader.d.ts +21 -0
  133. package/dist/skills/loader.d.ts.map +1 -1
  134. package/dist/skills/loader.js +51 -1
  135. package/dist/skills/loader.js.map +1 -1
  136. package/dist/tools/builtins/bash.d.ts.map +1 -1
  137. package/dist/tools/builtins/bash.js +18 -6
  138. package/dist/tools/builtins/bash.js.map +1 -1
  139. package/dist/tools/builtins/browser-url.d.ts +83 -0
  140. package/dist/tools/builtins/browser-url.d.ts.map +1 -0
  141. package/dist/tools/builtins/browser-url.js +240 -0
  142. package/dist/tools/builtins/browser-url.js.map +1 -0
  143. package/dist/tools/builtins/browser.d.ts +367 -0
  144. package/dist/tools/builtins/browser.d.ts.map +1 -0
  145. package/dist/tools/builtins/browser.js +704 -0
  146. package/dist/tools/builtins/browser.js.map +1 -0
  147. package/dist/tools/builtins/skill.d.ts +2 -9
  148. package/dist/tools/builtins/skill.d.ts.map +1 -1
  149. package/dist/tools/builtins/skill.js +74 -51
  150. package/dist/tools/builtins/skill.js.map +1 -1
  151. package/dist/tools/command-shell.d.ts +90 -0
  152. package/dist/tools/command-shell.d.ts.map +1 -0
  153. package/dist/tools/command-shell.js +129 -0
  154. package/dist/tools/command-shell.js.map +1 -0
  155. package/dist/tools/defineTool.d.ts +13 -0
  156. package/dist/tools/defineTool.d.ts.map +1 -1
  157. package/dist/tools/defineTool.js +30 -1
  158. package/dist/tools/defineTool.js.map +1 -1
  159. package/dist/tools/schedules/index.d.ts +5 -0
  160. package/dist/tools/schedules/index.d.ts.map +1 -0
  161. package/dist/tools/schedules/index.js +4 -0
  162. package/dist/tools/schedules/index.js.map +1 -0
  163. package/dist/tools/schedules/loop-tool.d.ts +14 -0
  164. package/dist/tools/schedules/loop-tool.d.ts.map +1 -0
  165. package/dist/tools/schedules/loop-tool.js +81 -0
  166. package/dist/tools/schedules/loop-tool.js.map +1 -0
  167. package/dist/tools/schedules/present.d.ts +16 -0
  168. package/dist/tools/schedules/present.d.ts.map +1 -0
  169. package/dist/tools/schedules/present.js +69 -0
  170. package/dist/tools/schedules/present.js.map +1 -0
  171. package/dist/tools/schedules/prompt-scan.d.ts +17 -0
  172. package/dist/tools/schedules/prompt-scan.d.ts.map +1 -0
  173. package/dist/tools/schedules/prompt-scan.js +92 -0
  174. package/dist/tools/schedules/prompt-scan.js.map +1 -0
  175. package/dist/tools/schedules/schedule-tool.d.ts +16 -0
  176. package/dist/tools/schedules/schedule-tool.d.ts.map +1 -0
  177. package/dist/tools/schedules/schedule-tool.js +327 -0
  178. package/dist/tools/schedules/schedule-tool.js.map +1 -0
  179. package/dist/tools/schedules/types.d.ts +184 -0
  180. package/dist/tools/schedules/types.d.ts.map +1 -0
  181. package/dist/tools/schedules/types.js +11 -0
  182. package/dist/tools/schedules/types.js.map +1 -0
  183. package/dist/types/authorization/index.d.ts +86 -9
  184. package/dist/types/authorization/index.d.ts.map +1 -1
  185. package/dist/types/authorization/index.js +11 -1
  186. package/dist/types/authorization/index.js.map +1 -1
  187. package/dist/types/browser/index.d.ts +280 -0
  188. package/dist/types/browser/index.d.ts.map +1 -0
  189. package/dist/types/browser/index.js +12 -0
  190. package/dist/types/browser/index.js.map +1 -0
  191. package/dist/types/hitl/index.d.ts +23 -0
  192. package/dist/types/hitl/index.d.ts.map +1 -1
  193. package/dist/types/hitl/index.js.map +1 -1
  194. package/dist/types/session/events.d.ts +6 -0
  195. package/dist/types/session/events.d.ts.map +1 -1
  196. package/dist/types/session/events.js.map +1 -1
  197. package/dist/types/session/records.d.ts +23 -0
  198. package/dist/types/session/records.d.ts.map +1 -1
  199. package/dist/types/session/records.js +9 -0
  200. package/dist/types/session/records.js.map +1 -1
  201. package/dist/types/tool/index.d.ts +108 -5
  202. package/dist/types/tool/index.d.ts.map +1 -1
  203. package/dist/types/tool/index.js.map +1 -1
  204. package/dist/types/tool/presentation.d.ts +7 -0
  205. package/dist/types/tool/presentation.d.ts.map +1 -1
  206. package/dist/utils/frontmatter.d.ts +35 -3
  207. package/dist/utils/frontmatter.d.ts.map +1 -1
  208. package/dist/utils/frontmatter.js +45 -5
  209. package/dist/utils/frontmatter.js.map +1 -1
  210. package/dist/utils/id.d.ts +8 -0
  211. package/dist/utils/id.d.ts.map +1 -1
  212. package/dist/utils/id.js +12 -0
  213. package/dist/utils/id.js.map +1 -1
  214. package/package.json +1 -1
  215. package/src/authorization/command-line.ts +148 -293
  216. package/src/authorization/gate.ts +22 -2
  217. package/src/authorization/rules.ts +67 -8
  218. package/src/authorization/shell-lexer.ts +2349 -0
  219. package/src/authorization/skill-grant.ts +400 -0
  220. package/src/bridge/a2a/mapper.ts +2 -0
  221. package/src/bridge/sse/mapper.ts +1 -0
  222. package/src/directory/types.ts +2 -0
  223. package/src/persona/assembler.ts +5 -2
  224. package/src/prompt/coding-agent-doctrine.ts +1 -0
  225. package/src/public-runtime.ts +37 -0
  226. package/src/public-tools.ts +37 -1
  227. package/src/public-types.ts +57 -0
  228. package/src/runtime/jobs/registry.ts +19 -11
  229. package/src/runtime/query/declined.ts +12 -0
  230. package/src/runtime/query/executor.ts +116 -56
  231. package/src/runtime/query/index.ts +8 -0
  232. package/src/runtime/query/iteration/index.ts +13 -0
  233. package/src/runtime/query/iteration/phases/context.ts +13 -0
  234. package/src/runtime/query/iteration/phases/handoff.ts +74 -0
  235. package/src/runtime/query/iteration/phases/index.ts +1 -0
  236. package/src/runtime/query/iteration/phases/tool-review.ts +55 -3
  237. package/src/runtime/query/resume-pending.ts +3 -2
  238. package/src/runtime/query/review-policy.ts +57 -3
  239. package/src/runtime/query/tooling.ts +4 -0
  240. package/src/schedules/cron.ts +202 -0
  241. package/src/schedules/describe.ts +138 -0
  242. package/src/schedules/errors.ts +15 -0
  243. package/src/schedules/evaluate.ts +178 -0
  244. package/src/schedules/index.ts +18 -0
  245. package/src/schedules/next-fire.ts +257 -0
  246. package/src/schedules/spec.ts +210 -0
  247. package/src/schedules/types.ts +163 -0
  248. package/src/schedules/tz.ts +155 -0
  249. package/src/skills/index.ts +1 -1
  250. package/src/skills/loader.ts +59 -1
  251. package/src/tools/builtins/bash.ts +24 -6
  252. package/src/tools/builtins/browser-url.ts +251 -0
  253. package/src/tools/builtins/browser.ts +817 -0
  254. package/src/tools/builtins/skill.ts +92 -53
  255. package/src/tools/command-shell.ts +166 -0
  256. package/src/tools/defineTool.ts +36 -1
  257. package/src/tools/schedules/index.ts +4 -0
  258. package/src/tools/schedules/loop-tool.ts +85 -0
  259. package/src/tools/schedules/present.ts +86 -0
  260. package/src/tools/schedules/prompt-scan.ts +96 -0
  261. package/src/tools/schedules/schedule-tool.ts +376 -0
  262. package/src/tools/schedules/types.ts +197 -0
  263. package/src/types/authorization/index.ts +60 -2
  264. package/src/types/browser/index.ts +341 -0
  265. package/src/types/hitl/index.ts +21 -0
  266. package/src/types/session/events.ts +6 -0
  267. package/src/types/session/records.ts +10 -0
  268. package/src/types/tool/index.ts +109 -5
  269. package/src/types/tool/presentation.ts +7 -0
  270. package/src/utils/frontmatter.ts +81 -5
  271. package/src/utils/id.ts +14 -0
@@ -14,6 +14,7 @@ import type { ChatCompletionResponse } from '../../types/provider/index.js'
14
14
  import type { ToolExecutionSnapshot } from '../../types/session/tool-execution.js'
15
15
  import type { Logger } from '../../utils/logger.js'
16
16
  import type { RestoredCheckpoint } from './checkpoint.js'
17
+ import { DECLINED_TOOL_CALL_FEEDBACK } from './declined.js'
17
18
  import type { PriorToolResults, ToolCallDenials, ToolExecutor } from './executor.js'
18
19
  import { PendingAnswers } from './question-park.js'
19
20
  import { readToolExecutions } from './tool-executions.js'
@@ -630,7 +631,7 @@ function derriveDenials(
630
631
  return new Map()
631
632
 
632
633
  case 'reject_tools': {
633
- const reason = decision.feedback || 'The user rejected this tool call.'
634
+ const reason = decision.feedback || DECLINED_TOOL_CALL_FEEDBACK
634
635
  return new Map(toolCalls.map((tc) => [tc.id, reason]))
635
636
  }
636
637
 
@@ -642,7 +643,7 @@ function derriveDenials(
642
643
  const denials = new Map<string, string>()
643
644
  for (const mod of decision.modifications) {
644
645
  if (mod.action === 'deny') {
645
- denials.set(mod.toolCallId, 'The user denied this tool call.')
646
+ denials.set(mod.toolCallId, DECLINED_TOOL_CALL_FEEDBACK)
646
647
  }
647
648
  }
648
649
  for (const mod of decision.modifications) {
@@ -27,6 +27,7 @@ import type { HITLResumeDecision, ResumeHandler, ToolCallSummary } from '../../t
27
27
  import type { ApprovalPolicy } from '../../types/hitl/policy.js'
28
28
  import type { SessionId, TurnId } from '../../types/ids/index.js'
29
29
  import { PLAN_MODE_REFUSAL } from '../../types/permission/index.js'
30
+ import { DECLINED_TOOL_CALL_FEEDBACK } from './declined.js'
30
31
 
31
32
  export type ReviewMode =
32
33
  /** Ask a person. The default when a `prompt` is supplied. */
@@ -238,6 +239,33 @@ export interface ReviewPolicyOptions {
238
239
  * in every mode that does not refuse the call outright.
239
240
  */
240
241
  readonly unattendedSandboxEscape?: 'refuse' | 'allow'
242
+ /**
243
+ * Whether a skill's `allowed-tools` pre-approval stands in for a person.
244
+ *
245
+ * `'honour'` (the default) approves a batch without asking when every call
246
+ * that would be asked about carries `ToolCallSummary.skillGrant`. `'ignore'`
247
+ * asks as though no skill had been loaded — the behaviour before
248
+ * `allowed-tools` was read as a pre-approval, for a host that does not want
249
+ * repository content to reduce its prompts. `plan` and `strict` refuse
250
+ * either way.
251
+ */
252
+ readonly skillGrants?: 'honour' | 'ignore'
253
+ }
254
+
255
+ /**
256
+ * Whether a skill's `allowed-tools` grant may stand in for a person on this
257
+ * call: the kernel marked it, and nothing that outranks a skill is present.
258
+ * The second half repeats what the kernel checked before marking, so a mark
259
+ * on a persisted or host-built summary cannot carry more than it should.
260
+ */
261
+ function isSkillGranted(tc: ToolCallSummary): boolean {
262
+ return (
263
+ tc.skillGrant !== undefined &&
264
+ !tc.authorization?.explicitReview &&
265
+ tc.authorization?.decision !== 'deny' &&
266
+ !tc.isDestructive &&
267
+ tc.escalation === undefined
268
+ )
241
269
  }
242
270
 
243
271
  /** The handler behind `createReviewPolicy`, for a host that wants only the function. */
@@ -306,7 +334,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
306
334
  if (answer.kind === 'reject') {
307
335
  return {
308
336
  action: 'reject_tools',
309
- feedback: answer.feedback ?? 'User declined to run the proposed tool(s).',
337
+ feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
310
338
  }
311
339
  }
312
340
  // "Approve all" still latches for the calls that follow; it never
@@ -328,7 +356,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
328
356
  if (answer.kind === 'reject') {
329
357
  return {
330
358
  action: 'reject_tools',
331
- feedback: answer.feedback ?? 'User declined to run the proposed tool(s).',
359
+ feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
332
360
  }
333
361
  }
334
362
  // Latches for the ordinary calls that follow, never for the next path.
@@ -338,6 +366,32 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
338
366
  if (mode === 'auto' || !prompt || remembered.all) {
339
367
  return { action: 'approve_tools' }
340
368
  }
369
+ // Every call a person would be asked about is one a skill loaded in
370
+ // this turn pre-approved (`allowed-tools`). Reached only in `prompt`
371
+ // and `accept-edits`: `plan` and `strict` refused above, so a skill's
372
+ // word never outranks either, and the kernel only marks a call no
373
+ // deny, explicit ask, destructive flag or escalation stands behind.
374
+ // One unmarked call and the whole batch is asked about, as always.
375
+ const needsPerson = request.toolCalls.filter(
376
+ (tc) =>
377
+ tc.authorization?.explicitReview ||
378
+ tc.isDestructive ||
379
+ tc.escalation !== undefined ||
380
+ !(
381
+ exempt(tc.name, tc.input) ||
382
+ (mode === 'accept-edits' && ACCEPT_EDITS_TOOLS.has(tc.name))
383
+ ),
384
+ )
385
+ if (
386
+ options.skillGrants !== 'ignore' &&
387
+ needsPerson.length > 0 &&
388
+ needsPerson.every(isSkillGranted)
389
+ ) {
390
+ return {
391
+ action: 'approve_tools',
392
+ skillGranted: needsPerson.map((tc) => tc.id),
393
+ }
394
+ }
341
395
  const answer = await prompt({
342
396
  sessionId: request.sessionId,
343
397
  turnId: request.turnId,
@@ -352,7 +406,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
352
406
  case 'reject':
353
407
  return {
354
408
  action: 'reject_tools',
355
- feedback: answer.feedback ?? 'User declined to run the proposed tool(s).',
409
+ feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
356
410
  }
357
411
  }
358
412
  }
@@ -1,4 +1,5 @@
1
1
  import type { AuthorizationGate } from '../../authorization/gate.js'
2
+ import type { SkillGrantSet } from '../../authorization/skill-grant.js'
2
3
  import type { PluginLifecycleManager } from '../../plugin/lifecycle.js'
3
4
  import type { ActivityStore } from '../../store/activity/memory.js'
4
5
  import type { SessionId, TurnId } from '../../types/ids/index.js'
@@ -67,6 +68,8 @@ export interface ToolingBootstrapConfig {
67
68
  recordAudit?: (input: AuditEventInput) => Promise<unknown>
68
69
  /** Builds the durable-pause seam for one tool call; see ToolContext.requestPause. */
69
70
  toolPause?: (toolUseId: string) => RequestToolPause
71
+ /** The turn's skill pre-approvals; see the executor's own field. */
72
+ skillGrants?: SkillGrantSet
70
73
  }
71
74
 
72
75
  export class ToolingBootstrap {
@@ -130,6 +133,7 @@ export class ToolingBootstrap {
130
133
  : {}),
131
134
  ...(config.recordAudit !== undefined ? { recordAudit: config.recordAudit } : {}),
132
135
  ...(config.toolPause !== undefined ? { toolPause: config.toolPause } : {}),
136
+ ...(config.skillGrants !== undefined ? { skillGrants: config.skillGrants } : {}),
133
137
  },
134
138
  activityStore,
135
139
  emitEvent,
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Five-field cron, parsed into sets.
3
+ *
4
+ * Minute, hour, day of month, month, day of week. Lists, ranges, steps and
5
+ * three-letter names; `0` and `7` are both Sunday. Vixie semantics for the
6
+ * two day fields: when BOTH are restricted a day matches if EITHER does, which
7
+ * is what every cron and every existing crontab assumes.
8
+ *
9
+ * Refused, each by name: `L`, `W`, `#`, `?`, a sixth (seconds) field and
10
+ * `@reboot`. A refused token is a better answer than a job that silently
11
+ * means something the operator did not write.
12
+ */
13
+
14
+ import { ScheduleValidationError } from './errors.js'
15
+ import type { CronExpression } from './types.js'
16
+
17
+ const MACROS: Readonly<Record<string, string>> = {
18
+ '@yearly': '0 0 1 1 *',
19
+ '@annually': '0 0 1 1 *',
20
+ '@monthly': '0 0 1 * *',
21
+ '@weekly': '0 0 * * 0',
22
+ '@daily': '0 0 * * *',
23
+ '@midnight': '0 0 * * *',
24
+ '@hourly': '0 * * * *',
25
+ }
26
+
27
+ const MONTH_NAMES = [
28
+ 'jan',
29
+ 'feb',
30
+ 'mar',
31
+ 'apr',
32
+ 'may',
33
+ 'jun',
34
+ 'jul',
35
+ 'aug',
36
+ 'sep',
37
+ 'oct',
38
+ 'nov',
39
+ 'dec',
40
+ ]
41
+ const DAY_NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
42
+
43
+ interface FieldSpec {
44
+ readonly label: string
45
+ readonly min: number
46
+ readonly max: number
47
+ readonly names?: readonly string[]
48
+ /** Where `names[0]` sits in the numeric range. */
49
+ readonly nameBase?: number
50
+ }
51
+
52
+ const FIELDS: readonly FieldSpec[] = [
53
+ { label: 'minute', min: 0, max: 59 },
54
+ { label: 'hour', min: 0, max: 23 },
55
+ { label: 'day of month', min: 1, max: 31 },
56
+ { label: 'month', min: 1, max: 12, names: MONTH_NAMES, nameBase: 1 },
57
+ { label: 'day of week', min: 0, max: 7, names: DAY_NAMES, nameBase: 0 },
58
+ ]
59
+
60
+ function parseValue(raw: string, field: FieldSpec): number {
61
+ const lower = raw.toLowerCase()
62
+ if (field.names) {
63
+ const at = field.names.indexOf(lower)
64
+ if (at >= 0) return at + (field.nameBase ?? 0)
65
+ }
66
+ if (!/^\d+$/.test(raw)) {
67
+ throw new ScheduleValidationError(`${field.label}: "${raw}" is not a number or a name`, raw)
68
+ }
69
+ const n = Number(raw)
70
+ if (n < field.min || n > field.max) {
71
+ throw new ScheduleValidationError(
72
+ `${field.label}: ${n} is outside ${field.min}-${field.max}`,
73
+ raw,
74
+ )
75
+ }
76
+ return n
77
+ }
78
+
79
+ function parseField(text: string, field: FieldSpec): number[] {
80
+ if (text === '') throw new ScheduleValidationError(`${field.label}: empty field`, text)
81
+ const values = new Set<number>()
82
+ for (const part of text.split(',')) {
83
+ if (part === '') throw new ScheduleValidationError(`${field.label}: empty list entry`, text)
84
+ // Names (`jul`, `wed`) are removed first: they carry the letters the
85
+ // refused `L` and `W` are spelled with.
86
+ const withoutNames = part.replace(/[a-z]{3}/gi, (word) =>
87
+ field.names?.includes(word.toLowerCase()) ? '' : word,
88
+ )
89
+ for (const refused of ['L', 'W', '#', '?']) {
90
+ if (withoutNames.toUpperCase().includes(refused)) {
91
+ throw new ScheduleValidationError(
92
+ `${field.label}: "${refused}" is not supported (in "${part}")`,
93
+ refused,
94
+ )
95
+ }
96
+ }
97
+ const [rangeText = '', stepText, extra] = part.split('/')
98
+ if (extra !== undefined) {
99
+ throw new ScheduleValidationError(`${field.label}: "${part}" has more than one step`, part)
100
+ }
101
+ let step = 1
102
+ if (stepText !== undefined) {
103
+ if (!/^\d+$/.test(stepText) || Number(stepText) === 0) {
104
+ throw new ScheduleValidationError(
105
+ `${field.label}: step "${stepText}" must be a whole number above 0`,
106
+ part,
107
+ )
108
+ }
109
+ step = Number(stepText)
110
+ }
111
+ let lo: number
112
+ let hi: number
113
+ if (rangeText === '*') {
114
+ lo = field.min
115
+ hi = field.label === 'day of week' ? 6 : field.max
116
+ } else if (rangeText.includes('-')) {
117
+ const [a = '', b = '', more] = rangeText.split('-')
118
+ if (more !== undefined) {
119
+ throw new ScheduleValidationError(`${field.label}: "${rangeText}" is not a range`, part)
120
+ }
121
+ lo = parseValue(a, field)
122
+ hi = parseValue(b, field)
123
+ if (hi < lo) {
124
+ throw new ScheduleValidationError(
125
+ `${field.label}: range "${rangeText}" runs backwards`,
126
+ rangeText,
127
+ )
128
+ }
129
+ } else {
130
+ lo = parseValue(rangeText, field)
131
+ // `5/15` means "from 5, every 15", as in Vixie cron.
132
+ hi = stepText !== undefined ? (field.label === 'day of week' ? 6 : field.max) : lo
133
+ }
134
+ for (let v = lo; v <= hi; v += step) values.add(v)
135
+ }
136
+ return [...values].sort((a, b) => a - b)
137
+ }
138
+
139
+ /**
140
+ * Parse a five-field expression or a macro. Throws
141
+ * {@link ScheduleValidationError} naming the offending token.
142
+ */
143
+ export function parseCronExpression(input: string): CronExpression {
144
+ const trimmed = input.trim()
145
+ if (trimmed === '') throw new ScheduleValidationError('empty cron expression', '')
146
+ let text = trimmed
147
+ if (trimmed.startsWith('@')) {
148
+ const macro = MACROS[trimmed.toLowerCase()]
149
+ if (!macro) {
150
+ throw new ScheduleValidationError(`"${trimmed}" is not a supported macro`, trimmed)
151
+ }
152
+ text = macro
153
+ }
154
+ const fields = text.split(/\s+/)
155
+ if (fields.length === 6) {
156
+ throw new ScheduleValidationError(
157
+ 'six fields: a seconds field is not supported; use five fields (minute hour day month weekday)',
158
+ fields[0],
159
+ )
160
+ }
161
+ if (fields.length !== 5) {
162
+ throw new ScheduleValidationError(
163
+ `expected five fields (minute hour day month weekday), got ${fields.length}`,
164
+ text,
165
+ )
166
+ }
167
+ const [mi = '', ho = '', dom = '', mon = '', dow = ''] = fields
168
+ const minutes = parseField(mi, FIELDS[0] as FieldSpec)
169
+ const hours = parseField(ho, FIELDS[1] as FieldSpec)
170
+ const daysOfMonth = parseField(dom, FIELDS[2] as FieldSpec)
171
+ const months = parseField(mon, FIELDS[3] as FieldSpec)
172
+ const weekdays = parseField(dow, FIELDS[4] as FieldSpec)
173
+ const normalizedWeekdays = [...new Set(weekdays.map((d) => (d === 7 ? 0 : d)))].sort(
174
+ (a, b) => a - b,
175
+ )
176
+ const fixedTime = !/[*/]/.test(mi) && !/[*/]/.test(ho)
177
+ return {
178
+ source: fields.join(' '),
179
+ minutes,
180
+ hours,
181
+ daysOfMonth,
182
+ months,
183
+ daysOfWeek: normalizedWeekdays,
184
+ domRestricted: !dom.startsWith('*'),
185
+ dowRestricted: !dow.startsWith('*'),
186
+ fixedTime,
187
+ }
188
+ }
189
+
190
+ /** Whether the civil date matches the day fields (Vixie OR rule). */
191
+ export function cronMatchesDay(
192
+ expr: CronExpression,
193
+ month: number,
194
+ day: number,
195
+ weekday: number,
196
+ ): boolean {
197
+ if (!expr.months.includes(month)) return false
198
+ const domMatch = expr.daysOfMonth.includes(day)
199
+ const dowMatch = expr.daysOfWeek.includes(weekday)
200
+ if (expr.domRestricted && expr.dowRestricted) return domMatch || dowMatch
201
+ return domMatch && dowMatch
202
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * A schedule in words, for a confirmation screen or a listing.
3
+ *
4
+ * Covers the common shapes exactly and falls back to the expression itself
5
+ * for anything else, rather than paraphrasing it wrongly.
6
+ */
7
+
8
+ import { parseCronExpression } from './cron.js'
9
+ import type { CronExpression, ScheduleSpec } from './types.js'
10
+ import { formatWall } from './tz.js'
11
+
12
+ const MINUTE = 60_000
13
+ const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
14
+ const MONTH_NAMES = [
15
+ 'January',
16
+ 'February',
17
+ 'March',
18
+ 'April',
19
+ 'May',
20
+ 'June',
21
+ 'July',
22
+ 'August',
23
+ 'September',
24
+ 'October',
25
+ 'November',
26
+ 'December',
27
+ ]
28
+
29
+ export interface DescribeScheduleOptions {
30
+ /** Zone an `at` or `every` anchor is shown in. Default UTC. */
31
+ readonly tz?: string
32
+ }
33
+
34
+ function pad(n: number): string {
35
+ return String(n).padStart(2, '0')
36
+ }
37
+
38
+ function plural(n: number, unit: string): string {
39
+ return `${n} ${unit}${n === 1 ? '' : 's'}`
40
+ }
41
+
42
+ function describeInterval(ms: number): string {
43
+ if (ms % (7 * 24 * 60 * MINUTE) === 0) {
44
+ const w = ms / (7 * 24 * 60 * MINUTE)
45
+ return w === 1 ? 'every week' : `every ${plural(w, 'week')}`
46
+ }
47
+ if (ms % (24 * 60 * MINUTE) === 0) {
48
+ const d = ms / (24 * 60 * MINUTE)
49
+ return d === 1 ? 'every 24 hours' : `every ${plural(d, 'day')}`
50
+ }
51
+ if (ms % (60 * MINUTE) === 0) {
52
+ const h = ms / (60 * MINUTE)
53
+ return h === 1 ? 'every hour' : `every ${plural(h, 'hour')}`
54
+ }
55
+ const m = ms / MINUTE
56
+ return m === 1 ? 'every minute' : `every ${plural(m, 'minute')}`
57
+ }
58
+
59
+ /** `1,2,3,5` → `1–3, 5`. */
60
+ function ranges(values: readonly number[], name: (n: number) => string): string {
61
+ const out: string[] = []
62
+ let i = 0
63
+ while (i < values.length) {
64
+ let j = i
65
+ while (j + 1 < values.length && (values[j + 1] as number) === (values[j] as number) + 1) j++
66
+ const a = values[i] as number
67
+ const b = values[j] as number
68
+ if (j - i >= 2) out.push(`${name(a)} through ${name(b)}`)
69
+ else for (let k = i; k <= j; k++) out.push(name(values[k] as number))
70
+ i = j + 1
71
+ }
72
+ return out.join(', ')
73
+ }
74
+
75
+ function isFull(values: readonly number[], min: number, max: number): boolean {
76
+ return values.length === max - min + 1
77
+ }
78
+
79
+ function describeDays(e: CronExpression): string {
80
+ const allDom = !e.domRestricted
81
+ const allDow = !e.dowRestricted
82
+ const allMonths = isFull(e.months, 1, 12)
83
+ let days: string
84
+ if (allDom && allDow) days = 'every day'
85
+ else if (allDom) days = `on ${ranges(e.daysOfWeek, (d) => DAY_NAMES[d] ?? String(d))}`
86
+ else if (allDow) days = `on day ${ranges(e.daysOfMonth, String)} of the month`
87
+ else
88
+ days = `on day ${ranges(e.daysOfMonth, String)} of the month and on ${ranges(e.daysOfWeek, (d) => DAY_NAMES[d] ?? String(d))}`
89
+ if (!allMonths) days += ` in ${ranges(e.months, (m) => MONTH_NAMES[m - 1] ?? String(m))}`
90
+ return days
91
+ }
92
+
93
+ function describeTimes(e: CronExpression, source: string): string {
94
+ const [mi = '', ho = ''] = source.split(' ')
95
+ const allMinutes = isFull(e.minutes, 0, 59)
96
+ const allHours = isFull(e.hours, 0, 23)
97
+ if (allMinutes && allHours) return 'every minute'
98
+ const stepMinute = /^\*\/(\d+)$/.exec(mi)
99
+ if (stepMinute && allHours) return `every ${plural(Number(stepMinute[1]), 'minute')}`
100
+ if (allHours && e.minutes.length <= 4) {
101
+ return e.minutes.length === 1 && e.minutes[0] === 0
102
+ ? 'every hour, on the hour'
103
+ : `every hour at minute ${e.minutes.join(', ')}`
104
+ }
105
+ const stepHour = /^\*\/(\d+)$/.exec(ho)
106
+ if (stepHour && e.minutes.length === 1) {
107
+ return `every ${plural(Number(stepHour[1]), 'hour')} at minute ${e.minutes[0]}`
108
+ }
109
+ if (e.hours.length * e.minutes.length <= 6) {
110
+ const times: string[] = []
111
+ for (const h of e.hours) for (const m of e.minutes) times.push(`${pad(h)}:${pad(m)}`)
112
+ return `at ${times.join(', ')}`
113
+ }
114
+ return `at minute ${ranges(e.minutes, String)} of hour ${ranges(e.hours, String)}`
115
+ }
116
+
117
+ /** The schedule in words: `at 03:00 every day (Europe/Istanbul)`. */
118
+ export function describeSchedule(
119
+ spec: ScheduleSpec,
120
+ options: DescribeScheduleOptions = {},
121
+ ): string {
122
+ const tz = options.tz ?? 'UTC'
123
+ switch (spec.kind) {
124
+ case 'at':
125
+ return `once at ${formatWall(Date.parse(spec.at), tz)} (${tz})`
126
+ case 'every':
127
+ return describeInterval(spec.everyMs)
128
+ case 'cron': {
129
+ let e: CronExpression
130
+ try {
131
+ e = parseCronExpression(spec.expr)
132
+ } catch {
133
+ return `cron "${spec.expr}" (${spec.tz})`
134
+ }
135
+ return `${describeTimes(e, e.source)} ${describeDays(e)} (${spec.tz})`
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A schedule, cron expression or time zone that cannot be used.
3
+ *
4
+ * `token` names the piece of the input that was refused, so a caller can
5
+ * point at it instead of repeating the whole expression back.
6
+ */
7
+ export class ScheduleValidationError extends Error {
8
+ readonly token: string | undefined
9
+
10
+ constructor(message: string, token?: string) {
11
+ super(message)
12
+ this.name = 'ScheduleValidationError'
13
+ this.token = token
14
+ }
15
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * What to do about one job now: fire, skip, record a miss, or nothing.
3
+ *
4
+ * Pure: no clock, no I/O. The host passes the instant, the state it kept and
5
+ * whether an occurrence was already claimed, and applies the decision itself
6
+ * (append history, claim, dispatch). Evaluating the same `(state, now)` twice
7
+ * gives the same answer, so a host that lost its in-memory queue rebuilds it
8
+ * by evaluating again; an already-claimed key is never fired twice.
9
+ *
10
+ * Rules, in order:
11
+ *
12
+ * 1. Occurrences due are those in `(lastEvaluatedAt, now]`, counted with a
13
+ * cap and never enumerated. A new job counts from its creation. A changed
14
+ * revision counts from the change, so editing `0 3 * * *` into `0 4 * * *`
15
+ * does not invent catch-ups for 04:00s that never belonged to the job.
16
+ * 2. `lastEvaluatedAt` never moves backwards: after a backward clock jump
17
+ * nothing is due until the wall clock passes it again.
18
+ * 3. A job that is not active produces skip records only (paused and
19
+ * awaiting-confirmation are collapsed per evaluation). A one-shot whose
20
+ * time passed while inactive is expired.
21
+ * 4. The latest due occurrence fires on time (`scheduled`, or `late` once
22
+ * more than five seconds late) within the late grace; within the catch-up
23
+ * window it fires once as `catch-up` and everything earlier is one
24
+ * `missed` record; older than the window, nothing fires.
25
+ * 5. An unfinished run of the same job skips what came due meanwhile.
26
+ * 6. A provider quota hold skips what comes due before the hold ends.
27
+ */
28
+
29
+ import { countOccurrences, nextFireTime, previousFireTime } from './next-fire.js'
30
+ import type {
31
+ ScheduleDecision,
32
+ ScheduleEvaluationInput,
33
+ ScheduleMissedReason,
34
+ ScheduleSkipReason,
35
+ } from './types.js'
36
+
37
+ /** Default catch-up window: seven days. */
38
+ export const SCHEDULE_CATCH_UP_WINDOW_MS = 7 * 24 * 60 * 60 * 1000
39
+ /** Default grace for a fire to still count as on time: two minutes. */
40
+ export const SCHEDULE_LATE_GRACE_MS = 120_000
41
+ /** A fire later than this is reported `late` rather than `scheduled`. */
42
+ const LATE_THRESHOLD_MS = 5_000
43
+
44
+ type Skip = ScheduleDecision['skip'][number]
45
+
46
+ function missedReason(input: ScheduleEvaluationInput, firstMissed: Date): ScheduleMissedReason {
47
+ const gap = input.daemon.observedGap
48
+ if (gap && firstMissed.getTime() >= gap.from.getTime()) {
49
+ if (gap.kind === 'asleep') return 'machine-asleep'
50
+ if (gap.kind === 'clock-forward') return 'clock-jumped-forward'
51
+ }
52
+ return 'daemon-not-running'
53
+ }
54
+
55
+ /** Evaluate one job at `now`. */
56
+ export function evaluateJob(input: ScheduleEvaluationInput): ScheduleDecision {
57
+ const { job, state, now } = input
58
+ const nowMs = now.getTime()
59
+ const lateGrace = input.lateGraceMs ?? SCHEDULE_LATE_GRACE_MS
60
+ const windowMs = job.catchUp?.windowMs ?? SCHEDULE_CATCH_UP_WINDOW_MS
61
+
62
+ let baseMs = Date.parse(state.lastEvaluatedAt ?? job.createdAt)
63
+ if (!Number.isFinite(baseMs)) baseMs = Date.parse(job.createdAt)
64
+ if (state.jobRevision !== undefined && state.jobRevision !== job.revision) {
65
+ baseMs = Math.max(baseMs, Date.parse(job.updatedAt))
66
+ }
67
+ const nextState = (evaluatedMs: number): ScheduleDecision['nextState'] => {
68
+ const next = nextFireTime(job.spec, new Date(evaluatedMs))
69
+ return {
70
+ lastEvaluatedAt: new Date(evaluatedMs),
71
+ ...(next ? { nextFireAt: next } : {}),
72
+ jobRevision: job.revision,
73
+ }
74
+ }
75
+
76
+ if (job.state === 'completed' || job.state === 'expired') {
77
+ return {
78
+ skip: [],
79
+ nextState: { lastEvaluatedAt: new Date(Math.max(baseMs, nowMs)), jobRevision: job.revision },
80
+ }
81
+ }
82
+ // Rule 2: a clock that went backwards decides nothing new.
83
+ if (nowMs <= baseMs) return { skip: [], nextState: nextState(baseMs) }
84
+
85
+ const due = countOccurrences(job.spec, new Date(baseMs), now)
86
+ if (due.count === 0 || !due.latest) {
87
+ if (job.spec.kind === 'at' && Date.parse(job.spec.at) <= baseMs && job.state !== 'active') {
88
+ return { skip: [], jobTransition: 'expired', nextState: nextState(nowMs) }
89
+ }
90
+ return { skip: [], nextState: nextState(nowMs) }
91
+ }
92
+ const latest = due.latest
93
+
94
+ const collapsed = (reason: ScheduleSkipReason): Skip[] => [
95
+ { scheduledFor: latest, reason, count: due.count },
96
+ ]
97
+
98
+ if (job.state === 'paused' || job.state === 'pending-confirmation') {
99
+ const reason: ScheduleSkipReason = job.state === 'paused' ? 'paused' : 'awaiting-confirmation'
100
+ return {
101
+ skip: collapsed(reason),
102
+ ...(job.spec.kind === 'at' ? { jobTransition: 'expired' as const } : {}),
103
+ nextState: nextState(nowMs),
104
+ }
105
+ }
106
+
107
+ const hold = state.quotaHoldUntil ? Date.parse(state.quotaHoldUntil) : Number.NaN
108
+ if (Number.isFinite(hold) && hold > nowMs) {
109
+ return { skip: collapsed('quota-hold'), nextState: nextState(nowMs) }
110
+ }
111
+
112
+ if (state.activeRun) {
113
+ return {
114
+ skip: collapsed(
115
+ state.activeRun.status === 'awaiting-approval'
116
+ ? 'previous-run-awaiting-approval'
117
+ : 'previous-run-active',
118
+ ),
119
+ nextState: nextState(nowMs),
120
+ }
121
+ }
122
+
123
+ const key = String(latest.getTime())
124
+ const lateBy = nowMs - latest.getTime()
125
+ const earlier = due.count - 1
126
+ const missedBefore = (): ScheduleDecision['missed'] | undefined => {
127
+ if (earlier <= 0 || !due.first) return undefined
128
+ // The occurrence just before the fired one.
129
+ const to = previousFireTime(job.spec, new Date(latest.getTime() - 1)) ?? due.first
130
+ return {
131
+ from: due.first,
132
+ to,
133
+ count: earlier,
134
+ capped: due.capped,
135
+ reason: missedReason(input, due.first),
136
+ }
137
+ }
138
+ const transition = job.spec.kind === 'at' ? { jobTransition: 'completed' as const } : {}
139
+
140
+ if (input.isClaimed?.(key)) {
141
+ // Somebody already started it; nothing to fire, nothing missed.
142
+ return { skip: [], ...transition, nextState: nextState(nowMs) }
143
+ }
144
+
145
+ if (lateBy <= lateGrace || lateBy <= windowMs) {
146
+ const trigger =
147
+ lateBy <= lateGrace ? (lateBy > LATE_THRESHOLD_MS ? 'late' : 'scheduled') : 'catch-up'
148
+ const missed = missedBefore()
149
+ return {
150
+ fire: { key, scheduledFor: latest, trigger },
151
+ skip: [],
152
+ ...(missed ? { missed } : {}),
153
+ ...transition,
154
+ nextState: nextState(nowMs),
155
+ }
156
+ }
157
+
158
+ // Everything due is older than the window.
159
+ const first = due.first ?? latest
160
+ return {
161
+ skip: [
162
+ {
163
+ scheduledFor: latest,
164
+ reason: job.spec.kind === 'at' ? 'one-shot-expired' : 'beyond-catch-up-window',
165
+ count: 1,
166
+ },
167
+ ],
168
+ missed: {
169
+ from: first,
170
+ to: latest,
171
+ count: due.count,
172
+ capped: due.capped,
173
+ reason: missedReason(input, first),
174
+ },
175
+ ...(job.spec.kind === 'at' ? { jobTransition: 'expired' as const } : {}),
176
+ nextState: nextState(nowMs),
177
+ }
178
+ }