@foldrun/core 0.2.0 → 0.4.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 +31 -10
- package/dist/src/approvals.d.ts +104 -1
- package/dist/src/approvals.js +192 -2
- package/dist/src/budget.d.ts +32 -0
- package/dist/src/budget.js +177 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/confine.js +6 -1
- package/dist/src/deploy.js +4 -2
- package/dist/src/egress.d.ts +19 -2
- package/dist/src/egress.js +2 -1
- package/dist/src/evals.d.ts +4 -0
- package/dist/src/evals.js +20 -11
- package/dist/src/flow-lint.d.ts +4 -0
- package/dist/src/flow-lint.js +108 -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/library.js +13 -1
- package/dist/src/notify.d.ts +79 -1
- package/dist/src/notify.js +243 -9
- package/dist/src/paths.d.ts +5 -0
- package/dist/src/paths.js +32 -0
- package/dist/src/platform.d.ts +112 -2
- package/dist/src/platform.js +20 -2
- package/dist/src/provider-health.d.ts +55 -0
- package/dist/src/provider-health.js +232 -0
- package/dist/src/run-container.d.ts +80 -4
- package/dist/src/run-container.js +176 -37
- package/dist/src/runner.d.ts +41 -5
- package/dist/src/runner.js +513 -81
- package/dist/src/script-tools.d.ts +12 -0
- package/dist/src/script-tools.js +10 -2
- package/dist/src/secret-health.d.ts +59 -0
- package/dist/src/secret-health.js +155 -0
- package/dist/src/secrets.d.ts +19 -1
- package/dist/src/secrets.js +44 -12
- package/dist/src/starter.js +4 -0
- package/dist/src/step-exec.d.ts +62 -6
- package/dist/src/step-exec.js +199 -68
- package/dist/src/store.d.ts +300 -3
- package/dist/src/store.js +574 -26
- 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-test.js +48 -21
- package/dist/src/translator.js +2 -1
- package/dist/src/trigger-log.d.ts +43 -0
- package/dist/src/trigger-log.js +122 -0
- package/dist/src/triggers.d.ts +20 -0
- package/dist/src/triggers.js +91 -10
- package/dist/src/webhook.d.ts +15 -3
- package/dist/src/webhook.js +11 -11
- package/package.json +6 -5
- package/src/approvals.ts +253 -2
- package/src/budget.ts +178 -0
- package/src/catalog.ts +2 -1
- package/src/confine.ts +6 -1
- package/src/deploy.ts +4 -1
- package/src/egress.ts +20 -4
- package/src/evals.ts +25 -9
- package/src/flow-lint.ts +118 -0
- package/src/host-env.ts +51 -0
- package/src/json-schema.ts +184 -0
- package/src/library.ts +13 -1
- package/src/notify.ts +272 -11
- package/src/paths.ts +29 -0
- package/src/platform.ts +119 -3
- package/src/provider-health.ts +274 -0
- package/src/run-container.ts +215 -35
- package/src/runner.ts +525 -73
- package/src/script-tools.ts +18 -2
- package/src/secret-health.ts +185 -0
- package/src/secrets.ts +44 -10
- package/src/starter.ts +4 -0
- package/src/step-exec.ts +176 -21
- package/src/storage.ts +8 -2
- package/src/store.ts +785 -25
- package/src/tar.ts +2 -1
- package/src/test-mode.ts +311 -0
- package/src/tool-test.ts +49 -20
- package/src/translator.ts +2 -1
- package/src/trigger-log.ts +159 -0
- package/src/triggers.ts +92 -10
- package/src/webhook.ts +17 -11
package/src/triggers.ts
CHANGED
|
@@ -70,11 +70,23 @@ export async function fireChainedFlows(tenant: string, workspace: string, finish
|
|
|
70
70
|
(result ? `\n${result}` : "");
|
|
71
71
|
for (const flow of flows) {
|
|
72
72
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
const gate = await platform.admitTrigger(tenant, workspace, flow, { body, tag: "previous_run" });
|
|
74
|
+
if (!gate.admit) {
|
|
75
|
+
console.log(`[foldrun] trigger: flow — ${tenant}/${workspace}/${flow.name} not started: ${gate.detail}`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
// A test run's chain stays a test: the flow after it reads a result
|
|
80
|
+
// nothing real produced, and must not act on it for real either.
|
|
81
|
+
const run = await platform.enqueueFlowRun(tenant, workspace, withTask(flow.steps, "previous_run", body), flow.name, flow.model, [
|
|
82
|
+
`after:${finished.flow}`,
|
|
83
|
+
], null, finished.test ? { test: true } : {});
|
|
84
|
+
started.push(run.id);
|
|
85
|
+
console.log(`[foldrun] trigger: flow — ${tenant}/${workspace}/${flow.name} started after ${finished.flow} ${finished.status}`);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
await platform.rollbackTrigger(tenant, workspace, flow, { reason: err instanceof Error ? err.message : String(err) });
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
78
90
|
} catch (err) {
|
|
79
91
|
console.error(`[foldrun] trigger: flow — ${tenant}/${workspace}/${flow.name}:`, err instanceof Error ? err.message : err);
|
|
80
92
|
}
|
|
@@ -121,10 +133,22 @@ export async function fireStorageTriggers(
|
|
|
121
133
|
if (hits.length === 0) continue;
|
|
122
134
|
try {
|
|
123
135
|
const body = `by: ${by}\n${hits.map((h) => `- storage/${h}`).join("\n")}`;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
// A deploy that writes forty files under one prefix is one event, not
|
|
137
|
+
// forty runs — which is exactly what `debounce:` is for.
|
|
138
|
+
const gate = await platform.admitTrigger(tenant, workspace, flow, { body, tag: "storage_event" });
|
|
139
|
+
if (!gate.admit) {
|
|
140
|
+
console.log(`[foldrun] trigger: storage — ${tenant}/${workspace}/${flow.name} not started: ${gate.detail}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const run = await platform.enqueueFlowRun(tenant, workspace, withTask(flow.steps, "storage_event", body), flow.name, flow.model, [
|
|
145
|
+
"storage",
|
|
146
|
+
]);
|
|
147
|
+
started.push(run.id);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
await platform.rollbackTrigger(tenant, workspace, flow, { reason: err instanceof Error ? err.message : String(err) });
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
128
152
|
} catch (err) {
|
|
129
153
|
console.error(`[foldrun] trigger: storage — ${tenant}/${workspace}/${flow.name}:`, err instanceof Error ? err.message : err);
|
|
130
154
|
}
|
|
@@ -132,6 +156,64 @@ export async function fireStorageTriggers(
|
|
|
132
156
|
return started;
|
|
133
157
|
}
|
|
134
158
|
|
|
159
|
+
/** HTML to the text a prompt reads. Style and script blocks are removed
|
|
160
|
+
* until none remain — one pass leaves `<sty<style>le>` behind, which is
|
|
161
|
+
* the shape a sanitiser checker (rightly) flags. Nothing here is rendered
|
|
162
|
+
* as HTML afterwards; it becomes a task for an agent. */
|
|
163
|
+
function stripTags(html: string): string {
|
|
164
|
+
let out = html;
|
|
165
|
+
for (let prev = ""; prev !== out; ) {
|
|
166
|
+
prev = out;
|
|
167
|
+
out = out.replace(/<(style|script)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, "");
|
|
168
|
+
}
|
|
169
|
+
return out.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ------------------------------------------------------- idempotency keys
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The value that identifies this delivery, as the flow's `idempotency:`
|
|
176
|
+
* names it.
|
|
177
|
+
*
|
|
178
|
+
* Two shapes, and only two:
|
|
179
|
+
*
|
|
180
|
+
* idempotency: x-github-delivery a request header
|
|
181
|
+
* idempotency: body:id a top-level field of a JSON body
|
|
182
|
+
*
|
|
183
|
+
* A NAME, never an expression — the grammar has no expressions, and this is
|
|
184
|
+
* not the place to grow one. Nothing is computed, combined or nested: if the
|
|
185
|
+
* sender does not put an identifier somewhere a name can reach, the flow
|
|
186
|
+
* cannot dedupe on it, and saying so is better than inventing a syntax.
|
|
187
|
+
*
|
|
188
|
+
* Returns null when the flow asks for no key, or when the delivery does not
|
|
189
|
+
* carry the one it asked for. A missing key admits the delivery: refusing
|
|
190
|
+
* one because a sender omitted a header would turn "dedupe when you can"
|
|
191
|
+
* into an outage.
|
|
192
|
+
*/
|
|
193
|
+
export function deliveryKey(
|
|
194
|
+
idempotency: string | null,
|
|
195
|
+
header: (name: string) => string | null,
|
|
196
|
+
body: string,
|
|
197
|
+
): string | null {
|
|
198
|
+
if (!idempotency) return null;
|
|
199
|
+
if (idempotency.startsWith("body:")) {
|
|
200
|
+
const field = idempotency.slice(5).trim();
|
|
201
|
+
if (!field) return null;
|
|
202
|
+
try {
|
|
203
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
204
|
+
const value = parsed?.[field];
|
|
205
|
+
// A scalar only. An object stringifies to "[object Object]" for every
|
|
206
|
+
// delivery, which would make the second one and every one after it a
|
|
207
|
+
// "duplicate" of the first.
|
|
208
|
+
if (value === undefined || value === null || typeof value === "object") return null;
|
|
209
|
+
return String(value).slice(0, 200);
|
|
210
|
+
} catch {
|
|
211
|
+
return null; // not JSON, so no field to read
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return header(idempotency)?.slice(0, 200) || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
135
217
|
// ------------------------------------------------------ signed webhooks
|
|
136
218
|
|
|
137
219
|
export interface SignatureVerdict {
|
|
@@ -276,7 +358,7 @@ export function normaliseInboundEmail(contentType: string | null, raw: string):
|
|
|
276
358
|
const html = pick("html", "HtmlBody", "body-html");
|
|
277
359
|
const text =
|
|
278
360
|
pick("text", "TextBody", "body-plain", "stripped-text", "plain") ||
|
|
279
|
-
html
|
|
361
|
+
stripTags(html);
|
|
280
362
|
if (!from && !subject && !text) return null;
|
|
281
363
|
return { from, to: pick("to", "To", "recipient", "ToFull"), subject, text };
|
|
282
364
|
}
|
package/src/webhook.ts
CHANGED
|
@@ -4,22 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { dataRoot } from "./paths.ts";
|
|
8
7
|
import { workspaceDir } from "./store.ts";
|
|
8
|
+
import { installKeyMaterial } from "./secrets.ts";
|
|
9
9
|
import crypto from "node:crypto";
|
|
10
10
|
|
|
11
|
-
const keyFile = () => path.join(dataRoot(), ".secret-key");
|
|
12
|
-
|
|
13
11
|
/**
|
|
14
|
-
* The install's secret key. Every derived token — flow hooks, git secrets
|
|
15
|
-
* hangs off this one value, so rotating it invalidates all
|
|
12
|
+
* The install's secret key. Every derived token — flow hooks, git secrets,
|
|
13
|
+
* approval links — hangs off this one value, so rotating it invalidates all
|
|
14
|
+
* of them together.
|
|
15
|
+
*
|
|
16
|
+
* The same material the vault encrypts with, created on first use. This used
|
|
17
|
+
* to fall back to the constant "foldrun-dev-install" until the vault lazily
|
|
18
|
+
* wrote `.secret-key`, which meant two things nobody wanted: every hook and
|
|
19
|
+
* approval token on a fresh install was computable from the source, and all
|
|
20
|
+
* of them silently changed the first time a secret was stored.
|
|
16
21
|
*/
|
|
17
22
|
export function installKey(): string {
|
|
18
|
-
|
|
19
|
-
if (fs.existsSync(keyFile())) return fs.readFileSync(keyFile(), "utf8");
|
|
20
|
-
// secrets.ts creates this on first use; fall back to a fixed dev value so
|
|
21
|
-
// hook URLs stay stable before any secret has been set.
|
|
22
|
-
return "foldrun-dev-install";
|
|
23
|
+
return installKeyMaterial();
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
// Per-hook rotation without storing tokens: what is stored is a generation
|
|
@@ -133,7 +134,12 @@ export function publicUrl(): string | null {
|
|
|
133
134
|
export interface HookDelivery {
|
|
134
135
|
t: string;
|
|
135
136
|
flow: string;
|
|
136
|
-
|
|
137
|
+
/** `dropped` is a delivery that arrived, was authentic, and did not
|
|
138
|
+
* become a run because the flow's own gates said one was enough —
|
|
139
|
+
* a duplicate, a throttled burst, a quarantined flow. Distinct from
|
|
140
|
+
* `accepted` so "why did nothing run" has an answer in the log, and
|
|
141
|
+
* distinct from the refusals so nobody hunts for a bad token. */
|
|
142
|
+
outcome: "accepted" | "invalid-token" | "invalid-signature" | "not-webhook" | "no-flow" | "error" | "dropped";
|
|
137
143
|
runId?: string;
|
|
138
144
|
bytes?: number;
|
|
139
145
|
detail?: string;
|