@namzu/sdk 45.0.0 → 46.0.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 (281) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/dist/authorization/gate.d.ts +5 -2
  3. package/dist/authorization/gate.d.ts.map +1 -1
  4. package/dist/authorization/gate.js +35 -4
  5. package/dist/authorization/gate.js.map +1 -1
  6. package/dist/authorization/rules.d.ts.map +1 -1
  7. package/dist/authorization/rules.js +56 -6
  8. package/dist/authorization/rules.js.map +1 -1
  9. package/dist/authorization/shell-lexer.d.ts +38 -0
  10. package/dist/authorization/shell-lexer.d.ts.map +1 -1
  11. package/dist/authorization/shell-lexer.js +88 -57
  12. package/dist/authorization/shell-lexer.js.map +1 -1
  13. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  14. package/dist/bridge/a2a/mapper.js +2 -0
  15. package/dist/bridge/a2a/mapper.js.map +1 -1
  16. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  17. package/dist/bridge/sse/mapper.js +1 -0
  18. package/dist/bridge/sse/mapper.js.map +1 -1
  19. package/dist/directory/types.d.ts +2 -0
  20. package/dist/directory/types.d.ts.map +1 -1
  21. package/dist/directory/types.js.map +1 -1
  22. package/dist/manager/resident/outbox.d.ts +4 -4
  23. package/dist/pricing/catalogue.generated.d.ts.map +1 -1
  24. package/dist/pricing/catalogue.generated.js +28 -4
  25. package/dist/pricing/catalogue.generated.js.map +1 -1
  26. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  27. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  28. package/dist/prompt/coding-agent-doctrine.js +1 -0
  29. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  30. package/dist/public-runtime.d.ts +6 -3
  31. package/dist/public-runtime.d.ts.map +1 -1
  32. package/dist/public-runtime.js +14 -2
  33. package/dist/public-runtime.js.map +1 -1
  34. package/dist/public-tools.d.ts +7 -2
  35. package/dist/public-tools.d.ts.map +1 -1
  36. package/dist/public-tools.js +15 -2
  37. package/dist/public-tools.js.map +1 -1
  38. package/dist/public-types.d.ts +8 -2
  39. package/dist/public-types.d.ts.map +1 -1
  40. package/dist/registry/tool/callable.d.ts +22 -0
  41. package/dist/registry/tool/callable.d.ts.map +1 -0
  42. package/dist/registry/tool/callable.js +29 -0
  43. package/dist/registry/tool/callable.js.map +1 -0
  44. package/dist/registry/tool/execute.d.ts.map +1 -1
  45. package/dist/registry/tool/execute.js +8 -1
  46. package/dist/registry/tool/execute.js.map +1 -1
  47. package/dist/runtime/query/declined.d.ts +12 -0
  48. package/dist/runtime/query/declined.d.ts.map +1 -0
  49. package/dist/runtime/query/declined.js +12 -0
  50. package/dist/runtime/query/declined.js.map +1 -0
  51. package/dist/runtime/query/executor/tool-call-admission.d.ts +37 -11
  52. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  53. package/dist/runtime/query/executor/tool-call-admission.js +38 -12
  54. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  55. package/dist/runtime/query/executor.d.ts +7 -3
  56. package/dist/runtime/query/executor.d.ts.map +1 -1
  57. package/dist/runtime/query/executor.js +32 -7
  58. package/dist/runtime/query/executor.js.map +1 -1
  59. package/dist/runtime/query/index.d.ts.map +1 -1
  60. package/dist/runtime/query/index.js +1 -0
  61. package/dist/runtime/query/index.js.map +1 -1
  62. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  63. package/dist/runtime/query/iteration/index.js +7 -0
  64. package/dist/runtime/query/iteration/index.js.map +1 -1
  65. package/dist/runtime/query/iteration/phases/context.d.ts +6 -0
  66. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  68. package/dist/runtime/query/iteration/phases/handoff.d.ts +22 -0
  69. package/dist/runtime/query/iteration/phases/handoff.d.ts.map +1 -0
  70. package/dist/runtime/query/iteration/phases/handoff.js +65 -0
  71. package/dist/runtime/query/iteration/phases/handoff.js.map +1 -0
  72. package/dist/runtime/query/iteration/phases/index.d.ts +1 -0
  73. package/dist/runtime/query/iteration/phases/index.d.ts.map +1 -1
  74. package/dist/runtime/query/iteration/phases/index.js +1 -0
  75. package/dist/runtime/query/iteration/phases/index.js.map +1 -1
  76. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  77. package/dist/runtime/query/iteration/phases/tool-review.js +8 -3
  78. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  79. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  80. package/dist/runtime/query/resume-pending.js +3 -2
  81. package/dist/runtime/query/resume-pending.js.map +1 -1
  82. package/dist/runtime/query/review-policy.d.ts +43 -0
  83. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  84. package/dist/runtime/query/review-policy.js +63 -19
  85. package/dist/runtime/query/review-policy.js.map +1 -1
  86. package/dist/schedules/cron.d.ts +21 -0
  87. package/dist/schedules/cron.d.ts.map +1 -0
  88. package/dist/schedules/cron.js +167 -0
  89. package/dist/schedules/cron.js.map +1 -0
  90. package/dist/schedules/describe.d.ts +14 -0
  91. package/dist/schedules/describe.d.ts.map +1 -0
  92. package/dist/schedules/describe.js +133 -0
  93. package/dist/schedules/describe.js.map +1 -0
  94. package/dist/schedules/errors.d.ts +11 -0
  95. package/dist/schedules/errors.d.ts.map +1 -0
  96. package/dist/schedules/errors.js +15 -0
  97. package/dist/schedules/errors.js.map +1 -0
  98. package/dist/schedules/evaluate.d.ts +35 -0
  99. package/dist/schedules/evaluate.d.ts.map +1 -0
  100. package/dist/schedules/evaluate.js +158 -0
  101. package/dist/schedules/evaluate.js.map +1 -0
  102. package/dist/schedules/index.d.ts +11 -0
  103. package/dist/schedules/index.d.ts.map +1 -0
  104. package/dist/schedules/index.js +8 -0
  105. package/dist/schedules/index.js.map +1 -0
  106. package/dist/schedules/next-fire.d.ts +50 -0
  107. package/dist/schedules/next-fire.d.ts.map +1 -0
  108. package/dist/schedules/next-fire.js +250 -0
  109. package/dist/schedules/next-fire.js.map +1 -0
  110. package/dist/schedules/spec.d.ts +30 -0
  111. package/dist/schedules/spec.d.ts.map +1 -0
  112. package/dist/schedules/spec.js +169 -0
  113. package/dist/schedules/spec.js.map +1 -0
  114. package/dist/schedules/types.d.ts +144 -0
  115. package/dist/schedules/types.d.ts.map +1 -0
  116. package/dist/schedules/types.js +11 -0
  117. package/dist/schedules/types.js.map +1 -0
  118. package/dist/schedules/tz.d.ts +44 -0
  119. package/dist/schedules/tz.d.ts.map +1 -0
  120. package/dist/schedules/tz.js +141 -0
  121. package/dist/schedules/tz.js.map +1 -0
  122. package/dist/skills/index.d.ts +1 -1
  123. package/dist/skills/index.d.ts.map +1 -1
  124. package/dist/skills/index.js +1 -1
  125. package/dist/skills/index.js.map +1 -1
  126. package/dist/skills/loader.d.ts +16 -3
  127. package/dist/skills/loader.d.ts.map +1 -1
  128. package/dist/skills/loader.js +48 -4
  129. package/dist/skills/loader.js.map +1 -1
  130. package/dist/skills/registry.d.ts +6 -0
  131. package/dist/skills/registry.d.ts.map +1 -1
  132. package/dist/skills/registry.js +1 -0
  133. package/dist/skills/registry.js.map +1 -1
  134. package/dist/tools/builtins/browser-url.d.ts +83 -0
  135. package/dist/tools/builtins/browser-url.d.ts.map +1 -0
  136. package/dist/tools/builtins/browser-url.js +240 -0
  137. package/dist/tools/builtins/browser-url.js.map +1 -0
  138. package/dist/tools/builtins/browser.d.ts +367 -0
  139. package/dist/tools/builtins/browser.d.ts.map +1 -0
  140. package/dist/tools/builtins/browser.js +704 -0
  141. package/dist/tools/builtins/browser.js.map +1 -0
  142. package/dist/tools/builtins/computer-use-coordinates.d.ts +65 -0
  143. package/dist/tools/builtins/computer-use-coordinates.d.ts.map +1 -0
  144. package/dist/tools/builtins/computer-use-coordinates.js +123 -0
  145. package/dist/tools/builtins/computer-use-coordinates.js.map +1 -0
  146. package/dist/tools/builtins/computer-use-image.d.ts +77 -0
  147. package/dist/tools/builtins/computer-use-image.d.ts.map +1 -0
  148. package/dist/tools/builtins/computer-use-image.js +223 -0
  149. package/dist/tools/builtins/computer-use-image.js.map +1 -0
  150. package/dist/tools/builtins/computer-use.d.ts +519 -14
  151. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  152. package/dist/tools/builtins/computer-use.js +1188 -183
  153. package/dist/tools/builtins/computer-use.js.map +1 -1
  154. package/dist/tools/builtins/index.d.ts +2 -1
  155. package/dist/tools/builtins/index.d.ts.map +1 -1
  156. package/dist/tools/builtins/index.js +1 -1
  157. package/dist/tools/builtins/index.js.map +1 -1
  158. package/dist/tools/builtins/skill.d.ts +74 -0
  159. package/dist/tools/builtins/skill.d.ts.map +1 -1
  160. package/dist/tools/builtins/skill.js +214 -159
  161. package/dist/tools/builtins/skill.js.map +1 -1
  162. package/dist/tools/defineTool.d.ts +4 -0
  163. package/dist/tools/defineTool.d.ts.map +1 -1
  164. package/dist/tools/defineTool.js +8 -0
  165. package/dist/tools/defineTool.js.map +1 -1
  166. package/dist/tools/schedules/index.d.ts +5 -0
  167. package/dist/tools/schedules/index.d.ts.map +1 -0
  168. package/dist/tools/schedules/index.js +4 -0
  169. package/dist/tools/schedules/index.js.map +1 -0
  170. package/dist/tools/schedules/loop-tool.d.ts +14 -0
  171. package/dist/tools/schedules/loop-tool.d.ts.map +1 -0
  172. package/dist/tools/schedules/loop-tool.js +81 -0
  173. package/dist/tools/schedules/loop-tool.js.map +1 -0
  174. package/dist/tools/schedules/present.d.ts +16 -0
  175. package/dist/tools/schedules/present.d.ts.map +1 -0
  176. package/dist/tools/schedules/present.js +71 -0
  177. package/dist/tools/schedules/present.js.map +1 -0
  178. package/dist/tools/schedules/prompt-scan.d.ts +17 -0
  179. package/dist/tools/schedules/prompt-scan.d.ts.map +1 -0
  180. package/dist/tools/schedules/prompt-scan.js +92 -0
  181. package/dist/tools/schedules/prompt-scan.js.map +1 -0
  182. package/dist/tools/schedules/schedule-tool.d.ts +17 -0
  183. package/dist/tools/schedules/schedule-tool.d.ts.map +1 -0
  184. package/dist/tools/schedules/schedule-tool.js +418 -0
  185. package/dist/tools/schedules/schedule-tool.js.map +1 -0
  186. package/dist/tools/schedules/types.d.ts +252 -0
  187. package/dist/tools/schedules/types.d.ts.map +1 -0
  188. package/dist/tools/schedules/types.js +11 -0
  189. package/dist/tools/schedules/types.js.map +1 -0
  190. package/dist/types/authorization/index.d.ts +107 -9
  191. package/dist/types/authorization/index.d.ts.map +1 -1
  192. package/dist/types/authorization/index.js +16 -1
  193. package/dist/types/authorization/index.js.map +1 -1
  194. package/dist/types/browser/index.d.ts +280 -0
  195. package/dist/types/browser/index.d.ts.map +1 -0
  196. package/dist/types/browser/index.js +12 -0
  197. package/dist/types/browser/index.js.map +1 -0
  198. package/dist/types/computer-use/index.d.ts +176 -0
  199. package/dist/types/computer-use/index.d.ts.map +1 -1
  200. package/dist/types/computer-use/index.js.map +1 -1
  201. package/dist/types/session/events.d.ts +6 -0
  202. package/dist/types/session/events.d.ts.map +1 -1
  203. package/dist/types/session/events.js.map +1 -1
  204. package/dist/types/session/records.d.ts +23 -0
  205. package/dist/types/session/records.d.ts.map +1 -1
  206. package/dist/types/session/records.js +9 -0
  207. package/dist/types/session/records.js.map +1 -1
  208. package/dist/types/tool/index.d.ts +60 -0
  209. package/dist/types/tool/index.d.ts.map +1 -1
  210. package/dist/types/tool/index.js.map +1 -1
  211. package/dist/types/tool/presentation.d.ts +7 -0
  212. package/dist/types/tool/presentation.d.ts.map +1 -1
  213. package/dist/utils/frontmatter.d.ts +18 -2
  214. package/dist/utils/frontmatter.d.ts.map +1 -1
  215. package/dist/utils/frontmatter.js +13 -3
  216. package/dist/utils/frontmatter.js.map +1 -1
  217. package/dist/utils/id.d.ts +8 -0
  218. package/dist/utils/id.d.ts.map +1 -1
  219. package/dist/utils/id.js +12 -0
  220. package/dist/utils/id.js.map +1 -1
  221. package/package.json +3 -1
  222. package/src/authorization/gate.ts +37 -4
  223. package/src/authorization/rules.ts +55 -7
  224. package/src/authorization/shell-lexer.ts +109 -51
  225. package/src/bridge/a2a/mapper.ts +2 -0
  226. package/src/bridge/sse/mapper.ts +1 -0
  227. package/src/directory/types.ts +2 -0
  228. package/src/pricing/catalogue.generated.ts +28 -4
  229. package/src/pricing/rates.source.json +27 -6
  230. package/src/prompt/coding-agent-doctrine.ts +1 -0
  231. package/src/public-runtime.ts +43 -1
  232. package/src/public-tools.ts +54 -1
  233. package/src/public-types.ts +55 -0
  234. package/src/registry/tool/callable.ts +37 -0
  235. package/src/registry/tool/execute.ts +8 -1
  236. package/src/runtime/query/declined.ts +12 -0
  237. package/src/runtime/query/executor/tool-call-admission.ts +60 -16
  238. package/src/runtime/query/executor.ts +36 -5
  239. package/src/runtime/query/index.ts +1 -0
  240. package/src/runtime/query/iteration/index.ts +8 -0
  241. package/src/runtime/query/iteration/phases/context.ts +6 -0
  242. package/src/runtime/query/iteration/phases/handoff.ts +74 -0
  243. package/src/runtime/query/iteration/phases/index.ts +1 -0
  244. package/src/runtime/query/iteration/phases/tool-review.ts +11 -4
  245. package/src/runtime/query/resume-pending.ts +3 -2
  246. package/src/runtime/query/review-policy.ts +107 -18
  247. package/src/schedules/cron.ts +202 -0
  248. package/src/schedules/describe.ts +138 -0
  249. package/src/schedules/errors.ts +15 -0
  250. package/src/schedules/evaluate.ts +178 -0
  251. package/src/schedules/index.ts +18 -0
  252. package/src/schedules/next-fire.ts +257 -0
  253. package/src/schedules/spec.ts +210 -0
  254. package/src/schedules/types.ts +163 -0
  255. package/src/schedules/tz.ts +155 -0
  256. package/src/skills/index.ts +1 -1
  257. package/src/skills/loader.ts +55 -4
  258. package/src/skills/registry.ts +8 -0
  259. package/src/tools/builtins/browser-url.ts +251 -0
  260. package/src/tools/builtins/browser.ts +817 -0
  261. package/src/tools/builtins/computer-use-coordinates.ts +144 -0
  262. package/src/tools/builtins/computer-use-image.ts +278 -0
  263. package/src/tools/builtins/computer-use.ts +1485 -191
  264. package/src/tools/builtins/index.ts +7 -1
  265. package/src/tools/builtins/skill.ts +304 -156
  266. package/src/tools/defineTool.ts +13 -0
  267. package/src/tools/schedules/index.ts +4 -0
  268. package/src/tools/schedules/loop-tool.ts +85 -0
  269. package/src/tools/schedules/present.ts +88 -0
  270. package/src/tools/schedules/prompt-scan.ts +96 -0
  271. package/src/tools/schedules/schedule-tool.ts +482 -0
  272. package/src/tools/schedules/types.ts +265 -0
  273. package/src/types/authorization/index.ts +81 -2
  274. package/src/types/browser/index.ts +341 -0
  275. package/src/types/computer-use/index.ts +202 -0
  276. package/src/types/session/events.ts +6 -0
  277. package/src/types/session/records.ts +10 -0
  278. package/src/types/tool/index.ts +61 -0
  279. package/src/types/tool/presentation.ts +7 -0
  280. package/src/utils/frontmatter.ts +29 -3
  281. package/src/utils/id.ts +14 -0
