@foldrun/core 0.3.0 → 0.5.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 (175) hide show
  1. package/README.md +27 -10
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.js +3 -0
  4. package/dist/src/account.d.ts +45 -0
  5. package/dist/src/account.js +130 -0
  6. package/dist/src/api-tools.d.ts +8 -0
  7. package/dist/src/api-tools.js +22 -4
  8. package/dist/src/approvals.d.ts +104 -1
  9. package/dist/src/approvals.js +192 -2
  10. package/dist/src/browser-pod.d.ts +129 -0
  11. package/dist/src/browser-pod.js +284 -0
  12. package/dist/src/budget.d.ts +32 -0
  13. package/dist/src/budget.js +177 -0
  14. package/dist/src/catalog.js +3 -1
  15. package/dist/src/clock.d.ts +49 -0
  16. package/dist/src/clock.js +121 -0
  17. package/dist/src/completions.js +17 -8
  18. package/dist/src/confine.d.ts +22 -0
  19. package/dist/src/confine.js +224 -4
  20. package/dist/src/deploy.d.ts +33 -0
  21. package/dist/src/deploy.js +145 -6
  22. package/dist/src/egress.d.ts +26 -2
  23. package/dist/src/egress.js +8 -1
  24. package/dist/src/eol.d.ts +2 -0
  25. package/dist/src/eol.js +7 -0
  26. package/dist/src/evals.d.ts +9 -0
  27. package/dist/src/evals.js +26 -13
  28. package/dist/src/flow-lint.d.ts +12 -0
  29. package/dist/src/flow-lint.js +331 -1
  30. package/dist/src/flow-patterns.d.ts +141 -0
  31. package/dist/src/flow-patterns.js +568 -0
  32. package/dist/src/gitrepo.d.ts +18 -0
  33. package/dist/src/gitrepo.js +49 -0
  34. package/dist/src/host-env.d.ts +8 -0
  35. package/dist/src/host-env.js +48 -0
  36. package/dist/src/json-schema.d.ts +12 -0
  37. package/dist/src/json-schema.js +202 -0
  38. package/dist/src/kinds.js +1 -1
  39. package/dist/src/language.d.ts +17 -0
  40. package/dist/src/language.js +55 -0
  41. package/dist/src/layout.d.ts +69 -0
  42. package/dist/src/layout.js +200 -0
  43. package/dist/src/library.d.ts +9 -0
  44. package/dist/src/library.js +43 -4
  45. package/dist/src/limits.d.ts +84 -0
  46. package/dist/src/limits.js +287 -0
  47. package/dist/src/locale.d.ts +47 -0
  48. package/dist/src/locale.js +177 -0
  49. package/dist/src/mail.d.ts +88 -0
  50. package/dist/src/mail.js +245 -0
  51. package/dist/src/model-probe.d.ts +3 -0
  52. package/dist/src/model-probe.js +1 -0
  53. package/dist/src/notify.d.ts +132 -8
  54. package/dist/src/notify.js +360 -58
  55. package/dist/src/openapi.js +5 -3
  56. package/dist/src/operator.d.ts +201 -0
  57. package/dist/src/operator.js +251 -0
  58. package/dist/src/paths.d.ts +33 -0
  59. package/dist/src/paths.js +87 -0
  60. package/dist/src/platform.d.ts +144 -4
  61. package/dist/src/platform.js +27 -3
  62. package/dist/src/provider-health.d.ts +55 -0
  63. package/dist/src/provider-health.js +233 -0
  64. package/dist/src/providers.d.ts +481 -0
  65. package/dist/src/providers.js +1105 -6
  66. package/dist/src/refusal.d.ts +83 -0
  67. package/dist/src/refusal.js +281 -0
  68. package/dist/src/run-container.d.ts +206 -17
  69. package/dist/src/run-container.js +547 -80
  70. package/dist/src/runner.d.ts +179 -10
  71. package/dist/src/runner.js +1423 -169
  72. package/dist/src/runtime.d.ts +52 -0
  73. package/dist/src/runtime.js +493 -58
  74. package/dist/src/script-tools.d.ts +15 -2
  75. package/dist/src/script-tools.js +44 -4
  76. package/dist/src/secret-files.d.ts +6 -5
  77. package/dist/src/secret-files.js +24 -11
  78. package/dist/src/secret-health.d.ts +83 -0
  79. package/dist/src/secret-health.js +177 -0
  80. package/dist/src/secrets.d.ts +38 -1
  81. package/dist/src/secrets.js +197 -96
  82. package/dist/src/starter.d.ts +9 -0
  83. package/dist/src/starter.js +95 -11
  84. package/dist/src/step-exec.d.ts +106 -1
  85. package/dist/src/step-exec.js +558 -85
  86. package/dist/src/step-removal.d.ts +15 -0
  87. package/dist/src/step-removal.js +49 -0
  88. package/dist/src/storage.d.ts +23 -4
  89. package/dist/src/storage.js +125 -23
  90. package/dist/src/store.d.ts +492 -38
  91. package/dist/src/store.js +882 -69
  92. package/dist/src/subagents.d.ts +75 -0
  93. package/dist/src/subagents.js +149 -0
  94. package/dist/src/tar.js +2 -1
  95. package/dist/src/test-mode.d.ts +111 -0
  96. package/dist/src/test-mode.js +285 -0
  97. package/dist/src/tool-names.d.ts +13 -1
  98. package/dist/src/tool-names.js +38 -11
  99. package/dist/src/tool-programs.d.ts +26 -0
  100. package/dist/src/tool-programs.js +172 -0
  101. package/dist/src/tool-test.js +152 -33
  102. package/dist/src/translator.d.ts +8 -2
  103. package/dist/src/translator.js +71 -24
  104. package/dist/src/trigger-log.d.ts +43 -0
  105. package/dist/src/trigger-log.js +122 -0
  106. package/dist/src/triggers.d.ts +23 -0
  107. package/dist/src/triggers.js +101 -12
  108. package/dist/src/untrusted-fetch.d.ts +11 -0
  109. package/dist/src/untrusted-fetch.js +77 -0
  110. package/dist/src/web-actions.d.ts +91 -0
  111. package/dist/src/web-actions.js +199 -0
  112. package/dist/src/webhook.d.ts +19 -3
  113. package/dist/src/webhook.js +50 -11
  114. package/index.ts +3 -0
  115. package/package.json +5 -5
  116. package/src/account.ts +149 -0
  117. package/src/api-tools.ts +32 -4
  118. package/src/approvals.ts +253 -2
  119. package/src/browser-pod.ts +313 -0
  120. package/src/budget.ts +178 -0
  121. package/src/catalog.ts +3 -1
  122. package/src/clock.ts +149 -0
  123. package/src/completions.ts +17 -8
  124. package/src/confine.ts +224 -4
  125. package/src/deploy.ts +137 -6
  126. package/src/egress.ts +32 -4
  127. package/src/eol.ts +7 -0
  128. package/src/evals.ts +36 -11
  129. package/src/flow-lint.ts +371 -1
  130. package/src/flow-patterns.ts +597 -0
  131. package/src/gitrepo.ts +48 -0
  132. package/src/host-env.ts +51 -0
  133. package/src/json-schema.ts +184 -0
  134. package/src/kinds.ts +1 -1
  135. package/src/language.ts +62 -0
  136. package/src/layout.ts +228 -0
  137. package/src/library.ts +50 -4
  138. package/src/limits.ts +290 -0
  139. package/src/locale.ts +217 -0
  140. package/src/mail.ts +316 -0
  141. package/src/model-probe.ts +4 -0
  142. package/src/notify.ts +391 -44
  143. package/src/openapi.ts +5 -3
  144. package/src/operator.ts +300 -0
  145. package/src/paths.ts +83 -0
  146. package/src/platform.ts +158 -5
  147. package/src/provider-health.ts +275 -0
  148. package/src/providers.ts +1368 -6
  149. package/src/refusal.ts +320 -0
  150. package/src/run-container.ts +611 -76
  151. package/src/runner.ts +1463 -165
  152. package/src/runtime.ts +487 -55
  153. package/src/script-tools.ts +55 -6
  154. package/src/secret-files.ts +22 -10
  155. package/src/secret-health.ts +230 -0
  156. package/src/secrets.ts +191 -95
  157. package/src/starter.ts +96 -11
  158. package/src/step-exec.ts +548 -41
  159. package/src/step-removal.ts +59 -0
  160. package/src/storage.ts +149 -24
  161. package/src/store.ts +1227 -74
  162. package/src/subagents.ts +189 -0
  163. package/src/tar.ts +2 -1
  164. package/src/test-mode.ts +311 -0
  165. package/src/tool-names.ts +40 -11
  166. package/src/tool-programs.ts +179 -0
  167. package/src/tool-test.ts +159 -32
  168. package/src/translator.ts +74 -23
  169. package/src/trigger-log.ts +160 -0
  170. package/src/triggers.ts +102 -12
  171. package/src/untrusted-fetch.ts +88 -0
  172. package/src/web-actions.ts +237 -0
  173. package/src/webhook.ts +58 -11
  174. package/templates/hello/agents/curator/agent.md +1 -1
  175. package/templates/hello/agents/notetaker/agent.md +1 -1
