@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.
- package/README.md +27 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/src/account.d.ts +45 -0
- package/dist/src/account.js +130 -0
- package/dist/src/api-tools.d.ts +8 -0
- package/dist/src/api-tools.js +22 -4
- package/dist/src/approvals.d.ts +104 -1
- package/dist/src/approvals.js +192 -2
- package/dist/src/browser-pod.d.ts +129 -0
- package/dist/src/browser-pod.js +284 -0
- package/dist/src/budget.d.ts +32 -0
- package/dist/src/budget.js +177 -0
- package/dist/src/catalog.js +3 -1
- package/dist/src/clock.d.ts +49 -0
- package/dist/src/clock.js +121 -0
- package/dist/src/completions.js +17 -8
- package/dist/src/confine.d.ts +22 -0
- package/dist/src/confine.js +224 -4
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +26 -2
- package/dist/src/egress.js +8 -1
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +9 -0
- package/dist/src/evals.js +26 -13
- package/dist/src/flow-lint.d.ts +12 -0
- package/dist/src/flow-lint.js +331 -1
- package/dist/src/flow-patterns.d.ts +141 -0
- package/dist/src/flow-patterns.js +568 -0
- package/dist/src/gitrepo.d.ts +18 -0
- package/dist/src/gitrepo.js +49 -0
- package/dist/src/host-env.d.ts +8 -0
- package/dist/src/host-env.js +48 -0
- package/dist/src/json-schema.d.ts +12 -0
- package/dist/src/json-schema.js +202 -0
- package/dist/src/kinds.js +1 -1
- package/dist/src/language.d.ts +17 -0
- package/dist/src/language.js +55 -0
- package/dist/src/layout.d.ts +69 -0
- package/dist/src/layout.js +200 -0
- package/dist/src/library.d.ts +9 -0
- package/dist/src/library.js +43 -4
- package/dist/src/limits.d.ts +84 -0
- package/dist/src/limits.js +287 -0
- package/dist/src/locale.d.ts +47 -0
- package/dist/src/locale.js +177 -0
- package/dist/src/mail.d.ts +88 -0
- package/dist/src/mail.js +245 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +132 -8
- package/dist/src/notify.js +360 -58
- package/dist/src/openapi.js +5 -3
- package/dist/src/operator.d.ts +201 -0
- package/dist/src/operator.js +251 -0
- package/dist/src/paths.d.ts +33 -0
- package/dist/src/paths.js +87 -0
- package/dist/src/platform.d.ts +144 -4
- package/dist/src/platform.js +27 -3
- package/dist/src/provider-health.d.ts +55 -0
- package/dist/src/provider-health.js +233 -0
- package/dist/src/providers.d.ts +481 -0
- package/dist/src/providers.js +1105 -6
- package/dist/src/refusal.d.ts +83 -0
- package/dist/src/refusal.js +281 -0
- package/dist/src/run-container.d.ts +206 -17
- package/dist/src/run-container.js +547 -80
- package/dist/src/runner.d.ts +179 -10
- package/dist/src/runner.js +1423 -169
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +15 -2
- package/dist/src/script-tools.js +44 -4
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +83 -0
- package/dist/src/secret-health.js +177 -0
- package/dist/src/secrets.d.ts +38 -1
- package/dist/src/secrets.js +197 -96
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +95 -11
- package/dist/src/step-exec.d.ts +106 -1
- package/dist/src/step-exec.js +558 -85
- package/dist/src/step-removal.d.ts +15 -0
- package/dist/src/step-removal.js +49 -0
- package/dist/src/storage.d.ts +23 -4
- package/dist/src/storage.js +125 -23
- package/dist/src/store.d.ts +492 -38
- package/dist/src/store.js +882 -69
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/tar.js +2 -1
- package/dist/src/test-mode.d.ts +111 -0
- package/dist/src/test-mode.js +285 -0
- package/dist/src/tool-names.d.ts +13 -1
- package/dist/src/tool-names.js +38 -11
- package/dist/src/tool-programs.d.ts +26 -0
- package/dist/src/tool-programs.js +172 -0
- package/dist/src/tool-test.js +152 -33
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +71 -24
- package/dist/src/trigger-log.d.ts +43 -0
- package/dist/src/trigger-log.js +122 -0
- package/dist/src/triggers.d.ts +23 -0
- package/dist/src/triggers.js +101 -12
- package/dist/src/untrusted-fetch.d.ts +11 -0
- package/dist/src/untrusted-fetch.js +77 -0
- package/dist/src/web-actions.d.ts +91 -0
- package/dist/src/web-actions.js +199 -0
- package/dist/src/webhook.d.ts +19 -3
- package/dist/src/webhook.js +50 -11
- package/index.ts +3 -0
- package/package.json +5 -5
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/approvals.ts +253 -2
- package/src/browser-pod.ts +313 -0
- package/src/budget.ts +178 -0
- package/src/catalog.ts +3 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +224 -4
- package/src/deploy.ts +137 -6
- package/src/egress.ts +32 -4
- package/src/eol.ts +7 -0
- package/src/evals.ts +36 -11
- package/src/flow-lint.ts +371 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/host-env.ts +51 -0
- package/src/json-schema.ts +184 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +50 -4
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +316 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +391 -44
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +83 -0
- package/src/platform.ts +158 -5
- package/src/provider-health.ts +275 -0
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +611 -76
- package/src/runner.ts +1463 -165
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +55 -6
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +230 -0
- package/src/secrets.ts +191 -95
- package/src/starter.ts +96 -11
- package/src/step-exec.ts +548 -41
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +149 -24
- package/src/store.ts +1227 -74
- package/src/subagents.ts +189 -0
- package/src/tar.ts +2 -1
- package/src/test-mode.ts +311 -0
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +159 -32
- package/src/translator.ts +74 -23
- package/src/trigger-log.ts +160 -0
- package/src/triggers.ts +102 -12
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +58 -11
- package/templates/hello/agents/curator/agent.md +1 -1
- 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
|
+
}
|
package/dist/src/catalog.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/src/completions.js
CHANGED
|
@@ -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: "
|
|
33
|
-
{ label: "
|
|
34
|
-
{ label: "
|
|
35
|
-
{ label: "
|
|
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 -
|
|
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
|
|
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}: ` })),
|
package/dist/src/confine.d.ts
CHANGED
|
@@ -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;
|