@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,155 @@
1
+ /**
2
+ * Time-zone arithmetic over `Intl` alone.
3
+ *
4
+ * The SDK carries no time-zone database of its own: the runtime's ICU data is
5
+ * the source, read through `Intl.DateTimeFormat#formatToParts`. Everything in
6
+ * this file is expressed in "wall" milliseconds — a local date and time
7
+ * written as if it were UTC (`Date.UTC(y, m - 1, d, h, min)`) — so local
8
+ * arithmetic is plain integer arithmetic and only the conversions touch Intl.
9
+ */
10
+
11
+ import { ScheduleValidationError } from './errors.js'
12
+
13
+ const MINUTE = 60_000
14
+ const HOUR = 60 * MINUTE
15
+
16
+ const formatters = new Map<string, Intl.DateTimeFormat>()
17
+
18
+ function formatterFor(tz: string): Intl.DateTimeFormat {
19
+ let f = formatters.get(tz)
20
+ if (!f) {
21
+ f = new Intl.DateTimeFormat('en-US', {
22
+ timeZone: tz,
23
+ hourCycle: 'h23',
24
+ year: 'numeric',
25
+ month: 'numeric',
26
+ day: 'numeric',
27
+ hour: 'numeric',
28
+ minute: 'numeric',
29
+ second: 'numeric',
30
+ })
31
+ formatters.set(tz, f)
32
+ }
33
+ return f
34
+ }
35
+
36
+ /**
37
+ * Throw unless `tz` is an IANA zone this runtime knows. Returns the zone as
38
+ * the runtime spells it (`europe/istanbul` → `Europe/Istanbul`).
39
+ */
40
+ export function validateTimeZone(tz: string): string {
41
+ if (typeof tz !== 'string' || tz.trim() === '') {
42
+ throw new ScheduleValidationError('a time zone is required', String(tz))
43
+ }
44
+ try {
45
+ return new Intl.DateTimeFormat('en-US', { timeZone: tz.trim() }).resolvedOptions().timeZone
46
+ } catch {
47
+ throw new ScheduleValidationError(`unknown time zone "${tz}"`, tz)
48
+ }
49
+ }
50
+
51
+ /** The zone this host is in, as the runtime reports it. */
52
+ export function hostTimeZone(): string {
53
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
54
+ }
55
+
56
+ /** The wall clock of `instant` in `tz`, as wall milliseconds (seconds dropped). */
57
+ export function wallOf(instant: number, tz: string): number {
58
+ const parts = formatterFor(tz).formatToParts(new Date(instant))
59
+ let year = 0
60
+ let month = 1
61
+ let day = 1
62
+ let hour = 0
63
+ let minute = 0
64
+ for (const part of parts) {
65
+ switch (part.type) {
66
+ case 'year':
67
+ year = Number(part.value)
68
+ break
69
+ case 'month':
70
+ month = Number(part.value)
71
+ break
72
+ case 'day':
73
+ day = Number(part.value)
74
+ break
75
+ case 'hour':
76
+ hour = Number(part.value) % 24
77
+ break
78
+ case 'minute':
79
+ minute = Number(part.value)
80
+ break
81
+ }
82
+ }
83
+ return Date.UTC(year, month - 1, day, hour, minute)
84
+ }
85
+
86
+ /** `tz`'s offset from UTC at `instant`, in milliseconds, minute precision. */
87
+ export function offsetAt(instant: number, tz: string): number {
88
+ const floored = Math.floor(instant / MINUTE) * MINUTE
89
+ return wallOf(floored, tz) - floored
90
+ }
91
+
92
+ /**
93
+ * Every instant whose wall clock in `tz` reads `wall`.
94
+ *
95
+ * One instant normally, two in the hour a fall-back repeats (earliest first),
96
+ * none in the hour a spring-forward skips.
97
+ */
98
+ export function instantsForWall(wall: number, tz: string): number[] {
99
+ const offsets = new Set<number>([
100
+ offsetAt(wall - 26 * HOUR, tz),
101
+ offsetAt(wall, tz),
102
+ offsetAt(wall + 26 * HOUR, tz),
103
+ ])
104
+ const found = new Set<number>()
105
+ for (const offset of offsets) {
106
+ const candidate = wall - offset
107
+ if (wallOf(candidate, tz) === wall) found.add(candidate)
108
+ }
109
+ return [...found].sort((a, b) => a - b)
110
+ }
111
+
112
+ /**
113
+ * The first instant whose wall clock is later than `wall`, for a `wall` that
114
+ * falls inside a spring-forward gap: the moment the clocks jumped.
115
+ */
116
+ export function firstInstantAfterGap(wall: number, tz: string): number {
117
+ const a = offsetAt(wall - 26 * HOUR, tz)
118
+ const b = offsetAt(wall + 26 * HOUR, tz)
119
+ let lo = Math.floor((wall - Math.max(a, b)) / MINUTE)
120
+ let hi = Math.ceil((wall - Math.min(a, b)) / MINUTE)
121
+ // Smallest minute m in [lo, hi] with wallOf(m) > wall.
122
+ while (lo < hi) {
123
+ const mid = Math.floor((lo + hi) / 2)
124
+ if (wallOf(mid * MINUTE, tz) > wall) hi = mid
125
+ else lo = mid + 1
126
+ }
127
+ return lo * MINUTE
128
+ }
129
+
130
+ /** Civil date parts of a wall value. */
131
+ export function civil(wall: number): {
132
+ year: number
133
+ month: number
134
+ day: number
135
+ hour: number
136
+ minute: number
137
+ weekday: number
138
+ } {
139
+ const d = new Date(wall)
140
+ return {
141
+ year: d.getUTCFullYear(),
142
+ month: d.getUTCMonth() + 1,
143
+ day: d.getUTCDate(),
144
+ hour: d.getUTCHours(),
145
+ minute: d.getUTCMinutes(),
146
+ weekday: d.getUTCDay(),
147
+ }
148
+ }
149
+
150
+ /** `YYYY-MM-DD HH:MM` for an instant in `tz`. */
151
+ export function formatWall(instant: number, tz: string): string {
152
+ const c = civil(wallOf(instant, tz))
153
+ const p = (n: number) => String(n).padStart(2, '0')
154
+ return `${c.year}-${p(c.month)}-${p(c.day)} ${p(c.hour)}:${p(c.minute)}`
155
+ }
@@ -1,2 +1,2 @@
1
- export { loadSkill, discoverSkills } from './loader.js'
1
+ export { SKILL_FRONTMATTER_KEYS, loadSkill, discoverSkills } from './loader.js'
2
2
  export { SkillRegistry, resolveSkillChain } from './registry.js'
