@foldrun/core 1.1.3 → 1.3.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/LICENSE +29 -192
- package/README.md +14 -17
- package/dist/index.js +3 -3
- package/dist/src/deploy.d.ts +16 -0
- package/dist/src/deploy.js +35 -1
- package/dist/src/platform.d.ts +4 -4
- package/dist/src/run-container.d.ts +1 -1
- package/dist/src/run-container.js +1 -1
- package/dist/src/scan.d.ts +174 -0
- package/dist/src/scan.js +784 -0
- package/dist/src/starter.js +9 -0
- package/index.ts +3 -3
- package/package.json +4 -6
- package/src/deploy.ts +40 -2
- package/src/platform.ts +4 -4
- package/src/run-container.ts +1 -1
- package/src/scan.ts +890 -0
- package/src/starter.ts +9 -0
package/dist/src/starter.js
CHANGED
|
@@ -248,6 +248,14 @@ foldrun deploy --url <server> --token $FOLDRUN_TOKEN
|
|
|
248
248
|
\`check\` is cheap and catches a broken flow before a schedule fires it at 3am.
|
|
249
249
|
Run it after edits, the way you would run a typecheck.
|
|
250
250
|
|
|
251
|
+
It is also the security scanner: a key written into a file, hidden text in
|
|
252
|
+
knowledge or skills, an agent that reads the web while holding a secret and
|
|
253
|
+
an open way out, \`curl | sh\`, an MCP server at no fixed version. Every
|
|
254
|
+
finding names its rule (\`secret/hardcoded\`). \`foldrun check --security\`
|
|
255
|
+
fails on any of them; \`<!-- foldrun-ignore <rule>: <reason> -->\` on the line
|
|
256
|
+
before accepts one; \`--write-baseline\` accepts what is there today, and
|
|
257
|
+
\`--format sarif\` feeds GitHub code scanning (\`foldrun docs checks\`).
|
|
258
|
+
|
|
251
259
|
A deploy that would delete files this folder no longer has lists them and
|
|
252
260
|
stops: with no terminal it is refused. Tell the person what it would delete; add \`--yes\`
|
|
253
261
|
only when they asked for exactly that deletion.
|
|
@@ -345,6 +353,7 @@ All of it, \`/api/workspaces/<workspace>\` unless noted:
|
|
|
345
353
|
| \`/export\` · \`/import\` | GET · POST | the workspace, a flow (\`?kind=flow&name=\`) or an agent as a .zip; take one in (\`?dryRun=1\` first, \`?overwrite=1\` to replace) |
|
|
346
354
|
| \`/restore\` | POST | the workspace's source back to a point in its history — \`dryRun\` first, then \`confirm\` |
|
|
347
355
|
| \`/vocabulary\` | GET | what this workspace's documents may say |
|
|
356
|
+
| \`/check\` | GET | what \`foldrun check\` says about the deployed copy, as findings with rule ids (\`?security=1\`) |
|
|
348
357
|
| \`/assistant\` | POST | ask the assistant, or have it write agents, flows, tools and skills — from the docs only; proposals come back, nothing is written |
|
|
349
358
|
|
|
350
359
|
Account-level, outside \`/workspaces\`: \`/api/secrets\`, \`/api/keys\`, \`/api/schedule\`,
|
package/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// @foldrun/core — the
|
|
1
|
+
// @foldrun/core — the runtime.
|
|
2
2
|
//
|
|
3
3
|
// Everything needed to read a workspace of markdown files and run the agents
|
|
4
4
|
// in it. No server, no database, no account: point it at a folder.
|
|
5
5
|
//
|
|
6
6
|
// The hosted platform imports these same modules. That is deliberate — if the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// local version were a reduced copy it would rot, and running on a laptop
|
|
8
|
+
// would behave differently from running hosted. What the platform adds — a queue and
|
|
9
9
|
// its worker, metering and billing, pods on a cluster, per-account keys in a
|
|
10
10
|
// database, previews, share links, tenancy — lives in its own private package
|
|
11
11
|
// and plugs in through platform.ts. Nothing here imports it.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldrun/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Agents are just folders. The format, the runner, the checks.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"./network",
|
|
31
31
|
"./providers",
|
|
32
32
|
"./subagents",
|
|
33
|
-
"./tool-names"
|
|
33
|
+
"./tool-names",
|
|
34
|
+
"./scan"
|
|
34
35
|
]
|
|
35
36
|
},
|
|
36
37
|
"main": "./dist/index.js",
|
|
@@ -67,9 +68,6 @@
|
|
|
67
68
|
"url": "git+https://github.com/foldrun-io/foldrun-core.git"
|
|
68
69
|
},
|
|
69
70
|
"homepage": "https://foldrun.io",
|
|
70
|
-
"bugs": {
|
|
71
|
-
"url": "https://github.com/foldrun-io/foldrun-core/issues"
|
|
72
|
-
},
|
|
73
71
|
"keywords": [
|
|
74
72
|
"agents",
|
|
75
73
|
"llm",
|
package/src/deploy.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { languageProblem } from "./language.ts";
|
|
|
46
46
|
import { localeProblems } from "./locale.ts";
|
|
47
47
|
import { repoDir } from "./gitrepo.ts";
|
|
48
48
|
import { entriesNoFollow } from "./paths.ts";
|
|
49
|
+
import { filterIgnored, findingFingerprint, ruleForWhere, scanFiles, type ScanFinding, type ScanOptions } from "./scan.ts";
|
|
49
50
|
|
|
50
51
|
/** Directories a deploy never reads out of a source tree. */
|
|
51
52
|
const NOT_SOURCE = new Set([".git", "node_modules", "runs", "outputs", ".foldrun", ".results"]);
|
|
@@ -228,7 +229,44 @@ export function unopenedFrontmatter(raw: string): string | null {
|
|
|
228
229
|
return "the frontmatter has no opening `---`, so none of it is read: the agent runs with no tools and the default model. Add `---` as the first line.";
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
/**
|
|
233
|
+
* What a deploy says but does not refuse: the structural warnings below, and
|
|
234
|
+
* the scanner's findings about what should not be in the files at all — a
|
|
235
|
+
* credential written in, text a reviewer cannot see. The scanner's design
|
|
236
|
+
* advice (an agent that could leak, an unpinned package) is for `foldrun
|
|
237
|
+
* check` and the Checks page, not for every deploy's output.
|
|
238
|
+
*/
|
|
231
239
|
export function deployWarnings(files: DeployFile[]): DeployIssue[] {
|
|
240
|
+
const scanned = scanFiles(files, { only: ["secret", "content"] })
|
|
241
|
+
.filter((f) => f.level !== "info")
|
|
242
|
+
.map((f) => ({ where: `${f.file}${f.line ? `:${f.line}` : ""}`, message: `${f.message} [${f.rule}]` }));
|
|
243
|
+
return [...structuralWarnings(files), ...scanned];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Everything the platform can say about a workspace's files, as findings
|
|
248
|
+
* with rule ids: what a deploy refuses (errors), what it warns about, and the
|
|
249
|
+
* whole scanner. The Checks page and `GET …/check` answer with this; `foldrun
|
|
250
|
+
* check` asks more (it resolves every library and the platform's), so treat
|
|
251
|
+
* this as the server's view of the same rules, not a second set.
|
|
252
|
+
*/
|
|
253
|
+
export function checkFiles(files: DeployFile[], options: ScanOptions = {}): ScanFinding[] {
|
|
254
|
+
const finding = (i: DeployIssue, level: "error" | "warn", kind?: string): ScanFinding => {
|
|
255
|
+
const m = /^(.*?)(?::(\d+))?(?: .*)?$/.exec(i.where);
|
|
256
|
+
const file = m?.[1] || i.where;
|
|
257
|
+
const rule = ruleForWhere(i.where, kind);
|
|
258
|
+
return { rule, level, file, ...(m?.[2] ? { line: Number(m[2]) } : {}), message: i.message, fingerprint: findingFingerprint(rule, file, i.message) };
|
|
259
|
+
};
|
|
260
|
+
const own = [
|
|
261
|
+
...deployIssues(files).map((i) => finding(i, "error")),
|
|
262
|
+
...structuralWarnings(files).map((i) => finding(i, "warn", i.kind)),
|
|
263
|
+
];
|
|
264
|
+
return [...filterIgnored(own, files), ...scanFiles(files, options)].sort(
|
|
265
|
+
(a, b) => a.file.localeCompare(b.file) || (a.line ?? 0) - (b.line ?? 0) || a.rule.localeCompare(b.rule),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function structuralWarnings(files: DeployFile[]): (DeployIssue & { kind?: string })[] {
|
|
232
270
|
const front = (content: string): Record<string, unknown> => {
|
|
233
271
|
try { return matter(content).data as Record<string, unknown>; } catch { return {}; }
|
|
234
272
|
};
|
|
@@ -246,7 +284,7 @@ export function deployWarnings(files: DeployFile[]): DeployIssue[] {
|
|
|
246
284
|
// Agents that grant one of them.
|
|
247
285
|
const outwardAgents: string[] = [];
|
|
248
286
|
const agentNames: string[] = [];
|
|
249
|
-
const out: DeployIssue[] = [];
|
|
287
|
+
const out: (DeployIssue & { kind?: string })[] = [];
|
|
250
288
|
if (!files.some((f) => /^agents\/[^/]+\/agent\.md$/.test(f.path)) && !files.some((f) => /^\.(claude|agents)\/agents\/[^/]+\.md$/.test(f.path))) {
|
|
251
289
|
out.push({ where: "agents/", message: "no agents yet — add one (agents/<name>/agent.md), or import one from another workspace" });
|
|
252
290
|
}
|
|
@@ -268,7 +306,7 @@ export function deployWarnings(files: DeployFile[]): DeployIssue[] {
|
|
|
268
306
|
let flow;
|
|
269
307
|
try { flow = parseFlow(path.basename(f.path), f.content); } catch { continue; }
|
|
270
308
|
for (const w of lintFlow(flow, { agents: agentNames, outwardAgents })) {
|
|
271
|
-
if (w.kind === "outward") out.push({ where: `${f.path}${w.line ? `:${w.line}` : ""}`, message: w.message });
|
|
309
|
+
if (w.kind === "outward") out.push({ where: `${f.path}${w.line ? `:${w.line}` : ""}`, message: w.message, kind: "outward" });
|
|
272
310
|
}
|
|
273
311
|
}
|
|
274
312
|
return out;
|
package/src/platform.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type IsolatedStepRunner = (args: RunInContainerArgs) => Promise<Container
|
|
|
20
20
|
|
|
21
21
|
/** Who this install is for. `hosted` means someone is paying the operator
|
|
22
22
|
* for it — a wallet, a plan, a bill. `self-hosted` is everything else: a
|
|
23
|
-
* laptop, a
|
|
23
|
+
* laptop, a dev install, a box nobody bills on. */
|
|
24
24
|
export type Edition = "self-hosted" | "hosted";
|
|
25
25
|
|
|
26
26
|
/** What a trigger gate decided. `admit: false` is never an error: the
|
|
@@ -36,9 +36,9 @@ export interface PlatformHooks {
|
|
|
36
36
|
* the wallet, top-ups, plan limits, whatever is added next that assumes
|
|
37
37
|
* a customer — asks before it renders or answers. Default: self-hosted,
|
|
38
38
|
* which is what a process that never registered a platform is, and what
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* any install is unless its operator configures billing. One predicate,
|
|
40
|
+
* one place, so "does an unbilled install show X" is a test and not an
|
|
41
|
+
* audit. */
|
|
42
42
|
edition(): Edition;
|
|
43
43
|
/** Put a flow run where a worker will pick it up. Default: start it here, now. */
|
|
44
44
|
enqueueFlowRun(
|
package/src/run-container.ts
CHANGED
|
@@ -475,7 +475,7 @@ const DOCKER_VOLUME_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
|
|
|
475
475
|
* as a trusted local file, which is precisely the path gVisor, the capability
|
|
476
476
|
* drop and the egress policy all exist to close. Disk is the cheaper thing to
|
|
477
477
|
* spend. One volume per tenant keeps that boundary; each carries a
|
|
478
|
-
* `foldrun.io/tenant` label so it can be found and pruned
|
|
478
|
+
* `foldrun.io/tenant` label so it can be found and pruned.
|
|
479
479
|
*
|
|
480
480
|
* The venvs inside carry the runner's own absolute paths, distinct from the
|
|
481
481
|
* host's `<tenant>/.runtimes` cache — two roots, so a hit is always a real hit.
|