foldrun 1.1.1 → 1.2.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 +3 -4
- package/bin/foldrun.mjs +11 -4
- package/docs/api.md +1 -0
- package/docs/assistant.md +4 -0
- package/docs/checks.md +277 -0
- package/docs/cli.md +2 -1
- package/docs/faq.md +1 -1
- package/docs/introduction.md +1 -1
- package/docs/self-hosting.md +4 -3
- package/package.json +4 -7
- package/src/commands.mjs +337 -29
- package/src/guide.mjs +1 -1
package/LICENSE
CHANGED
|
@@ -1,202 +1,39 @@
|
|
|
1
|
+
foldrun proprietary licence
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
Version 2.0, January 2004
|
|
4
|
-
http://www.apache.org/licenses/
|
|
3
|
+
Copyright (c) 2026 foldrun. All rights reserved.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
This software, its source code and its documentation are the property of
|
|
6
|
+
foldrun. They are licensed, not sold.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
You may install and run the foldrun packages published to npm (`foldrun`
|
|
9
|
+
and `@foldrun/core`) to write, check, run and deploy your own agents,
|
|
10
|
+
flows and workspaces, on your own machines and with the foldrun platform.
|
|
11
|
+
That includes the copies installing and running need, for your own use:
|
|
12
|
+
package caches and mirrors, CI and build machines, container images and
|
|
13
|
+
backups.
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
You may not, except where the law allows it despite this restriction:
|
|
16
|
+
copy, modify, merge, publish, distribute, sublicense, rent or sell the
|
|
17
|
+
software or any part of it; reverse engineer, decompile or disassemble it;
|
|
18
|
+
or use it to build a product or service that competes with foldrun.
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
What you write with the software (your agents, flows, knowledge and other
|
|
21
|
+
files) is yours.
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
control with that entity. For the purposes of this definition,
|
|
19
|
-
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
-
direction or management of such entity, whether by contract or
|
|
21
|
-
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
-
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
Third-party components the software includes or installs (its npm
|
|
24
|
+
dependencies) remain under their own licences.
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
This licence ends automatically if you break its terms, and you must then
|
|
27
|
+
stop using the software.
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
source, and configuration files.
|
|
29
|
+
Versions published before 17 October 2026 under the Apache License 2.0
|
|
30
|
+
remain available under that licence for those versions only.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
33
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
34
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. TO THE EXTENT THE
|
|
35
|
+
LAW ALLOWS, IN NO EVENT SHALL FOLDRUN BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
36
|
+
OTHER LIABILITY ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
|
|
37
|
+
ITS USE.
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
Object form, made available under the License, as indicated by a
|
|
38
|
-
copyright notice that is included in or attached to the work
|
|
39
|
-
(an example is provided in the Appendix below).
|
|
40
|
-
|
|
41
|
-
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
-
form, that is based on (or derived from) the Work and for which the
|
|
43
|
-
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
-
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
-
of this License, Derivative Works shall not include works that remain
|
|
46
|
-
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
-
the Work and Derivative Works thereof.
|
|
48
|
-
|
|
49
|
-
"Contribution" shall mean any work of authorship, including
|
|
50
|
-
the original version of the Work and any modifications or additions
|
|
51
|
-
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
-
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
-
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
-
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
-
means any form of electronic, verbal, or written communication sent
|
|
56
|
-
to the Licensor or its representatives, including but not limited to
|
|
57
|
-
communication on electronic mailing lists, source code control systems,
|
|
58
|
-
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
-
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
-
excluding communication that is conspicuously marked or otherwise
|
|
61
|
-
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
-
|
|
63
|
-
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
-
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
-
subsequently incorporated within the Work.
|
|
66
|
-
|
|
67
|
-
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
-
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
-
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
-
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
-
Work and such Derivative Works in Source or Object form.
|
|
73
|
-
|
|
74
|
-
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
-
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
-
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
-
(except as stated in this section) patent license to make, have made,
|
|
78
|
-
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
-
where such license applies only to those patent claims licensable
|
|
80
|
-
by such Contributor that are necessarily infringed by their
|
|
81
|
-
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
-
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
-
institute patent litigation against any entity (including a
|
|
84
|
-
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
-
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
-
or contributory patent infringement, then any patent licenses
|
|
87
|
-
granted to You under this License for that Work shall terminate
|
|
88
|
-
as of the date such litigation is filed.
|
|
89
|
-
|
|
90
|
-
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
-
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
-
modifications, and in Source or Object form, provided that You
|
|
93
|
-
meet the following conditions:
|
|
94
|
-
|
|
95
|
-
(a) You must give any other recipients of the Work or
|
|
96
|
-
Derivative Works a copy of this License; and
|
|
97
|
-
|
|
98
|
-
(b) You must cause any modified files to carry prominent notices
|
|
99
|
-
stating that You changed the files; and
|
|
100
|
-
|
|
101
|
-
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
-
that You distribute, all copyright, patent, trademark, and
|
|
103
|
-
attribution notices from the Source form of the Work,
|
|
104
|
-
excluding those notices that do not pertain to any part of
|
|
105
|
-
the Derivative Works; and
|
|
106
|
-
|
|
107
|
-
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
-
distribution, then any Derivative Works that You distribute must
|
|
109
|
-
include a readable copy of the attribution notices contained
|
|
110
|
-
within such NOTICE file, excluding those notices that do not
|
|
111
|
-
pertain to any part of the Derivative Works, in at least one
|
|
112
|
-
of the following places: within a NOTICE text file distributed
|
|
113
|
-
as part of the Derivative Works; within the Source form or
|
|
114
|
-
documentation, if provided along with the Derivative Works; or,
|
|
115
|
-
within a display generated by the Derivative Works, if and
|
|
116
|
-
wherever such third-party notices normally appear. The contents
|
|
117
|
-
of the NOTICE file are for informational purposes only and
|
|
118
|
-
do not modify the License. You may add Your own attribution
|
|
119
|
-
notices within Derivative Works that You distribute, alongside
|
|
120
|
-
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
-
that such additional attribution notices cannot be construed
|
|
122
|
-
as modifying the License.
|
|
123
|
-
|
|
124
|
-
You may add Your own copyright statement to Your modifications and
|
|
125
|
-
may provide additional or different license terms and conditions
|
|
126
|
-
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
-
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
-
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
-
the conditions stated in this License.
|
|
130
|
-
|
|
131
|
-
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
-
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
-
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
-
this License, without any additional terms or conditions.
|
|
135
|
-
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
-
the terms of any separate license agreement you may have executed
|
|
137
|
-
with Licensor regarding such Contributions.
|
|
138
|
-
|
|
139
|
-
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
-
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
-
except as required for reasonable and customary use in describing the
|
|
142
|
-
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
-
|
|
144
|
-
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
-
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
-
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
-
implied, including, without limitation, any warranties or conditions
|
|
149
|
-
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
-
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
-
appropriateness of using or redistributing the Work and assume any
|
|
152
|
-
risks associated with Your exercise of permissions under this License.
|
|
153
|
-
|
|
154
|
-
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
-
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
-
unless required by applicable law (such as deliberate and grossly
|
|
157
|
-
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
-
liable to You for damages, including any direct, indirect, special,
|
|
159
|
-
incidental, or consequential damages of any character arising as a
|
|
160
|
-
result of this License or out of the use or inability to use the
|
|
161
|
-
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
-
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
-
other commercial damages or losses), even if such Contributor
|
|
164
|
-
has been advised of the possibility of such damages.
|
|
165
|
-
|
|
166
|
-
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
-
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
-
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
-
or other liability obligations and/or rights consistent with this
|
|
170
|
-
License. However, in accepting such obligations, You may act only
|
|
171
|
-
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
-
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
-
defend, and hold each Contributor harmless for any liability
|
|
174
|
-
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
-
of your accepting any such warranty or additional liability.
|
|
176
|
-
|
|
177
|
-
END OF TERMS AND CONDITIONS
|
|
178
|
-
|
|
179
|
-
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
-
|
|
181
|
-
To apply the Apache License to your work, attach the following
|
|
182
|
-
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
-
replaced with your own identifying information. (Don't include
|
|
184
|
-
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
-
comment syntax for the file format. We also recommend that a
|
|
186
|
-
file or class name and description of purpose be included on the
|
|
187
|
-
same "printed page" as the copyright notice for easier
|
|
188
|
-
identification within third-party archives.
|
|
189
|
-
|
|
190
|
-
Copyright 2026 foldrun contributors
|
|
191
|
-
|
|
192
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
-
you may not use this file except in compliance with the License.
|
|
194
|
-
You may obtain a copy of the License at
|
|
195
|
-
|
|
196
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
-
|
|
198
|
-
Unless required by applicable law or agreed to in writing, software
|
|
199
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
-
See the License for the specific language governing permissions and
|
|
202
|
-
limitations under the License.
|
|
39
|
+
Questions: hello@foldrun.io
|
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# foldrun
|
|
2
2
|
|
|
3
|
-
[](https://github.com/foldrun-io/foldrun-cli/actions/workflows/ci.yml)
|
|
4
3
|
[](https://www.npmjs.com/package/foldrun)
|
|
5
|
-
[](LICENSE)
|
|
6
4
|
|
|
7
5
|
**Agents are just folders.** An agent is a folder with a markdown file in it. A flow is
|
|
8
6
|
a numbered list naming agents. You write them, read them, diff them in a pull
|
|
@@ -234,6 +232,7 @@ foldrun invoke publish --wait
|
|
|
234
232
|
In CI, set `FOLDRUN_URL` and `FOLDRUN_TOKEN` instead — the environment always
|
|
235
233
|
wins over the credentials file, so a job never reads one from disk.
|
|
236
234
|
|
|
237
|
-
##
|
|
235
|
+
## Licence
|
|
238
236
|
|
|
239
|
-
|
|
237
|
+
Proprietary; see [LICENSE](LICENSE). Versions published before 17 October
|
|
238
|
+
2026 were Apache-2.0 and stay so.
|
package/bin/foldrun.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// foldrun — the
|
|
2
|
+
// foldrun — the CLI.
|
|
3
3
|
//
|
|
4
4
|
// Local-first by design: every command here works on a plain folder with no
|
|
5
5
|
// account, no server and no network beyond the model call itself. That is the
|
|
@@ -104,6 +104,8 @@ const HELP = `foldrun — agents are just folders
|
|
|
104
104
|
foldrun agent ls the agents deployed in a workspace: model, description (--to, --json) — flow ls the flows and how each fires
|
|
105
105
|
foldrun check [dir] validate every workspace here, and the shared library
|
|
106
106
|
foldrun check --to <ws> validate the DEPLOYED copy instead — fetched from the platform, checked here
|
|
107
|
+
foldrun check --security only the security rules: hard-coded secrets, hidden text, agents that can leak, unpinned code — fails on a warning
|
|
108
|
+
foldrun check --rules every rule check knows: id, level, what it guards against (--json)
|
|
107
109
|
foldrun agent run <name> run one agent once on the platform (--task "…", --to, --wait, --test)
|
|
108
110
|
foldrun agent link <agent> add to its team, in its frontmatter: --subagent <worker>, --consult <agent> or --can-ask (--description for a worker with none)
|
|
109
111
|
foldrun agent unlink <agent> take one away: --subagent <name>, --consult <name> or --can-ask
|
|
@@ -302,7 +304,7 @@ Options
|
|
|
302
304
|
--clear secrets sites: unbind it
|
|
303
305
|
--add <site> agent network: add a site to its network: list (repeatable)
|
|
304
306
|
--remove <site> agent network: take a site off its network: list (repeatable)
|
|
305
|
-
--out <file> api spec: where to write the OpenAPI document (default: stdout)
|
|
307
|
+
--out <file> api spec: where to write the OpenAPI document (default: stdout); check: where to write the JSON or SARIF report
|
|
306
308
|
--file <path> source put, library put, flow paste: the local file to send (default: stdin); secrets set --kind file|service-account: the file it holds; storage get, report get, report live, account export, export, billing statement: where to write it (- for stdout)
|
|
307
309
|
--message "<why>" source put: recorded on the file's revision
|
|
308
310
|
--account secrets: account scope instead of the workspace's
|
|
@@ -344,6 +346,11 @@ Platform options (deploy, invoke, secrets, logs, keys)
|
|
|
344
346
|
--force deploy: deploy even while runs are in flight; pull, storage get: overwrite local files
|
|
345
347
|
--yes deploy: allow deleting files this folder no longer has (asked otherwise; required with no terminal)
|
|
346
348
|
--platform --yes workspaces rm: delete it on the platform, deliberately
|
|
349
|
+
--format <text|json|sarif> check: how to print the findings (--json is --format json; sarif is for GitHub code scanning); --out <file> writes it there
|
|
350
|
+
--fail-on <level> check: exit 1 on a finding at this level or above — error (default), warn, info, never; --security makes it warn
|
|
351
|
+
--rule <id> check: only this rule, or a category (secret, security, content, supply-chain, config); repeat or comma-separate
|
|
352
|
+
--changed check: only findings in files changed against git HEAD (--base <ref> compares with that ref instead, e.g. origin/main)
|
|
353
|
+
--write-baseline check: accept every current finding into .foldrun/check-baseline.json, so later checks show only new ones (--no-baseline ignores it)
|
|
347
354
|
--platform workspaces new, agent new, flow new, tool new: make it on the platform, not in this folder
|
|
348
355
|
|
|
349
356
|
Nothing here needs an account. Set ANTHROPIC_API_KEY to run; init and check
|
|
@@ -360,9 +367,9 @@ if (!command || command === "--help" || command === "-h") {
|
|
|
360
367
|
// `--value` as the account's argument and stored an empty secret; `--force
|
|
361
368
|
// ./dir` swallowed the directory. A flag followed by another flag is also
|
|
362
369
|
// boolean, so an unlisted switch at least does not eat its neighbour.
|
|
363
|
-
const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list", "library", "clear"]);
|
|
370
|
+
const BOOLEAN_FLAGS = new Set(["account", "follow", "force", "oauth2", "wait", "watch", "print", "dry-run", "help", "no-browser", "local", "test", "flat", "yes", "platform", "json", "forever", "all", "check", "new-client", "quiet", "off", "can-ask", "preview", "csv", "failed", "starter", "history", "list", "library", "clear", "security", "changed", "write-baseline", "no-baseline", "rules"]);
|
|
364
371
|
// Flags said more than once collect into a list: `--tag a --tag b`.
|
|
365
|
-
const REPEATABLE = new Set(["tag", "expect", "header", "site", "add", "remove"]);
|
|
372
|
+
const REPEATABLE = new Set(["tag", "expect", "header", "site", "add", "remove", "rule"]);
|
|
366
373
|
// `--wait` is a switch everywhere but `flow add <flow> wait`, where it takes
|
|
367
374
|
// a span: `--wait 30m`, `--wait event`. Only a value that reads as one is
|
|
368
375
|
// taken, so `invoke x --wait` and friends are as they were.
|
package/docs/api.md
CHANGED
|
@@ -68,6 +68,7 @@ api workspaces/$WS/flows/publish/run -X POST # start a flow
|
|
|
68
68
|
| `/api/workspaces` | GET, POST | list; create (`name`, plus one of: `template: true` — blank, just an AGENTS.md, what the dashboard sends; `starter: true` — the example researcher, writer and publish flow; `demo: true`; `files`). A POST to a name that exists deploys over it; add `ifAbsent: true` to create only — a name that exists is then `409` and nothing changes, decided atomically (the assistant's Create workspace and `foldrun assistant` send it) |
|
|
69
69
|
| `/api/workspaces/<ws>` | GET, PATCH, DELETE | read; rename or re-describe (`name`, `description`), or set its defaults (`timezone`, `budget`, `notify` — null clears one, edited in AGENTS.md in place); delete |
|
|
70
70
|
| `/api/workspaces/<ws>/vocabulary` | GET | which document kinds this workspace admits — its `docs` list is what `[[ ]]` resolves: knowledge, memory, `state/` and `storage/` files, plus the folders themselves |
|
|
71
|
+
| `/api/workspaces/<ws>/check` | GET | what `foldrun check` says about the deployed copy, as findings with rule ids — what a deploy refuses or warns about, and the scanner's secret, hidden-text, agent-risk and supply-chain rules ([Checks](checks)). `?security=1` keeps the security rules, `?rule=<id or category>` (repeatable) one; `{ counts, findings: [{ rule, level, file, line, message, fingerprint }], rules }`. Read-only, no model call |
|
|
71
72
|
|
|
72
73
|
## Source — the files themselves
|
|
73
74
|
|
package/docs/assistant.md
CHANGED
|
@@ -104,6 +104,10 @@ panel says so. The model only ever reads through the assistant's own tools,
|
|
|
104
104
|
on the server, scoped to your account and workspace; it runs no code and has
|
|
105
105
|
no network or shell of its own.
|
|
106
106
|
|
|
107
|
+
The workers themselves reach the internet at the model API and nowhere
|
|
108
|
+
else. A workspace whose `provider:` points at another address gets a reply
|
|
109
|
+
saying so, not a timeout; that provider still serves the workspace's runs.
|
|
110
|
+
|
|
107
111
|
## Who can use it, and what it costs
|
|
108
112
|
|
|
109
113
|
You need to be able to edit the workspace (`workspace:write`: editor,
|
package/docs/checks.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# Checks
|
|
2
|
+
|
|
3
|
+
`foldrun check` answers two questions about a workspace before anything
|
|
4
|
+
runs: **will it run?** (a tool that exists, a step that names a real agent,
|
|
5
|
+
an option the parser can read) and **is it safe to run?** (a key pasted into
|
|
6
|
+
a prompt, a knowledge file carrying hidden instructions, an agent that reads
|
|
7
|
+
the open web, holds a secret and can send it anywhere). It is offline, calls
|
|
8
|
+
no model and costs nothing, so run it after every edit, the way you would run
|
|
9
|
+
a typecheck, and in CI on every pull request.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
foldrun check # everything, as text
|
|
13
|
+
foldrun check --security # only the security rules; fails on a warning
|
|
14
|
+
foldrun check --changed # only files changed against git HEAD
|
|
15
|
+
foldrun check --format sarif --out check.sarif # for GitHub code scanning
|
|
16
|
+
foldrun check --rules # every rule, its level, what it guards against
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The same findings appear in three other places, from the same rules in
|
|
20
|
+
`@foldrun/core`:
|
|
21
|
+
|
|
22
|
+
- **The dashboard**: the workspace's **Checks** page lists every finding on
|
|
23
|
+
the deployed copy, grouped by rule, each linking to its file. The editor
|
|
24
|
+
shows a file's own findings above it.
|
|
25
|
+
- **The API**: `GET /api/workspaces/<ws>/check` answers with the same
|
|
26
|
+
findings as JSON (`?security=1`, `?rule=`). See [api](api).
|
|
27
|
+
- **A deploy**: what a deploy refuses is still refused. A credential written
|
|
28
|
+
into a file and hidden text are reported as deploy warnings: the deploy
|
|
29
|
+
goes ahead and says so.
|
|
30
|
+
|
|
31
|
+
## Reading a finding
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
warn knowledge/setup.md:6 an Anthropic key (sk-a…a5) is written into the file — move it to the vault and write `${NAME}` secret/hardcoded
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The level, the file and line, what is wrong, and the **rule id** at the end.
|
|
38
|
+
A secret is never printed whole: the first four characters and the last two
|
|
39
|
+
are enough to find it, and too few to use it.
|
|
40
|
+
|
|
41
|
+
| Level | Means | Fails `check` |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `error` | it will not work, or a deploy refuses it | yes |
|
|
44
|
+
| `warn` | it works, and something about it should change | with `--fail-on warn` or `--security` |
|
|
45
|
+
| `info` | worth knowing; often a judgement call | with `--fail-on info` |
|
|
46
|
+
|
|
47
|
+
Every rule added with the scanner is a warning for now, so a workspace that
|
|
48
|
+
passed yesterday still passes. To fail a build on them, use `--security` (which
|
|
49
|
+
fails on a warning) or `--fail-on warn`.
|
|
50
|
+
|
|
51
|
+
## Options
|
|
52
|
+
|
|
53
|
+
| Flag | Does |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `--security` | only the categories `secret`, `security`, `content` and `supply-chain`; exits 1 on a warning |
|
|
56
|
+
| `--rule <id>` | only this rule, or a whole category (`secret`, `config`, …); repeat it, or comma-separate |
|
|
57
|
+
| `--fail-on <level>` | exit 1 on a finding at `error` (the default), `warn`, `info`, or `never` |
|
|
58
|
+
| `--format text\|json\|sarif` | how to print it; `--json` is `--format json`. `--out <file>` writes it to a file |
|
|
59
|
+
| `--changed` | only findings in files changed against git `HEAD`, tracked or new; `--base <ref>` compares with that ref (`--base origin/main` in CI) |
|
|
60
|
+
| `--write-baseline` | accept every current finding into `.foldrun/check-baseline.json`; later checks show only new ones. `--no-baseline` ignores the file |
|
|
61
|
+
| `--rules` | list every rule (`--json` for the table) |
|
|
62
|
+
| `--to <ws>` | check the copy deployed on the platform instead of this folder |
|
|
63
|
+
|
|
64
|
+
## Ignoring one
|
|
65
|
+
|
|
66
|
+
Put a comment on the line before the finding, or on the same line, naming
|
|
67
|
+
the rule and saying why:
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
<!-- foldrun-ignore secret/hardcoded: a revoked key, kept to show the format -->
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
# foldrun-ignore supply-chain/pipe-to-shell: the vendor's pinned installer, checksum verified below
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`foldrun-ignore-file <rule>: <reason>` anywhere in a file covers all of it.
|
|
78
|
+
A category (`secret`) covers its rules. **The reason is required**: an ignore
|
|
79
|
+
without one ignores nothing and is reported itself
|
|
80
|
+
(`check/ignore-needs-reason`), because "why is this allowed?" is the question
|
|
81
|
+
the next reviewer will ask. The same comments work for every rule, check's
|
|
82
|
+
own included, in the CLI, the dashboard and the API.
|
|
83
|
+
|
|
84
|
+
Ignores in `memory/` are not honoured. Agents write their memory
|
|
85
|
+
themselves, so an ignore there could have been written by whatever an agent
|
|
86
|
+
read: an injection that silences its own finding.
|
|
87
|
+
|
|
88
|
+
## Adopting it on an existing workspace
|
|
89
|
+
|
|
90
|
+
A workspace with years of history will have findings on day one. Do not fix
|
|
91
|
+
them all before the check can be useful:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
foldrun check --write-baseline # accept what is there today
|
|
95
|
+
git add .foldrun/check-baseline.json && git commit -m "check: baseline"
|
|
96
|
+
foldrun check --security # from now on, only new findings fail
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A baseline entry is matched by the rule, the file and the text of the
|
|
100
|
+
flagged line, not by its line number, so adding a paragraph above an
|
|
101
|
+
accepted finding does not bring it back. Changing the flagged line does,
|
|
102
|
+
because then it is a different line. A baseline that is not valid JSON
|
|
103
|
+
hides nothing. `.foldrun/` is never deployed.
|
|
104
|
+
|
|
105
|
+
## In CI
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
# .github/workflows/check.yml
|
|
109
|
+
on: [pull_request]
|
|
110
|
+
permissions: { contents: read, security-events: write }
|
|
111
|
+
jobs:
|
|
112
|
+
check:
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
steps:
|
|
115
|
+
- uses: actions/checkout@v4
|
|
116
|
+
with: { fetch-depth: 0 }
|
|
117
|
+
- uses: actions/setup-node@v4
|
|
118
|
+
with: { node-version: 22 }
|
|
119
|
+
- run: npm i -g foldrun
|
|
120
|
+
- run: foldrun check --format sarif --out check.sarif --fail-on never
|
|
121
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
122
|
+
with: { sarif_file: check.sarif }
|
|
123
|
+
- run: foldrun check --security --changed --base origin/${{ github.base_ref }}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The SARIF upload puts each finding on the line it is about, in the pull
|
|
127
|
+
request. The last step fails the build on a new security finding in the
|
|
128
|
+
files the pull request touched. Each result carries a stable fingerprint,
|
|
129
|
+
so code scanning tracks a finding across commits instead of opening a new
|
|
130
|
+
one every time.
|
|
131
|
+
|
|
132
|
+
As a pre-commit hook: `foldrun check --security --changed`.
|
|
133
|
+
|
|
134
|
+
## The rules
|
|
135
|
+
|
|
136
|
+
The OWASP ids are from the
|
|
137
|
+
[OWASP Top 10 for Agentic Applications (2026)](https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026).
|
|
138
|
+
|
|
139
|
+
### secret/hardcoded
|
|
140
|
+
|
|
141
|
+
*warn · ASI03.* A credential is written into a file: an Anthropic, OpenAI,
|
|
142
|
+
AWS, GitHub, GitLab, Slack, Google, Stripe, SendGrid, Twilio, Telegram, npm,
|
|
143
|
+
Hugging Face, Linear, DigitalOcean, Shopify or Resend key, a JSON web token,
|
|
144
|
+
a private key, a webhook URL that carries its own secret, a password inside a
|
|
145
|
+
URL, or a field named like a credential (`api_key:`, `token:`, `password=`)
|
|
146
|
+
holding a literal value that looks random. Everything in a workspace is
|
|
147
|
+
deployed, versioned, readable by every agent and shown to the model.
|
|
148
|
+
|
|
149
|
+
Move it to the vault (`foldrun secrets set NAME`), write `${NAME}` where it
|
|
150
|
+
was, and bind it to the sites it is for (`foldrun secrets sites NAME
|
|
151
|
+
api.example.com`) so it never enters a sandbox at all. Then **rotate the
|
|
152
|
+
key**: it has already been copied, into git history if nowhere else.
|
|
153
|
+
References (`${NAME}`, `$NAME`, `process.env.X`), names, and obvious samples
|
|
154
|
+
(`your-key-here`, `xxxx`, AWS's `…EXAMPLE`) are not findings.
|
|
155
|
+
|
|
156
|
+
### content/hidden-unicode
|
|
157
|
+
|
|
158
|
+
*warn · ASI01, ASI04, ASI06.* Zero-width characters, bidirectional overrides
|
|
159
|
+
or Unicode tag characters: invisible to a person reading the file, read as
|
|
160
|
+
text by the model, and the standard way to hide an instruction from a
|
|
161
|
+
reviewer. Tag characters are decoded in the message so you can read what
|
|
162
|
+
they say. Delete them. A byte-order mark at the very start of a file is
|
|
163
|
+
allowed.
|
|
164
|
+
|
|
165
|
+
### content/injection-phrase
|
|
166
|
+
|
|
167
|
+
*warn · ASI01, ASI06.* A line in knowledge, memory, a skill or a tool's
|
|
168
|
+
description that reads as an order to the agent: "ignore previous
|
|
169
|
+
instructions", "you are no longer…", "new instructions:", a fake `<system>`
|
|
170
|
+
tag, "do not tell the user", "send the secrets to…". Agents read those files
|
|
171
|
+
as material, and a line like that is either an attack that arrived with the
|
|
172
|
+
content or a paste that will confuse the model. Remove it, or quote it as an
|
|
173
|
+
example inside a fenced code block, where it is not flagged. An agent's own
|
|
174
|
+
prompt is not checked, since a guard agent may need to talk about exactly
|
|
175
|
+
this.
|
|
176
|
+
|
|
177
|
+
### content/encoded-blob
|
|
178
|
+
|
|
179
|
+
*info · ASI01, ASI04.* Three hundred or more characters of base64 in a
|
|
180
|
+
markdown file. A reviewer cannot read it and the model can. Move data to a
|
|
181
|
+
file and link it. An inline image (`data:image/…;base64,`) says what it is
|
|
182
|
+
and is not flagged.
|
|
183
|
+
|
|
184
|
+
### security/lethal-trifecta
|
|
185
|
+
|
|
186
|
+
*warn · ASI01, ASI02, ASI03.* One agent that **reads untrusted content** (the
|
|
187
|
+
`web` tool, or a browse/fetch/scrape tool), **holds something private** (a
|
|
188
|
+
secret, an `apis:` entry, an MCP server) and **has a way out** (an open
|
|
189
|
+
network, which is the default for an agent that reads the web, a tool marked
|
|
190
|
+
`outward: true`, or an API it may POST, PUT, PATCH or DELETE to). Any page it
|
|
191
|
+
reads can tell it to send what it holds anywhere it can reach.
|
|
192
|
+
|
|
193
|
+
Take a leg away:
|
|
194
|
+
- take it off the open network: `network: { level: trusted, allow:
|
|
195
|
+
[api.example.com] }`, or `network: trusted` ([Network](network)). A bare
|
|
196
|
+
list of sites only adds to a level, so an agent that reads the web stays
|
|
197
|
+
`open` with one. The level is resolved the way a run resolves it: the
|
|
198
|
+
agent's, then the workspace's `AGENTS.md`, then the account's;
|
|
199
|
+
- split it into a reader with no secrets and a sender that reads nothing
|
|
200
|
+
untrusted;
|
|
201
|
+
- guard its calls with a `hooks:` gate ([Hooks](hooks)).
|
|
202
|
+
|
|
203
|
+
`permissionMode: plan` also clears it. An approval (`!`) on each outward step
|
|
204
|
+
is the other answer, though this rule looks at the agent, not the flow.
|
|
205
|
+
|
|
206
|
+
### security/subagent-wider
|
|
207
|
+
|
|
208
|
+
*info · ASI03.* A sub-agent declares tools its parent does not have. A
|
|
209
|
+
sub-agent never gets more than its parent, so they are dropped inside the
|
|
210
|
+
delegation. Grant them to the parent, or remove them from the sub-agent.
|
|
211
|
+
|
|
212
|
+
### flow/outward-ungated
|
|
213
|
+
|
|
214
|
+
*the step's level · ASI02, ASI09.* A step whose agent holds an `outward:
|
|
215
|
+
true` tool, with no `verify:` and no approval. Give it a `verify:` that reads
|
|
216
|
+
what it did, or a `!`, so a wrong post or send is caught before it happens
|
|
217
|
+
again. An error in `foldrun check`, a warning at deploy.
|
|
218
|
+
|
|
219
|
+
### supply-chain/pipe-to-shell
|
|
220
|
+
|
|
221
|
+
*warn · ASI04, ASI05.* `curl … | sh` (or `wget`, `bash`, `zsh`) runs whatever
|
|
222
|
+
the server returns today, unreviewed, in the sandbox that holds the
|
|
223
|
+
workspace. Declare the package under `runtime: system:` or `packages:`, or
|
|
224
|
+
download it, verify a checksum, then run it.
|
|
225
|
+
|
|
226
|
+
### supply-chain/unpinned-mcp
|
|
227
|
+
|
|
228
|
+
*warn · ASI04.* An MCP server started with `npx`, `bunx`, `pnpx` or `uvx`
|
|
229
|
+
with no version, or at `@latest`. Every run installs whatever was published
|
|
230
|
+
last, so a compromised release reaches you the moment it ships. Pin it:
|
|
231
|
+
`npx -y some-server@1.4.2`.
|
|
232
|
+
|
|
233
|
+
### supply-chain/unpinned-package
|
|
234
|
+
|
|
235
|
+
*info · ASI04.* A `runtime: packages:` or `npm:` entry with no version. Pin a
|
|
236
|
+
range (`pandas>=2,<3`, `lodash@^4`) so tomorrow's run uses what was tested
|
|
237
|
+
today.
|
|
238
|
+
|
|
239
|
+
### config/unknown-field
|
|
240
|
+
|
|
241
|
+
*warn.* A frontmatter key in an agent, flow or tool that is one or two
|
|
242
|
+
letters away from a field foldrun reads: `tolls:`, `triger:`,
|
|
243
|
+
`disallowed_tools:`. YAML accepts it, so it is silently ignored, and `tolls:
|
|
244
|
+
[web]` leaves the agent with no tools. A key that looks like no known field
|
|
245
|
+
is left alone, since another tool may own it.
|
|
246
|
+
|
|
247
|
+
### check/ignore-needs-reason
|
|
248
|
+
|
|
249
|
+
*warn.* A `foldrun-ignore` with no reason after a colon. It ignores nothing
|
|
250
|
+
until it has one.
|
|
251
|
+
|
|
252
|
+
### check's own rules
|
|
253
|
+
|
|
254
|
+
These are what `check` has always reported, now with ids so they can be
|
|
255
|
+
filtered, ignored and baselined. Each problem keeps its own level.
|
|
256
|
+
|
|
257
|
+
| Rule | Covers |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `agent/config` | frontmatter that cannot be read; a tool, skill, colleague, sub-agent or hook that does not exist; retired tool names |
|
|
260
|
+
| `flow/config` | a step naming nothing, an option the parser cannot read, `verify:` written as prose, an arrangement that cannot have what its instruction asks for |
|
|
261
|
+
| `tool/config` | a `run:` program that is missing, an import nothing declares, a leftover `type:` |
|
|
262
|
+
| `skill/config` | a `SKILL.md` without a usable name or description |
|
|
263
|
+
| `eval/config` | an eval that names nothing, or has no cases |
|
|
264
|
+
| `hook/config` | a hook folder that is not a valid hook; a `hooks:` name that resolves to nothing |
|
|
265
|
+
| `knowledge/okf` | a knowledge or memory document that is not a conformant bundle entry, an unreadable date, a stale document |
|
|
266
|
+
| `workspace/config` | an `AGENTS.md` setting that cannot be read; a path a deploy refuses |
|
|
267
|
+
| `library/config` | the same, in the account's shared library |
|
|
268
|
+
| `paths/workspace-spelling` | `../../storage/x` where `workspace/storage/x` reads the same everywhere |
|
|
269
|
+
|
|
270
|
+
## What it does not do
|
|
271
|
+
|
|
272
|
+
It reads files; it does not run them. Static rules catch what the files
|
|
273
|
+
say, not what a model will do with a page it has not read yet, so they are
|
|
274
|
+
the first line, not the only one: [network](network) policy, bound
|
|
275
|
+
[secrets](secrets), [hooks](hooks) and approvals hold at run time whatever
|
|
276
|
+
the files say. MCP servers are never started to inspect their tools, and no
|
|
277
|
+
URL is fetched.
|
package/docs/cli.md
CHANGED
|
@@ -39,7 +39,8 @@ claude.ai or Claude Code there, nor any MCP configuration on disk.
|
|
|
39
39
|
| `foldrun agent ls` | the agents deployed in a workspace — name, model, description (`GET …/agents`); `flow ls` the flows and how each fires (`GET …/flows`). `--to <workspace>`, `--json` for the list as the platform returns it |
|
|
40
40
|
| `foldrun guide` | the coding-agent rules, copied from what Next.js does: a short managed block in the account's `AGENTS.md`, between `<!-- BEGIN:foldrun-agent-rules -->` markers, telling a coding agent (Claude Code, Cursor, Codex) to read `foldrun docs` before it writes an agent, flow or tool, and a `CLAUDE.md` that imports it with `@AGENTS.md`. `foldrun init` writes them, `foldrun pull` refreshes them, and `foldrun check` re-adds them when a coding agent runs it and they are missing or old — as `next dev` does. Only the block is rewritten, in place; text around it is kept. The runtime strips the block, so the agents that run in the account never see it; the rest of `AGENTS.md` stays their shared context. `--check` exits 1 when the block is missing or old (for CI), `--print` writes it to stdout |
|
|
41
41
|
| `foldrun docs [page]` | these docs, from the copy that ships inside the CLI — the way Next.js ships `node_modules/next/dist/docs` — so a coding agent reads the version it is driving, offline. No page lists them; `foldrun docs flows` prints one; `--path` prints the folder. Design records and the platform operator's routes are left out |
|
|
42
|
-
| `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`). Hooks: a `hooks/<name>/` folder that is not a valid hook (`on:`, `run.mjs` beside `hook.md`, `tool:` only on tool events, `timeout:` 1 to 120), and a `hooks:` name in an agent or an AGENTS.md that is in neither `hooks/` nor the library, are errors; a `tool.call` hook on an agent whose `web.search:` (or `web.fetch:`) the model provider runs on its own servers is a warning — those calls never reach the hook |
|
|
42
|
+
| `foldrun check [dir]` | validate agents, flows, tools, evals, knowledge — offline. At an account root: every workspace, then the shared library. Includes each agent's colleagues: an `agents:` or `subagents:` name that is not an agent, a sub-agent with no `description:`, a sub-agent that would hold none of its tools inside its parent's. Signed in, it also asks the platform's open `/api/version` which Chrome its runner image has, and warns when a pinned `web.browse.user_agent:` claims another major ([why](tools#how-this-agents-browser-presents-itself)); it warns about the old `web.browse.session:` key too (now `vendor_session:`). Hooks: a `hooks/<name>/` folder that is not a valid hook (`on:`, `run.mjs` beside `hook.md`, `tool:` only on tool events, `timeout:` 1 to 120), and a `hooks:` name in an agent or an AGENTS.md that is in neither `hooks/` nor the library, are errors; a `tool.call` hook on an agent whose `web.search:` (or `web.fetch:`) the model provider runs on its own servers is a warning — those calls never reach the hook. It is also the security scanner: a credential written into any file, invisible Unicode, override phrases in knowledge and skills, an agent that reads the web while holding a secret and an open way out, `curl \| sh`, an MCP server at no fixed version, a typo of a frontmatter field. Every finding names its rule (`secret/hardcoded`, `agent/config`, …); see [Checks](checks) |
|
|
43
|
+
| `foldrun check --security` | only the security rules (secret, security, content, supply-chain), exiting 1 on a warning. Also `--rule <id or category>` (repeatable), `--fail-on error\|warn\|info\|never`, `--format text\|json\|sarif` (`--json`; `--out <file>`), `--changed` (only files changed against git HEAD; `--base <ref>`), `--write-baseline` (accept today's findings into `.foldrun/check-baseline.json`; `--no-baseline`), `--rules` (list every rule). A `foldrun-ignore <rule>: <reason>` comment on the line before a finding silences it; the reason is required. [Checks](checks) |
|
|
43
44
|
| `foldrun extract [dir]` | move each single-file script tool — a `tools/<name>.md` with its program in a fenced block — into a folder: `tools/<name>/tool.md` with `run:` pointing at `run.js`, `run.py` or `run.sh` beside it. Frontmatter is edited as text, never re-serialised, so the diff is only what moved. A tool that already has a folder is left for you; `--dry-run` lists what it would do |
|
|
44
45
|
| `foldrun check --to <ws>` | validate the copy that is DEPLOYED, rather than the folder in front of you — a workspace edited through the API, by an agent or by `foldrun source put`, has had no way to be checked short of spending a run. The files are fetched into a temporary folder and the ordinary check runs over them, so there is one copy of the rules; the output names the workspace and the platform it checked |
|
|
45
46
|
| `foldrun agent run <name>` | run one agent once on a platform, with no flow file: a flow of one step. `--task "…"` is the instruction and is required. Prints the run id and how to follow it; `--wait` holds on (in 25-second pieces, as `invoke` does) and then prints the same report `foldrun report` would; `--test` makes it a [test run](runs#test-runs) |
|
package/docs/faq.md
CHANGED
|
@@ -52,5 +52,5 @@ file store. Nothing is hidden in a database only the platform can read, and
|
|
|
52
52
|
a deploy never touches what an agent learned or a run recorded.
|
|
53
53
|
|
|
54
54
|
**Can I run it on my own hardware?** The CLI and runtime, yes — that is
|
|
55
|
-
what they are for, and they are
|
|
55
|
+
what they are for, and they are free. The platform itself is hosted
|
|
56
56
|
only. See [Running it yourself](self-hosting).
|
package/docs/introduction.md
CHANGED
|
@@ -36,7 +36,7 @@ The design bets on three things:
|
|
|
36
36
|
|
|
37
37
|
The same workspace moves between them with `foldrun deploy` or a `git push`.
|
|
38
38
|
Start locally; that is where every workspace should be born. The CLI and
|
|
39
|
-
runtime are
|
|
39
|
+
runtime are free from npm; the platform is not distributed — see
|
|
40
40
|
[Running it yourself](self-hosting).
|
|
41
41
|
|
|
42
42
|
## Where to go
|
package/docs/self-hosting.md
CHANGED
|
@@ -5,11 +5,12 @@ them unchanged.
|
|
|
5
5
|
|
|
6
6
|
| shape | for | what is yours |
|
|
7
7
|
|---|---|---|
|
|
8
|
-
| **your machine** | one person, no server — the
|
|
8
|
+
| **your machine** | one person, no server — the foldrun CLI and runtime, free from npm | everything: the files, the runs, the model bill |
|
|
9
9
|
| **the hosted platform** | a team; schedules, sandboxes, gates, a vault, a dashboard | your workspaces and their records; we run the machines |
|
|
10
10
|
|
|
11
|
-
The framework — the format, the runner, the checks, the CLI — is
|
|
12
|
-
|
|
11
|
+
The framework — the format, the runner, the checks, the CLI — is free to
|
|
12
|
+
install from npm (`foldrun`, `@foldrun/core`) and use under foldrun's
|
|
13
|
+
licence, which ships in each package. The platform that runs it
|
|
13
14
|
for other people (the queue and its worker, per-step sandboxes on a cluster,
|
|
14
15
|
metering, tenancy) is ours and is not distributed: there is no self-hosted
|
|
15
16
|
platform. If you need it inside your own network, that is a conversation —
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldrun",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Agents are just folders. Write them, check them, run them — on your machine.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"foldrun": "bin/foldrun.mjs"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"node": ">=22"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@foldrun/core": "^1.
|
|
14
|
+
"@foldrun/core": "^1.3.0"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"e2e": "FOLDRUN_E2E=1 FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-e2e-XXXXXX) node --test --test-timeout=300000 tests/e2e.test.ts",
|
|
24
|
-
"test": "FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-test-XXXXXX) node --test tests/cli-account.test.ts tests/model-credential.test.ts tests/cli-approvals.test.ts tests/cli-stop.test.ts tests/cli-account-settings.test.ts tests/cli-schedule.test.ts tests/cli-billing.test.ts tests/cli-storage.test.ts tests/cli-scaffold.test.ts tests/cli-secrets.test.ts tests/cli-site-login.test.ts tests/cli-check-library.test.ts tests/cli-check-timezone.test.ts tests/cli-check-frontmatter.test.ts tests/cli-agent-import.test.ts tests/cli-check-subagents.test.ts tests/cli-connect.test.ts tests/cli-accounts.test.ts tests/cli-remote.test.ts tests/cli-stream.test.ts tests/cli-doctor.test.ts tests/cli-tool-test.test.ts tests/cli-agent-run.test.ts tests/cli-check-remote.test.ts tests/cli-shares.test.ts tests/cli-triggers.test.ts tests/cli-providers.test.ts tests/cli-rerun-put.test.ts tests/cli-guide.test.ts tests/cli-runtimes.test.ts tests/cli-deploy-safety.test.ts tests/cli-operator.test.ts tests/cli-flow-patterns.test.ts tests/cli-flow-rm-step.test.ts tests/cli-flow-dup-step.test.ts tests/cli-flow-write-stale.test.ts tests/cli-agent-link-stale.test.ts tests/cli-parity.test.ts tests/cli-invoke-inputs.test.ts tests/cli-audit-keys.test.ts tests/cli-version.test.ts tests/cli-check-web-warnings.test.ts tests/cli-report-image.test.ts tests/cli-check-workspace-paths.test.ts tests/cli-parity-dashboard.test.ts tests/cli-api-standards.test.ts tests/cli-backups-onboarding.test.ts tests/cli-check-limits.test.ts tests/cli-gallery.test.ts tests/cli-status-platform.test.ts tests/cli-webhooks-notifications.test.ts tests/cli-flags.test.ts tests/cli-api-coverage.test.ts tests/cli-hooks.test.ts tests/cli-release.test.ts tests/cli-terminal.test.ts tests/cli-network.test.ts tests/cli-assistant.test.ts",
|
|
24
|
+
"test": "FOLDRUN_DATA=$(mktemp -d /tmp/foldrun-test-XXXXXX) node --test tests/cli-account.test.ts tests/model-credential.test.ts tests/cli-approvals.test.ts tests/cli-stop.test.ts tests/cli-account-settings.test.ts tests/cli-schedule.test.ts tests/cli-billing.test.ts tests/cli-storage.test.ts tests/cli-scaffold.test.ts tests/cli-secrets.test.ts tests/cli-site-login.test.ts tests/cli-check-library.test.ts tests/cli-check-timezone.test.ts tests/cli-check-frontmatter.test.ts tests/cli-agent-import.test.ts tests/cli-check-subagents.test.ts tests/cli-connect.test.ts tests/cli-accounts.test.ts tests/cli-remote.test.ts tests/cli-stream.test.ts tests/cli-doctor.test.ts tests/cli-tool-test.test.ts tests/cli-agent-run.test.ts tests/cli-check-remote.test.ts tests/cli-shares.test.ts tests/cli-triggers.test.ts tests/cli-providers.test.ts tests/cli-rerun-put.test.ts tests/cli-guide.test.ts tests/cli-runtimes.test.ts tests/cli-deploy-safety.test.ts tests/cli-operator.test.ts tests/cli-flow-patterns.test.ts tests/cli-flow-rm-step.test.ts tests/cli-flow-dup-step.test.ts tests/cli-flow-write-stale.test.ts tests/cli-agent-link-stale.test.ts tests/cli-parity.test.ts tests/cli-invoke-inputs.test.ts tests/cli-audit-keys.test.ts tests/cli-version.test.ts tests/cli-check-web-warnings.test.ts tests/cli-report-image.test.ts tests/cli-check-workspace-paths.test.ts tests/cli-parity-dashboard.test.ts tests/cli-api-standards.test.ts tests/cli-backups-onboarding.test.ts tests/cli-check-limits.test.ts tests/cli-gallery.test.ts tests/cli-status-platform.test.ts tests/cli-webhooks-notifications.test.ts tests/cli-flags.test.ts tests/cli-api-coverage.test.ts tests/cli-hooks.test.ts tests/cli-release.test.ts tests/cli-terminal.test.ts tests/cli-network.test.ts tests/cli-assistant.test.ts tests/cli-check-scan.test.ts",
|
|
25
25
|
"release": "node scripts/release.mjs",
|
|
26
26
|
"typecheck": "tsc -p tsconfig.json",
|
|
27
27
|
"docs:sync": "node scripts/sync-docs.mjs",
|
|
@@ -33,9 +33,6 @@
|
|
|
33
33
|
"url": "git+https://github.com/foldrun-io/foldrun-cli.git"
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://foldrun.io",
|
|
36
|
-
"bugs": {
|
|
37
|
-
"url": "https://github.com/foldrun-io/foldrun-cli/issues"
|
|
38
|
-
},
|
|
39
36
|
"keywords": [
|
|
40
37
|
"agents",
|
|
41
38
|
"llm",
|
package/src/commands.mjs
CHANGED
|
@@ -758,54 +758,351 @@ async function platformBrowsers(url) {
|
|
|
758
758
|
*/
|
|
759
759
|
async function checkAccount(layout, flags) {
|
|
760
760
|
const { readLibraryTree, LIBRARY_KINDS } = await core();
|
|
761
|
-
|
|
761
|
+
const opts = checkOptions(flags);
|
|
762
|
+
const text = opts.format === "text";
|
|
762
763
|
if (!layout.workspaces.length) {
|
|
763
764
|
console.log(`\n ${c.amber("!")} no workspaces in ${layout.accountRoot} — \`foldrun new <name>\` makes one\n`);
|
|
764
765
|
return 1;
|
|
765
766
|
}
|
|
767
|
+
const all = [];
|
|
768
|
+
let baselined = 0;
|
|
766
769
|
for (const name of layout.workspaces) {
|
|
767
770
|
const dir = path.join(layout.workspacesDir, name);
|
|
768
771
|
process.env.FOLDRUN_WORKSPACE = dir;
|
|
769
|
-
|
|
770
|
-
|
|
772
|
+
const r = await checkWorkspace(dir, flags, opts);
|
|
773
|
+
baselined += r.baselined;
|
|
774
|
+
if (text) {
|
|
775
|
+
console.log(`\n ${c.bold(name)} ${c.dim(path.relative(layout.accountRoot, dir))}`);
|
|
776
|
+
printCheck(r, opts);
|
|
777
|
+
}
|
|
778
|
+
const prefix = path.relative(layout.accountRoot, dir).split(path.sep).join("/");
|
|
779
|
+
// A library finding is the account's, reported once however many
|
|
780
|
+
// workspaces resolve that tool.
|
|
781
|
+
for (const f of r.findings) {
|
|
782
|
+
if (!f.account) all.push({ ...f, file: `${prefix}/${f.file}`, where: `${prefix}/${f.where}` });
|
|
783
|
+
else if (!all.some((x) => x.account && x.rule === f.rule && x.file === f.file && x.line === f.line && x.fingerprint === f.fingerprint)) all.push(f);
|
|
784
|
+
}
|
|
771
785
|
}
|
|
772
786
|
|
|
773
787
|
// The library, once — it is one thing however many workspaces read it.
|
|
774
788
|
const files = readLibraryTree(layout.accountRoot);
|
|
775
789
|
const problems = [];
|
|
776
|
-
const note = (level, where, message) => problems.push({ level, where, message });
|
|
790
|
+
const note = (level, where, message) => problems.push({ level, where, message, rule: checkRuleFor(`library/${where}`) });
|
|
777
791
|
validateSkills(path.join(layout.accountRoot, "library"), note);
|
|
778
792
|
const hc = await hooksCore();
|
|
779
793
|
for (const p of hc ? hc.checkHooksDir(path.join(layout.accountRoot, "library", "hooks"), "account") : []) {
|
|
780
794
|
const m = /^(hooks\/[^/:]+(?:\/hook\.md)?): (.*)$/s.exec(p);
|
|
781
795
|
note("error", m ? m[1] : "hooks/", m ? m[2] : p);
|
|
782
796
|
}
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
797
|
+
const scan = await scanCore();
|
|
798
|
+
const fp = scan?.findingFingerprint ?? localFingerprint;
|
|
799
|
+
let lib = problems.map((p) => toFinding({ ...p, where: `library/${p.where}` }, layout.accountRoot, fp));
|
|
800
|
+
if (scan?.filterIgnored) lib = scan.filterIgnored(lib, files.map((f) => ({ path: `library/${f.kind}/${f.path}`, content: f.content })));
|
|
801
|
+
if (scan) {
|
|
802
|
+
const outward = files.filter((f) => f.kind === "tools" && /^[^/]+(?:\/tool)?\.md$/.test(f.path) && /^outward:\s*true\s*$/m.test(f.content)).map((f) => f.path.replace(/(?:\/tool)?\.md$/, ""));
|
|
803
|
+
lib.push(...scan.scanFiles(files.map((f) => ({ path: `library/${f.kind}/${f.path}`, content: f.content })), { outwardTools: outward }).map((f) => ({ ...f, where: `${f.file}${f.line ? `:${f.line}` : ""}` })));
|
|
804
|
+
}
|
|
805
|
+
const libResult = narrowCheck(lib, layout.accountRoot, opts, "library");
|
|
806
|
+
baselined += libResult.baselined;
|
|
807
|
+
if (text) {
|
|
808
|
+
const counts = LIBRARY_KINDS.map((k) => ({ kind: k, n: files.filter((f) => f.kind === k).length })).filter((x) => x.n > 0);
|
|
809
|
+
console.log(`\n ${c.bold("library")} ${c.dim(counts.length ? counts.map((x) => `${x.n} ${x.kind}`).join(" · ") : "empty")}`);
|
|
810
|
+
for (const f of libResult.findings) {
|
|
811
|
+
console.log(` ${f.level === "error" ? c.red("✗") : f.level === "warn" ? c.amber("!") : c.dim("·")} ${c.bold(f.where)} ${f.message} ${c.dim(f.rule)}`);
|
|
812
|
+
}
|
|
813
|
+
if (libResult.wrote) console.log(` ${c.dim(`baseline: ${libResult.wrote}`)}`);
|
|
814
|
+
console.log();
|
|
787
815
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
return
|
|
816
|
+
all.push(...libResult.findings);
|
|
817
|
+
if (!text) emitCheck(all, opts, { root: layout.accountRoot, summary: `${layout.workspaces.length} workspaces and the library`, baselined });
|
|
818
|
+
return checkExit(all, opts);
|
|
791
819
|
}
|
|
792
820
|
|
|
793
821
|
async function check(workspace, flags = {}) {
|
|
794
|
-
const
|
|
795
|
-
const
|
|
796
|
-
|
|
822
|
+
const opts = checkOptions(flags);
|
|
823
|
+
const r = await checkWorkspace(workspace, flags, opts);
|
|
824
|
+
if (opts.format === "text") printCheck(r, opts);
|
|
825
|
+
else emitCheck(r.findings, opts, { root: workspace, summary: r.summary, baselined: r.baselined });
|
|
826
|
+
return checkExit(r.findings, opts);
|
|
827
|
+
}
|
|
797
828
|
|
|
829
|
+
function printCheck(r, opts) {
|
|
830
|
+
const errors = r.findings.filter((p) => p.level === "error");
|
|
831
|
+
const warnings = r.findings.filter((p) => p.level === "warn");
|
|
798
832
|
console.log("");
|
|
799
|
-
for (const p of
|
|
833
|
+
for (const p of r.findings) {
|
|
800
834
|
const tag = p.level === "error" ? c.red("error") : p.level === "warn" ? c.amber(" warn") : c.dim(" info");
|
|
801
|
-
console.log(` ${tag} ${c.bold(p.where)} ${p.message}`);
|
|
802
|
-
}
|
|
835
|
+
console.log(` ${tag} ${c.bold(p.where)} ${p.message} ${c.dim(p.rule)}`);
|
|
836
|
+
}
|
|
837
|
+
const notes = [
|
|
838
|
+
...(r.baselined ? [`${r.baselined} known in the baseline`] : []),
|
|
839
|
+
...(opts.changed ? [`changed files only (vs ${opts.base})`] : []),
|
|
840
|
+
...(opts.security ? ["security rules only"] : []),
|
|
841
|
+
...(r.wrote ? [`baseline written: ${r.wrote}`] : []),
|
|
842
|
+
...(r.noScanner ? ["this @foldrun/core has no scanner — `npm i -g foldrun@latest` for the security rules"] : []),
|
|
843
|
+
];
|
|
844
|
+
const tail = notes.length ? ` ${c.dim(`(${notes.join("; ")})`)}` : "";
|
|
803
845
|
console.log(
|
|
804
846
|
errors.length === 0 && warnings.length === 0
|
|
805
|
-
? ` ${c.green("✓")} ${summary} — no problems\n`
|
|
806
|
-
: `\n ${summary} · ${errors.length} error${errors.length === 1 ? "" : "s"}, ${warnings.length} warning${warnings.length === 1 ? "" : "s"}\n`,
|
|
847
|
+
? ` ${c.green("✓")} ${r.summary} — no problems${tail}\n`
|
|
848
|
+
: `\n ${r.summary} · ${errors.length} error${errors.length === 1 ? "" : "s"}, ${warnings.length} warning${warnings.length === 1 ? "" : "s"}${tail}\n`,
|
|
807
849
|
);
|
|
808
|
-
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ---------------------------------------------------------------- check: rules, formats, baseline
|
|
853
|
+
//
|
|
854
|
+
// `check` is the linter and the scanner in one: its own rules (will this
|
|
855
|
+
// run?) and core's scanner (is it safe to run? — secrets, hidden text, an
|
|
856
|
+
// agent that can be talked into leaking, unpinned code). Every finding has a
|
|
857
|
+
// rule id, a level, a file and a fingerprint, so the same result prints for
|
|
858
|
+
// a person, as JSON for a script, or as SARIF for GitHub code scanning.
|
|
859
|
+
|
|
860
|
+
/** Core's scanner, or null on a core from before it — check then runs its
|
|
861
|
+
* own rules alone and says the security rules need a newer install. */
|
|
862
|
+
async function scanCore() {
|
|
863
|
+
try {
|
|
864
|
+
scanRules = /** @type {any} */ (await import("@foldrun/core/scan"));
|
|
865
|
+
return scanRules;
|
|
866
|
+
} catch {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// The scanner module, once scanCore has loaded it — categoryOf and SARIF read it.
|
|
872
|
+
let scanRules = null;
|
|
873
|
+
|
|
874
|
+
const SECURITY_CATEGORIES = new Set(["secret", "security", "content", "supply-chain"]);
|
|
875
|
+
const RANK = { error: 3, warn: 2, info: 1 };
|
|
876
|
+
const FAIL_ON = { error: 3, warn: 2, info: 1, never: Infinity };
|
|
877
|
+
|
|
878
|
+
/** The rule id of one of check's own problems, from where it was found —
|
|
879
|
+
* core's ruleForWhere, so the deploy and the Checks page name the same
|
|
880
|
+
* problem the same way. A core from before the scanner has no table, and
|
|
881
|
+
* then this fallback reads the path the same way. */
|
|
882
|
+
function checkRuleFor(where, kind) {
|
|
883
|
+
if (scanRules?.ruleForWhere) return scanRules.ruleForWhere(where, kind);
|
|
884
|
+
if (kind === "outward") return "flow/outward-ungated";
|
|
885
|
+
if (kind === "spelling") return "paths/workspace-spelling";
|
|
886
|
+
if (/(?:^|\/)(?:knowledge|memory)\//.test(where)) return "knowledge/okf";
|
|
887
|
+
const lib = where.startsWith("library/");
|
|
888
|
+
const top = where.replace(/^library\//, "").split(/[/:]/)[0];
|
|
889
|
+
const byTop = { agents: "agent/config", flows: "flow/config", tools: "tool/config", skills: "skill/config", ".agents": "skill/config", evals: "eval/config", hooks: "hook/config" };
|
|
890
|
+
if (byTop[top]) return byTop[top];
|
|
891
|
+
return lib || top === "library" ? "library/config" : "workspace/config";
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function categoryOf(rule) {
|
|
895
|
+
return scanRules?.scanRule?.(rule)?.category ?? (rule === "flow/outward-ungated" ? "security" : "config");
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function checkOptions(flags) {
|
|
899
|
+
const format = flags.json === true ? "json" : typeof flags.format === "string" ? flags.format : "text";
|
|
900
|
+
if (!["text", "json", "sarif"].includes(format)) throw new Error(`--format ${format} — use text, json or sarif`);
|
|
901
|
+
const security = flags.security === true;
|
|
902
|
+
const failOn = typeof flags["fail-on"] === "string" ? flags["fail-on"] : security ? "warn" : "error";
|
|
903
|
+
if (!(failOn in FAIL_ON)) throw new Error(`--fail-on ${failOn} — use error, warn, info or never`);
|
|
904
|
+
const rules = [].concat(flags.rule ?? []).flatMap((r) => String(r).split(",")).map((r) => r.trim()).filter(Boolean);
|
|
905
|
+
return {
|
|
906
|
+
format,
|
|
907
|
+
security,
|
|
908
|
+
failOn,
|
|
909
|
+
rules,
|
|
910
|
+
changed: flags.changed === true || typeof flags.base === "string",
|
|
911
|
+
base: typeof flags.base === "string" ? flags.base : "HEAD",
|
|
912
|
+
baseline: flags["no-baseline"] !== true,
|
|
913
|
+
writeBaseline: flags["write-baseline"] === true,
|
|
914
|
+
out: typeof flags.out === "string" ? flags.out : null,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** sha256 over rule, file and text — core's findingFingerprint, for a core
|
|
919
|
+
* without the scanner, so a baseline written by either matches. */
|
|
920
|
+
function localFingerprint(rule, file, text) {
|
|
921
|
+
return crypto.createHash("sha256").update(`${rule}\0${file}\0${String(text).trim().replace(/\s+/g, " ")}`).digest("hex").slice(0, 16);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** One of check's problems as a finding: a file (not a folder) and a line. */
|
|
925
|
+
function toFinding(p, root, fp) {
|
|
926
|
+
const m = /^(.*?):(\d+)$/.exec(p.where);
|
|
927
|
+
let file = m ? m[1] : p.where;
|
|
928
|
+
const line = m ? Number(m[2]) : undefined;
|
|
929
|
+
if (/^agents\/[^/]+$/.test(file)) file += "/agent.md";
|
|
930
|
+
else if (/^(?:library\/)?tools\/[^/]+$/.test(file)) file = fs.existsSync(path.join(root, file, "tool.md")) ? `${file}/tool.md` : `${file}.md`;
|
|
931
|
+
const rule = p.rule ?? checkRuleFor(p.where);
|
|
932
|
+
return { rule, level: p.level, file, ...(line ? { line } : {}), message: p.message, fingerprint: fp(rule, file, p.message), where: p.where };
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* The file a finding is really in. readTree carries a single-file subagent
|
|
937
|
+
* (`.agents/agents/<name>.md`, `.claude/agents/<name>.md`) as
|
|
938
|
+
* `agents/<name>/agent.md`, so a finding on it names a file that is not
|
|
939
|
+
* there — and --changed, which asks git, would drop it. The content is the
|
|
940
|
+
* same, so the line still holds.
|
|
941
|
+
*/
|
|
942
|
+
function sourcePathOf(workspace, file) {
|
|
943
|
+
const m = /^agents\/([^/]+)\/agent\.md$/.exec(file);
|
|
944
|
+
if (!m || fs.existsSync(path.join(workspace, file))) return file;
|
|
945
|
+
// The same eligibility readTree uses: a regular file, never a link.
|
|
946
|
+
for (const alt of [`.agents/agents/${m[1]}.md`, `.claude/agents/${m[1]}.md`]) {
|
|
947
|
+
try {
|
|
948
|
+
if (fs.lstatSync(path.join(workspace, alt)).isFile()) return alt;
|
|
949
|
+
} catch {
|
|
950
|
+
// not there
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return file;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Everything check finds in one workspace, narrowed by the flags. Findings
|
|
957
|
+
* about the account library carry `account: true` and paths from the
|
|
958
|
+
* account root, where they live — not from this workspace. */
|
|
959
|
+
async function checkWorkspace(workspace, flags, opts) {
|
|
960
|
+
const { problems, summary, outwardTools } = await checkProblems(workspace, flags);
|
|
961
|
+
const scan = await scanCore();
|
|
962
|
+
const { libraryDir } = await core();
|
|
963
|
+
const accountRoot = path.dirname(libraryDir("default"));
|
|
964
|
+
const fp = scan?.findingFingerprint ?? localFingerprint;
|
|
965
|
+
/** @type {any[]} */
|
|
966
|
+
let findings = problems.map((p) => ({ ...toFinding(p, p.where.startsWith("library/") ? accountRoot : workspace, fp), account: p.where.startsWith("library/") }));
|
|
967
|
+
if (scan) {
|
|
968
|
+
const { readTree } = await core();
|
|
969
|
+
const files = readTree(workspace);
|
|
970
|
+
// One ignore syntax for every rule: check's own findings honour the
|
|
971
|
+
// same `foldrun-ignore <rule>: <reason>` comments the scanner does.
|
|
972
|
+
if (scan.filterIgnored) findings = [...scan.filterIgnored(findings.filter((f) => !f.account), files), ...findings.filter((f) => f.account)];
|
|
973
|
+
let accountNetwork;
|
|
974
|
+
try {
|
|
975
|
+
const md = path.join(accountRoot, "AGENTS.md");
|
|
976
|
+
if (md !== path.join(workspace, "AGENTS.md") && fs.existsSync(md)) accountNetwork = (await import("gray-matter")).default(fs.readFileSync(md, "utf8")).data?.network;
|
|
977
|
+
} catch {
|
|
978
|
+
// an unreadable account AGENTS.md is reported by its own check
|
|
979
|
+
}
|
|
980
|
+
for (const f of scan.scanFiles(files, { outwardTools, accountNetwork })) {
|
|
981
|
+
findings.push({ ...f, where: `${f.file}${f.line ? `:${f.line}` : ""}` });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
findings = findings.map((f) => {
|
|
985
|
+
if (f.account) return f;
|
|
986
|
+
const file = sourcePathOf(workspace, f.file);
|
|
987
|
+
return file === f.file ? f : { ...f, file, where: `${file}${f.line ? `:${f.line}` : ""}` };
|
|
988
|
+
});
|
|
989
|
+
const r = narrowCheck(findings, workspace, opts, workspace, accountRoot);
|
|
990
|
+
return { ...r, summary, noScanner: !scan };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** --rule / --security, then --changed, then the baseline (or writing one).
|
|
994
|
+
* `accountRoot` is where findings marked `account: true` live. */
|
|
995
|
+
function narrowCheck(findings, root, opts, label, accountRoot = root) {
|
|
996
|
+
let out = findings;
|
|
997
|
+
if (opts.rules.length) {
|
|
998
|
+
const want = new Set(opts.rules);
|
|
999
|
+
out = out.filter((f) => want.has(f.rule) || want.has(f.rule.split("/")[0]) || want.has(categoryOf(f.rule)));
|
|
1000
|
+
}
|
|
1001
|
+
if (opts.security) out = out.filter((f) => SECURITY_CATEGORIES.has(categoryOf(f.rule)));
|
|
1002
|
+
const file = path.join(root, ".foldrun", "check-baseline.json");
|
|
1003
|
+
if (opts.writeBaseline) {
|
|
1004
|
+
const created = new Date().toISOString();
|
|
1005
|
+
const seen = new Map(out.map((f) => [`${f.rule}\0${f.file}\0${f.fingerprint}`, { rule: f.rule, file: f.file, fingerprint: f.fingerprint }]));
|
|
1006
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1007
|
+
fs.writeFileSync(file, JSON.stringify({ version: 1, created, findings: [...seen.values()].sort((a, b) => a.file.localeCompare(b.file) || a.rule.localeCompare(b.rule)) }, null, 2) + "\n");
|
|
1008
|
+
return { findings: [], baselined: out.length, wrote: path.relative(process.cwd(), file) || file };
|
|
1009
|
+
}
|
|
1010
|
+
let baselined = 0;
|
|
1011
|
+
if (opts.baseline && fs.existsSync(file)) {
|
|
1012
|
+
let b = null;
|
|
1013
|
+
try {
|
|
1014
|
+
b = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1015
|
+
} catch {
|
|
1016
|
+
// Unreadable hides nothing: failing open would hide every new finding.
|
|
1017
|
+
out = [...out, { rule: "workspace/config", level: "warn", file: ".foldrun/check-baseline.json", message: "the baseline is not JSON — it hides nothing until it is rewritten (`foldrun check --write-baseline`)", fingerprint: "baseline", where: ".foldrun/check-baseline.json" }];
|
|
1018
|
+
}
|
|
1019
|
+
if (b?.version === 1 && Array.isArray(b.findings)) {
|
|
1020
|
+
// An entry that is not {rule, file, fingerprint} hides nothing.
|
|
1021
|
+
const entries = b.findings.filter((f) => f && typeof f === "object" && typeof f.rule === "string" && typeof f.file === "string" && typeof f.fingerprint === "string");
|
|
1022
|
+
const known = new Set(entries.map((f) => `${f.rule}\0${f.file}\0${f.fingerprint}`));
|
|
1023
|
+
const kept = out.filter((f) => !known.has(`${f.rule}\0${f.file}\0${f.fingerprint}`));
|
|
1024
|
+
baselined = out.length - kept.length;
|
|
1025
|
+
out = kept;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (opts.changed) {
|
|
1029
|
+
const here = changedFiles(root, opts.base, label);
|
|
1030
|
+
// The account may not be the same repository, or have that ref: then
|
|
1031
|
+
// nothing in the library counts as changed, and the workspace's own
|
|
1032
|
+
// comparison still stands.
|
|
1033
|
+
let there = here;
|
|
1034
|
+
if (out.some((f) => f.account) && accountRoot !== root) {
|
|
1035
|
+
try {
|
|
1036
|
+
there = changedFiles(accountRoot, opts.base, "library");
|
|
1037
|
+
} catch {
|
|
1038
|
+
there = new Set();
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
out = out.filter((f) => {
|
|
1042
|
+
const changed = f.account ? there : here;
|
|
1043
|
+
return changed.has(f.file) || [...changed].some((c) => c.startsWith(`${f.file}/`));
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
return { findings: out, baselined };
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/** Files changed against a ref, tracked or not, relative to `root`. */
|
|
1050
|
+
function changedFiles(root, base, label) {
|
|
1051
|
+
const git = (args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1052
|
+
try {
|
|
1053
|
+
// -z: literal paths. Without it git quotes and escapes a name outside
|
|
1054
|
+
// ASCII ("knowledge/\346\227\245.md"), which matches no finding.
|
|
1055
|
+
const diff = git(["diff", "-z", "--name-only", "--relative", base]);
|
|
1056
|
+
const untracked = git(["ls-files", "-z", "--others", "--exclude-standard"]);
|
|
1057
|
+
return new Set([...diff.split("\0"), ...untracked.split("\0")].filter(Boolean));
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
const why = String(/** @type {any} */ (err).stderr ?? err).trim().split("\n")[0];
|
|
1060
|
+
throw new Error(`--changed needs ${label === "library" ? "the account" : "this workspace"} in a git repository and a ref git knows (${base}) — ${why}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function checkExit(findings, opts) {
|
|
1065
|
+
const bar = FAIL_ON[opts.failOn];
|
|
1066
|
+
return findings.some((f) => RANK[f.level] >= bar) ? 1 : 0;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/** JSON or SARIF, to stdout or --out. */
|
|
1070
|
+
function emitCheck(findings, opts, { root, summary, baselined }) {
|
|
1071
|
+
const clean = findings.map(({ where, account, ...f }) => f);
|
|
1072
|
+
let body;
|
|
1073
|
+
if (opts.format === "sarif") {
|
|
1074
|
+
body = scanRules?.toSarif
|
|
1075
|
+
? scanRules.toSarif(clean, { version: CLI_VERSION })
|
|
1076
|
+
: { version: "2.1.0", runs: [{ tool: { driver: { name: "foldrun check", version: CLI_VERSION, rules: [] } }, results: [] }], error: "this @foldrun/core has no scanner — update foldrun for SARIF" };
|
|
1077
|
+
} else {
|
|
1078
|
+
const counts = { error: 0, warn: 0, info: 0 };
|
|
1079
|
+
for (const f of clean) counts[f.level]++;
|
|
1080
|
+
body = { version: 1, tool: `foldrun ${CLI_VERSION}`, root, summary, counts, baselined, findings: clean };
|
|
1081
|
+
}
|
|
1082
|
+
const text = JSON.stringify(body, null, 2) + "\n";
|
|
1083
|
+
if (opts.out) fs.writeFileSync(opts.out, text);
|
|
1084
|
+
else process.stdout.write(text);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** `foldrun check --rules`: every rule check knows, with its level and why. */
|
|
1088
|
+
async function checkRulesCmd(flags) {
|
|
1089
|
+
const scan = await scanCore();
|
|
1090
|
+
const rules = [
|
|
1091
|
+
...(scan?.SCAN_RULES ?? []).map((r) => ({ ...r, source: "scanner" })),
|
|
1092
|
+
...(scan?.CHECK_RULES ?? []).map((r) => ({ ...r, source: "check" })),
|
|
1093
|
+
];
|
|
1094
|
+
if (flags.json === true || flags.format === "json") {
|
|
1095
|
+
process.stdout.write(JSON.stringify({ rules }, null, 2) + "\n");
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
console.log("");
|
|
1099
|
+
for (const r of rules) {
|
|
1100
|
+
const lvl = r.source === "check" ? c.dim(" var") : r.level === "warn" ? c.amber(" warn") : r.level === "error" ? c.red("error") : c.dim(" info");
|
|
1101
|
+
console.log(` ${lvl} ${c.bold(r.id.padEnd(28))} ${r.title}${r.owasp?.length ? c.dim(` ${r.owasp.join(" ")}`) : ""}`);
|
|
1102
|
+
}
|
|
1103
|
+
if (!scan) console.log(`\n ${c.amber("!")} this @foldrun/core has no scanner — \`npm i -g foldrun@latest\` adds the secret, security, content and supply-chain rules`);
|
|
1104
|
+
console.log(`\n ${c.dim("var: check's own rules carry the level of each problem. Ignore one with a comment on the line before: foldrun-ignore <rule>: <reason>")}\n`);
|
|
1105
|
+
return 0;
|
|
809
1106
|
}
|
|
810
1107
|
|
|
811
1108
|
/**
|
|
@@ -823,10 +1120,12 @@ async function checkProblems(workspace, flags = {}) {
|
|
|
823
1120
|
const T = "default";
|
|
824
1121
|
const P = "workspace";
|
|
825
1122
|
const problems = [];
|
|
1123
|
+
await scanCore(); // the rule table checkRuleFor reads
|
|
826
1124
|
// file:line, like every other linter — so an editor can jump to it and CI
|
|
827
|
-
// can annotate the right row.
|
|
828
|
-
|
|
829
|
-
|
|
1125
|
+
// can annotate the right row. Every problem carries a rule id, so --rule,
|
|
1126
|
+
// a foldrun-ignore, a baseline and SARIF can all name it.
|
|
1127
|
+
const note = (level, where, message, line, kind) =>
|
|
1128
|
+
problems.push({ level, where: line ? `${where}:${line}` : where, message, rule: checkRuleFor(where, kind) });
|
|
830
1129
|
|
|
831
1130
|
const agents = listAgents(T, P);
|
|
832
1131
|
const flows = listFlows(T, P);
|
|
@@ -1113,7 +1412,7 @@ async function checkProblems(workspace, flags = {}) {
|
|
|
1113
1412
|
}
|
|
1114
1413
|
// A spelling hint (../../storage → workspace/storage) is info, not a warning:
|
|
1115
1414
|
// the old path works, and a check that cries wolf gets ignored.
|
|
1116
|
-
for (const w of lintFlow(f, { agents: [...agentNames], outwardAgents })) note(w.level ?? (w.kind === "spelling" ? "info" : "warn"), `flows/${f.file}`, w.message, w.line);
|
|
1415
|
+
for (const w of lintFlow(f, { agents: [...agentNames], outwardAgents })) note(w.level ?? (w.kind === "spelling" ? "info" : "warn"), `flows/${f.file}`, w.message, w.line, w.kind);
|
|
1117
1416
|
}
|
|
1118
1417
|
|
|
1119
1418
|
for (const e of evals) {
|
|
@@ -1176,7 +1475,10 @@ async function checkProblems(workspace, flags = {}) {
|
|
|
1176
1475
|
}
|
|
1177
1476
|
|
|
1178
1477
|
const summary = `${agentNames.size} agents · ${flows.length} flows · ${evals.length} evals · ${Object.keys(tools).length} tools`;
|
|
1179
|
-
|
|
1478
|
+
// The scanner needs to know which tools act outside the workspace, across
|
|
1479
|
+
// the library too — resolved here already, so it is handed over.
|
|
1480
|
+
const outwardTools = Object.entries(usable).filter(([, d]) => /** @type {any} */ (d)?.outward === true).map(([n]) => n);
|
|
1481
|
+
return { problems, summary, outwardTools };
|
|
1180
1482
|
}
|
|
1181
1483
|
|
|
1182
1484
|
/**
|
|
@@ -6267,11 +6569,14 @@ async function checkRemote(flags, layout) {
|
|
|
6267
6569
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
6268
6570
|
fs.writeFileSync(abs, content);
|
|
6269
6571
|
}
|
|
6270
|
-
|
|
6572
|
+
// JSON and SARIF own stdout; the framing goes to stderr then.
|
|
6573
|
+
const say = checkOptions(flags).format === "text" ? console.log : console.error;
|
|
6574
|
+
say(`\n ${c.bold(ws)} ${c.dim(`the deployed copy on ${url} · ${files.size} file${files.size === 1 ? "" : "s"}`)}`);
|
|
6271
6575
|
process.env.FOLDRUN_WORKSPACE = dir;
|
|
6272
6576
|
process.env.FOLDRUN_ACCOUNT = root;
|
|
6273
|
-
|
|
6274
|
-
|
|
6577
|
+
// The baseline belongs to the folder it was written in, not to a temp copy.
|
|
6578
|
+
const code = await check(dir, { ...flags, "no-baseline": true });
|
|
6579
|
+
say(` ${c.dim(`checked what is deployed, not this folder — ${ws} · ${url}`)}\n`);
|
|
6275
6580
|
return code;
|
|
6276
6581
|
} finally {
|
|
6277
6582
|
if (before.workspace === undefined) delete process.env.FOLDRUN_WORKSPACE;
|
|
@@ -10048,8 +10353,11 @@ export async function run(command, positional, flags, workspace, layout) {
|
|
|
10048
10353
|
// rules: write them, once, and say so — what `next dev` does.
|
|
10049
10354
|
if (layout.kind !== "empty" && codingAgent() && !hasCurrentAgentRules(layout.accountRoot)) {
|
|
10050
10355
|
const r = writeAgentFiles(layout.accountRoot);
|
|
10051
|
-
|
|
10356
|
+
// JSON and SARIF own stdout; a note there would break the parse.
|
|
10357
|
+
const say = flags.json === true || (typeof flags.format === "string" && flags.format !== "text") ? console.error : console.log;
|
|
10358
|
+
say(` ${c.dim(`coding-agent rules written for ${codingAgent()} — AGENTS.md ${r.agentsMd}, CLAUDE.md ${r.claudeMd}; read \`foldrun docs\` before editing`)}`);
|
|
10052
10359
|
}
|
|
10360
|
+
if (flags.rules === true) return checkRulesCmd(flags);
|
|
10053
10361
|
// --to names a workspace on a PLATFORM, so there is nothing local to
|
|
10054
10362
|
// check and the folder this terminal stands in is beside the point.
|
|
10055
10363
|
return typeof flags.to === "string"
|
package/src/guide.mjs
CHANGED
|
@@ -30,7 +30,7 @@ export function agentRulesBlock() {
|
|
|
30
30
|
|
|
31
31
|
# Read foldrun's docs before you edit this folder
|
|
32
32
|
|
|
33
|
-
This is a foldrun account: agents, flows, tools and knowledge as markdown, run by foldrun. Its formats — frontmatter fields, step options, \`verify:\` prefixes, which folder a file goes in — are foldrun's own and may differ from your training data. Before you write or change an agent, flow, tool or skill, read the relevant guide: \`foldrun docs\` lists them and \`foldrun docs <page>\` prints one (start with \`foldrun docs coding-agents\`). They ship with the CLI, so they match the version you are driving. Check every edit with \`foldrun check\` — offline, no model calls. For a flow's shape, \`foldrun flow show <flow>\` draws it, and \`foldrun flow add\`, \`foldrun flow rm-step\`, \`foldrun flow dup-step\` and \`foldrun agent link\` make the same edits as the dashboard canvas (patterns, sub-agents, consults, \`ask\`). A script tool's own \`secrets: [NAMES]\` are granted with the tool; list on an agent only the secrets it uses directly. A hook (\`hooks/<name>/hook.md\` + \`run.mjs\`, turned on with \`hooks: [name]\`) is code at every tool call, step start and step end — \`foldrun hook add\`/\`test\` and \`foldrun docs hooks\`. Paths to the workspace's own folders are written \`workspace/storage/…\` and \`workspace/state/…\` — in an agent's prose, a flow's options and a script's arguments — not \`../../\`, which still works but is the older spelling. Which sites a step's sandbox may reach is \`network:\` — open for an agent that may fetch or browse the web, the trusted list plus what it declares for every other; add a site with \`foldrun agent network <agent> --add <site>\` (\`foldrun docs network\`). Bind a secret to the sites it is for (\`foldrun secrets sites NAME <site>\`) and no sandbox ever holds its value.
|
|
33
|
+
This is a foldrun account: agents, flows, tools and knowledge as markdown, run by foldrun. Its formats — frontmatter fields, step options, \`verify:\` prefixes, which folder a file goes in — are foldrun's own and may differ from your training data. Before you write or change an agent, flow, tool or skill, read the relevant guide: \`foldrun docs\` lists them and \`foldrun docs <page>\` prints one (start with \`foldrun docs coding-agents\`). They ship with the CLI, so they match the version you are driving. Check every edit with \`foldrun check\` — offline, no model calls; it is also the security scanner (a key written into a file, hidden text, an agent that can leak), and \`foldrun check --security\` fails on what it finds. Never paste a credential into a file: \`foldrun secrets set NAME\` and write \`\${NAME}\`. For a flow's shape, \`foldrun flow show <flow>\` draws it, and \`foldrun flow add\`, \`foldrun flow rm-step\`, \`foldrun flow dup-step\` and \`foldrun agent link\` make the same edits as the dashboard canvas (patterns, sub-agents, consults, \`ask\`). A script tool's own \`secrets: [NAMES]\` are granted with the tool; list on an agent only the secrets it uses directly. A hook (\`hooks/<name>/hook.md\` + \`run.mjs\`, turned on with \`hooks: [name]\`) is code at every tool call, step start and step end — \`foldrun hook add\`/\`test\` and \`foldrun docs hooks\`. Paths to the workspace's own folders are written \`workspace/storage/…\` and \`workspace/state/…\` — in an agent's prose, a flow's options and a script's arguments — not \`../../\`, which still works but is the older spelling. Which sites a step's sandbox may reach is \`network:\` — open for an agent that may fetch or browse the web, the trusted list plus what it declares for every other; add a site with \`foldrun agent network <agent> --add <site>\` (\`foldrun docs network\`). Bind a secret to the sites it is for (\`foldrun secrets sites NAME <site>\`) and no sandbox ever holds its value.
|
|
34
34
|
|
|
35
35
|
This block is for coding tools. It is written and re-added by the foldrun CLI, and the runtime strips it, so the agents that run here never see it; the rest of this file is the account's own context, which they do read. Committing the block with your work keeps the tree clean.
|
|
36
36
|
|