@@ -0,0 +1,74 @@
1
+ import { GENAI, NAMZU } from '../../../../telemetry/attributes.js'
2
+ import { NamzuError } from '../../../../types/errors/index.js'
3
+ import type { SessionEvent } from '../../../../types/session/index.js'
4
+ import type { ToolCallOutcome } from '../../executor.js'
5
+ import type { IterationContext, PhaseSignal } from './context.js'
6
+
7
+ /**
8
+ * A tool asked for a person: stop before the next model call.
9
+ *
10
+ * Runs after the batch settled, so every result — the one that asked and
11
+ * its siblings — is already in the transcript and queued for the session
12
+ * log. The checkpoint written here is taken from that state, which is what
13
+ * lets a resume continue exactly as it does after a provider pause: the
14
+ * next step is a model call that sees the results.
15
+ *
16
+ * The first request in the batch speaks for it. Two tools that both need a
17
+ * person need the same thing from the operator — to come and look — and
18
+ * one pause answers both.
19
+ *
20
+ * In a delegated child the turn fails with the reason instead. Nobody
21
+ * resumes a child's turn: its parent is waiting on a result, and a failed
22
+ * child with the reason in it is a result the parent's model can act on.
23
+ */
24
+ export async function* runHandoffPause(
25
+ ctx: IterationContext,
26
+ iterationNum: number,
27
+ results: readonly ToolCallOutcome[],
28
+ ): AsyncGenerator<SessionEvent, PhaseSignal> {
29
+ const requested = results.find((result) => result.handoff !== undefined)
30
+ const handoff = requested?.handoff
31
+ if (!requested || !handoff) return 'continue'
32
+
33
+ if (ctx.delegated) {
34
+ ctx.log.info('A tool in a delegated turn needs a person; the turn fails', {
35
+ [NAMZU.TURN_ID]: ctx.recorder.turnId,
36
+ [NAMZU.ITERATION]: iterationNum,
37
+ [GENAI.TOOL_NAME]: requested.toolName,
38
+ })
39
+ throw new NamzuError({
40
+ code: 'tool_error',
41
+ message: `${requested.toolName} needs a person: ${handoff.reason}`,
42
+ details: { toolName: requested.toolName, handoff },
43
+ retryable: false,
44
+ })
45
+ }
46
+
47
+ const checkpoint = await ctx.checkpointMgr.create(ctx.recorder, iterationNum)
48
+ await ctx.emitEvent({
49
+ type: 'checkpoint_created',
50
+ turnId: ctx.recorder.turnId,
51
+ checkpointId: checkpoint.id,
52
+ iteration: iterationNum,
53
+ })
54
+ yield* ctx.drainPending()
55
+
56
+ await ctx.emitEvent({
57
+ type: 'turn_paused',
58
+ budget: ctx.recorder.budget?.summary(),
59
+ turnId: ctx.recorder.turnId,
60
+ checkpointId: checkpoint.id,
61
+ reason: handoff.reason,
62
+ handoff,
63
+ })
64
+ yield* ctx.drainPending()
65
+ ctx.recorder.setStopReason('paused')
66
+ ctx.log.info('Turn paused for a person', {
67
+ [NAMZU.TURN_ID]: ctx.recorder.turnId,
68
+ [NAMZU.ITERATION]: iterationNum,
69
+ [GENAI.TOOL_NAME]: requested.toolName,
70
+ 'namzu.checkpoint.id': checkpoint.id,
71
+ 'namzu.runtime.reason': handoff.reason,
72
+ })
73
+ return 'stop'
74
+ }
@@ -6,3 +6,4 @@ export type { ToolReviewOutcome } from './tool-review.js'
6
6
  export { runIterationCheckpoint } from './checkpoint.js'
