@boardwalk-labs/runner 0.2.0 → 0.2.2

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.
@@ -27,6 +27,24 @@ const exec = promisify(execFile);
27
27
  * workspace never enters memory. NOT a security boundary (tenant isolation is the per-workflow key);
28
28
  * purely a guardrail, like the run budget's `max_usd`. */
29
29
  export const WORKSPACE_SNAPSHOT_MAX_BYTES = 512 * 1024 * 1024;
30
+ /**
31
+ * Resolve what this run persists (docs/WORKSPACE_PERSISTENCE.md §3): the manifest's declaration
32
+ * UNIONED with every `agent({ memory })` dir the run actually used.
33
+ *
34
+ * The union is why this is resolved at PERSIST time, not construction time: memory dirs are
35
+ * undeclared by design (`sdk/src/types.ts` — "`mcp` servers, `skills`, and `memory` — the manifest
36
+ * declares none of them"), so they are only known once the run has made the agent calls. A workflow
37
+ * whose manifest says nothing at all still persists, iff it used memory.
38
+ *
39
+ * `true` swallows the list: the whole workspace already contains every memory dir. An empty array
40
+ * means persist nothing, which is the common case and must stay cheap.
41
+ */
42
+ export function resolvePersistSelection(declared, memoryDirs) {
43
+ if (declared === true)
44
+ return true;
45
+ const list = declared === undefined || declared === false ? [] : declared;
46
+ return [...new Set([...list, ...memoryDirs])];
47
+ }
30
48
  export class WorkspaceStore {
31
49
  deps;
32
50
  tmpPath;
@@ -71,8 +89,15 @@ export class WorkspaceStore {
71
89
  * only redundant archive is a self-hosted+persist run, where the broker returns a null URL. */
72
90
  async persist() {
73
91
  try {
92
+ // What this run actually compounds: the manifest's declaration ∪ the memory dirs it used.
93
+ // Nothing selected is the common case (a workflow that opted into neither) — return before any
94
+ // fs or broker work, so persistence costs a run that doesn't use it precisely nothing.
95
+ const selection = this.deps.selection();
96
+ const paths = selection === true ? undefined : await this.presentPaths(selection);
97
+ if (paths !== undefined && paths.length === 0)
98
+ return 0;
74
99
  await mkdir(dirname(this.tmpPath), { recursive: true }); // per-run TMPDIR may not exist yet
75
- const size = await this.deps.archiver.archive(this.deps.workspaceRoot, this.tmpPath);
100
+ const size = await this.deps.archiver.archive(this.deps.workspaceRoot, this.tmpPath, paths);
76
101
  // Guardrail: an oversized snapshot is dropped (logged), never read into memory or uploaded — the
77
102
  // workflow re-does filesystem work next run, as it would without persistence. Checked on the
78
103
  // on-disk archive size so the big tarball never hits the worker's heap.
@@ -98,12 +123,23 @@ export class WorkspaceStore {
98
123
  return 0;
99
124
  }
100
125
  }
126
+ /** Narrow a selection to the dirs that exist: `tar` fails the whole archive on one missing member,
127
+ * and declaring a dir the run hasn't written yet is ordinary (first run of `persist: ["cache"]`). */
128
+ async presentPaths(selection) {
129
+ const checked = await Promise.all(selection.map(async (p) => (await this.deps.fs.exists(join(this.deps.workspaceRoot, p))) ? p : null));
130
+ return checked.filter((p) => p !== null);
131
+ }
101
132
  }
102
133
  /** Production archiver — shells out to the runner image's `tar` (the runner has full shell tooling). */
103
134
  export class TarWorkspaceArchiver {
104
- async archive(dir, destPath) {
105
- // `-C dir .` archives the CONTENTS of dir (not the dir itself), so extract restores it in place.
106
- await exec("tar", ["czf", destPath, "-C", dir, "."]);
135
+ async archive(dir, destPath, paths) {
136
+ // `-C dir <members>` archives relative to dir, so extract restores in place either way: `.` for
137
+ // the whole tree (`persist: true`), or exactly the named dirs (`persist: [...]` ∪ memory dirs).
138
+ // Members are workspace-relative and schema-validated (no `..`, no absolute, no backslashes —
139
+ // sdk/src/manifest.ts `persistPath`) and pre-filtered to those that exist; `--` keeps a name
140
+ // that starts with `-` from being read as a flag.
141
+ const members = paths === undefined ? ["."] : [...paths];
142
+ await exec("tar", ["czf", destPath, "-C", dir, "--", ...members]);
107
143
  return (await stat(destPath)).size;
108
144
  }
109
145
  async extract(srcPath, dir) {
@@ -128,6 +164,9 @@ export class NodeWorkspaceFs {
128
164
  rm(path) {
129
165
  return rm(path, { force: true });
130
166
  }
167
+ async exists(path) {
168
+ return (await stat(path).catch(() => null)) !== null;
169
+ }
131
170
  }
132
171
  function errMsg(err) {
133
172
  return err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boardwalk-labs/runner",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Boardwalk self-hosted runner: execute cloud-scheduled runs on your own machines. Currently: the canonical runner contract types.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -53,8 +53,8 @@
53
53
  "prebuild": "prebuildify --napi --strip -t 24.0.0"
54
54
  },
55
55
  "dependencies": {
56
- "@boardwalk-labs/engine": "^0.2.0",
57
- "@boardwalk-labs/workflow": "^0.2.0",
56
+ "@boardwalk-labs/engine": "0.2.2",
57
+ "@boardwalk-labs/workflow": "0.2.1",
58
58
  "@modelcontextprotocol/sdk": "^1.29.0",
59
59
  "node-gyp-build": "^4.8.4",
60
60
  "tar": "^7.5.16",