@foldrun/core 8.0.0 → 9.0.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/dist/src/history.d.ts +5 -0
- package/dist/src/history.js +31 -4
- package/dist/src/hooks.d.ts +2 -0
- package/dist/src/hooks.js +13 -0
- package/dist/src/library.js +4 -0
- package/dist/src/notify.d.ts +15 -1
- package/dist/src/notify.js +81 -11
- package/dist/src/runner.js +12 -7
- package/dist/src/runtime.js +0 -1
- package/dist/src/secrets.js +8 -2
- package/dist/src/storage.d.ts +76 -4
- package/dist/src/storage.js +219 -22
- package/dist/src/store.d.ts +20 -2
- package/dist/src/store.js +50 -15
- package/dist/src/webhook.js +5 -1
- package/package.json +1 -1
- package/src/deploy.ts +0 -1
- package/src/history.ts +31 -4
- package/src/hooks.ts +14 -0
- package/src/library.ts +3 -0
- package/src/notify.ts +82 -12
- package/src/runner.ts +12 -9
- package/src/runtime.ts +0 -1
- package/src/secrets.ts +7 -2
- package/src/storage.ts +223 -19
- package/src/store.ts +49 -14
- package/src/webhook.ts +4 -1
package/src/store.ts
CHANGED
|
@@ -26,14 +26,14 @@ import { EFFORT_LEVELS, FLOW_TRIGGERS } from "./frontmatter-fields.ts";
|
|
|
26
26
|
import type { BrowserPodOutcome } from "./browser-pod.ts";
|
|
27
27
|
import fs from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
|
-
import { recordRevision, registerTreeReader, type RevisionFile } from "./history.ts";
|
|
29
|
+
import { recordRevision, registerTreeReader, assertCommitId, type RevisionFile } from "./history.ts";
|
|
30
30
|
import { dataRoot, singleWorkspace, singleAccountRoot, entriesNoFollow } from "./paths.ts";
|
|
31
31
|
import { platform } from "./platform.ts";
|
|
32
32
|
import { toLF, asRaw } from "./eol.ts";
|
|
33
33
|
import type { TestEffect } from "./test-mode.ts";
|
|
34
34
|
import type { OperatorEvent } from "./operator.ts";
|
|
35
35
|
import matter, { frontmatterErrorText, configMatter } from "./matter.ts";
|
|
36
|
-
import { readAgentsMdKeys } from "./agents-md.ts";
|
|
36
|
+
import { readAgentsMdKeys, patchFrontmatter } from "./agents-md.ts";
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* The frontmatter of one markdown file, as the runtime reads it. Exported
|
|
@@ -184,6 +184,19 @@ export function assertSafeName(name: string, kind: string) {
|
|
|
184
184
|
if (!SAFE_NAME.test(name)) throw new Error(`invalid ${kind} "${name}" — kebab-case only`);
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* A name that is ONE folder under its parent — agents/<name>, never a path.
|
|
189
|
+
* Wider than SAFE_NAME on purpose: an agent imported from
|
|
190
|
+
* `.claude/agents/code_reviewer.md` keeps that file's name, and a run of it
|
|
191
|
+
* must keep working. What it refuses is what makes a name a path: a
|
|
192
|
+
* separator, a dot segment, a NUL. 30 Oct 2026: POST …/agents/<agent>/run
|
|
193
|
+
* and the runner joined the name unchecked, so `..%2F..%2F<tenant>%2F…`
|
|
194
|
+
* reached another account's agent.
|
|
195
|
+
*/
|
|
196
|
+
export function isOneSegment(name: unknown): name is string {
|
|
197
|
+
return typeof name === "string" && name !== "" && name !== "." && name !== ".." && !/[/\\\0]/.test(name);
|
|
198
|
+
}
|
|
199
|
+
|
|
187
200
|
export function workspaceDir(tenant: string, workspace: string) {
|
|
188
201
|
const single = singleWorkspace();
|
|
189
202
|
if (single) return path.resolve(single);
|
|
@@ -382,6 +395,9 @@ export function saveWorkspace(
|
|
|
382
395
|
} = {},
|
|
383
396
|
) {
|
|
384
397
|
if (files.length > 500) throw new Error("too many files");
|
|
398
|
+
// The commit becomes the revision's id: refused here, before the tree is
|
|
399
|
+
// replaced, never after it (history.ts assertCommitId).
|
|
400
|
+
if (meta.commit) assertCommitId(meta.commit);
|
|
385
401
|
for (const f of files) {
|
|
386
402
|
const norm = path.normalize(f.path);
|
|
387
403
|
if (norm.startsWith("..") || path.isAbsolute(norm) || norm.startsWith("runs")) {
|
|
@@ -2856,6 +2872,13 @@ export function writeWorkspaceFile(
|
|
|
2856
2872
|
if (content.length > MAX_EDITABLE_FILE) throw new Error("file too large");
|
|
2857
2873
|
matter(content); // reject files whose frontmatter doesn't parse
|
|
2858
2874
|
const dir = workspaceDir(tenant, workspace);
|
|
2875
|
+
// A write goes INTO a workspace; it never makes one. The mkdir below is
|
|
2876
|
+
// recursive, so until 30 Oct 2026 a write to a name that did not exist
|
|
2877
|
+
// created that workspace — with workspace:write alone, past the
|
|
2878
|
+
// workspace:manage role and the plan's workspace count that POST
|
|
2879
|
+
// /api/workspaces, deploy and import apply. Those create through
|
|
2880
|
+
// saveWorkspace / applyImport, not through here.
|
|
2881
|
+
if (!fs.existsSync(dir)) throw Object.assign(new Error(`workspace ${workspace} not found`), { status: 404 });
|
|
2859
2882
|
const p = path.join(dir, assertEditablePath(rel));
|
|
2860
2883
|
// Before anything is created: mkdir below would otherwise make folders
|
|
2861
2884
|
// through a linked directory too.
|
|
@@ -3069,6 +3092,15 @@ export function deleteWorkspace(tenant: string, workspace: string) {
|
|
|
3069
3092
|
const dir = workspaceDir(tenant, workspace);
|
|
3070
3093
|
if (!fs.existsSync(dir)) throw new Error(`workspace ${workspace} not found`);
|
|
3071
3094
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
3095
|
+
// Its file store too, kept by name beside the folder (storage.ts). Until
|
|
3096
|
+
// 30 Oct 2026 the store outlived the delete: its files stayed downloadable
|
|
3097
|
+
// and shared, and the next workspace given this name inherited them. The
|
|
3098
|
+
// index going is what makes them unreachable; the API route first calls
|
|
3099
|
+
// purgeWorkspaceStorage, which also removes the objects from a bucket
|
|
3100
|
+
// (async, so it cannot live in here).
|
|
3101
|
+
if (!singleWorkspace()) {
|
|
3102
|
+
fs.rmSync(path.join(dataRoot(), tenant, STORAGE_DIR, workspace), { recursive: true, force: true });
|
|
3103
|
+
}
|
|
3072
3104
|
workspaceChanged(tenant, workspace, "delete");
|
|
3073
3105
|
}
|
|
3074
3106
|
|
|
@@ -3120,23 +3152,26 @@ export function renameWorkspace(tenant: string, from: string, to: string) {
|
|
|
3120
3152
|
}
|
|
3121
3153
|
}
|
|
3122
3154
|
|
|
3123
|
-
/** Set a workspace's description, in AGENTS.md, without touching anything else.
|
|
3124
|
-
|
|
3155
|
+
/** Set a workspace's description, in AGENTS.md, without touching anything else.
|
|
3156
|
+
*
|
|
3157
|
+
* 30 Oct 2026: the value was spliced in as `description: ${text}`, so a
|
|
3158
|
+
* newline in it wrote any other key (notify:, network_enforce:, past the
|
|
3159
|
+
* settings' allowed list), and String.replace read `$'` / `$&` in it as
|
|
3160
|
+
* patterns; the write went around writeWorkspaceFile, so History never saw
|
|
3161
|
+
* it. Now the key is written as YAML (patchFrontmatter, which reads the
|
|
3162
|
+
* result back and refuses an edit that changed any other key) and saved
|
|
3163
|
+
* through writeWorkspaceFile, as a revision. */
|
|
3164
|
+
export function setWorkspaceDescription(tenant: string, workspace: string, description: string, meta: ChangeMeta = {}) {
|
|
3125
3165
|
const file = path.join(workspaceDir(tenant, workspace), "AGENTS.md");
|
|
3126
3166
|
if (!fs.existsSync(file)) throw new Error("this workspace has no AGENTS.md");
|
|
3127
3167
|
|
|
3128
3168
|
const raw = fs.readFileSync(file, "utf8");
|
|
3129
|
-
|
|
3130
|
-
if (!block) throw new Error("AGENTS.md has no frontmatter");
|
|
3169
|
+
if (!/^---\r?\n[\s\S]*?\r?\n---/.test(raw)) throw new Error("AGENTS.md has no frontmatter");
|
|
3131
3170
|
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
if (at === -1) lines.push(line);
|
|
3137
|
-
else lines[at] = line;
|
|
3138
|
-
|
|
3139
|
-
fs.writeFileSync(file, raw.replace(open + body + close, open + lines.join("\n") + close));
|
|
3171
|
+
writeWorkspaceFile(tenant, workspace, "AGENTS.md", patchFrontmatter(raw, { description }), {
|
|
3172
|
+
message: "settings: description",
|
|
3173
|
+
...meta,
|
|
3174
|
+
});
|
|
3140
3175
|
}
|
|
3141
3176
|
|
|
3142
3177
|
/**
|
package/src/webhook.ts
CHANGED
|
@@ -57,7 +57,10 @@ export function rotateWebhook(tenant: string, workspace: string, flow: string):
|
|
|
57
57
|
const hooks = readHooks(tenant, workspace);
|
|
58
58
|
hooks[flow] = { gen: (hooks[flow]?.gen ?? 0) + 1, rotatedAt: new Date().toISOString() };
|
|
59
59
|
const file = hooksFile(tenant, workspace);
|
|
60
|
-
|
|
60
|
+
// Into a workspace that exists, never making one (30 Oct 2026): the mkdir
|
|
61
|
+
// here turned a rotate under a mistyped name into a workspace holding only
|
|
62
|
+
// hooks.json, past the plan's workspace count.
|
|
63
|
+
if (!fs.existsSync(path.dirname(file))) throw Object.assign(new Error(`workspace ${workspace} not found`), { status: 404 });
|
|
61
64
|
fs.writeFileSync(file, JSON.stringify(hooks, null, 2));
|
|
62
65
|
return webhookPath(tenant, workspace, flow);
|
|
63
66
|
}
|