7
7
  export { runCompactionCheck } from './compaction.js'
8
8
  export { runAdvisoryPhase } from './advisory.js'
9
+ export { runHandoffPause } from './handoff.js'
@@ -3,6 +3,7 @@ import type { ToolCallSummary } from '../../../../types/hitl/index.js'
3
3
  import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
4
4
  import type { SessionEvent } from '../../../../types/session/index.js'
5
5
  import type { ShellDialect } from '../../../../types/tool/index.js'
6
+ import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
6
7
  import type { PreparedToolBatch, ToolCallDenials } from '../../executor.js'
7
8
  import {
8
9
  awaitProjectInstructionCallback,
@@ -58,7 +59,9 @@ export async function* runToolReview(
58
59
  // skill grants to read it the same way. A test double without the method
59
60
  // leaves it unset, which reads the line for any POSIX shell.
60
61
  const dialectFor = (toolName: string): { commandDialect?: ShellDialect } => {
61
- const executor = ctx.toolExecutor as { commandDialect?: (name: string) => ShellDialect }
62
+ const executor = ctx.toolExecutor as {
63
+ commandDialect?: (name: string) => ShellDialect
64
+ }
62
65
  return typeof executor.commandDialect === 'function'
63
66
  ? { commandDialect: executor.commandDialect(toolName) }
64
67
  : {}
@@ -448,7 +451,11 @@ export async function* runToolReview(
448
451
  }
449
452
  for (const path of tc.escalation?.outsidePaths ?? []) {
450
453
  await ctx.recorder.recordAudit({
451
- what: { action: 'outside_root_access', tool: tc.name, resource: path },
454
+ what: {
455
+ action: 'outside_root_access',
456
+ tool: tc.name,
457
+ resource: path,
458
+ },
452
459
  outcome: 'approved',
453
460
  reason: "the turn's review approved this call",
454
461
  })
@@ -465,7 +472,7 @@ export async function* runToolReview(
465
472
  })
466
473
  yield* ctx.drainPending()
467
474
 
468
- const feedback = reviewDecision.feedback || 'The user rejected this tool call.'
475
+ const feedback = reviewDecision.feedback || DECLINED_TOOL_CALL_FEEDBACK
469
476
  const denials = new Map(denyAll(feedback))
470
477
  await settleEscalations(denials, undefined)
471
478
  await settle(denials)
@@ -495,7 +502,7 @@ export async function* runToolReview(
495
502
  }
496
503
  }
497
504
  if (mod.action === 'deny' && !denials.has(mod.toolCallId)) {
498
- denials.set(mod.toolCallId, 'The user denied this tool call.')
505
+ denials.set(mod.toolCallId, DECLINED_TOOL_CALL_FEEDBACK)
499
506
  }
500
507
  }
501
508
 
@@ -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. */
@@ -196,6 +197,28 @@ export const SANDBOX_ESCAPE_UNATTENDED_REFUSAL =
196
197
  export const OUTSIDE_ROOTS_UNATTENDED_REFUSAL =
197
198
  "Refused: a call in this batch reaches a path outside the working directory and the added directories, which needs a person to approve it each time, and nobody can be asked in this session. Nothing in this batch ran. Stay inside the working directory, or tell the user which directory you need so they can add it to the session (the CLI's --add-dir)."
198
199
 
200
+ /**
201
+ * What the model is told when a batch would show it the operator's screen for
202
+ * the first time in a session and nobody can be asked.
203
+ */
204
+ export const SCREEN_CONSENT_UNATTENDED_REFUSAL =
205
+ "Refused: this call would send what is on the user's screen to the model provider, which a person agrees to once per session, and nobody can be asked in this session. Nothing in this batch ran. Tell the user computer use needs their consent in an interactive session."
206
+
207
+ /** What the model is told when the operator declines to share the screen. */
208
+ export const SCREEN_CONSENT_DECLINED_FEEDBACK =
209
+ 'The user declined to share their screen in this session. Nothing in this batch ran. Do not take screenshots or read windows again; ask the user how they want to proceed.'
210
+
211
+ /**
212
+ * The sessions whose operator agreed to let the model see the screen.
213
+ *
214
+ * A host keeps one for as long as its sessions live and hands the same box to
215
+ * every policy it builds, so a mode switch keeps the answer and a new session
216
+ * (a different id) is asked again. The policy only adds to it.
217
+ */
218
+ export interface ScreenConsentRecord {
219
+ readonly sessions: Set<string>
220
+ }
221
+
199
222
  /** The batch a person is asked about. */
200
223
  export interface ToolReviewRequest {
201
224
  /** Originating session, preserved by createReviewHandler for host attribution. */
@@ -203,6 +226,14 @@ export interface ToolReviewRequest {
203
226
  /** Originating turn, preserved by createReviewHandler for host attribution. */
204
227
  readonly turnId?: TurnId
205
228
  readonly toolCalls: readonly ToolCallSummary[]
229
+ /**
230
+ * This batch would send the screen to the model provider for the first
231
+ * time in the session (see {@link ReviewPolicyOptions.screenConsent}).
232
+ * The question is whether the model may see the screen for the rest of
233
+ * the session; a yes also approves this batch, and later screen captures
234
+ * in the session run without asking.
235
+ */
236
+ readonly screenConsent?: true
206
237
  }
207
238
 
208
239
  export type ToolReviewAnswer =
@@ -249,6 +280,24 @@ export interface ReviewPolicyOptions {
249
280
  * either way.
250
281
  */
251
282
  readonly skillGrants?: 'honour' | 'ignore'
283
+ /**
284
+ * Ask once per session before the model first sees the screen.
285
+ *
286
+ * With a record, a batch holding a call that captures the screen
287
+ * ({@link capturesScreen}) in a session not yet in `sessions` is put to
288
+ * a person first, as a {@link ToolReviewRequest.screenConsent} request,
289
+ * even when every call in it only reads: in `prompt`, `accept-edits` and
290
+ * `plan`. A yes adds the session; a no refuses the batch. `strict`
291
+ * refuses such a call unless a rule allowed it, `auto` never asks, and a
292
+ * policy without a `prompt` refuses. A call a rule allowed is never
293
+ * asked about. Omitted, the screen is treated like any other read.
294
+ */
295
+ readonly screenConsent?: ScreenConsentRecord
296
+ /**
297
+ * Which calls capture the screen. Default: the tool's own
298
+ * `capturesScreen` declaration, read from `registry`; nothing without one.
299
+ */
300
+ readonly capturesScreen?: (name: string, input: unknown) => boolean
252
301
  }
253
302
 
254
303
  /**
@@ -275,10 +324,61 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
275
324
  options.exempt ??
276
325
  (registry ? (name, input) => isReviewExempt(registry, name, input) : () => false)
277
326
  const remembered = options.remembered ?? { all: false }
327
+ const capturesScreen =
328
+ options.capturesScreen ??
329
+ (registry
330
+ ? (name: string, input: unknown) => {
331
+ const tool = registry.get(name) ?? registry.get(name.toLowerCase())
332
+ return tool?.capturesScreen?.(input) === true
333
+ }
334
+ : () => false)
278
335
  return async (request): Promise<HITLResumeDecision> => {
279
336
  if (request.type !== 'tool_review') {
280
337
  return request.type === 'plan_approval' ? { action: 'approve_plan' } : { action: 'continue' }
281
338
  }
339
+ // The one answer a person gives for this batch. The screen question
340
+ // below shows the whole batch, so its yes also answers any question
341
+ // the rest of this function would ask; nobody is asked twice.
342
+ let answered: ToolReviewAnswer | undefined
343
+ const ask = async (screenConsent?: true): Promise<ToolReviewAnswer> =>
344
+ answered ??
345
+ (prompt as ToolReviewPrompt)({
346
+ sessionId: request.sessionId,
347
+ turnId: request.turnId,
348
+ toolCalls: request.toolCalls,
349
+ ...(screenConsent ? { screenConsent } : {}),
350
+ })
351
+ // The first look at the screen in a session is the operator's to allow:
352
+ // what is on it goes to the model provider, and a screenshot reads as
353
+ // harmlessly as a file read to every rule below. Asked once, in the
354
+ // modes where a person decides; a rule that allowed the call already
355
+ // said yes, and a call a rule denied will not run.
356
+ const consent = options.screenConsent
357
+ const sessionKey = String(request.sessionId ?? '')
358
+ if (
359
+ consent &&
360
+ mode !== 'auto' &&
361
+ !consent.sessions.has(sessionKey) &&
362
+ request.toolCalls.some(
363
+ (tc) =>
364
+ tc.authorization?.decision !== 'allow' &&
365
+ tc.authorization?.decision !== 'deny' &&
366
+ capturesScreen(tc.name, tc.input),
367
+ )
368
+ ) {
369
+ if (mode === 'strict') return { action: 'reject_tools', feedback: STRICT_MODE_REFUSAL }
370
+ if (!prompt) return { action: 'reject_tools', feedback: SCREEN_CONSENT_UNATTENDED_REFUSAL }
371
+ const answer = await ask(true)
372
+ if (answer.kind === 'reject') {
373
+ return {
374
+ action: 'reject_tools',
375
+ feedback: answer.feedback ?? SCREEN_CONSENT_DECLINED_FEEDBACK,
376
+ }
377
+ }
378
+ consent.sessions.add(sessionKey)
379
+ if (answer.kind === 'approve-all') remembered.all = true
380
+ answered = answer
381
+ }
282
382
  if (!batchNeedsReview(request.toolCalls, exempt)) {
283
383
  return { action: 'approve_tools' }
284
384
  }
@@ -325,15 +425,11 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
325
425
  ? { action: 'approve_tools', confirmedEscalations: escapes }
326
426
  : { action: 'reject_tools', feedback: SANDBOX_ESCAPE_UNATTENDED_REFUSAL }
327
427
  }
328
- const answer = await prompt({
329
- sessionId: request.sessionId,
330
- turnId: request.turnId,
331
- toolCalls: request.toolCalls,
332
- })
428
+ const answer = await ask()
333
429
  if (answer.kind === 'reject') {
334
430
  return {
335
431
  action: 'reject_tools',
336
- feedback: answer.feedback ?? 'User declined to run the proposed tool(s).',
432
+ feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
337
433
  }
338
434
  }
339
435
  // "Approve all" still latches for the calls that follow; it never
@@ -347,15 +443,11 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
347
443
  // With nobody to ask it is refused, not approved.
348
444
  if (request.toolCalls.some((tc) => (tc.escalation?.outsidePaths?.length ?? 0) > 0)) {
349
445
  if (!prompt) return { action: 'reject_tools', feedback: OUTSIDE_ROOTS_UNATTENDED_REFUSAL }
350
- const answer = await prompt({
351
- sessionId: request.sessionId,
352
- turnId: request.turnId,
353
- toolCalls: request.toolCalls,
354
- })
446
+ const answer = await ask()
355
447
  if (answer.kind === 'reject') {
356
448
  return {
357
449
  action: 'reject_tools',
358
- feedback: answer.feedback ?? 'User declined to run the proposed tool(s).',
450
+ feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
359
451
  }
360
452
  }
361
453
  // Latches for the ordinary calls that follow, never for the next path.
@@ -382,6 +474,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
382
474
  ),
383
475
  )
384
476
  if (
477
+ answered === undefined &&
385
478
  options.skillGrants !== 'ignore' &&
386
479
  needsPerson.length > 0 &&
387
480
  needsPerson.every(isSkillGranted)
@@ -391,11 +484,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
391
484
  skillGranted: needsPerson.map((tc) => tc.id),
392
485
  }
393
486
  }
394
- const answer = await prompt({
395
- sessionId: request.sessionId,
396
- turnId: request.turnId,
397
- toolCalls: request.toolCalls,
398
- })
487
+ const answer = await ask()
399
488
  switch (answer.kind) {
400
489
  case 'approve':
401
490
  return { action: 'approve_tools' }
@@ -405,7 +494,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
405
494
  case 'reject':
406
495
  return {
407
496
  action: 'reject_tools',
408
- feedback: answer.feedback ?? 'User declined to run the proposed tool(s).',
497
+ feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
409
498
  }
410
499
  }
411
500
  }
@@ -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
+ }