@@ -0,0 +1,177 @@
1
+ // A budget is a number and a period — what may be spent, and over what.
2
+ //
3
+ // One grammar, read in four places: an agent's `budget:` (the most that
4
+ // agent may spend in one run), a flow's (the most one run may spend), a
5
+ // workspace's and the account's (a cap over a window of time). The value
6
+ // was a bare number meaning "per month" in a workspace or account file and
7
+ // "per run" in a flow; it still is, and it can now also say which:
8
+ //
9
+ // budget: 60 a number alone: per run in a flow or an agent,
10
+ // per month in a workspace or the account
11
+ // budget: 60/day or 60/week, 60/month
12
+ // budget: unlimited no cap — the same as leaving the key out
13
+ //
14
+ // Unset means no limit. That is the platform's rule, not an accident of
15
+ // parsing: a customer who has not written a cap has not asked for one, and
16
+ // nothing here invents a default they would then have to find and raise.
17
+ //
18
+ // Windows follow the account's calendar (`timezone:` in AGENTS.md), the
19
+ // same one the scheduler and "today" already use, because a daily cap that
20
+ // resets at 10am Sydney time is not a daily cap anyone meant.
21
+ /** Words that mean "no cap", case-insensitively. */
22
+ const UNLIMITED = new Set(["unlimited", "none", "no limit", "no-limit", "off", "-", "~"]);
23
+ const PERIODS = {
24
+ run: "run",
25
+ day: "day",
26
+ daily: "day",
27
+ week: "week",
28
+ weekly: "week",
29
+ month: "month",
30
+ monthly: "month",
31
+ };
32
+ // `\d+(?:\.\d+)?` with an optional separator and one period word: every
33
+ // part has one way to match, so a hostile string cannot make it backtrack.
34
+ const FORM_RE = /^\$?(\d+(?:\.\d+)?)(?:\s*(?:\/|per|a|each)?\s*([a-z]+))?$/;
35
+ /**
36
+ * The budget a frontmatter value declares, or null for "no cap".
37
+ *
38
+ * Lenient on purpose: a value nobody can read is treated as no cap rather
39
+ * than as a cap of zero, because refusing every run over a typo is the worse
40
+ * failure. `budgetProblem` is the strict reading, for lint and the editor.
41
+ */
42
+ export function parseBudget(raw, defaultPeriod) {
43
+ if (raw === undefined || raw === null)
44
+ return null;
45
+ if (typeof raw === "number")
46
+ return Number.isFinite(raw) && raw > 0 ? { usd: raw, period: defaultPeriod } : null;
47
+ if (typeof raw !== "string")
48
+ return null;
49
+ const s = raw.trim().toLowerCase();
50
+ if (!s || UNLIMITED.has(s))
51
+ return null;
52
+ const m = FORM_RE.exec(s);
53
+ if (!m)
54
+ return null;
55
+ const usd = Number(m[1]);
56
+ if (!Number.isFinite(usd) || usd <= 0)
57
+ return null;
58
+ const period = m[2] ? PERIODS[m[2]] : defaultPeriod;
59
+ if (!period)
60
+ return null;
61
+ return { usd, period };
62
+ }
63
+ /**
64
+ * What is wrong with a `budget:` value, in a sentence, or null when it is
65
+ * fine. `allowed` names the periods this file may use — an agent's or a
66
+ * flow's budget is per run and nothing else; a workspace's or the account's
67
+ * is over time and never per run.
68
+ */
69
+ export function budgetProblem(raw, allowed) {
70
+ if (raw === undefined || raw === null)
71
+ return null;
72
+ if (typeof raw === "number") {
73
+ if (!Number.isFinite(raw) || raw < 0)
74
+ return `budget: ${String(raw)} is not an amount in USD`;
75
+ return null;
76
+ }
77
+ if (typeof raw !== "string")
78
+ return `budget: must be a number, "<number>/<day|week|month>", or unlimited`;
79
+ const s = raw.trim().toLowerCase();
80
+ if (!s || UNLIMITED.has(s))
81
+ return null;
82
+ const m = FORM_RE.exec(s);
83
+ if (!m)
84
+ return `budget: "${raw}" is not a number, "<number>/<day|week|month>", or unlimited`;
85
+ if (m[2]) {
86
+ const period = PERIODS[m[2]];
87
+ if (!period)
88
+ return `budget: "${m[2]}" is not a period — day, week or month`;
89
+ if (!allowed.includes(period)) {
90
+ return period === "run"
91
+ ? `budget: is per run here already — write the number alone`
92
+ : `budget: "${period}" does not apply here — this cap is per run; a cap over time belongs in the workspace's or the account's AGENTS.md`;
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ /** "$60/day", "$500/month", "$6/run". */
98
+ export function formatBudget(b) {
99
+ const usd = Number.isInteger(b.usd) ? String(b.usd) : b.usd.toFixed(2);
100
+ return `$${usd}/${b.period}`;
101
+ }
102
+ /** "today", "this week", "this month" — the window a spend is measured in. */
103
+ export function windowName(period) {
104
+ return period === "day" ? "today" : period === "week" ? "this week" : period === "month" ? "this month" : "this run";
105
+ }
106
+ /** What to wait for when the cap is reached. */
107
+ export function windowTurnsHint(period) {
108
+ return period === "day" ? "wait for tomorrow" : period === "week" ? "wait for next week" : "wait for the new month";
109
+ }
110
+ // ------------------------------------------------------------ the window
111
+ function partsIn(timeZone, at) {
112
+ const f = new Intl.DateTimeFormat("en-US", {
113
+ timeZone,
114
+ hourCycle: "h23",
115
+ year: "numeric",
116
+ month: "numeric",
117
+ day: "numeric",
118
+ hour: "numeric",
119
+ minute: "numeric",
120
+ second: "numeric",
121
+ weekday: "short",
122
+ });
123
+ const p = {};
124
+ for (const { type, value } of f.formatToParts(at))
125
+ p[type] = value;
126
+ const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
127
+ return {
128
+ y: Number(p.year),
129
+ m: Number(p.month),
130
+ d: Number(p.day),
131
+ h: Number(p.hour),
132
+ min: Number(p.minute),
133
+ s: Number(p.second),
134
+ weekday: weekdays.indexOf(p.weekday),
135
+ };
136
+ }
137
+ /** The zone's offset from UTC at an instant, in ms (positive east). */
138
+ function offsetMs(timeZone, atMs) {
139
+ const p = partsIn(timeZone, new Date(atMs));
140
+ return Date.UTC(p.y, p.m - 1, p.d, p.h, p.min, p.s) - Math.floor(atMs / 1000) * 1000;
141
+ }
142
+ /** Midnight at the start of a local calendar date, as a UTC instant. */
143
+ function localMidnightMs(timeZone, y, m, d) {
144
+ const naive = Date.UTC(y, m - 1, d);
145
+ let t = naive - offsetMs(timeZone, naive);
146
+ // Once more from the answer: across a DST change the first guess can be
147
+ // off by the shift, and the second pass lands on the real midnight.
148
+ const again = naive - offsetMs(timeZone, t);
149
+ if (again !== t)
150
+ t = again;
151
+ return t;
152
+ }
153
+ /**
154
+ * When the current window began, as a UTC instant — the moment spend is
155
+ * summed from. Today's midnight, Monday's midnight, or the first of the
156
+ * month, all in the given calendar. A per-run budget has no window.
157
+ */
158
+ export function windowStartMs(period, timeZone, now = new Date()) {
159
+ if (period === "run")
160
+ return null;
161
+ let tz = timeZone;
162
+ try {
163
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
164
+ }
165
+ catch {
166
+ tz = "UTC";
167
+ }
168
+ const p = partsIn(tz, now);
169
+ if (period === "day")
170
+ return localMidnightMs(tz, p.y, p.m, p.d);
171
+ if (period === "month")
172
+ return localMidnightMs(tz, p.y, p.m, 1);
173
+ // Monday. Walk back on the naive calendar, then take that date's midnight.
174
+ const back = (p.weekday + 6) % 7;
175
+ const monday = new Date(Date.UTC(p.y, p.m - 1, p.d - back));
176
+ return localMidnightMs(tz, monday.getUTCFullYear(), monday.getUTCMonth() + 1, monday.getUTCDate());
177
+ }
@@ -21,6 +21,8 @@ import fs from "node:fs";
21
21
  import path from "node:path";
22
22
  import { dataRoot } from "./paths.js";
23
23
  import { resolveModel, resolveTier, MODEL_TIERS, EFFORT_LEVELS } from "./store.js";
24
+ import { trimSlashes } from "./paths.js";
25
+ import { fetchUntrusted } from "./untrusted-fetch.js";
24
26
  /** A day. Model lists move weekly, not hourly, and a stale price in an
25
27
  * autocomplete hint is a smaller failure than a fetch on every keystroke. */
26
28
  const TTL_MS = 24 * 60 * 60 * 1000;
@@ -83,7 +85,7 @@ export async function loadCatalog(baseUrl) {
83
85
  if (cached && Date.now() - Date.parse(cached.fetchedAt) < TTL_MS)
84
86
  return cached;
85
87
  try {
86
- const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/v1/models`, {
88
+ const res = await fetchUntrusted(`${trimSlashes(baseUrl)}/v1/models`, {
87
89
  signal: AbortSignal.timeout(10_000),
88
90
  });
89
91
  if (!res.ok)
@@ -0,0 +1,49 @@
1
+ /** A zone the runtime can actually use: the name Intl understands, and the
2
+ * value to put in `TZ` for the shell and Node inside the sandbox. They are
3
+ * the same for an IANA name and differ for a fixed offset. */
4
+ export interface Zone {
5
+ /** What `Intl.DateTimeFormat({ timeZone })` is given. */
6
+ name: string;
7
+ /** What `TZ` is set to. */
8
+ tz: string;
9
+ }
10
+ /**
11
+ * Read a written timezone into something usable, or null if nothing can be.
12
+ *
13
+ * Two shapes are accepted, because both are what people type: an IANA name
14
+ * (`Australia/Sydney`) and a plain fixed offset (`UTC+10`, `+10:00`,
15
+ * `-05:30`, `GMT+5`, or `UTC` on its own).
16
+ */
17
+ export declare function normalizeZone(raw: unknown): Zone | null;
18
+ /** Is this a value `timezone:` may carry? Used by the lint, so `foldrun
19
+ * check` refuses `Sydney/Australia` and `UTC+99` before a run does. */
20
+ export declare function isTimezone(raw: unknown): boolean;
21
+ /** The lint's sentence for a `timezone:` nobody can read. */
22
+ export declare function timezoneProblem(raw: unknown): string | null;
23
+ /** One level of the cascade: where the value was written, and what it said. */
24
+ export interface ZoneLevel {
25
+ /** The word the trace uses: "agent", "flow", "workspace", "account". */
26
+ level: string;
27
+ value: unknown;
28
+ }
29
+ export interface ClockChoice {
30
+ /** The zone, ready for Intl. */
31
+ timezone: string;
32
+ /** The value for `TZ`. */
33
+ tz: string;
34
+ /** Which level set it — or "FOLDRUN_TIMEZONE", or "default". */
35
+ source: string;
36
+ /** One sentence per unusable value found on the way down. Reported as a
37
+ * step event; never thrown, because a mistyped zone must not stop work. */
38
+ problems: string[];
39
+ }
40
+ /**
41
+ * Walk the levels, nearest first, and take the first zone that works.
42
+ *
43
+ * `FOLDRUN_TIMEZONE` is appended by the caller's level list or read here as
44
+ * the last written level; UTC is what remains when nothing said anything —
45
+ * the clock the pods actually run on.
46
+ */
47
+ export declare function resolveClock(levels: ZoneLevel[], env?: string | undefined): ClockChoice;
48
+ /** YYYY-MM-DD in a timezone — the date the step believes it is. */
49
+ export declare function localDate(timeZone: string, now?: Date): string;
@@ -0,0 +1,121 @@
1
+ // The clock an agent works to.
2
+ //
3
+ // A step used to run on whatever the pod's clock said, which is UTC, while
4
+ // the flow that fired it was scheduled in Australia/Sydney. A run at 08:00
5
+ // Sydney on the 16th wrote files stamped the 15th and the next step, looking
6
+ // for today's files, found none. The schedule and the work disagreed about
7
+ // what day it was.
8
+ //
9
+ // So the calendar is resolvable at every level, nearest wins:
10
+ //
11
+ // agent frontmatter → flow frontmatter → workspace AGENTS.md →
12
+ // account AGENTS.md → FOLDRUN_TIMEZONE → UTC
13
+ //
14
+ // Leaving `timezone:` out inherits the next level up; setting it anywhere
15
+ // wins for that level and everything under it. A value nothing can read
16
+ // never fails a step — it falls through to the next level and says so once.
17
+ /** `UTC+10`, `GMT-3:30`, `+10:00`, `-0530`, or a bare `UTC`/`GMT`. */
18
+ const OFFSET_RE = /^(?:UTC|GMT)?\s*(?:([+-])\s*(\d{1,2})(?::?(\d{2}))?)?$/i;
19
+ /**
20
+ * Read a written timezone into something usable, or null if nothing can be.
21
+ *
22
+ * Two shapes are accepted, because both are what people type: an IANA name
23
+ * (`Australia/Sydney`) and a plain fixed offset (`UTC+10`, `+10:00`,
24
+ * `-05:30`, `GMT+5`, or `UTC` on its own).
25
+ */
26
+ export function normalizeZone(raw) {
27
+ const text = typeof raw === "string" ? raw.trim() : "";
28
+ if (!text)
29
+ return null;
30
+ const offset = text.match(OFFSET_RE);
31
+ if (offset) {
32
+ const [, sign, hh, mm] = offset;
33
+ // A bare UTC/GMT, or the empty match — no offset at all.
34
+ if (!sign)
35
+ return { name: "UTC", tz: "UTC" };
36
+ const hours = Number(hh);
37
+ const minutes = Number(mm ?? "0");
38
+ // The real range of civil offsets. `UTC+99` is a typo, not a zone.
39
+ if (hours > 14 || minutes > 59)
40
+ return null;
41
+ if (hours === 14 && minutes > 0)
42
+ return null;
43
+ const total = hours * 60 + minutes;
44
+ if (total === 0)
45
+ return { name: "UTC", tz: "UTC" };
46
+ const pad = (n) => String(n).padStart(2, "0");
47
+ const name = `${sign}${pad(hours)}:${pad(minutes)}`; // Intl reads this form
48
+ if (minutes === 0) {
49
+ // Etc/GMT zones have their sign INVERTED — POSIX counts degrees west of
50
+ // Greenwich as positive, so Etc/GMT-10 is ten hours AHEAD of UTC. It is
51
+ // worth the confusion: `Etc/GMT-10` is understood by ICU (so Node in
52
+ // the sandbox honours it) and by the shell's date, which the offset
53
+ // form `+10:00` is not.
54
+ return { name, tz: `Etc/GMT${sign === "+" ? "-" : "+"}${hours}` };
55
+ }
56
+ // No Etc zone exists for a half-hour offset, so `TZ` gets the POSIX
57
+ // spec — which the shell reads, and whose sign is inverted for the same
58
+ // reason. FOLDRUN_DATE is computed from `name` either way, so the date
59
+ // an agent is told is right whatever TZ a program makes of it.
60
+ const posix = `${sign === "+" ? "-" : "+"}${pad(hours)}:${pad(minutes)}`;
61
+ return { name, tz: `<${sign}${pad(hours)}${pad(minutes)}>${posix}` };
62
+ }
63
+ // Not an offset: an IANA name, if the platform knows it.
64
+ try {
65
+ new Intl.DateTimeFormat("en-CA", { timeZone: text });
66
+ return { name: text, tz: text };
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /** Is this a value `timezone:` may carry? Used by the lint, so `foldrun
73
+ * check` refuses `Sydney/Australia` and `UTC+99` before a run does. */
74
+ export function isTimezone(raw) {
75
+ return normalizeZone(raw) !== null;
76
+ }
77
+ /** The lint's sentence for a `timezone:` nobody can read. */
78
+ export function timezoneProblem(raw) {
79
+ if (raw === undefined || raw === null || raw === "")
80
+ return null;
81
+ if (isTimezone(raw))
82
+ return null;
83
+ return (`timezone: ${String(raw)} — not a zone; use an IANA name (Australia/Sydney) ` +
84
+ `or a fixed offset (UTC+10, +10:00, -05:30)`);
85
+ }
86
+ /**
87
+ * Walk the levels, nearest first, and take the first zone that works.
88
+ *
89
+ * `FOLDRUN_TIMEZONE` is appended by the caller's level list or read here as
90
+ * the last written level; UTC is what remains when nothing said anything —
91
+ * the clock the pods actually run on.
92
+ */
93
+ export function resolveClock(levels, env = process.env.FOLDRUN_TIMEZONE) {
94
+ const problems = [];
95
+ const bad = [];
96
+ const all = [...levels, { level: "FOLDRUN_TIMEZONE", value: env }];
97
+ for (const { level, value } of all) {
98
+ const written = typeof value === "string" ? value.trim() : "";
99
+ if (!written)
100
+ continue;
101
+ const zone = normalizeZone(written);
102
+ if (zone) {
103
+ for (const b of bad)
104
+ problems.push(`${b} — using ${zone.name} (${level}) instead`);
105
+ return { timezone: zone.name, tz: zone.tz, source: level, problems };
106
+ }
107
+ bad.push(`timezone: "${written}" (${level}) is not a zone this runtime knows`);
108
+ }
109
+ for (const b of bad)
110
+ problems.push(`${b} — using UTC instead`);
111
+ return { timezone: "UTC", tz: "UTC", source: "default", problems };
112
+ }
113
+ /** YYYY-MM-DD in a timezone — the date the step believes it is. */
114
+ export function localDate(timeZone, now = new Date()) {
115
+ return new Intl.DateTimeFormat("en-CA", {
116
+ timeZone,
117
+ year: "numeric",
118
+ month: "2-digit",
119
+ day: "2-digit",
120
+ }).format(now);
121
+ }
@@ -28,11 +28,14 @@ const EFFORTS = [
28
28
  // survive vendor renames — then the exact SDK names, accepted so a Claude Code
29
29
  // subagent runs here unchanged.
30
30
  const TOOL_GROUPS = [
31
+ // `files`, `bash` and `fetch` are retired (tool-names.ts) and not offered.
32
+ // `web` is our gallery tool, offered here because it is the one way onto
33
+ // the web.
31
34
  { label: "read", hint: "Read, Glob, Grep — may look, never write" },
32
- { label: "files", hint: "Read, Write, Edit, Glob, Grep" },
33
- { label: "web", hint: "WebSearch (Anthropic's servers, billed per search), WebFetch" },
34
- { label: "fetch", hint: "WebFetch only — local; pair with the websearch tool" },
35
- { label: "bash", hint: "Bash — arbitrary commands" },
35
+ { label: "write", hint: "Read, Write, Edit, Glob, Grep" },
36
+ { label: "code", hint: "Bash — runs anything in the sandbox" },
37
+ { label: "web", hint: "search, fetch, browse, crawl, map, extract, answer, monitor — providers under web:" },
38
+ { label: "ask", hint: "ask_person — ask the person running the desk mid-step, and wait for the answer" },
36
39
  ];
37
40
  const SDK_TOOL_NAMES = [
38
41
  { label: "Read", hint: "exact SDK name" },
@@ -41,8 +44,6 @@ const SDK_TOOL_NAMES = [
41
44
  { label: "Glob", hint: "exact SDK name" },
42
45
  { label: "Grep", hint: "exact SDK name" },
43
46
  { label: "Bash", hint: "exact SDK name" },
44
- { label: "WebSearch", hint: "exact SDK name" },
45
- { label: "WebFetch", hint: "exact SDK name" },
46
47
  ];
47
48
  const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"].map((m) => ({ label: m }));
48
49
  const TRIGGERS = [
@@ -85,14 +86,17 @@ const FIELDS = {
85
86
  { label: "description", insert: "description: ", hint: "required — what it does, and when" },
86
87
  { label: "model", insert: "model: default" },
87
88
  { label: "effort", insert: "effort: high", hint: "how hard it thinks" },
88
- { label: "tools", insert: "tools:\n - files", hint: "what the runtime gives it" },
89
+ { label: "tools", insert: "tools:\n - write", hint: "what the runtime gives it" },
89
90
  { label: "secrets", insert: "secrets:\n - ", hint: "credentials it may spend" },
90
91
  { label: "skills", insert: "skills:\n - ", hint: "allowlist; omit to inherit all" },
91
92
  { label: "scripts", insert: "scripts:\n - name: \n run: \n description: " },
92
93
  { label: "runtime", insert: "runtime:\n python: \"3\"\n packages: []" },
93
94
  { label: "mcpServers", insert: "mcpServers:\n " },
95
+ { label: "ask", insert: "ask:\n timeout: 30m", hint: "tools: [ask] — how long ask_person waits for a person (max 24h)" },
94
96
  { label: "permissionMode", insert: "permissionMode: plan", hint: "plan = read-only" },
95
97
  { label: "disallowedTools", insert: "disallowedTools:\n - " },
98
+ { label: "subagents", insert: "subagents:\n - ", hint: "colleagues it may delegate a job to — own context, own tools, no wider than its own" },
99
+ { label: "limits", insert: "limits:\n web.search: 40\n calls: 300", hint: "per step: a call past a limit is refused, never run" },
96
100
  {
97
101
  label: "provider",
98
102
  insert: "provider:\n name: \n token: ${PROVIDER_TOKEN}",
@@ -151,6 +155,8 @@ const FIELDS = {
151
155
  { label: "trigger", insert: "trigger: manual" },
152
156
  { label: "schedule", insert: "schedule: \"0 6 * * MON\"" },
153
157
  { label: "timezone", insert: "timezone: Australia/Sydney" },
158
+ { label: "language", insert: "language: en-AU" },
159
+ { label: "region", insert: "region: au", hint: "currency, units, calendar, weekend and text direction follow" },
154
160
  { label: "model", insert: "model: fast", hint: "every step, unless the step says" },
155
161
  { label: "effort", insert: "effort: high", hint: "every step, unless the step says" },
156
162
  ],
@@ -475,7 +481,7 @@ export function completionsAt(path, text, cursor, vocab) {
475
481
  each: [
476
482
  { label: "lines", hint: "one instance per line of the previous result" },
477
483
  { label: "items", hint: "one per element of the previous output: json array" },
478
- { label: "rows of ../../storage/x.csv", hint: "one per CSV row" },
484
+ { label: "rows of workspace/storage/x.csv", hint: "one per CSV row" },
479
485
  ],
480
486
  };
481
487
  const items = byKey[key];
@@ -500,6 +506,9 @@ export function completionsAt(path, text, cursor, vocab) {
500
506
  ],
501
507
  secrets: vocab.secrets.map((s) => ({ label: s, hint: "secret" })),
502
508
  skills: vocab.skills.map((s) => ({ label: s, hint: "skill" })),
509
+ // Colleagues: a consult (agents:) or a delegate (subagents:).
510
+ agents: vocab.agents.map((a) => ({ label: a, hint: "consult — answers, no tools" })),
511
+ subagents: vocab.agents.map((a) => ({ label: a, hint: "delegate — own context and tools" })),
503
512
  disallowedTools: [...TOOL_GROUPS, { label: "Bash" }, { label: "Write" }],
504
513
  when: [{ label: "schema" }, { label: "urgent" }],
505
514
  expect: ASSERTIONS.map((a) => ({ ...a, insert: `${a.label}: ` })),
@@ -22,6 +22,16 @@ export declare const isFilesystemTool: (toolName: string) => boolean;
22
22
  * enforced; it is advice in the error text.
23
23
  */
24
24
  export declare function suggestPath(raw: string): string;
25
+ /**
26
+ * A path an author wrote in a flow or an agent wrote in a tool call, as an
27
+ * absolute path: `workspace/…` from the workspace root, anything else from
28
+ * the agent's own folder (so the older `../../storage/x` lands in the same
29
+ * place as `workspace/storage/x`). Confinement is the caller's: this only
30
+ * says where the path points.
31
+ */
32
+ export declare function resolveAgentPath(workspaceRoot: string, agentDir: string, raw: string): string;
33
+ /** True when `abs` is `root` or under it. */
34
+ export declare function isWithin(root: string, abs: string): boolean;
25
35
  /** Expand a virtual prefix to a real absolute path, or null if it has none. */
26
36
  export declare function expandVirtual(raw: string, roots: Roots): {
27
37
  abs: string;
@@ -29,3 +39,15 @@ export declare function expandVirtual(raw: string, roots: Roots): {
29
39
  } | null;
30
40
  export declare function checkPaths(toolName: string, input: Record<string, unknown>, roots: Roots): ConfineVerdict;
31
41
  export declare function checkBash(command: string): ConfineVerdict;
42
+ export declare function linkWorkspace(agentDir: string, workspaceRoot: string): {
43
+ release: () => void;
44
+ note: string | null;
45
+ };
46
+ /**
47
+ * `env` plus what makes `node workspace/…` its own main module (above):
48
+ * the link's path(s) in FOLDRUN_WORKSPACE_LINK, the preload appended to
49
+ * NODE_OPTIONS — after what it held, or the host's when it held nothing.
50
+ * Idempotent, and harmless where no link exists: the preload matches only
51
+ * a path under it.
52
+ */
53
+ export declare function workspaceLinkEnv<T extends Record<string, string | undefined>>(env: T, agentDir: string): T;