@boardwalk-labs/runner 0.1.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.
- package/LICENSE +202 -0
- package/README.md +68 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +214 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +196 -0
- package/dist/daemon/daemon.d.ts +45 -0
- package/dist/daemon/daemon.js +129 -0
- package/dist/daemon/identity.d.ts +15 -0
- package/dist/daemon/identity.js +44 -0
- package/dist/daemon/index.d.ts +3 -0
- package/dist/daemon/index.js +4 -0
- package/dist/daemon/pool_client.d.ts +31 -0
- package/dist/daemon/pool_client.js +101 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -0
- package/dist/runtime/agent/budget.d.ts +123 -0
- package/dist/runtime/agent/budget.js +174 -0
- package/dist/runtime/agent/events.d.ts +46 -0
- package/dist/runtime/agent/events.js +8 -0
- package/dist/runtime/agent/model_rates.d.ts +7 -0
- package/dist/runtime/agent/model_rates.js +21 -0
- package/dist/runtime/agent/secret_redactor.d.ts +42 -0
- package/dist/runtime/agent/secret_redactor.js +98 -0
- package/dist/runtime/broker_artifact_store.d.ts +17 -0
- package/dist/runtime/broker_artifact_store.js +46 -0
- package/dist/runtime/broker_child_dispatcher.d.ts +31 -0
- package/dist/runtime/broker_child_dispatcher.js +88 -0
- package/dist/runtime/broker_event_publisher.d.ts +25 -0
- package/dist/runtime/broker_event_publisher.js +69 -0
- package/dist/runtime/broker_tool_host.d.ts +49 -0
- package/dist/runtime/broker_tool_host.js +240 -0
- package/dist/runtime/cancel_watcher.d.ts +31 -0
- package/dist/runtime/cancel_watcher.js +77 -0
- package/dist/runtime/credit_watcher.d.ts +30 -0
- package/dist/runtime/credit_watcher.js +78 -0
- package/dist/runtime/direct_inference.d.ts +29 -0
- package/dist/runtime/direct_inference.js +78 -0
- package/dist/runtime/index.d.ts +85 -0
- package/dist/runtime/index.js +525 -0
- package/dist/runtime/inference_transport.d.ts +5 -0
- package/dist/runtime/inference_transport.js +9 -0
- package/dist/runtime/leaf_executor.d.ts +104 -0
- package/dist/runtime/leaf_executor.js +333 -0
- package/dist/runtime/lease_renewer.d.ts +31 -0
- package/dist/runtime/lease_renewer.js +80 -0
- package/dist/runtime/main.d.ts +1 -0
- package/dist/runtime/main.js +7 -0
- package/dist/runtime/phase_tracker.d.ts +26 -0
- package/dist/runtime/phase_tracker.js +45 -0
- package/dist/runtime/program_log_capture.d.ts +20 -0
- package/dist/runtime/program_log_capture.js +79 -0
- package/dist/runtime/program_runner.d.ts +82 -0
- package/dist/runtime/program_runner.js +116 -0
- package/dist/runtime/program_worker.d.ts +177 -0
- package/dist/runtime/program_worker.js +272 -0
- package/dist/runtime/recording_secret_resolver.d.ts +9 -0
- package/dist/runtime/recording_secret_resolver.js +22 -0
- package/dist/runtime/run_abort.d.ts +16 -0
- package/dist/runtime/run_abort.js +38 -0
- package/dist/runtime/run_event_emitter.d.ts +24 -0
- package/dist/runtime/run_event_emitter.js +63 -0
- package/dist/runtime/runner_control_client.d.ts +184 -0
- package/dist/runtime/runner_control_client.js +501 -0
- package/dist/runtime/runtime_flusher.d.ts +41 -0
- package/dist/runtime/runtime_flusher.js +90 -0
- package/dist/runtime/sandbox_config.d.ts +21 -0
- package/dist/runtime/sandbox_config.js +34 -0
- package/dist/runtime/support/index.d.ts +53 -0
- package/dist/runtime/support/index.js +114 -0
- package/dist/runtime/suspension.d.ts +141 -0
- package/dist/runtime/suspension.js +120 -0
- package/dist/runtime/testing_artifact_build.d.ts +27 -0
- package/dist/runtime/testing_artifact_build.js +96 -0
- package/dist/runtime/tools/artifacts.d.ts +146 -0
- package/dist/runtime/tools/artifacts.js +125 -0
- package/dist/runtime/tools/types.d.ts +114 -0
- package/dist/runtime/tools/types.js +61 -0
- package/dist/runtime/tools/web_search.d.ts +66 -0
- package/dist/runtime/tools/web_search.js +165 -0
- package/dist/runtime/wire/artifact_storage.d.ts +37 -0
- package/dist/runtime/wire/artifact_storage.js +93 -0
- package/dist/runtime/wire/artifact_verify.d.ts +1 -0
- package/dist/runtime/wire/artifact_verify.js +8 -0
- package/dist/runtime/wire/human_input.d.ts +62 -0
- package/dist/runtime/wire/human_input.js +156 -0
- package/dist/runtime/wire/inference_proxy.d.ts +70 -0
- package/dist/runtime/wire/inference_proxy.js +157 -0
- package/dist/runtime/wire/manifest.d.ts +16 -0
- package/dist/runtime/wire/manifest.js +18 -0
- package/dist/runtime/wire/run.d.ts +54 -0
- package/dist/runtime/wire/run.js +2 -0
- package/dist/runtime/workflow_host.d.ts +194 -0
- package/dist/runtime/workflow_host.js +382 -0
- package/dist/runtime/workspace_store.d.ts +73 -0
- package/dist/runtime/workspace_store.js +118 -0
- package/package.json +70 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Hard ceiling on a persisted `/workspace` snapshot (the gzipped tarball). A run whose snapshot
|
|
2
|
+
* exceeds this SKIPS persisting (logged) rather than uploading — a cost/abuse guardrail on both
|
|
3
|
+
* unbounded S3 growth and the worker's own in-memory read of the tarball (the worker task has 2 GiB;
|
|
4
|
+
* this keeps the read buffer to ~1/4 of it). Generous: 512 MiB COMPRESSED is a large amount of
|
|
5
|
+
* compounding state. The check is on the ON-DISK archive size, BEFORE the read, so an oversized
|
|
6
|
+
* workspace never enters memory. NOT a security boundary (tenant isolation is the per-workflow key);
|
|
7
|
+
* purely a guardrail, like the run budget's `max_usd`. */
|
|
8
|
+
export declare const WORKSPACE_SNAPSHOT_MAX_BYTES: number;
|
|
9
|
+
/** tar+gzip a directory to a file / extract one. Shells out to `tar` in production; injected in tests. */
|
|
10
|
+
export interface WorkspaceArchiver {
|
|
11
|
+
/** Create a gzipped tar of `dir`'s CONTENTS at `destPath`; resolve to the archive's byte size. */
|
|
12
|
+
archive(dir: string, destPath: string): Promise<number>;
|
|
13
|
+
/** Extract a gzipped tar `srcPath` into `dir` (created if absent). */
|
|
14
|
+
extract(srcPath: string, dir: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/** The broker surface the store needs (RunnerControlClient satisfies it). */
|
|
17
|
+
export interface WorkspaceBrokerTransport {
|
|
18
|
+
workspaceHydrateUrl(): Promise<string | null>;
|
|
19
|
+
/** `sizeBytes` is the archive's on-disk size — the worker tars BEFORE presigning so the broker can
|
|
20
|
+
* record the workflow's storage footprint. `null` when the run isn't eligible. */
|
|
21
|
+
workspacePersistUrl(sizeBytes: number): Promise<{
|
|
22
|
+
url: string;
|
|
23
|
+
contentType: string;
|
|
24
|
+
} | null>;
|
|
25
|
+
uploadBytes(url: string, headers: Record<string, string>, body: Uint8Array): Promise<void>;
|
|
26
|
+
downloadBytes(url: string): Promise<Uint8Array | null>;
|
|
27
|
+
}
|
|
28
|
+
/** Minimal fs surface (so the store is unit-tested without touching disk). */
|
|
29
|
+
export interface WorkspaceFs {
|
|
30
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
31
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
32
|
+
rm(path: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface WorkspaceStoreDeps {
|
|
35
|
+
broker: WorkspaceBrokerTransport;
|
|
36
|
+
archiver: WorkspaceArchiver;
|
|
37
|
+
fs: WorkspaceFs;
|
|
38
|
+
/** The `/workspace` root to snapshot/restore. */
|
|
39
|
+
workspaceRoot: string;
|
|
40
|
+
/** Scratch path for the in-flight tarball. */
|
|
41
|
+
tmpPath?: string;
|
|
42
|
+
/** Snapshot tarballs larger than this are skipped (logged). Defaults to {@link WORKSPACE_SNAPSHOT_MAX_BYTES}. */
|
|
43
|
+
maxSnapshotBytes?: number;
|
|
44
|
+
}
|
|
45
|
+
export declare class WorkspaceStore {
|
|
46
|
+
private readonly deps;
|
|
47
|
+
private readonly tmpPath;
|
|
48
|
+
private readonly maxSnapshotBytes;
|
|
49
|
+
constructor(deps: WorkspaceStoreDeps);
|
|
50
|
+
/** Restore the workflow's last `/workspace` snapshot at run start. No-op (logged) on any failure or
|
|
51
|
+
* when there's nothing to restore (not eligible, or a first run with no snapshot yet). */
|
|
52
|
+
hydrate(): Promise<void>;
|
|
53
|
+
/** Snapshot `/workspace` to durable storage. Best-effort; no-op when the run isn't eligible.
|
|
54
|
+
* Returns the snapshot's byte size (0 on a no-op / failure) for the caller's logging.
|
|
55
|
+
*
|
|
56
|
+
* Archives FIRST so the snapshot's exact byte size travels to the broker on the presign request
|
|
57
|
+
* (the broker records it for the org storage counter + daily meter — the snapshot overwrites one
|
|
58
|
+
* per-workflow key, so it IS the workflow's footprint). This store is only constructed when the
|
|
59
|
+
* manifest opts into persistence, so archiving-first never runs for a non-persist workflow; the
|
|
60
|
+
* only redundant archive is a self-hosted+persist run, where the broker returns a null URL. */
|
|
61
|
+
persist(): Promise<number>;
|
|
62
|
+
}
|
|
63
|
+
/** Production archiver — shells out to the runner image's `tar` (the runner has full shell tooling). */
|
|
64
|
+
export declare class TarWorkspaceArchiver implements WorkspaceArchiver {
|
|
65
|
+
archive(dir: string, destPath: string): Promise<number>;
|
|
66
|
+
extract(srcPath: string, dir: string): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
/** Production fs — node's fs/promises. */
|
|
69
|
+
export declare class NodeWorkspaceFs implements WorkspaceFs {
|
|
70
|
+
readFile(path: string): Promise<Uint8Array>;
|
|
71
|
+
writeFile(path: string, data: Uint8Array): Promise<void>;
|
|
72
|
+
rm(path: string): Promise<void>;
|
|
73
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// WorkspaceStore — per-workflow persistent /workspace, broker-mediated (docs/RUNNER_BROKER.md §5).
|
|
2
|
+
//
|
|
3
|
+
// Hosted runs that opt in (manifest `workspace.persist`) get a `/workspace` that survives ACROSS runs
|
|
4
|
+
// of the workflow + across a crash-restart. It is NOT a mounted shared filesystem — that would be
|
|
5
|
+
// readable by the untrusted in-process program across tenants (the program is arbitrary JS with raw
|
|
6
|
+
// `fs` access). Instead the worker tars `/workspace` and pushes/pulls it through BROKER-scoped S3
|
|
7
|
+
// URLs keyed PER WORKFLOW (the broker derives the key from the run token), so even raw `fs` can't
|
|
8
|
+
// reach another tenant's snapshot. The broker also gates eligibility server-side (hosted + opt-in)
|
|
9
|
+
// and returns null URLs otherwise — so this store no-ops cleanly when not eligible.
|
|
10
|
+
//
|
|
11
|
+
// Best-effort by design: hydrate/persist failures are logged, never thrown — a snapshot miss must not
|
|
12
|
+
// fail the run (worst case the workflow re-does filesystem work, as it would without persistence).
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { promisify } from "node:util";
|
|
15
|
+
import { stat, mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
16
|
+
import { createLogger } from "./support/index.js";
|
|
17
|
+
const log = createLogger("WorkspaceStore");
|
|
18
|
+
const exec = promisify(execFile);
|
|
19
|
+
/** Hard ceiling on a persisted `/workspace` snapshot (the gzipped tarball). A run whose snapshot
|
|
20
|
+
* exceeds this SKIPS persisting (logged) rather than uploading — a cost/abuse guardrail on both
|
|
21
|
+
* unbounded S3 growth and the worker's own in-memory read of the tarball (the worker task has 2 GiB;
|
|
22
|
+
* this keeps the read buffer to ~1/4 of it). Generous: 512 MiB COMPRESSED is a large amount of
|
|
23
|
+
* compounding state. The check is on the ON-DISK archive size, BEFORE the read, so an oversized
|
|
24
|
+
* workspace never enters memory. NOT a security boundary (tenant isolation is the per-workflow key);
|
|
25
|
+
* purely a guardrail, like the run budget's `max_usd`. */
|
|
26
|
+
export const WORKSPACE_SNAPSHOT_MAX_BYTES = 512 * 1024 * 1024;
|
|
27
|
+
export class WorkspaceStore {
|
|
28
|
+
deps;
|
|
29
|
+
tmpPath;
|
|
30
|
+
maxSnapshotBytes;
|
|
31
|
+
constructor(deps) {
|
|
32
|
+
this.deps = deps;
|
|
33
|
+
this.tmpPath = deps.tmpPath ?? "/tmp/workspace-snapshot.tgz";
|
|
34
|
+
this.maxSnapshotBytes = deps.maxSnapshotBytes ?? WORKSPACE_SNAPSHOT_MAX_BYTES;
|
|
35
|
+
}
|
|
36
|
+
/** Restore the workflow's last `/workspace` snapshot at run start. No-op (logged) on any failure or
|
|
37
|
+
* when there's nothing to restore (not eligible, or a first run with no snapshot yet). */
|
|
38
|
+
async hydrate() {
|
|
39
|
+
try {
|
|
40
|
+
const url = await this.deps.broker.workspaceHydrateUrl();
|
|
41
|
+
if (url === null)
|
|
42
|
+
return; // not eligible (not opted-in / self-hosted)
|
|
43
|
+
const bytes = await this.deps.broker.downloadBytes(url);
|
|
44
|
+
if (bytes === null)
|
|
45
|
+
return; // 404 — no snapshot yet (the workflow's first run)
|
|
46
|
+
await this.deps.fs.writeFile(this.tmpPath, bytes);
|
|
47
|
+
await this.deps.archiver.extract(this.tmpPath, this.deps.workspaceRoot);
|
|
48
|
+
await this.deps.fs.rm(this.tmpPath);
|
|
49
|
+
log.info("workspace_hydrated", { bytes: bytes.length });
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
log.warn("workspace_hydrate_failed", { error: errMsg(err) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Snapshot `/workspace` to durable storage. Best-effort; no-op when the run isn't eligible.
|
|
56
|
+
* Returns the snapshot's byte size (0 on a no-op / failure) for the caller's logging.
|
|
57
|
+
*
|
|
58
|
+
* Archives FIRST so the snapshot's exact byte size travels to the broker on the presign request
|
|
59
|
+
* (the broker records it for the org storage counter + daily meter — the snapshot overwrites one
|
|
60
|
+
* per-workflow key, so it IS the workflow's footprint). This store is only constructed when the
|
|
61
|
+
* manifest opts into persistence, so archiving-first never runs for a non-persist workflow; the
|
|
62
|
+
* only redundant archive is a self-hosted+persist run, where the broker returns a null URL. */
|
|
63
|
+
async persist() {
|
|
64
|
+
try {
|
|
65
|
+
const size = await this.deps.archiver.archive(this.deps.workspaceRoot, this.tmpPath);
|
|
66
|
+
// Guardrail: an oversized snapshot is dropped (logged), never read into memory or uploaded — the
|
|
67
|
+
// workflow re-does filesystem work next run, as it would without persistence. Checked on the
|
|
68
|
+
// on-disk archive size so the big tarball never hits the worker's heap.
|
|
69
|
+
if (size > this.maxSnapshotBytes) {
|
|
70
|
+
await this.deps.fs.rm(this.tmpPath);
|
|
71
|
+
log.warn("workspace_persist_too_large", { bytes: size, maxBytes: this.maxSnapshotBytes });
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
const presign = await this.deps.broker.workspacePersistUrl(size);
|
|
75
|
+
if (presign === null) {
|
|
76
|
+
// Not eligible (e.g. self-hosted) — discard the archive we speculatively built.
|
|
77
|
+
await this.deps.fs.rm(this.tmpPath);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const bytes = await this.deps.fs.readFile(this.tmpPath);
|
|
81
|
+
await this.deps.broker.uploadBytes(presign.url, { "content-type": presign.contentType }, bytes);
|
|
82
|
+
await this.deps.fs.rm(this.tmpPath);
|
|
83
|
+
log.info("workspace_persisted", { bytes: size });
|
|
84
|
+
return size;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
log.warn("workspace_persist_failed", { error: errMsg(err) });
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Production archiver — shells out to the runner image's `tar` (the runner has full shell tooling). */
|
|
93
|
+
export class TarWorkspaceArchiver {
|
|
94
|
+
async archive(dir, destPath) {
|
|
95
|
+
// `-C dir .` archives the CONTENTS of dir (not the dir itself), so extract restores it in place.
|
|
96
|
+
await exec("tar", ["czf", destPath, "-C", dir, "."]);
|
|
97
|
+
return (await stat(destPath)).size;
|
|
98
|
+
}
|
|
99
|
+
async extract(srcPath, dir) {
|
|
100
|
+
await mkdir(dir, { recursive: true });
|
|
101
|
+
await exec("tar", ["xzf", srcPath, "-C", dir]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Production fs — node's fs/promises. */
|
|
105
|
+
export class NodeWorkspaceFs {
|
|
106
|
+
readFile(path) {
|
|
107
|
+
return readFile(path);
|
|
108
|
+
}
|
|
109
|
+
writeFile(path, data) {
|
|
110
|
+
return writeFile(path, data);
|
|
111
|
+
}
|
|
112
|
+
rm(path) {
|
|
113
|
+
return rm(path, { force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function errMsg(err) {
|
|
117
|
+
return err instanceof Error ? err.message : String(err);
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boardwalk-labs/runner",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Boardwalk self-hosted runner: execute cloud-scheduled runs on your own machines. Currently: the canonical runner contract types.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/boardwalk-labs/runner.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/boardwalk-labs/runner#readme",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=24"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./contract": {
|
|
21
|
+
"types": "./dist/contract.d.ts",
|
|
22
|
+
"default": "./dist/contract.js"
|
|
23
|
+
},
|
|
24
|
+
"./runtime": {
|
|
25
|
+
"types": "./dist/runtime/index.d.ts",
|
|
26
|
+
"default": "./dist/runtime/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./runtime/main": {
|
|
29
|
+
"types": "./dist/runtime/main.d.ts",
|
|
30
|
+
"default": "./dist/runtime/main.js"
|
|
31
|
+
},
|
|
32
|
+
"./daemon": {
|
|
33
|
+
"types": "./dist/daemon/index.d.ts",
|
|
34
|
+
"default": "./dist/daemon/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.build.json",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"lint": "eslint . --max-warnings 0",
|
|
46
|
+
"format": "prettier --write .",
|
|
47
|
+
"format:check": "prettier --check .",
|
|
48
|
+
"test": "vitest run --coverage",
|
|
49
|
+
"test:watch": "vitest"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"zod": "^4.0.0",
|
|
53
|
+
"@boardwalk-labs/engine": "^0.1.29",
|
|
54
|
+
"@boardwalk-labs/workflow": "^0.1.17",
|
|
55
|
+
"tar": "^7.5.16"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@eslint/js": "^9.18.0",
|
|
59
|
+
"@types/node": "^24.0.0",
|
|
60
|
+
"@vitest/coverage-v8": "^3.2.6",
|
|
61
|
+
"eslint": "^9.18.0",
|
|
62
|
+
"prettier": "^3.4.0",
|
|
63
|
+
"typescript": "^5.6.0",
|
|
64
|
+
"typescript-eslint": "^8.20.0",
|
|
65
|
+
"vitest": "^3.0.0"
|
|
66
|
+
},
|
|
67
|
+
"bin": {
|
|
68
|
+
"boardwalk-runner": "./dist/bin.js"
|
|
69
|
+
}
|
|
70
|
+
}
|