@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
@@ -0,0 +1,96 @@
1
+ /**
2
+ * A tripwire over a scheduled prompt, run when the job is proposed.
3
+ *
4
+ * A scheduled prompt runs later with nobody watching, so a prompt that hides
5
+ * characters, tells the model to ignore its instructions, or reads secrets and
6
+ * sends them somewhere deserves a second look before anyone confirms it.
7
+ * Findings are shown to the person confirming; they never block on their own,
8
+ * because the operator may mean exactly what they wrote.
9
+ */
10
+
11
+ /** Characters that render as nothing or reorder what is shown. */
12
+ /** Invisible and direction-changing code point ranges, inclusive. */
13
+ const INVISIBLE_RANGES: readonly (readonly [number, number])[] = [
14
+ [0x200b, 0x200f],
15
+ [0x202a, 0x202e],
16
+ [0x2060, 0x2064],
17
+ [0x2066, 0x2069],
18
+ [0xfeff, 0xfeff],
19
+ [0x00ad, 0x00ad],
20
+ ]
21
+
22
+ function isInvisible(code: number): boolean {
23
+ return INVISIBLE_RANGES.some(([lo, hi]) => code >= lo && code <= hi)
24
+ }
25
+
26
+ function hasInvisible(text: string): boolean {
27
+ for (const ch of text) if (isInvisible(ch.codePointAt(0) ?? 0)) return true
28
+ return false
29
+ }
30
+
31
+ /** C0 controls other than tab, newline and carriage return, and DEL. */
32
+ function isControl(code: number): boolean {
33
+ return (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) || code === 0x7f
34
+ }
35
+
36
+ function hasControl(text: string): boolean {
37
+ for (let i = 0; i < text.length; i++) if (isControl(text.charCodeAt(i))) return true
38
+ return false
39
+ }
40
+
41
+ const DIRECTIVES: readonly RegExp[] = [
42
+ /\bignore (all |any )?(the )?(previous|prior|above|earlier) (instructions|rules|messages)\b/i,
43
+ /\bdisregard (all |any )?(the )?(previous|prior|above|system) /i,
44
+ /\b(reveal|print|show|repeat) (your|the) (system prompt|instructions)\b/i,
45
+ /\byou are no longer\b/i,
46
+ ]
47
+
48
+ const SECRET_READS: readonly RegExp[] = [
49
+ /~?\/?\.ssh\b|\bid_(rsa|ed25519|ecdsa)\b/i,
50
+ /\.aws\/credentials|\.config\/gcloud|\.kube\/config|\.netrc\b|\.npmrc\b|\.pypirc\b/i,
51
+ /\bcredentials\.json\b|\.env\b/i,
52
+ /\b(printenv|env)\b\s*($|[|;&>])/i,
53
+ /\b[A-Z0-9_]*(API_KEY|SECRET|TOKEN|PASSWORD)\b/,
54
+ ]
55
+
56
+ const EXFILTRATION: readonly RegExp[] = [
57
+ /\b(curl|wget)\b[^\n|]*\|\s*(ba|z|da)?sh\b/i,
58
+ /\bcurl\b[^\n]*\s(-d|--data(-binary|-raw)?|-F|--form|-T|--upload-file|-X\s*(POST|PUT))\b/i,
59
+ /\b(nc|ncat|netcat|socat)\b\s+\S+\s+\d+/i,
60
+ /\bbase64\b[^\n]*\|\s*(curl|wget|nc)\b/i,
61
+ ]
62
+
63
+ /** What the scan found, as sentences for the confirmation screen. Empty when nothing. */
64
+ export function scanSchedulePrompt(prompt: string): string[] {
65
+ const findings: string[] = []
66
+ if (hasInvisible(prompt)) {
67
+ findings.push('The prompt contains invisible or direction-changing characters.')
68
+ }
69
+ if (hasControl(prompt)) findings.push('The prompt contains control characters.')
70
+ if (DIRECTIVES.some((re) => re.test(prompt))) {
71
+ findings.push('The prompt tells the model to ignore or reveal its instructions.')
72
+ }
73
+ if (SECRET_READS.some((re) => re.test(prompt))) {
74
+ findings.push('The prompt mentions credentials, keys or secret files.')
75
+ }
76
+ if (EXFILTRATION.some((re) => re.test(prompt))) {
77
+ findings.push('The prompt pipes a download into a shell or sends data to a remote address.')
78
+ }
79
+ return findings
80
+ }
81
+
82
+ /**
83
+ * The prompt with every invisible and control character made visible as
84
+ * `<U+XXXX>`, so the person confirming sees what the model will read.
85
+ */
86
+ export function revealHiddenCharacters(prompt: string): string {
87
+ let out = ''
88
+ for (const ch of prompt) {
89
+ const code = ch.codePointAt(0) ?? 0
90
+ out +=
91
+ isControl(code) || isInvisible(code)
92
+ ? `<U+${code.toString(16).toUpperCase().padStart(4, '0')}>`
93
+ : ch
94
+ }
95
+ return out
96
+ }
@@ -0,0 +1,376 @@
1
+ import { z } from 'zod'
2
+ import type { ToolContext, ToolDefinition, ToolResult } from '../../types/tool/index.js'
3
+ import { canonicalizeBrowserSitePattern } from '../builtins/browser-url.js'
4
+ import { defineTool } from '../defineTool.js'
5
+ import { presentScheduleCall, presentScheduleResult } from './present.js'
6
+ import { scanSchedulePrompt } from './prompt-scan.js'
7
+ import type {
8
+ ScheduleBrowserGrant,
9
+ ScheduleBrowserSiteLevel,
10
+ ScheduleJobDraft,
11
+ ScheduleToolHost,
12
+ } from './types.js'
13
+
14
+ export const SCHEDULE_TOOL_NAME = 'schedule'
15
+
16
+ /**
17
+ * A person reads the whole proposal — the prompt, the rules, the schedule,
18
+ * the credential source — before answering `create`, `resume` or `delete`.
19
+ * The executor's `DEFAULT_TOOL_TIMEOUT_MS` (two minutes) is sized for a tool
20
+ * call, not a person on the other end of a screen, and would abandon the
21
+ * call out from under them mid-read. `save_skill`
22
+ * (`packages/cli/src/skills/save.ts`) waits on the same kind of answer and
23
+ * uses the same thirty minutes, for the same reason.
24
+ */
25
+ const OPERATOR_CONFIRM_TIMEOUT_MS = 30 * 60_000
26
+
27
+ const EFFECT = z.enum(['allow', 'ask', 'deny'])
28
+ const NETWORK_TOOLS = ['web_fetch', 'web_search']
29
+ const BROWSER_PROFILE = /^[a-z0-9][a-z0-9-]{0,62}$/
30
+
31
+ const inputSchema = z.object({
32
+ action: z
33
+ .enum(['create', 'list', 'pause', 'resume', 'delete'])
34
+ .describe('What to do. create, resume and delete ask the operator first.'),
35
+ name: z
36
+ .string()
37
+ .regex(/^[a-z0-9][a-z0-9-]{0,62}$/)
38
+ .optional()
39
+ .describe('create: job name, lowercase letters, digits and dashes'),
40
+ prompt: z.string().min(1).max(20_000).optional().describe('create: what the run is asked to do'),
41
+ when: z
42
+ .string()
43
+ .optional()
44
+ .describe('create: "every 30m", "0 9 * * 1-5" (cron), "at 2026-09-24 09:00", "in 2h"'),
45
+ folder: z
46
+ .string()
47
+ .optional()
48
+ .describe("create: folder to run in; leave unset for the session's unless the user named one"),
49
+ tz: z
50
+ .string()
51
+ .optional()
52
+ .describe(
53
+ "create: IANA time zone; leave unset for the operator's own zone unless the user named another",
54
+ ),
55
+ permissions: z
56
+ .object({
57
+ preset: z
58
+ .enum(['read-only', 'edit-in-folder'])
59
+ .optional()
60
+ .describe(
61
+ 'read-only: read/glob/grep/ls only. edit-in-folder: also edit and write, bash asks',
62
+ ),
63
+ unmatched: z
64
+ .enum(['park', 'deny'])
65
+ .describe('A call no rule covers: park (wait for the operator) or deny'),
66
+ execution: z
67
+ .enum(['host', 'sandbox'])
68
+ .optional()
69
+ .describe(
70
+ 'Where commands run; leave unset (this machine) unless the user asked for a sandbox',
71
+ ),
72
+ rules: z
73
+ .record(z.string(), z.union([EFFECT, z.record(z.string(), EFFECT)]))
74
+ .optional()
75
+ .describe('Extra rules, e.g. {"bash": {"npm test*": "allow"}}'),
76
+ browser: z
77
+ .object({
78
+ profile: z
79
+ .string()
80
+ .regex(BROWSER_PROFILE)
81
+ .describe('Browser profile the operator signed in with, e.g. "work"'),
82
+ sites: z
83
+ .record(z.string(), z.enum(['read', 'ask', 'act']))
84
+ .describe(
85
+ 'Site to level, e.g. {"https://github.com": "read"}. read: open and read only; ask: changes wait for the operator; act: changes run unasked. Unlisted sites are denied; there is no "*".',
86
+ ),
87
+ headed: z.boolean().optional().describe('Show the browser window; default no window'),
88
+ })
89
+ .optional()
90
+ .describe('Browser access; omit for none'),
91
+ })
92
+ .optional()
93
+ .describe('create: REQUIRED explicit permission set; there is no default'),
94
+ budget: z
95
+ .object({
96
+ maxIterations: z
97
+ .number()
98
+ .int()
99
+ .positive()
100
+ .optional()
101
+ .describe(
102
+ 'Model steps one run may take (each model call with its tool calls is one step), not how many times the job runs. Omit for the default; a browser task takes 10 or more.',
103
+ ),
104
+ tokenBudget: z
105
+ .number()
106
+ .int()
107
+ .positive()
108
+ .optional()
109
+ .describe(
110
+ 'Tokens one run may spend in total. Every model call resends the whole prompt (often 10,000-30,000 tokens each), so a run needs far more than its answer; omit for the default.',
111
+ ),
112
+ timeoutMs: z
113
+ .number()
114
+ .int()
115
+ .positive()
116
+ .optional()
117
+ .describe('Wall clock of one run, in milliseconds'),
118
+ })
119
+ .optional()
120
+ .describe('Limits of ONE run; omit to use the defaults'),
121
+ job: z.string().optional().describe('pause/resume/delete: job name'),
122
+ allFolders: z.boolean().optional().describe('list: include jobs of other folders (names only)'),
123
+ })
124
+
125
+ type Input = z.infer<typeof inputSchema>
126
+
127
+ function refuse(error: string): ToolResult {
128
+ return { success: false, output: '', error }
129
+ }
130
+
131
+ function effectsOf(rule: unknown): string[] {
132
+ if (typeof rule === 'string') return [rule]
133
+ if (rule && typeof rule === 'object') return Object.values(rule as Record<string, string>)
134
+ return []
135
+ }
136
+
137
+ /**
138
+ * Refuse a network tool beside a shell that runs on the host: a scheduled run
139
+ * that can both read the machine and reach the internet, unattended, is the
140
+ * exfiltration shape, and only the operator can choose it.
141
+ */
142
+ function networkWithHostShell(draft: ScheduleJobDraft): boolean {
143
+ const rules = draft.permissions.rules ?? {}
144
+ const network =
145
+ draft.permissions.browser !== undefined ||
146
+ NETWORK_TOOLS.some((t) => effectsOf(rules[t]).some((e) => e === 'allow' || e === 'ask'))
147
+ if (!network) return false
148
+ const bashEffects = effectsOf(rules.bash)
149
+ // The read-only preset denies bash; its rules are expanded by the host,
150
+ // so the draft carries only its name.
151
+ const shellPossible =
152
+ bashEffects.length > 0
153
+ ? bashEffects.some((e) => e !== 'deny')
154
+ : draft.permissions.preset !== 'read-only' && draft.permissions.unmatched !== 'deny'
155
+ return shellPossible && (draft.permissions.execution ?? 'host') === 'host'
156
+ }
157
+
158
+ /**
159
+ * The browser grant with its site keys canonicalised, or why it is refused.
160
+ * Keys are rewritten so the host, the confirmation and the compiled rules
161
+ * all see one spelling of each site.
162
+ */
163
+ function browserGrant(
164
+ host: ScheduleToolHost,
165
+ proposed: NonNullable<NonNullable<Input['permissions']>['browser']>,
166
+ ): { ok: true; grant: ScheduleBrowserGrant } | { ok: false; error: string } {
167
+ if (host.browserGrants !== true) {
168
+ return {
169
+ ok: false,
170
+ error:
171
+ 'This host cannot give a scheduled job browser access. Propose the job without permissions.browser, or ask the operator to add it with `namzu schedule add`.',
172
+ }
173
+ }
174
+ const entries = Object.entries(proposed.sites)
175
+ if (entries.length === 0) {
176
+ return {
177
+ ok: false,
178
+ error:
179
+ 'permissions.browser.sites is empty; list each site the run may open, e.g. {"https://github.com": "read"}.',
180
+ }
181
+ }
182
+ const sites: Record<string, ScheduleBrowserSiteLevel> = {}
183
+ for (const [key, level] of entries) {
184
+ if (key.trim() === '*') {
185
+ return {
186
+ ok: false,
187
+ error:
188
+ 'A scheduled job cannot grant every site ("*"); unlisted sites are always denied. List each site.',
189
+ }
190
+ }
191
+ const verdict = canonicalizeBrowserSitePattern(key)
192
+ if (!verdict.ok) return { ok: false, error: `permissions.browser.sites: ${verdict.reason}` }
193
+ if (verdict.pattern in sites && sites[verdict.pattern] !== level) {
194
+ return {
195
+ ok: false,
196
+ error: `permissions.browser.sites names ${verdict.pattern} twice with different levels.`,
197
+ }
198
+ }
199
+ sites[verdict.pattern] = level
200
+ }
201
+ return {
202
+ ok: true,
203
+ grant: {
204
+ profile: proposed.profile,
205
+ sites,
206
+ ...(proposed.headed !== undefined ? { headed: proposed.headed } : {}),
207
+ },
208
+ }
209
+ }
210
+
211
+ async function create(
212
+ host: ScheduleToolHost,
213
+ input: Input,
214
+ signal: AbortSignal | undefined,
215
+ ): Promise<ToolResult> {
216
+ const missing = (['name', 'prompt', 'when', 'permissions'] as const).filter(
217
+ (k) => input[k] === undefined,
218
+ )
219
+ if (missing.length > 0) {
220
+ return refuse(
221
+ `create needs ${missing.join(', ')}. permissions is required: propose an explicit set (a preset and/or rules, plus unmatched).`,
222
+ )
223
+ }
224
+ const proposed = input.permissions as NonNullable<Input['permissions']>
225
+ if (!proposed.preset && !proposed.rules && !proposed.browser) {
226
+ return refuse(
227
+ 'permissions needs a preset, rules or a browser grant; an empty permission set is not a choice.',
228
+ )
229
+ }
230
+ let permissions: ScheduleJobDraft['permissions'] = proposed
231
+ if (proposed.browser) {
232
+ const grant = browserGrant(host, proposed.browser)
233
+ if (!grant.ok) return refuse(grant.error)
234
+ permissions = { ...proposed, browser: grant.grant }
235
+ }
236
+ const draft: ScheduleJobDraft = {
237
+ name: input.name as string,
238
+ prompt: input.prompt as string,
239
+ when: input.when as string,
240
+ ...(input.folder !== undefined ? { folder: input.folder } : {}),
241
+ ...(input.tz !== undefined ? { tz: input.tz } : {}),
242
+ permissions,
243
+ ...(input.budget ? { budget: input.budget } : {}),
244
+ }
245
+ if (networkWithHostShell(draft)) {
246
+ return refuse(
247
+ 'A scheduled job proposed here cannot combine web or browser access with a shell on the host. Deny bash, use execution "sandbox", or ask the operator to create it with `namzu schedule add`.',
248
+ )
249
+ }
250
+ let preview: Awaited<ReturnType<ScheduleToolHost['preview']>>
251
+ try {
252
+ preview = await host.preview(draft)
253
+ } catch (error) {
254
+ return refuse(error instanceof Error ? error.message : String(error))
255
+ }
256
+ let answer: string
257
+ try {
258
+ answer = await host.confirm(
259
+ {
260
+ preview,
261
+ promptFindings: scanSchedulePrompt(preview.prompt),
262
+ proposedBy: 'model',
263
+ },
264
+ signal,
265
+ )
266
+ } catch {
267
+ answer = 'cancel'
268
+ }
269
+ // The deadline or the turn can settle `host.confirm` from underneath a
270
+ // host that resolves optimistically on its own close; never create on an
271
+ // answer that arrived after the operator was no longer being asked.
272
+ if (signal?.aborted) answer = 'cancel'
273
+ if (answer !== 'create' && answer !== 'create-paused') {
274
+ return {
275
+ success: false,
276
+ output: '',
277
+ error: 'The operator did not confirm the job, so it was not created.',
278
+ data: { cancelled: true },
279
+ }
280
+ }
281
+ const created = await host.create(draft, preview, { paused: answer === 'create-paused' })
282
+ const said =
283
+ answer === 'create-paused'
284
+ ? `Job "${created.name}" was created paused. The operator can resume it with /schedule.`
285
+ : `Job "${created.name}" was created. It runs ${preview.schedule}, with nobody watching; results arrive as a notification and a session.`
286
+ return {
287
+ success: true,
288
+ output: created.note ? `${said} ${created.note}` : said,
289
+ data: { name: created.name, paused: answer === 'create-paused' },
290
+ }
291
+ }
292
+
293
+ async function list(host: ScheduleToolHost, input: Input): Promise<ToolResult> {
294
+ const jobs = await host.list({ allFolders: input.allFolders === true })
295
+ if (jobs.length === 0) return { success: true, output: 'No scheduled jobs.', data: { jobs: [] } }
296
+ const lines = jobs.map(
297
+ (j) =>
298
+ `${j.name} · ${j.state} · ${j.schedule}${j.nextFireAt ? ` · next ${j.nextFireAt}` : ''}${j.lastStatus ? ` · last ${j.lastStatus}` : ''} · ${j.folder}`,
299
+ )
300
+ return { success: true, output: lines.join('\n'), data: { jobs } }
301
+ }
302
+
303
+ async function lifecycle(
304
+ host: ScheduleToolHost,
305
+ input: Input,
306
+ action: 'pause' | 'resume' | 'delete',
307
+ signal: AbortSignal | undefined,
308
+ ): Promise<ToolResult> {
309
+ if (!input.job) return refuse(`${action} needs job (the job's name).`)
310
+ const job = await host.find(input.job)
311
+ if (!job) return refuse(`No scheduled job is named "${input.job}".`)
312
+ if (action !== 'pause') {
313
+ let confirmed = false
314
+ try {
315
+ confirmed = await host.confirmAction(job, action, signal)
316
+ } catch {
317
+ confirmed = false
318
+ }
319
+ if (signal?.aborted) confirmed = false
320
+ if (!confirmed)
321
+ return {
322
+ ...refuse(`The operator did not confirm; "${job.name}" was not changed.`),
323
+ data: { cancelled: true },
324
+ }
325
+ }
326
+ if (action === 'pause') await host.pause(job.name)
327
+ else if (action === 'resume') await host.resume(job.name)
328
+ else await host.delete(job.name)
329
+ const verb = action === 'pause' ? 'paused' : action === 'resume' ? 'resumed' : 'deleted'
330
+ return { success: true, output: `Job "${job.name}" ${verb}.`, data: { name: job.name, action } }
331
+ }
332
+
333
+ /**
334
+ * The `schedule` tool: create, list, pause, resume and delete the operator's
335
+ * scheduled jobs from a conversation.
336
+ *
337
+ * Creation, resuming and deleting are confirmed by a person through the host
338
+ * (`ScheduleToolHost.confirm`), on a screen the host draws from its own
339
+ * computation. The model cannot propose that uncovered calls run without
340
+ * asking, nor web access beside a host shell. Register it only where a person
341
+ * is present to confirm: never in a headless run, a scheduled run or a
342
+ * sub-agent.
343
+ */
344
+ export function buildScheduleTools(host: ScheduleToolHost): ToolDefinition[] {
345
+ return [
346
+ defineTool({
347
+ name: SCHEDULE_TOOL_NAME,
348
+ description:
349
+ "Manage the operator's scheduled jobs: prompts that run later in a folder, with nobody watching, under an explicit permission set. Use it only when the user asks for something to happen on a schedule. create, resume and delete are confirmed by the operator; pause is not. A job needs name, prompt, when and permissions (unmatched: park or deny, plus a preset, rules or a browser grant). Leave every other field (folder, tz, execution, budget, headed) unset unless the user asked for it: the defaults are the operator's, and the confirmation marks each value you chose. Scheduled runs cannot ask questions.",
350
+ inputSchema,
351
+ category: 'custom',
352
+ permissions: [],
353
+ readOnly: (input) => input.action === 'list',
354
+ // `delete` removes a job only after the operator confirms it on the
355
+ // host's own screen; a destructive flag would put a second review,
356
+ // over the raw arguments, in front of that confirmation.
357
+ destructive: false,
358
+ concurrencySafe: false,
359
+ presentCall: presentScheduleCall,
360
+ presentResult: presentScheduleResult,
361
+ // A person, not a tool, answers `create`/`resume`/`delete`; see
362
+ // `OPERATOR_CONFIRM_TIMEOUT_MS`.
363
+ timeoutMs: OPERATOR_CONFIRM_TIMEOUT_MS,
364
+ async execute(input, context: ToolContext) {
365
+ switch (input.action) {
366
+ case 'create':
367
+ return create(host, input, context.abortSignal)
368
+ case 'list':
369
+ return list(host, input)
370
+ default:
371
+ return lifecycle(host, input, input.action, context.abortSignal)
372
+ }
373
+ },
374
+ }),
375
+ ]
376
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The host side of the `schedule` and `session_loop` tools.
3
+ *
4
+ * The SDK holds the tool's contract with the model — its schema, its refusals
5
+ * and its confirmation rule — and nothing about where jobs are stored or how
6
+ * a confirmation is drawn. The host supplies both, and it computes every
7
+ * field a person is asked to confirm: the model's own words are never what
8
+ * the confirmation shows as fact.
9
+ */
10
+
11
+ /** A rule effect, in the vocabulary of the CLI's `[permissions]` table. */
12
+ export type ScheduleRuleEffect = 'allow' | 'ask' | 'deny'
13
+
14
+ /** What the model proposes. Validated by the tool's schema, then by the host. */
15
+ export interface ScheduleJobDraft {
16
+ readonly name: string
17
+ readonly prompt: string
18
+ /** Schedule words: `every 30m`, `0 9 * * 1-5`, `at 09:00`, `in 2h`. */
19
+ readonly when: string
20
+ /** Folder the job runs in. Absent: the session's working directory. */
21
+ readonly folder?: string
22
+ /** IANA zone for a cron expression or a local time. Absent: the host's. */
23
+ readonly tz?: string
24
+ readonly permissions: {
25
+ readonly preset?: 'read-only' | 'edit-in-folder'
26
+ /**
27
+ * What happens to a call no rule covers. The model may choose `park`
28
+ * (wait for the operator) or `deny`; running uncovered calls without
29
+ * asking is an operator decision the tool cannot propose.
30
+ */
31
+ readonly unmatched: 'park' | 'deny'
32
+ readonly execution?: 'host' | 'sandbox'
33
+ readonly rules?: Readonly<
34
+ Record<string, ScheduleRuleEffect | Readonly<Record<string, ScheduleRuleEffect>>>
35
+ >
36
+ /**
37
+ * Browser access for the run. Absent: the `browser` and `browser_act`
38
+ * tools are denied. Present: only the listed sites are reachable, at
39
+ * the listed level, and every other site is denied — there is no
40
+ * catch-all key. The tool refuses this block unless the host sets
41
+ * {@link ScheduleToolHost.browserGrants}.
42
+ */
43
+ readonly browser?: ScheduleBrowserGrant
44
+ }
45
+ readonly budget?: {
46
+ readonly maxIterations?: number
47
+ readonly tokenBudget?: number
48
+ readonly timeoutMs?: number
49
+ }
50
+ }
51
+
52
+ /**
53
+ * How far a scheduled run may go on one site. `read`: open and read pages,
54
+ * never change them. `ask`: open and change pages, each change parked for the
55
+ * operator. `act`: open and change pages without asking.
56
+ */
57
+ export type ScheduleBrowserSiteLevel = 'read' | 'ask' | 'act'
58
+
59
+ /** A scheduled job's browser grant. */
60
+ export interface ScheduleBrowserGrant {
61
+ /** The browser profile the run uses; the operator's, signed in beforehand. */
62
+ readonly profile: string
63
+ /**
64
+ * Canonical site key (`https://github.com`, `https://*.example.com`,
65
+ * `http://localhost:*`) to level. The tool canonicalises the keys the
66
+ * model wrote (see `canonicalizeBrowserSitePattern`) before the host sees
67
+ * them. Unlisted sites are denied.
68
+ */
69
+ readonly sites: Readonly<Record<string, ScheduleBrowserSiteLevel>>
70
+ /** Show the browser window during the run. Default: no window. */
71
+ readonly headed?: boolean
72
+ }
73
+
74
+ /** What the person confirming is shown. Every field is the host's own computation. */
75
+ export interface ScheduleJobPreview {
76
+ readonly name: string
77
+ /** The canonical folder the job would run in. */
78
+ readonly folder: string
79
+ /** True when `folder` is outside the session's working directory and added directories. */
80
+ readonly outsideSessionRoots: boolean
81
+ readonly prompt: string
82
+ /** The schedule in words, with its zone. */
83
+ readonly schedule: string
84
+ /** The next fire times, ISO-8601 UTC. */
85
+ readonly nextFireTimes: readonly string[]
86
+ /** The permission set expanded to one line per rule, config denies included. */
87
+ readonly rules: readonly string[]
88
+ readonly unmatched: 'park' | 'deny' | 'allow'
89
+ readonly execution: 'host' | 'sandbox'
90
+ /** A rule lets the run reach the network. */
91
+ readonly networkAccess: boolean
92
+ readonly budget: {
93
+ readonly maxIterations: number
94
+ readonly tokenBudget: number
95
+ readonly timeoutMs: number
96
+ }
97
+ /** Runs per day at most, times the token budget. Absent for a one-shot. */
98
+ readonly dailyTokenCeiling?: number
99
+ /** Provider and model the job is pinned to. */
100
+ readonly model: string
101
+ /** Where the run's credential comes from, in words. */
102
+ readonly credentialSource?: string
103
+ /** Anything the host wants said in the warning colour. */
104
+ readonly warnings: readonly string[]
105
+ }
106
+
107
+ /** A job as the `list` action reports it. */
108
+ export interface ScheduleJobSummary {
109
+ readonly name: string
110
+ readonly folder: string
111
+ readonly state: string
112
+ readonly schedule: string
113
+ readonly nextFireAt?: string
114
+ readonly lastStatus?: string
115
+ /** Present only for jobs in the session's own folder. */
116
+ readonly prompt?: string
117
+ }
118
+
119
+ /** What the person answered to a proposed job. */
120
+ export type ScheduleConfirmAnswer = 'create' | 'create-paused' | 'cancel'
121
+
122
+ export interface ScheduleConfirmRequest {
123
+ readonly preview: ScheduleJobPreview
124
+ /** The prompt tripwire's findings over `preview.prompt`. */
125
+ readonly promptFindings: readonly string[]
126
+ /** Always `model` from this tool: the banner says it was not the operator. */
127
+ readonly proposedBy: 'model'
128
+ }
129
+
130
+ export interface ScheduleToolHost {
131
+ /**
132
+ * The host can store and enforce {@link ScheduleJobDraft.permissions}
133
+ * `.browser`. Absent or false: the tool refuses a draft carrying one,
134
+ * rather than let the host drop it and confirm a job the model believes
135
+ * can use the browser.
136
+ */
137
+ readonly browserGrants?: boolean
138
+ /** Validate the draft and compute what the person will be shown. Throws with a message on a refusal. */
139
+ preview(draft: ScheduleJobDraft): Promise<ScheduleJobPreview>
140
+ /**
141
+ * Ask the person. Anything but `create` or `create-paused` — `cancel`, a
142
+ * thrown error, a closed screen — means no job.
143
+ *
144
+ * `signal` fires when the tool's own confirmation deadline elapses or the
145
+ * turn is aborted; a host that draws a screen closes it then rather than
146
+ * leaving it live after the tool has given up on the answer.
147
+ */
148
+ confirm(request: ScheduleConfirmRequest, signal?: AbortSignal): Promise<ScheduleConfirmAnswer>
149
+ /**
150
+ * Create the job the person confirmed. `note`, when given, is appended to
151
+ * what the model is told, for what the person must still do before the
152
+ * job runs (install a scheduler, say): the model reports the job as set
153
+ * up otherwise.
154
+ */
155
+ create(
156
+ draft: ScheduleJobDraft,
157
+ preview: ScheduleJobPreview,
158
+ options: { readonly paused: boolean },
159
+ ): Promise<{ readonly name: string; readonly note?: string }>
160
+ /** Jobs, the session folder's in full, other folders' without their prompts. */
161
+ list(options: { readonly allFolders: boolean }): Promise<readonly ScheduleJobSummary[]>
162
+ /** A job by name or id prefix, or undefined. */
163
+ find(job: string): Promise<ScheduleJobSummary | undefined>
164
+ /** Ask the person to confirm resuming or deleting a job. See `confirm` on `signal`. */
165
+ confirmAction(
166
+ job: ScheduleJobSummary,
167
+ action: 'resume' | 'delete',
168
+ signal?: AbortSignal,
169
+ ): Promise<boolean>
170
+ pause(job: string): Promise<void>
171
+ resume(job: string): Promise<void>
172
+ delete(job: string): Promise<void>
173
+ }
174
+
175
+ /** A prompt the session re-sends to itself on an interval. */
176
+ export interface SessionLoop {
177
+ readonly id: string
178
+ /** The interval in words. */
179
+ readonly schedule: string
180
+ readonly prompt: string
181
+ readonly createdAt: string
182
+ readonly expiresAt?: string
183
+ readonly lastFiredAt?: string
184
+ readonly createdBy: 'operator' | 'model'
185
+ }
186
+
187
+ export interface SessionLoopHost {
188
+ /** Throws with a message when the interval is not usable or the loop limit is reached. */
189
+ create(request: {
190
+ readonly interval: string
191
+ readonly prompt: string
192
+ readonly createdBy: 'model'
193
+ }): Promise<SessionLoop>
194
+ list(): readonly SessionLoop[]
195
+ /** Stop one loop by id, or every loop with `all`. Returns how many stopped. */
196
+ delete(id: string): Promise<number>
197
+ }