@@ -12,6 +12,40 @@ import { type Logger, resolveLogger } from '../utils/logger.js'
12
12
 
13
13
  export const SKILL_FILENAME = 'SKILL.md'
14
14
 
15
+ /**
16
+ * The frontmatter keys this loader reads.
17
+ *
18
+ * Every other key is skipped whole, whatever YAML it is written in, so a
19
+ * skill written for another agent — `argument-hint: [file]`, a `hooks:`
20
+ * block, `user-invocable: false` — loads here with those fields ignored
21
+ * instead of being refused over syntax in a field nothing reads. The keys
22
+ * listed are still parsed strictly: a value this loader USES is never read
23
+ * wrongly.
24
+ */
25
+ export const SKILL_FRONTMATTER_KEYS: readonly string[] = Object.freeze([
26
+ 'name',
27
+ 'description',
28
+ 'license',
29
+ 'compatibility',
30
+ 'allowed-tools',
31
+ 'invocation',
32
+ 'disable-model-invocation',
33
+ 'metadata',
34
+ ])
35
+
36
+ const READS_SKILL_KEY = (key: string): boolean => SKILL_FRONTMATTER_KEYS.includes(key)
37
+
38
+ /**
39
+ * How this loader reads frontmatter. `allowed-tools` may be a YAML list: the
40
+ * Agent Skills format writes it space-separated, comma-separated or as a
41
+ * list, and all three must mean the same grant; the reader joins a list into
42
+ * the comma form. Keys the loader does not read are skipped whole.
43
+ */
44
+ export const SKILL_FRONTMATTER_OPTIONS = {
45
+ lists: ['allowed-tools'],
46
+ readsKey: READS_SKILL_KEY,
47
+ } as const
48
+
15
49
  /**
16
50
  * How this file's errors name themselves. Passed to the shared reader so a
17
51
  * frontmatter failure still reads as a `SKILL.md` failure — the reader is
@@ -103,6 +137,30 @@ function toSkillMetadata(parsed: ParsedFrontmatter, dirPath: string): SkillMetad
103
137
  skillMetadata.invocation = invocation
104
138
  }
105
139
 
140
+ // The other common spelling of "the model may not pick this":
141
+ // `disable-model-invocation: true` is `invocation: operator`. Read here,
142
+ // at the one parser, so a registry that re-reads the file on every load
143
+ // cannot lose it — a host-side override would be dropped by the first
144
+ // freshness reload and the model could then load an operator-only skill.
145
+ const disableModel = scalarAt(values, 'disable-model-invocation')
146
+ if (disableModel !== undefined) {
147
+ if (disableModel !== 'true' && disableModel !== 'false') {
148
+ // Refused for the reason `invocation` is: `yes` quietly reading as
149
+ // "not disabled" would put the skill back in front of the model.
150
+ throw new Error(
151
+ `${source}: disable-model-invocation must be true or false — got "${disableModel}"`,
152
+ )
153
+ }
154
+ if (disableModel === 'true') {
155
+ if (skillMetadata.invocation !== undefined && skillMetadata.invocation !== 'operator') {
156
+ throw new Error(
157
+ `${source}: disable-model-invocation: true contradicts invocation: ${skillMetadata.invocation}`,
158
+ )
159
+ }
160
+ skillMetadata.invocation = 'operator'
161
+ }
162
+ }
163
+
106
164
  const extra = mappingAt(values, 'metadata')
107
165
  if (extra && Object.keys(extra).length > 0) {
108
166
  skillMetadata.metadata = { ...extra }
@@ -152,7 +210,7 @@ export async function loadSkill(
152
210
  ): Promise<SkillLoadResult> {
153
211
  const skillMdPath = join(dirPath, SKILL_FILENAME)
154
212
  const raw = await readFile(skillMdPath, 'utf-8')
155
- const parsed = parseFrontmatter(raw, sourceLabel(dirPath))
213
+ const parsed = parseFrontmatter(raw, sourceLabel(dirPath), SKILL_FRONTMATTER_OPTIONS)
156
214
  const metadata = toSkillMetadata(parsed, dirPath)
157
215
 
158
216
  const skill: Skill = {
@@ -6,6 +6,12 @@ import { DANGEROUS_PATTERNS } from '../../constants/tools/index.js'
6
6
  import { killTree } from '../../process/kill-tree.js'
7
7
  import { subscribeToAbort } from '../../utils/abort.js'
8
8
  import { readPositiveIntEnv } from '../../utils/env.js'
9
+ import {
10
+ bashToolDialect,
11
+ hostShellSpawn,
12
+ sandboxShellSpawn,
13
+ withoutBashStartup,
14
+ } from '../command-shell.js'
9
15
  import { defineTool } from '../defineTool.js'
10
16
  import { scrubInheritedEnv } from '../env-scrub.js'
11
17
 
@@ -146,13 +152,20 @@ function execHostShell(
146
152
  ): Promise<{ stdout: string; stderr: string }> {
147
153
  options.signal?.throwIfAborted()
148
154
  return new Promise((resolve, reject) => {
149
- const child = spawn(command, {
155
+ // bash where the host has it, `/bin/sh` where it does not; the
156
+ // permission rules read the line in the matching dialect. See
157
+ // `../command-shell.ts`.
158
+ const shell = hostShellSpawn(command, options.env)
159
+ const spawnOptions = {
150
160
  cwd: options.cwd,
151
- env: options.env,
152
- shell: true,
161
+ env: shell.env,
153
162
  // killTree's negative PID must never target the caller's own group.
154
163
  detached: process.platform !== 'win32',
155
- })
164
+ }
165
+ const child =
166
+ shell.file === undefined
167
+ ? spawn(command, { ...spawnOptions, shell: true })
168
+ : spawn(shell.file, [...shell.args], spawnOptions)
156
169
  const captures = {
157
170
  stdout: {
158
171
  chunks: [] as Buffer[],
@@ -302,6 +315,8 @@ export const BashTool = defineTool({
302
315
  // so a permission rule about it is a rule about several commands more often
303
316
  // than not. Naming the argument is what lets the gate read it that way.
304
317
  commandArgument: 'command',
318
+ // The shell that runs it, so the rules read the line as that shell will.
319
+ commandDialect: bashToolDialect,
305
320
  // The kernel reviews a call that sets it under a sandbox every time and
306
321
  // confirms it only by id; see `ToolDefinition.sandboxEscapeArgument`.
307
322
  sandboxEscapeArgument: 'dangerously_disable_sandbox',
@@ -414,9 +429,12 @@ export const BashTool = defineTool({
414
429
  // builtin doesn't have that requirement today.
415
430
  const onOutput = shellProgress(context.report)
416
431
  if (context.sandbox && !leaveSandbox) {
417
- const result = await context.sandbox.exec('/bin/sh', ['-c', input.command], {
432
+ // bash if the guest has it, `/bin/sh` if not; read in the `sh`
433
+ // dialect, which holds for both.
434
+ const launch = sandboxShellSpawn(input.command)
435
+ const result = await context.sandbox.exec(launch.file, launch.args, {
418
436
  timeout: input.timeout,
419
- env: context.env,
437
+ ...(context.env ? { env: withoutBashStartup(context.env) } : {}),
420
438
  // Same reason as the host path below: a Stop must reach the
421
439
  // process, not just the promise waiting on it.
422
440
  signal: context.abortSignal,
@@ -0,0 +1,251 @@
1
+ /**
2
+ * One spelling per address, decided before anyone judges the address.
3
+ *
4
+ * The browser tools' `url` and `origin` arguments are canonicalised by their
5
+ * input schema, and the registry hands the gate and the reviewer the schema's
6
+ * OUTPUT (`ToolRegistry.prepareExecution`). So a site rule written as
7
+ * `^https://github\.com(?:[/?#]|$)` is tested against the one spelling the
8
+ * browser will load, never against `HTTPS://GitHub.com:443/`,
9
+ * `https://github.com./` or `https://%67ithub.com/`, which name the same page
10
+ * and would each slip past a deny written for the plain form.
11
+ *
12
+ * The canonical form is the WHATWG URL serialisation (`new URL(x).href`),
13
+ * which already lowercases the scheme and host, converts an internationalised
14
+ * host to punycode, drops a default port, decodes percent-encoded host bytes
15
+ * and reads every IPv4 spelling (`2852039166`, `0xA9FEA9FE`,
16
+ * `0251.0376.0251.0376`, `169.254.43518`) as dotted-quad. On top of that:
17
+ *
18
+ * - trailing dots on the host are removed (`github.com.` is `github.com` to
19
+ * DNS and to a person, and a different string to a pattern);
20
+ * - only `http:` and `https:` are accepted, plus the literal `about:blank`;
21
+ * - an address carrying a user name or password is refused — it is how
22
+ * `https://github.com@evil.example/` reads as GitHub to a person;
23
+ * - cloud metadata endpoints are refused outright, in every spelling the
24
+ * parser folds into them. This is a floor, not the site policy: a private
25
+ * or loopback address is left to the operator's site rules, and a DNS name
26
+ * that RESOLVES to a metadata address is the host's to catch after the
27
+ * navigation lands.
28
+ */
29
+
30
+ export type BrowserUrlVerdict =
31
+ | { readonly ok: true; readonly url: string; readonly origin: string }
32
+ | { readonly ok: false; readonly reason: string }
33
+
34
+ export type BrowserOriginVerdict =
35
+ | { readonly ok: true; readonly origin: string }
36
+ | { readonly ok: false; readonly reason: string }
37
+
38
+ /** Longest address the tools accept. Longer is not a page anyone meant to name. */
39
+ export const BROWSER_URL_MAX_LENGTH = 8192
40
+
41
+ const ABOUT_BLANK = 'about:blank'
42
+
43
+ /** Host names of cloud metadata services. Compared after lowercasing and trailing-dot removal. */
44
+ const METADATA_HOSTNAMES = new Set(['metadata.google.internal', 'metadata', 'metadata.goog'])
45
+
46
+ /**
47
+ * IPv4 metadata endpoints: AWS, GCP, Azure, Oracle and DigitalOcean
48
+ * (169.254.169.254), and Alibaba Cloud (100.100.100.200).
49
+ */
50
+ const METADATA_IPV4 = new Set(['169.254.169.254', '100.100.100.200'])
51
+
52
+ /** IPv6 metadata endpoints, in the parser's compressed form. AWS Nitro. */
53
+ const METADATA_IPV6 = new Set(['fd00:ec2::254'])
54
+
55
+ function stripTrailingDots(hostname: string): string {
56
+ return hostname.replace(/\.+$/, '')
57
+ }
58
+
59
+ /** Eight hextets of a compressed IPv6 literal, or undefined if it is not one. */
60
+ function expandIpv6(address: string): number[] | undefined {
61
+ const halves = address.split('::')
62
+ if (halves.length > 2) return undefined
63
+ const parse = (part: string | undefined): number[] | undefined => {
64
+ if (part === undefined || part === '') return []
65
+ const out: number[] = []
66
+ for (const piece of part.split(':')) {
67
+ if (!/^[0-9a-f]{1,4}$/.test(piece)) return undefined
68
+ out.push(Number.parseInt(piece, 16))
69
+ }
70
+ return out
71
+ }
72
+ const head = parse(halves[0])
73
+ const tail = parse(halves[1])
74
+ if (!head || !tail) return undefined
75
+ if (halves.length === 1) return head.length === 8 ? head : undefined
76
+ const fill = 8 - head.length - tail.length
77
+ if (fill < 1) return undefined
78
+ return [...head, ...new Array<number>(fill).fill(0), ...tail]
79
+ }
80
+
81
+ function ipv4Of(high: number, low: number): string {
82
+ return `${high >>> 8}.${high & 0xff}.${low >>> 8}.${low & 0xff}`
83
+ }
84
+
85
+ /**
86
+ * IPv4 addresses an IPv6 literal carries in a well-known embedding: mapped
87
+ * (`::ffff:a.b.c.d`), compatible (`::a.b.c.d`), SIIT (`::ffff:0:a.b.c.d`),
88
+ * NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`) and 6to4 (`2002:AABB:CCDD::`).
89
+ * Each is a way to reach the IPv4 address from an IPv6 spelling.
90
+ */
91
+ function embeddedIpv4(h: readonly number[]): string[] {
92
+ const out: string[] = []
93
+ const zero = (from: number, to: number) => h.slice(from, to).every((x) => x === 0)
94
+ const last = ipv4Of(h[6] ?? 0, h[7] ?? 0)
95
+ if (zero(0, 5) && (h[5] === 0xffff || h[5] === 0)) out.push(last)
96
+ if (zero(0, 4) && h[4] === 0xffff && h[5] === 0) out.push(last)
97
+ if (h[0] === 0x64 && h[1] === 0xff9b) out.push(last)
98
+ if (h[0] === 0x2002) out.push(ipv4Of(h[1] ?? 0, h[2] ?? 0))
99
+ return out
100
+ }
101
+
102
+ /**
103
+ * Is this host a cloud metadata endpoint, in any spelling the URL parser
104
+ * folds into one? `hostname` is a URL's `hostname` (IPv6 in brackets) or a
105
+ * bare name or address.
106
+ */
107
+ export function isCloudMetadataHost(hostname: string): boolean {
108
+ let host = stripTrailingDots(hostname.trim().toLowerCase())
109
+ if (host === '') return false
110
+ // Let the WHATWG parser fold every IPv4 and IPv6 spelling into one.
111
+ const bracketed = host.startsWith('[') ? host : host.includes(':') ? `[${host}]` : host
112
+ try {
113
+ host = stripTrailingDots(new URL(`http://${bracketed}/`).hostname)
114
+ } catch {
115
+ return false
116
+ }
117
+ if (METADATA_HOSTNAMES.has(host) || METADATA_IPV4.has(host)) return true
118
+ if (!host.startsWith('[')) return false
119
+ const v6 = host.slice(1, -1)
120
+ if (METADATA_IPV6.has(v6)) return true
121
+ const hextets = expandIpv6(v6)
122
+ if (!hextets) return false
123
+ return embeddedIpv4(hextets).some((v4) => METADATA_IPV4.has(v4))
124
+ }
125
+
126
+ /**
127
+ * The one spelling of an address the browser may be sent to, or why not.
128
+ *
129
+ * Accepts absolute `http:` and `https:` URLs and `about:blank`. See the
130
+ * module comment for what is folded and what is refused.
131
+ */
132
+ export function canonicalizeBrowserUrl(raw: string): BrowserUrlVerdict {
133
+ if (typeof raw !== 'string' || raw.trim() === '')
134
+ return { ok: false, reason: 'the address is empty' }
135
+ if (raw.length > BROWSER_URL_MAX_LENGTH)
136
+ return { ok: false, reason: `the address is longer than ${BROWSER_URL_MAX_LENGTH} characters` }
137
+ let url: URL
138
+ try {
139
+ url = new URL(raw.trim())
140
+ } catch {
141
+ return {
142
+ ok: false,
143
+ reason: 'the address is not an absolute URL; give the full https:// address',
144
+ }
145
+ }
146
+ if (url.protocol === 'about:') {
147
+ if (url.pathname.toLowerCase() === 'blank' && url.search === '' && url.hash === '')
148
+ return { ok: true, url: ABOUT_BLANK, origin: 'null' }
149
+ return { ok: false, reason: 'only about:blank is allowed among about: addresses' }
150
+ }
151
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
152
+ return {
153
+ ok: false,
154
+ reason: `the scheme "${url.protocol}" is not allowed; only http and https addresses (and about:blank) can be opened`,
155
+ }
156
+ }
157
+ if (url.username !== '' || url.password !== '') {
158
+ return {
159
+ ok: false,
160
+ reason:
161
+ 'the address carries a user name or password (user@host); credentials never go in an address — open the site and let the user sign in',
162
+ }
163
+ }
164
+ const host = stripTrailingDots(url.hostname)
165
+ if (host === '') return { ok: false, reason: 'the address has no host' }
166
+ if (host !== url.hostname) {
167
+ url.hostname = host
168
+ // The setter re-parses; a host that did not survive is not one to open.
169
+ if (stripTrailingDots(url.hostname) !== host)
170
+ return { ok: false, reason: 'the address host could not be canonicalised' }
171
+ }
172
+ if (isCloudMetadataHost(url.hostname)) {
173
+ return {
174
+ ok: false,
175
+ reason: `${url.hostname} is a cloud metadata endpoint, which the browser never opens`,
176
+ }
177
+ }
178
+ return { ok: true, url: url.href, origin: url.origin }
179
+ }
180
+
181
+ /**
182
+ * The canonical origin (`scheme://host[:port]`) a `browser_act` call names,
183
+ * or why it is not one. A trailing `/` is accepted; a path, query or fragment
184
+ * is not — the argument is an origin, copied from the snapshot header.
185
+ */
186
+ export function canonicalizeBrowserOrigin(raw: string): BrowserOriginVerdict {
187
+ const verdict = canonicalizeBrowserUrl(raw)
188
+ if (!verdict.ok) return verdict
189
+ if (verdict.url === ABOUT_BLANK)
190
+ return { ok: false, reason: 'about:blank has no origin to act on; open a page first' }
191
+ const url = new URL(verdict.url)
192
+ if (url.pathname !== '/' || url.search !== '' || url.hash !== '') {
193
+ return {
194
+ ok: false,
195
+ reason: `"${raw}" is a URL, not an origin; copy the origin from the snapshot header (${url.origin})`,
196
+ }
197
+ }
198
+ return { ok: true, origin: verdict.origin }
199
+ }
200
+
201
+ export type BrowserSitePatternVerdict =
202
+ | { readonly ok: true; readonly pattern: string }
203
+ | { readonly ok: false; readonly reason: string }
204
+
205
+ /**
206
+ * The canonical spelling of a site key an operator or a job grants —
207
+ * `https://github.com`, `https://*.example.com`, `http://localhost:*` — or
208
+ * why it is not one.
209
+ *
210
+ * The scheme is literal (`http` or `https`). The host may begin with `*.`,
211
+ * meaning one or more labels in front of the rest. The port may be `*`, any
212
+ * port; a default port is dropped. The host is lowercased and punycoded like
213
+ * a URL's. A bare `*`, a path, credentials and metadata hosts are refused.
214
+ */
215
+ export function canonicalizeBrowserSitePattern(raw: string): BrowserSitePatternVerdict {
216
+ const trimmed = typeof raw === 'string' ? raw.trim() : ''
217
+ const match = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/(\*\.)?([^/:?#@]+)(?::(\d{1,5}|\*))?\/?$/.exec(
218
+ trimmed,
219
+ )
220
+ if (!match) {
221
+ return {
222
+ ok: false,
223
+ reason: `"${raw}" is not a site; write scheme://host, optionally *.host and :port or :*, with no path`,
224
+ }
225
+ }
226
+ if ((match[3] ?? '').includes('*')) {
227
+ return {
228
+ ok: false,
229
+ reason: `"${raw}" has a wildcard inside the host; a wildcard may only be a leading *. before a domain`,
230
+ }
231
+ }
232
+ const scheme = (match[1] ?? '').toLowerCase()
233
+ const wildcard = match[2] !== undefined
234
+ const port = match[4]
235
+ if (scheme !== 'http' && scheme !== 'https')
236
+ return { ok: false, reason: `the scheme "${scheme}:" is not allowed; only http and https` }
237
+ const probe = canonicalizeBrowserOrigin(
238
+ `${scheme}://${match[3]}${port !== undefined && port !== '*' ? `:${port}` : ''}`,
239
+ )
240
+ if (!probe.ok) return { ok: false, reason: probe.reason }
241
+ const probeUrl = new URL(probe.origin)
242
+ if (
243
+ wildcard &&
244
+ (probeUrl.hostname.startsWith('[') || /^\d+(?:\.\d+){3}$/.test(probeUrl.hostname))
245
+ ) {
246
+ return { ok: false, reason: 'a wildcard cannot precede an IP address' }
247
+ }
248
+ const host = `${wildcard ? '*.' : ''}${probeUrl.hostname}`
249
+ const portPart = port === '*' ? ':*' : probeUrl.port !== '' ? `:${probeUrl.port}` : ''
250
+ return { ok: true, pattern: `${scheme}://${host}${portPart}` }
251
+ }