@foldrun/core 0.4.0 → 0.6.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 +7 -4
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -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/browser-pod.d.ts +148 -0
- package/dist/src/browser-pod.js +333 -0
- package/dist/src/catalog.js +2 -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 +218 -3
- package/dist/src/container.d.ts +9 -0
- package/dist/src/container.js +115 -5
- package/dist/src/context-tools.d.ts +1 -1
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +7 -0
- package/dist/src/egress.js +6 -0
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +5 -0
- package/dist/src/evals.js +9 -2
- package/dist/src/flow-lint.d.ts +8 -0
- package/dist/src/flow-lint.js +223 -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.js +5 -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 +30 -3
- 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 +247 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +58 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +53 -7
- package/dist/src/notify.js +194 -126
- 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 +28 -0
- package/dist/src/paths.js +55 -0
- package/dist/src/platform.d.ts +43 -3
- package/dist/src/platform.js +9 -1
- package/dist/src/provider-health.js +3 -2
- 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 +149 -13
- package/dist/src/run-container.js +424 -43
- package/dist/src/runner.d.ts +177 -9
- package/dist/src/runner.js +1146 -154
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +17 -2
- package/dist/src/script-tools.js +82 -10
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +30 -6
- package/dist/src/secret-health.js +42 -20
- package/dist/src/secrets.d.ts +19 -0
- package/dist/src/secrets.js +155 -86
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +92 -11
- package/dist/src/step-exec.d.ts +72 -2
- package/dist/src/step-exec.js +431 -37
- 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 +222 -37
- package/dist/src/store.js +349 -62
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/test-mode.d.ts +17 -2
- package/dist/src/test-mode.js +53 -13
- 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 +104 -12
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +69 -23
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/triggers.d.ts +3 -0
- package/dist/src/triggers.js +10 -2
- 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 +4 -0
- package/dist/src/webhook.js +39 -0
- package/index.ts +4 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +350 -0
- package/src/catalog.ts +2 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +218 -3
- package/src/container.ts +112 -5
- package/src/context-tools.ts +1 -1
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +14 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/host-env.ts +5 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +37 -3
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +318 -0
- package/src/model-credential.ts +71 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +189 -103
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +54 -0
- package/src/platform.ts +52 -3
- package/src/provider-health.ts +3 -2
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +460 -44
- package/src/runner.ts +1168 -155
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +81 -10
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +64 -19
- package/src/secrets.ts +149 -87
- package/src/starter.ts +93 -11
- package/src/step-exec.ts +447 -38
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +506 -70
- package/src/subagents.ts +189 -0
- package/src/test-mode.ts +51 -11
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +110 -12
- package/src/translator.ts +72 -22
- package/src/trigger-log.ts +1 -0
- package/src/triggers.ts +10 -2
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +41 -0
- package/templates/blog-desk/agents/writer/agent.md +1 -1
- package/templates/hello/agents/curator/agent.md +4 -4
- package/templates/hello/agents/notetaker/agent.md +3 -3
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// The web, as actions. One question answered in one place: which provider
|
|
2
|
+
// can do which action, and what does foldrun do when none is named?
|
|
3
|
+
//
|
|
4
|
+
// An action is a call with a fixed input and a fixed output — search takes a
|
|
5
|
+
// query and returns URLs, fetch takes a URL and returns the page — whoever
|
|
6
|
+
// answers it. A provider is the list of actions it has an adapter for. foldrun
|
|
7
|
+
// is a provider like any other, and the one used when `web: {<action>: …}` is
|
|
8
|
+
// unset. A provider named for an action it cannot do is an error in `check`,
|
|
9
|
+
// never a quiet fall back to ours: a fallback would bill and behave
|
|
10
|
+
// differently from what the file says.
|
|
11
|
+
//
|
|
12
|
+
// Browse has a second level: inside a page, the steps and modes of
|
|
13
|
+
// the browse action. A browser reached over CDP is driven by our own code, so it
|
|
14
|
+
// takes every step and mode unless it is listed as lacking one; a vendor
|
|
15
|
+
// that runs the steps itself would list the few it has.
|
|
16
|
+
//
|
|
17
|
+
// The vendor facts (endpoint, key, docs page, the date the adapter was last
|
|
18
|
+
// matched to those docs) stay on SEARCH_APIS, FETCH_APIS and BROWSER_APIS in
|
|
19
|
+
// providers.ts; this file reads them rather than restating them.
|
|
20
|
+
|
|
21
|
+
import { SEARCH_APIS, FETCH_APIS, BROWSER_APIS, ACTION_APIS, WEB_ACTIONS, webConfig, type WebAction } from "./providers.ts";
|
|
22
|
+
|
|
23
|
+
export { WEB_ACTIONS, type WebAction };
|
|
24
|
+
|
|
25
|
+
/** What the browse action can return — its `mode`. Kept equal to the tool's own
|
|
26
|
+
* MODES by the platform's gallery test. */
|
|
27
|
+
export const BROWSE_MODES = [
|
|
28
|
+
"text", "markdown", "html", "links", "meta", "table", "network", "screenshot", "pdf", "capture",
|
|
29
|
+
"aria", "map", "crawl", "console", "vitals", "a11y", "diff", "feed", "webmcp", "react",
|
|
30
|
+
] as const;
|
|
31
|
+
|
|
32
|
+
/** What the browse action can do in a page — its `actions` steps. Kept equal to
|
|
33
|
+
* the tool's own ACTIONS by the platform's gallery test. */
|
|
34
|
+
export const BROWSE_STEPS = [
|
|
35
|
+
"click", "dblclick", "rightclick", "hover", "check", "uncheck", "drag", "fill", "type", "press", "select",
|
|
36
|
+
"wait", "goto", "screenshot", "pdf", "scroll", "paste", "upload", "download", "extract", "frame", "tab",
|
|
37
|
+
"back", "forward", "reload", "focus", "clear", "highlight", "keyboard", "keydown", "keyup", "mouse", "wheel",
|
|
38
|
+
"tap", "swipe", "clipboard", "eval", "get", "expect", "if", "mock", "unmock", "offline", "cookie",
|
|
39
|
+
"localstorage", "sessionstorage", "viewport", "dialog", "solve", "pushstate", "insert", "webmcp", "login",
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export interface ActionSupport {
|
|
43
|
+
/** live: callable today. planned: agreed and not built — naming it is an
|
|
44
|
+
* error that says so, rather than a tool that does nothing. */
|
|
45
|
+
status: "live" | "planned";
|
|
46
|
+
/** How: which tool serves it, and for foldrun's own, with what. */
|
|
47
|
+
how: string;
|
|
48
|
+
/** Browse only. cdp: our code drives the browser, so every step and mode
|
|
49
|
+
* works unless listed in `lacks`. api: the vendor runs the steps, and
|
|
50
|
+
* `steps`/`modes` list what it has. */
|
|
51
|
+
via?: "cdp" | "api";
|
|
52
|
+
steps?: readonly string[];
|
|
53
|
+
modes?: readonly string[];
|
|
54
|
+
lacks?: readonly string[];
|
|
55
|
+
/** The vendor page the adapter was built from, and the day it was last
|
|
56
|
+
* matched to it (YYYY-MM-DD). Absent on foldrun's own. */
|
|
57
|
+
docs?: string;
|
|
58
|
+
checked?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WebProvider {
|
|
62
|
+
name: string;
|
|
63
|
+
title: string;
|
|
64
|
+
actions: Partial<Record<WebAction, ActionSupport>>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Ours: every action, and none of them with a second model in it. */
|
|
68
|
+
export const FOLDRUN: WebProvider = {
|
|
69
|
+
name: "foldrun",
|
|
70
|
+
title: "foldrun",
|
|
71
|
+
actions: {
|
|
72
|
+
search: { status: "live", how: "the account's own SearXNG" },
|
|
73
|
+
fetch: { status: "live", how: "one plain HTTP request in the sandbox" },
|
|
74
|
+
browse: { status: "live", how: "the account's browser pod", via: "cdp" },
|
|
75
|
+
crawl: { status: "live", how: "plain HTTP, same site, robots.txt honoured" },
|
|
76
|
+
map: { status: "live", how: "the site's sitemaps, else its links" },
|
|
77
|
+
extract: { status: "live", how: "selectors, or the page's own JSON-LD and OpenGraph — no model" },
|
|
78
|
+
answer: { status: "live", how: "search, read the top pages, rank the passages that answer — no model" },
|
|
79
|
+
monitor: { status: "live", how: "a page's lines or a query's results, against the last call, in state/" },
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type Facts = { name: string; title: string; aliases?: string[]; docs?: string; checked?: string; lacks?: readonly string[] };
|
|
84
|
+
|
|
85
|
+
/** Every provider and what it can do, foldrun first. Built from the vendor
|
|
86
|
+
* lists so a vendor added there is here without a second edit. */
|
|
87
|
+
export function webProviders(): WebProvider[] {
|
|
88
|
+
const byName = new Map<string, WebProvider>();
|
|
89
|
+
const add = (f: Facts, action: WebAction, support: ActionSupport) => {
|
|
90
|
+
const p = byName.get(f.name) ?? { name: f.name, title: f.title, actions: {} };
|
|
91
|
+
p.actions[action] = { ...support, ...(f.docs ? { docs: f.docs } : {}), ...(f.checked ? { checked: f.checked } : {}) };
|
|
92
|
+
byName.set(f.name, p);
|
|
93
|
+
};
|
|
94
|
+
for (const a of SEARCH_APIS as readonly Facts[]) add(a, "search", { status: "live", how: a.title });
|
|
95
|
+
for (const a of FETCH_APIS as readonly Facts[]) add(a, "fetch", { status: "live", how: a.title });
|
|
96
|
+
for (const a of BROWSER_APIS as readonly Facts[]) {
|
|
97
|
+
add(a, "browse", { status: "live", how: a.title, via: "cdp", ...(a.lacks?.length ? { lacks: a.lacks } : {}) });
|
|
98
|
+
}
|
|
99
|
+
for (const [action, apis] of Object.entries(ACTION_APIS) as [WebAction, readonly Facts[]][]) {
|
|
100
|
+
for (const a of apis) add(a, action, { status: "live", how: a.title });
|
|
101
|
+
}
|
|
102
|
+
return [FOLDRUN, ...byName.values()];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The names a provider answers to, for lookups. */
|
|
106
|
+
function aliasesOf(name: string): string[] {
|
|
107
|
+
const all = [...SEARCH_APIS, ...FETCH_APIS, ...BROWSER_APIS, ...Object.values(ACTION_APIS).flat()] as readonly Facts[];
|
|
108
|
+
return all.filter((a) => a.name === name).flatMap((a) => a.aliases ?? []);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function findWebProvider(name: string): WebProvider | undefined {
|
|
112
|
+
const key = name.trim().toLowerCase();
|
|
113
|
+
if (key === "" || key === "ours") return FOLDRUN;
|
|
114
|
+
return webProviders().find((p) => p.name === key || aliasesOf(p.name).includes(key));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The live providers for an action, foldrun first — for "these do" lines. */
|
|
118
|
+
export function providersFor(action: WebAction): string[] {
|
|
119
|
+
return webProviders().filter((p) => p.actions[action]?.status === "live").map((p) => p.name);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ActionChoice =
|
|
123
|
+
| { provider: WebProvider; support: ActionSupport; error?: undefined }
|
|
124
|
+
| { provider?: undefined; support?: undefined; error: string };
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Who does `action`. Unset is foldrun. A named provider must have the action
|
|
128
|
+
* live; anything else is an error naming the providers that do — the rule is
|
|
129
|
+
* "fix the file", never "we used ours instead".
|
|
130
|
+
*/
|
|
131
|
+
export function resolveWebAction(action: WebAction, name?: string | null): ActionChoice {
|
|
132
|
+
const field = `web.${action}`;
|
|
133
|
+
const p = name == null ? FOLDRUN : findWebProvider(name);
|
|
134
|
+
if (!p) {
|
|
135
|
+
return { error: `${field}: ${name} — no provider by that name. These ${action}: ${providersFor(action).join(", ")}.` };
|
|
136
|
+
}
|
|
137
|
+
const s = p.actions[action];
|
|
138
|
+
if (!s) {
|
|
139
|
+
const live = providersFor(action);
|
|
140
|
+
return {
|
|
141
|
+
error: `${field}: ${p.name} does not ${action} here.` +
|
|
142
|
+
(live.length ? ` These do: ${live.join(", ")}.` : " No provider has it wired yet.") +
|
|
143
|
+
(FOLDRUN.actions[action]?.status === "live" ? " Unset uses foldrun." : ""),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (s.status === "planned") {
|
|
147
|
+
const others = providersFor(action).filter((n) => n !== p.name);
|
|
148
|
+
return {
|
|
149
|
+
error: `${field}: ${p.title}'s own ${action} is not built yet` +
|
|
150
|
+
(others.length ? ` — name a provider that has it: ${others.join(", ")}.` : " and no provider has it wired either."),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return { provider: p, support: s };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Can this browser run this step, or return this mode? */
|
|
157
|
+
export function browseSupports(support: ActionSupport, stepOrMode: string): boolean {
|
|
158
|
+
if (support.via === "cdp") return !(support.lacks ?? []).includes(stepOrMode);
|
|
159
|
+
return (support.steps ?? []).includes(stepOrMode) || (support.modes ?? []).includes(stepOrMode);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** The browsers that can run a step or mode — for the error when one cannot. */
|
|
163
|
+
export function browsersFor(stepOrMode: string): string[] {
|
|
164
|
+
return webProviders()
|
|
165
|
+
.filter((p) => p.actions.browse?.status === "live" && browseSupports(p.actions.browse, stepOrMode))
|
|
166
|
+
.map((p) => p.name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Vendor actions whose adapter has not been matched to the vendor's docs
|
|
170
|
+
* within `days` — each as "provider action (checked …)". Empty is healthy.
|
|
171
|
+
* The test that calls this is the rule "integrations follow the latest
|
|
172
|
+
* docs" in a form CI can fail on. */
|
|
173
|
+
export function staleIntegrations(today: string, days = 90): string[] {
|
|
174
|
+
const out: string[] = [];
|
|
175
|
+
const limit = Date.parse(today) - days * 86_400_000;
|
|
176
|
+
for (const p of webProviders()) {
|
|
177
|
+
if (p === FOLDRUN) continue;
|
|
178
|
+
for (const [action, s] of Object.entries(p.actions)) {
|
|
179
|
+
if (!s || s.status !== "live") continue;
|
|
180
|
+
if (!s.docs || !s.checked) out.push(`${p.name} ${action} (no docs page or check date recorded)`);
|
|
181
|
+
else if (Date.parse(s.checked) < limit) out.push(`${p.name} ${action} (checked ${s.checked})`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The actions after search, fetch and browse. Those three are resolved by
|
|
188
|
+
* providers.ts, which also knows the model providers that search. */
|
|
189
|
+
export const NEWER_ACTIONS = ["crawl", "map", "extract", "answer", "monitor"] as const;
|
|
190
|
+
type NewerAction = (typeof NEWER_ACTIONS)[number];
|
|
191
|
+
|
|
192
|
+
export type ActionApiChoice =
|
|
193
|
+
| { provider: null; error?: undefined }
|
|
194
|
+
| { provider: string; secret: string; host: string; error?: undefined }
|
|
195
|
+
| { provider: null; error: string };
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Who does one of the newer actions, and with which key: unset (or
|
|
199
|
+
* "foldrun") is ours and needs none; a name, or `{name, key: ${VAULT_NAME}}`
|
|
200
|
+
* for a key stored under another name, is that provider — if it has the
|
|
201
|
+
* action. The runner turns a provider into FOLDRUN_WEB_<ACTION>_VIA, the
|
|
202
|
+
* secret the step holds, and the one host that secret may reach.
|
|
203
|
+
*/
|
|
204
|
+
export function resolveActionApi(action: NewerAction, raw: unknown): ActionApiChoice {
|
|
205
|
+
const field = `web.${action}`;
|
|
206
|
+
if (raw === undefined || raw === null || raw === "") return { provider: null };
|
|
207
|
+
let name: string;
|
|
208
|
+
let secret: string | undefined;
|
|
209
|
+
if (typeof raw === "string") name = raw;
|
|
210
|
+
else if (typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { name?: unknown }).name === "string") {
|
|
211
|
+
name = (raw as { name: string }).name;
|
|
212
|
+
const key = (raw as { key?: unknown }).key;
|
|
213
|
+
if (key !== undefined) {
|
|
214
|
+
const m = typeof key === "string" ? /^\$\{([A-Z][A-Z0-9_]*)\}$/.exec(key.trim()) : null;
|
|
215
|
+
if (!m) return { provider: null, error: `${field}.key must be a \${NAME} reference to a secret in the vault, not the key itself.` };
|
|
216
|
+
secret = m[1];
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
return { provider: null, error: `${field}: takes a provider name, or a block with name: (and key: for your own vault name).` };
|
|
220
|
+
}
|
|
221
|
+
const choice = resolveWebAction(action, name);
|
|
222
|
+
if (choice.error !== undefined) return { provider: null, error: choice.error };
|
|
223
|
+
if (choice.provider.name === "foldrun") return { provider: null };
|
|
224
|
+
const api = ACTION_APIS[action].find((a) => a.name === choice.provider.name)!;
|
|
225
|
+
return { provider: api.name, secret: secret ?? api.secret, host: api.host };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Every newer action in the agent's web: block that names a provider unable
|
|
229
|
+
* to do it — the sentences resolveActionApi gives. Empty when all are unset
|
|
230
|
+
* or answerable. */
|
|
231
|
+
export function actionProblems(front: Record<string, unknown>): string[] {
|
|
232
|
+
const { raw } = webConfig(front);
|
|
233
|
+
return NEWER_ACTIONS.flatMap((a) => {
|
|
234
|
+
const e = resolveActionApi(a, raw[a]).error;
|
|
235
|
+
return e ? [e] : [];
|
|
236
|
+
});
|
|
237
|
+
}
|
package/src/webhook.ts
CHANGED
|
@@ -143,8 +143,17 @@ export interface HookDelivery {
|
|
|
143
143
|
runId?: string;
|
|
144
144
|
bytes?: number;
|
|
145
145
|
detail?: string;
|
|
146
|
+
/** A run of refusals, collapsed: how many, and when the first came.
|
|
147
|
+
* `t` is the latest. Absent means one. */
|
|
148
|
+
count?: number;
|
|
149
|
+
first?: string;
|
|
146
150
|
}
|
|
147
151
|
|
|
152
|
+
/** What anyone can produce without the token or the signing secret. A run
|
|
153
|
+
* of these collapses into one line, so a stranger's flood costs the log a
|
|
154
|
+
* single entry and can never push an owner's real deliveries out of it. */
|
|
155
|
+
const UNAUTHENTICATED: HookDelivery["outcome"][] = ["invalid-token", "invalid-signature"];
|
|
156
|
+
|
|
148
157
|
function deliveriesFile(tenant: string, workspace: string) {
|
|
149
158
|
return path.join(workspaceDir(tenant, workspace), "hook-deliveries.jsonl");
|
|
150
159
|
}
|
|
@@ -154,6 +163,7 @@ const KEEP_DELIVERIES = 500;
|
|
|
154
163
|
export function recordDelivery(tenant: string, workspace: string, delivery: HookDelivery) {
|
|
155
164
|
const file = deliveriesFile(tenant, workspace);
|
|
156
165
|
if (!fs.existsSync(path.dirname(file))) return; // no workspace, nothing to log against
|
|
166
|
+
if (UNAUTHENTICATED.includes(delivery.outcome) && collapseInto(file, delivery)) return;
|
|
157
167
|
fs.appendFileSync(file, JSON.stringify(delivery) + "\n");
|
|
158
168
|
// Bounded: compact to the newest KEEP when it doubles. Amortised cheap,
|
|
159
169
|
// and a log that grows forever is a disk-full incident with a delay.
|
|
@@ -169,6 +179,37 @@ export function recordDelivery(tenant: string, workspace: string, delivery: Hook
|
|
|
169
179
|
}
|
|
170
180
|
}
|
|
171
181
|
|
|
182
|
+
/** Fold a refusal into the log's last line when that line is a refusal
|
|
183
|
+
* too. The flow name is not part of the match: it comes from the URL, so
|
|
184
|
+
* a flood that varies it would otherwise never collapse. True when folded. */
|
|
185
|
+
function collapseInto(file: string, delivery: HookDelivery): boolean {
|
|
186
|
+
let text: string;
|
|
187
|
+
try {
|
|
188
|
+
text = fs.readFileSync(file, "utf8");
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const end = text.trimEnd();
|
|
193
|
+
const cut = end.lastIndexOf("\n") + 1;
|
|
194
|
+
let last: HookDelivery;
|
|
195
|
+
try {
|
|
196
|
+
last = JSON.parse(end.slice(cut)) as HookDelivery;
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (!UNAUTHENTICATED.includes(last.outcome)) return false;
|
|
201
|
+
const merged: HookDelivery = {
|
|
202
|
+
...delivery,
|
|
203
|
+
count: (last.count ?? 1) + 1,
|
|
204
|
+
first: last.first ?? last.t,
|
|
205
|
+
...(last.detail === "more than one flow name" || last.flow !== delivery.flow ? { detail: "more than one flow name" } : {}),
|
|
206
|
+
};
|
|
207
|
+
const tmp = `${file}.tmp`;
|
|
208
|
+
fs.writeFileSync(tmp, end.slice(0, cut) + JSON.stringify(merged) + "\n");
|
|
209
|
+
fs.renameSync(tmp, file);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
172
213
|
export function readDeliveries(tenant: string, workspace: string, limit = 20): HookDelivery[] {
|
|
173
214
|
const file = deliveriesFile(tenant, workspace);
|
|
174
215
|
if (!fs.existsSync(file)) return [];
|
|
@@ -3,12 +3,12 @@ name: curator
|
|
|
3
3
|
description: Reviews what the workspace has remembered — stale, duplicated or contradicting facts — and tidies memory without inventing anything.
|
|
4
4
|
model: default
|
|
5
5
|
tools:
|
|
6
|
-
-
|
|
6
|
+
- write
|
|
7
7
|
- search
|
|
8
8
|
- history
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
You look after this workspace's memory: the files under
|
|
11
|
+
You look after this workspace's memory: the files under `workspace/memory/` and
|
|
12
12
|
under each agent's `memory/`. Agents write there one fact per file, and nobody
|
|
13
13
|
prunes it, so over months it gains duplicates, facts that later runs
|
|
14
14
|
contradicted, and notes that stopped being true.
|
|
@@ -24,9 +24,9 @@ Do exactly this:
|
|
|
24
24
|
- A fact a later run or a knowledge file contradicts: correct it, and say
|
|
25
25
|
in the file what changed and which run showed it.
|
|
26
26
|
- A note that was only ever true for one day (a count, a cursor, "today"):
|
|
27
|
-
move it to
|
|
27
|
+
move it to `workspace/state/` if it is still useful, else delete it.
|
|
28
28
|
- Anything you are not sure about: leave it, and list it in your report.
|
|
29
29
|
3. Never write a fact you did not find in a file or a run. Never touch
|
|
30
|
-
`knowledge/` — that is what people gave the agents, and it is theirs.
|
|
30
|
+
`workspace/knowledge/` — that is what people gave the agents, and it is theirs.
|
|
31
31
|
4. Reply with a one-line headline (how many files reviewed, merged, corrected,
|
|
32
32
|
removed), then the list of what you changed and what you left for a person.
|
|
@@ -3,7 +3,7 @@ name: notetaker
|
|
|
3
3
|
description: Reads the house style, writes one short note, checks its length, and records what it learned.
|
|
4
4
|
model: fast
|
|
5
5
|
tools:
|
|
6
|
-
-
|
|
6
|
+
- write
|
|
7
7
|
- wordcount
|
|
8
8
|
---
|
|
9
9
|
|
|
@@ -16,9 +16,9 @@ Do exactly this, in order:
|
|
|
16
16
|
2. Draft two sentences about rain gauges, then call the `wordcount` tool on
|
|
17
17
|
your draft. If it reports more than 2 sentences, cut it down and check
|
|
18
18
|
again. Save the final text to `outputs/note.md`.
|
|
19
|
-
3. Write one thing you learned to
|
|
19
|
+
3. Write one thing you learned to `workspace/memory/` as a new file — one fact,
|
|
20
20
|
plain markdown, a sentence or two. The platform records who wrote it.
|
|
21
|
-
4. Update
|
|
21
|
+
4. Update `workspace/state/publishing.md`: increase the note count by one and set
|
|
22
22
|
the last topic to what you wrote about. Keep it as markdown.
|
|
23
23
|
|
|
24
24
|
Then stop. Do not create anything else.
|