lastlight 0.3.0 → 0.5.1

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.
Files changed (87) hide show
  1. package/README.md +17 -6
  2. package/deploy/entrypoint.sh +21 -6
  3. package/deploy/sandbox-entrypoint.sh +1 -1
  4. package/dist/admin/routes.d.ts +5 -0
  5. package/dist/admin/routes.js +9 -0
  6. package/dist/admin/routes.js.map +1 -1
  7. package/dist/cli-server.d.ts +3 -1
  8. package/dist/cli-server.js +20 -2
  9. package/dist/cli-server.js.map +1 -1
  10. package/dist/cli.js +23 -2
  11. package/dist/cli.js.map +1 -1
  12. package/dist/config.d.ts +12 -1
  13. package/dist/config.js +26 -6
  14. package/dist/config.js.map +1 -1
  15. package/dist/connectors/messaging/base.js +22 -9
  16. package/dist/connectors/messaging/base.js.map +1 -1
  17. package/dist/connectors/slack/connector.d.ts +62 -9
  18. package/dist/connectors/slack/connector.js +216 -63
  19. package/dist/connectors/slack/connector.js.map +1 -1
  20. package/dist/connectors/slack/connector.test.d.ts +1 -0
  21. package/dist/connectors/slack/connector.test.js +135 -0
  22. package/dist/connectors/slack/connector.test.js.map +1 -0
  23. package/dist/connectors/types.d.ts +7 -0
  24. package/dist/engine/agent-executor.d.ts +32 -10
  25. package/dist/engine/agent-executor.js +239 -1
  26. package/dist/engine/agent-executor.js.map +1 -1
  27. package/dist/engine/agent-executor.seam.test.d.ts +1 -0
  28. package/dist/engine/agent-executor.seam.test.js +57 -0
  29. package/dist/engine/agent-executor.seam.test.js.map +1 -0
  30. package/dist/engine/dispatcher.d.ts +13 -0
  31. package/dist/engine/dispatcher.js +26 -7
  32. package/dist/engine/dispatcher.js.map +1 -1
  33. package/dist/engine/dispatcher.test.js.map +1 -1
  34. package/dist/engine/message-batcher.d.ts +32 -0
  35. package/dist/engine/message-batcher.js +109 -0
  36. package/dist/engine/message-batcher.js.map +1 -0
  37. package/dist/engine/message-batcher.test.d.ts +1 -0
  38. package/dist/engine/message-batcher.test.js +109 -0
  39. package/dist/engine/message-batcher.test.js.map +1 -0
  40. package/dist/engine/profiles.d.ts +9 -0
  41. package/dist/engine/profiles.js.map +1 -1
  42. package/dist/evals-api.d.ts +24 -0
  43. package/dist/evals-api.js +22 -0
  44. package/dist/evals-api.js.map +1 -0
  45. package/dist/fork-cli.d.ts +30 -0
  46. package/dist/fork-cli.js +240 -0
  47. package/dist/fork-cli.js.map +1 -0
  48. package/dist/fork-cli.test.d.ts +1 -0
  49. package/dist/fork-cli.test.js +109 -0
  50. package/dist/fork-cli.test.js.map +1 -0
  51. package/dist/index.js +43 -4
  52. package/dist/index.js.map +1 -1
  53. package/dist/overlay-assets.d.ts +20 -0
  54. package/dist/overlay-assets.js +114 -0
  55. package/dist/overlay-assets.js.map +1 -0
  56. package/dist/overlay-assets.test.d.ts +1 -0
  57. package/dist/overlay-assets.test.js +75 -0
  58. package/dist/overlay-assets.test.js.map +1 -0
  59. package/dist/sandbox/command-exec.integration.test.d.ts +1 -0
  60. package/dist/sandbox/command-exec.integration.test.js +183 -0
  61. package/dist/sandbox/command-exec.integration.test.js.map +1 -0
  62. package/dist/sandbox/docker.d.ts +32 -0
  63. package/dist/sandbox/docker.js +109 -4
  64. package/dist/sandbox/docker.js.map +1 -1
  65. package/dist/setup.js +9 -44
  66. package/dist/setup.js.map +1 -1
  67. package/dist/workflows/loader.test.js +83 -0
  68. package/dist/workflows/loader.test.js.map +1 -1
  69. package/dist/workflows/phase-executor.d.ts +28 -3
  70. package/dist/workflows/phase-executor.js +170 -45
  71. package/dist/workflows/phase-executor.js.map +1 -1
  72. package/dist/workflows/phase-executor.test.js +64 -1
  73. package/dist/workflows/phase-executor.test.js.map +1 -1
  74. package/dist/workflows/runner.test.js +32 -27
  75. package/dist/workflows/runner.test.js.map +1 -1
  76. package/dist/workflows/schema.d.ts +20 -0
  77. package/dist/workflows/schema.js +72 -2
  78. package/dist/workflows/schema.js.map +1 -1
  79. package/package.json +12 -2
  80. package/sandbox.Dockerfile +7 -0
  81. package/skills/browser-qa/scripts/agent-browser.mjs +15 -0
  82. package/skills/demo/SKILL.md +45 -19
  83. package/skills/demo/scripts/compose-demo.sh +77 -39
  84. package/workflows/examples/bash-smoke.yaml +41 -0
  85. package/workflows/prompts/demo.md +27 -5
  86. package/workflows/qa-test.yaml +1 -1
  87. package/workflows/verify.yaml +1 -1
@@ -0,0 +1,20 @@
1
+ export type OverlayAssetType = "workflow" | "cron" | "prompt" | "skill" | "agent-context";
2
+ export interface OverlayAsset {
3
+ type: OverlayAssetType;
4
+ /** Logical name — workflow/cron/skill name, or the filename for prompts/context. */
5
+ name: string;
6
+ /** True when a same-named built-in exists under `coreRoot` (overlay shadows it);
7
+ * false when the overlay adds something the built-ins don't have. */
8
+ shadowsDefault: boolean;
9
+ }
10
+ export interface EnumerateOpts {
11
+ /** Built-in asset tree (the lastlight checkout root). */
12
+ coreRoot?: string;
13
+ /** Overlay asset tree (the instance/ overlay root). */
14
+ overlayRoot?: string;
15
+ }
16
+ /**
17
+ * List every asset the overlay provides, tagging each as shadowing a built-in or
18
+ * a fresh addition. Returns `[]` when no overlay is configured or present.
19
+ */
20
+ export declare function enumerateOverlayAssets(opts: EnumerateOpts): OverlayAsset[];
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Enumerate the assets a deployment overlay overrides or adds.
3
+ *
4
+ * The instance overlay (`$LASTLIGHT_OVERLAY_DIR`, prod `instance/`) can replace
5
+ * or add workflows, prompts, skills, and agent-context files by logical name —
6
+ * the layer-aware resolution lives in `src/workflows/loader.ts`. This module is
7
+ * the read-only inverse: given the built-in (`coreRoot`) and overlay
8
+ * (`overlayRoot`) trees, list every asset the overlay provides and whether it
9
+ * **shadows** a same-named built-in or is a fresh **addition**.
10
+ *
11
+ * It's a pure filesystem walk (no loader caches), so the host-local CLI
12
+ * (`lastlight server status`, `lastlight fork`) and the in-harness admin API
13
+ * (`GET /admin/api/overrides`) can share one source of truth.
14
+ */
15
+ import { readFileSync, readdirSync, statSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { parse as parseYaml } from "yaml";
18
+ function isDir(path) {
19
+ try {
20
+ return statSync(path).isDirectory();
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ function listFiles(dir, predicate) {
27
+ if (!isDir(dir))
28
+ return [];
29
+ return readdirSync(dir).filter(predicate).sort();
30
+ }
31
+ function listDirs(dir) {
32
+ if (!isDir(dir))
33
+ return [];
34
+ return readdirSync(dir)
35
+ .filter((n) => isDir(join(dir, n)))
36
+ .sort();
37
+ }
38
+ const isYaml = (n) => n.endsWith(".yaml") || n.endsWith(".yml");
39
+ const isMarkdown = (n) => n.endsWith(".md");
40
+ /** Read a workflow YAML's `name`/`kind` without throwing on malformed files. */
41
+ function readWorkflowMeta(filePath) {
42
+ try {
43
+ const raw = parseYaml(readFileSync(filePath, "utf-8"));
44
+ if (!raw || typeof raw.name !== "string")
45
+ return null;
46
+ return { name: raw.name, kind: typeof raw.kind === "string" ? raw.kind : "" };
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Index built-in assets by logical name so overlay entries can be tagged as
54
+ * shadowing vs adding. Workflows/crons are keyed by their YAML `name`; prompts,
55
+ * skills, and agent-context by filename / directory name.
56
+ */
57
+ function indexBuiltins(coreRoot) {
58
+ const workflows = new Set();
59
+ const crons = new Set();
60
+ const workflowDir = join(coreRoot, "workflows");
61
+ for (const file of listFiles(workflowDir, isYaml)) {
62
+ const meta = readWorkflowMeta(join(workflowDir, file));
63
+ if (!meta)
64
+ continue;
65
+ (meta.kind === "cron" ? crons : workflows).add(meta.name);
66
+ }
67
+ return {
68
+ workflows,
69
+ crons,
70
+ prompts: new Set(listFiles(join(coreRoot, "workflows", "prompts"), isMarkdown)),
71
+ skills: new Set(listDirs(join(coreRoot, "skills"))),
72
+ agentContext: new Set(listFiles(join(coreRoot, "agent-context"), isMarkdown)),
73
+ };
74
+ }
75
+ /**
76
+ * List every asset the overlay provides, tagging each as shadowing a built-in or
77
+ * a fresh addition. Returns `[]` when no overlay is configured or present.
78
+ */
79
+ export function enumerateOverlayAssets(opts) {
80
+ const { coreRoot, overlayRoot } = opts;
81
+ if (!overlayRoot || !isDir(overlayRoot))
82
+ return [];
83
+ const builtins = coreRoot && isDir(coreRoot)
84
+ ? indexBuiltins(coreRoot)
85
+ : { workflows: new Set(), crons: new Set(), prompts: new Set(), skills: new Set(), agentContext: new Set() };
86
+ const assets = [];
87
+ // Workflows + crons — keyed by YAML `name`, split by `kind`.
88
+ const overlayWorkflowDir = join(overlayRoot, "workflows");
89
+ for (const file of listFiles(overlayWorkflowDir, isYaml)) {
90
+ const meta = readWorkflowMeta(join(overlayWorkflowDir, file));
91
+ if (!meta)
92
+ continue;
93
+ const isCron = meta.kind === "cron";
94
+ assets.push({
95
+ type: isCron ? "cron" : "workflow",
96
+ name: meta.name,
97
+ shadowsDefault: (isCron ? builtins.crons : builtins.workflows).has(meta.name),
98
+ });
99
+ }
100
+ // Prompts — keyed by filename under workflows/prompts/.
101
+ for (const file of listFiles(join(overlayRoot, "workflows", "prompts"), isMarkdown)) {
102
+ assets.push({ type: "prompt", name: file, shadowsDefault: builtins.prompts.has(file) });
103
+ }
104
+ // Skills — keyed by directory name under skills/.
105
+ for (const dir of listDirs(join(overlayRoot, "skills"))) {
106
+ assets.push({ type: "skill", name: dir, shadowsDefault: builtins.skills.has(dir) });
107
+ }
108
+ // Agent-context — keyed by filename under agent-context/.
109
+ for (const file of listFiles(join(overlayRoot, "agent-context"), isMarkdown)) {
110
+ assets.push({ type: "agent-context", name: file, shadowsDefault: builtins.agentContext.has(file) });
111
+ }
112
+ return assets;
113
+ }
114
+ //# sourceMappingURL=overlay-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-assets.js","sourceRoot":"","sources":["../src/overlay-assets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { mkdtempSync, mkdirSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { enumerateOverlayAssets } from "./overlay-assets.js";
6
+ function makeTree() {
7
+ const root = mkdtempSync(join(tmpdir(), "overlay-assets-"));
8
+ const core = join(root, "core");
9
+ const overlay = join(root, "instance");
10
+ // Built-in assets.
11
+ mkdirSync(join(core, "workflows", "prompts"), { recursive: true });
12
+ mkdirSync(join(core, "skills", "building"), { recursive: true });
13
+ mkdirSync(join(core, "agent-context"), { recursive: true });
14
+ writeFileSync(join(core, "workflows", "build.yaml"), "kind: build\nname: build\n");
15
+ writeFileSync(join(core, "workflows", "cron-health.yaml"), "kind: cron\nname: cron-health\nworkflow: repo-health\n");
16
+ writeFileSync(join(core, "workflows", "prompts", "architect.md"), "# architect");
17
+ writeFileSync(join(core, "skills", "building", "SKILL.md"), "# building");
18
+ writeFileSync(join(core, "agent-context", "soul.md"), "# soul");
19
+ return { core, overlay };
20
+ }
21
+ describe("enumerateOverlayAssets", () => {
22
+ let core;
23
+ let overlay;
24
+ beforeEach(() => {
25
+ ({ core, overlay } = makeTree());
26
+ });
27
+ it("returns [] when no overlay is given or it doesn't exist", () => {
28
+ expect(enumerateOverlayAssets({ coreRoot: core })).toEqual([]);
29
+ expect(enumerateOverlayAssets({ coreRoot: core, overlayRoot: join(overlay, "nope") })).toEqual([]);
30
+ });
31
+ it("tags overlay assets as shadowing a built-in vs added", () => {
32
+ mkdirSync(join(overlay, "workflows", "prompts"), { recursive: true });
33
+ mkdirSync(join(overlay, "skills", "building"), { recursive: true });
34
+ mkdirSync(join(overlay, "skills", "custom"), { recursive: true });
35
+ mkdirSync(join(overlay, "agent-context"), { recursive: true });
36
+ // Shadows the built-in build workflow.
37
+ writeFileSync(join(overlay, "workflows", "build.yaml"), "kind: build\nname: build\n");
38
+ // A brand-new workflow.
39
+ writeFileSync(join(overlay, "workflows", "ship.yaml"), "kind: build\nname: ship\n");
40
+ // Shadows a built-in cron.
41
+ writeFileSync(join(overlay, "workflows", "cron-health.yaml"), "kind: cron\nname: cron-health\nworkflow: repo-health\n");
42
+ // Prompt: one shadowing, none added.
43
+ writeFileSync(join(overlay, "workflows", "prompts", "architect.md"), "# overridden");
44
+ // Skills: building shadows, custom is added.
45
+ writeFileSync(join(overlay, "skills", "building", "SKILL.md"), "# overridden");
46
+ writeFileSync(join(overlay, "skills", "custom", "SKILL.md"), "# custom");
47
+ // Agent-context: soul shadows, extra is added.
48
+ writeFileSync(join(overlay, "agent-context", "soul.md"), "# overridden");
49
+ writeFileSync(join(overlay, "agent-context", "extra.md"), "# extra");
50
+ const assets = enumerateOverlayAssets({ coreRoot: core, overlayRoot: overlay });
51
+ const byKey = new Map(assets.map((a) => [`${a.type}:${a.name}`, a.shadowsDefault]));
52
+ expect(byKey.get("workflow:build")).toBe(true);
53
+ expect(byKey.get("workflow:ship")).toBe(false);
54
+ expect(byKey.get("cron:cron-health")).toBe(true);
55
+ expect(byKey.get("prompt:architect.md")).toBe(true);
56
+ expect(byKey.get("skill:building")).toBe(true);
57
+ expect(byKey.get("skill:custom")).toBe(false);
58
+ expect(byKey.get("agent-context:soul.md")).toBe(true);
59
+ expect(byKey.get("agent-context:extra.md")).toBe(false);
60
+ });
61
+ it("keys workflows by YAML name, not filename, and splits cron vs workflow", () => {
62
+ mkdirSync(join(overlay, "workflows"), { recursive: true });
63
+ // Filename differs from the logical name.
64
+ writeFileSync(join(overlay, "workflows", "my-build.yaml"), "kind: build\nname: build\n");
65
+ const assets = enumerateOverlayAssets({ coreRoot: core, overlayRoot: overlay });
66
+ expect(assets).toContainEqual({ type: "workflow", name: "build", shadowsDefault: true });
67
+ });
68
+ it("treats everything as 'added' when there are no built-ins to compare", () => {
69
+ mkdirSync(join(overlay, "skills", "custom"), { recursive: true });
70
+ writeFileSync(join(overlay, "skills", "custom", "SKILL.md"), "# custom");
71
+ const assets = enumerateOverlayAssets({ overlayRoot: overlay });
72
+ expect(assets).toEqual([{ type: "skill", name: "custom", shadowsDefault: false }]);
73
+ });
74
+ });
75
+ //# sourceMappingURL=overlay-assets.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-assets.test.js","sourceRoot":"","sources":["../src/overlay-assets.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import { execFileSync } from "child_process";
3
+ import { mkdtempSync, mkdirSync, rmSync, readdirSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { runWorkflow } from "./../workflows/runner.js";
6
+ /**
7
+ * Integration tests that ACTUALLY start a docker sandbox and execute
8
+ * deterministic `type: bash` / `type: script` phases end-to-end through the
9
+ * real workflow runner — no AI, no mocks. This is the one path the unit suite
10
+ * can't cover (it stubs `executeCommand`).
11
+ *
12
+ * Opt-in + self-gating: runs only when `RUN_SANDBOX_IT=1` AND the lean sandbox
13
+ * image is built, so the default `npx vitest run` (and CI without docker) skip
14
+ * it instantly. To run:
15
+ *
16
+ * docker compose --profile build-only build sandbox # if not already built
17
+ * RUN_SANDBOX_IT=1 npx vitest run src/sandbox/command-exec.integration.test.ts
18
+ *
19
+ * Each command phase spins up + tears down its own container, so these are
20
+ * slow (tens of seconds) — hence the long per-test timeout.
21
+ */
22
+ function sandboxImageBuilt() {
23
+ try {
24
+ const out = execFileSync("docker", ["images", "-q", "lastlight-sandbox:latest"], {
25
+ encoding: "utf-8",
26
+ stdio: ["ignore", "pipe", "ignore"],
27
+ });
28
+ return out.trim().length > 0;
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ const RUN = process.env.RUN_SANDBOX_IT === "1" && sandboxImageBuilt();
35
+ const TIMEOUT = 180_000;
36
+ let stateDir;
37
+ const savedEnv = {};
38
+ function setEnv(k, v) {
39
+ savedEnv[k] = process.env[k];
40
+ process.env[k] = v;
41
+ }
42
+ function baseCtx(taskId) {
43
+ return {
44
+ owner: "acme",
45
+ repo: "widget",
46
+ issueNumber: 1,
47
+ issueTitle: "integration",
48
+ issueBody: "",
49
+ issueLabels: [],
50
+ commentBody: "",
51
+ sender: "tester",
52
+ branch: "main",
53
+ taskId,
54
+ issueDir: ".lastlight/issue-1",
55
+ bootstrapLabel: "lastlight:bootstrap",
56
+ };
57
+ }
58
+ function dockerConfig() {
59
+ return {
60
+ sandbox: "docker",
61
+ stateDir,
62
+ sessionsDir: join(stateDir, "agent-sessions"),
63
+ };
64
+ }
65
+ describe.skipIf(!RUN)("sandbox command execution (integration)", () => {
66
+ beforeAll(() => {
67
+ // Base the workspace under the repo's gitignored `data/` dir (i.e. inside
68
+ // the project, under the user's home). Docker Desktop on macOS shares
69
+ // /Users by default but NOT /var/folders (os.tmpdir()) or /tmp — so the
70
+ // workspace bind mount is only visible to the daemon under a shared path.
71
+ // This mirrors scripts/dev-local.sh (STATE_DIR=$PROJECT_ROOT/data). On Linux
72
+ // any path works.
73
+ const base = join(process.cwd(), "data", "sandbox-it");
74
+ mkdirSync(base, { recursive: true });
75
+ stateDir = mkdtempSync(join(base, "run-"));
76
+ // Run on the default bridge (the compose sandbox-egress network + coredns
77
+ // sidecars don't exist outside docker-compose) and point DNS at a real
78
+ // resolver so the container has working name resolution + NAT.
79
+ setEnv("LASTLIGHT_SANDBOX_NETWORK", "default");
80
+ setEnv("LASTLIGHT_DNS_STRICT", "8.8.8.8");
81
+ setEnv("LASTLIGHT_DNS_OPEN", "8.8.8.8");
82
+ // Host-path data volume → bind-mount mode (mirrors scripts/dev-local.sh),
83
+ // so the harness and the daemon see the same workspace FS.
84
+ setEnv("SANDBOX_DATA_VOLUME", join(stateDir, "sandbox-data"));
85
+ });
86
+ afterAll(() => {
87
+ for (const [k, v] of Object.entries(savedEnv)) {
88
+ if (v === undefined)
89
+ delete process.env[k];
90
+ else
91
+ process.env[k] = v;
92
+ }
93
+ if (stateDir)
94
+ rmSync(stateDir, { recursive: true, force: true });
95
+ });
96
+ it("runs a no-AI bash workflow and threads output to a downstream phase", async () => {
97
+ const wf = {
98
+ kind: "agent",
99
+ name: "it-bash",
100
+ phases: [
101
+ { name: "phase_0", type: "context" },
102
+ { name: "emit", type: "bash", command: "echo hello-from-sandbox", output_var: "greeting" },
103
+ // Consumes the upstream output two ways: template substitution + the
104
+ // forwarded LL_OUT_<PHASE> env var.
105
+ { name: "consume", type: "bash", command: "echo template={{phaseOutputs.emit}} env=$LL_OUT_EMIT" },
106
+ ],
107
+ };
108
+ const result = await runWorkflow(wf, baseCtx("it-bash-1"), dockerConfig(), {});
109
+ expect(result.success).toBe(true);
110
+ const emit = result.phases.find((p) => p.phase === "emit");
111
+ expect(emit?.success).toBe(true);
112
+ expect(emit?.output).toContain("hello-from-sandbox");
113
+ const consume = result.phases.find((p) => p.phase === "consume");
114
+ expect(consume?.success).toBe(true);
115
+ expect(consume?.output).toContain("template=hello-from-sandbox");
116
+ expect(consume?.output).toContain("env=hello-from-sandbox");
117
+ // The command run is mirrored to a session jsonl (visible in the dashboard
118
+ // + CLI), exactly like an agent turn.
119
+ const projects = join(stateDir, "agent-sessions", "projects");
120
+ expect(existsSync(projects)).toBe(true);
121
+ const jsonls = readdirSync(projects, { recursive: true }).filter((f) => String(f).endsWith(".jsonl"));
122
+ expect(jsonls.length).toBeGreaterThan(0);
123
+ }, TIMEOUT);
124
+ it("fails the workflow when a bash phase exits non-zero", async () => {
125
+ const wf = {
126
+ kind: "agent",
127
+ name: "it-bash-fail",
128
+ phases: [
129
+ { name: "phase_0", type: "context" },
130
+ { name: "boom", type: "bash", command: "echo nope >&2; exit 3" },
131
+ ],
132
+ };
133
+ const result = await runWorkflow(wf, baseCtx("it-bash-fail-1"), dockerConfig(), {});
134
+ expect(result.success).toBe(false);
135
+ const boom = result.phases.find((p) => p.phase === "boom");
136
+ expect(boom?.success).toBe(false);
137
+ }, TIMEOUT);
138
+ it("runs an inline JS script via node", async () => {
139
+ const wf = {
140
+ kind: "agent",
141
+ name: "it-js",
142
+ phases: [
143
+ { name: "phase_0", type: "context" },
144
+ {
145
+ name: "calc",
146
+ type: "script",
147
+ runtime: "js",
148
+ script: "console.log('js-answer:', 6 * 7);",
149
+ output_var: "out",
150
+ },
151
+ ],
152
+ };
153
+ const result = await runWorkflow(wf, baseCtx("it-js-1"), dockerConfig(), {});
154
+ expect(result.success).toBe(true);
155
+ const calc = result.phases.find((p) => p.phase === "calc");
156
+ expect(calc?.success).toBe(true);
157
+ expect(calc?.output).toContain("js-answer: 42");
158
+ }, TIMEOUT);
159
+ it("runs an inline Python script via uv run (stdlib only, offline)", async () => {
160
+ const wf = {
161
+ kind: "agent",
162
+ name: "it-py",
163
+ phases: [
164
+ { name: "phase_0", type: "context" },
165
+ {
166
+ name: "calc",
167
+ type: "script",
168
+ runtime: "python",
169
+ timeout_seconds: 120,
170
+ script: "import sys\nprint('py-answer:', 6 * 7)\nprint('py-version-ok:', sys.version_info[0] == 3)",
171
+ output_var: "out",
172
+ },
173
+ ],
174
+ };
175
+ const result = await runWorkflow(wf, baseCtx("it-py-1"), dockerConfig(), {});
176
+ expect(result.success).toBe(true);
177
+ const calc = result.phases.find((p) => p.phase === "calc");
178
+ expect(calc?.success).toBe(true);
179
+ expect(calc?.output).toContain("py-answer: 42");
180
+ expect(calc?.output).toContain("py-version-ok: True");
181
+ }, TIMEOUT);
182
+ });
183
+ //# sourceMappingURL=command-exec.integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-exec.integration.test.js","sourceRoot":"","sources":["../../src/sandbox/command-exec.integration.test.ts"],"names":[],"mappings":"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"}
@@ -139,6 +139,38 @@ export declare class DockerSandbox {
139
139
  /** Called for each newline-terminated stdout line as it arrives. */
140
140
  onLine?: (line: string) => void;
141
141
  }): Promise<void>;
142
+ /**
143
+ * Run a deterministic shell command inside an already-provisioned sandbox
144
+ * container and return its exit code + captured output. This is the
145
+ * non-agent sibling of {@link runAgent}: same `docker exec --user agent -w
146
+ * <workdir> … sh -c <cmd>` machinery, but it captures a bounded stdout tail
147
+ * (runAgent discards stdout) and RESOLVES with the exit code instead of
148
+ * rejecting on non-zero — the caller decides success/failure.
149
+ *
150
+ * Powers `type: bash` / `type: script` workflow phases (and the in-sandbox
151
+ * `generic_loop.until_bash` check). `command` is passed as a single argv slot
152
+ * to `sh -c`, so it is NOT re-parsed by the host shell — it is the script we
153
+ * intend to run. Env is forwarded via `docker exec -e KEY=VALUE` (argv, not
154
+ * shell-interpolated); keys are charset-asserted defensively.
155
+ */
156
+ runCommand(taskId: string, command: string, opts?: {
157
+ /**
158
+ * Working directory inside the container. Defaults to WORKSPACE_DIR.
159
+ * Asserted against the workspace-root allowlist (same guard as runAgent).
160
+ */
161
+ cwd?: string;
162
+ /** Env forwarded into the command via `docker exec -e`. */
163
+ sandboxEnv?: Record<string, string>;
164
+ /** Per-command timeout in seconds (default: the sandbox config timeout). */
165
+ timeoutSeconds?: number;
166
+ /** Called for each newline-terminated stdout line as it arrives. */
167
+ onLine?: (line: string) => void;
168
+ }): Promise<{
169
+ exitCode: number;
170
+ stdout: string;
171
+ stderr: string;
172
+ timedOut: boolean;
173
+ }>;
142
174
  /**
143
175
  * Remove a sandbox container.
144
176
  */
@@ -67,10 +67,16 @@ export class DockerSandbox {
67
67
  dockerArgs.push("-v", `${pkgCacheVolume}:${PKG_CACHE_DIR}`);
68
68
  // Point each package manager at its shared subdir. npm reads npm_config_cache;
69
69
  // pnpm reads npm-style env config (npm_config_store_dir → store-dir); yarn
70
- // (classic + berry) reads YARN_CACHE_FOLDER. The agent already picks the PM
71
- // from the repo's lockfile (see skills/pr-review/SKILL.md), so all three are
72
- // wired regardless of which one a given repo uses.
73
- dockerArgs.push("-e", `npm_config_cache=${PKG_CACHE_DIR}/npm`, "-e", `npm_config_store_dir=${PKG_CACHE_DIR}/pnpm`, "-e", `YARN_CACHE_FOLDER=${PKG_CACHE_DIR}/yarn`);
70
+ // (classic + berry) reads YARN_CACHE_FOLDER; uv (python `type: script`
71
+ // phases) reads UV_CACHE_DIR. The agent already picks the PM from the repo's
72
+ // lockfile (see skills/pr-review/SKILL.md), so all are wired regardless of
73
+ // which one a given repo uses.
74
+ dockerArgs.push("-e", `npm_config_cache=${PKG_CACHE_DIR}/npm`, "-e", `npm_config_store_dir=${PKG_CACHE_DIR}/pnpm`, "-e", `YARN_CACHE_FOLDER=${PKG_CACHE_DIR}/yarn`, "-e", `UV_CACHE_DIR=${PKG_CACHE_DIR}/uv`,
75
+ // uv resolves PEP 723 deps from PyPI (pypi.org / pythonhosted.org are in
76
+ // the strict egress allowlist). Pin it to the baked-in system python3 so
77
+ // it never tries to fetch a managed interpreter from astral.sh / GitHub
78
+ // (not guaranteed reachable under strict egress).
79
+ "-e", "UV_PYTHON_DOWNLOADS=never");
74
80
  // Resolve git mounts for worktrees (if .git is a file pointing elsewhere).
75
81
  // In volume-subpath mode these are still emitted as bind mounts; the
76
82
  // gitMounts path is only hit for git worktrees that live alongside the
@@ -322,6 +328,105 @@ export class DockerSandbox {
322
328
  });
323
329
  });
324
330
  }
331
+ /**
332
+ * Run a deterministic shell command inside an already-provisioned sandbox
333
+ * container and return its exit code + captured output. This is the
334
+ * non-agent sibling of {@link runAgent}: same `docker exec --user agent -w
335
+ * <workdir> … sh -c <cmd>` machinery, but it captures a bounded stdout tail
336
+ * (runAgent discards stdout) and RESOLVES with the exit code instead of
337
+ * rejecting on non-zero — the caller decides success/failure.
338
+ *
339
+ * Powers `type: bash` / `type: script` workflow phases (and the in-sandbox
340
+ * `generic_loop.until_bash` check). `command` is passed as a single argv slot
341
+ * to `sh -c`, so it is NOT re-parsed by the host shell — it is the script we
342
+ * intend to run. Env is forwarded via `docker exec -e KEY=VALUE` (argv, not
343
+ * shell-interpolated); keys are charset-asserted defensively.
344
+ */
345
+ async runCommand(taskId, command, opts) {
346
+ const info = this.activeContainers.get(taskId);
347
+ if (!info)
348
+ throw new Error(`No sandbox for task ${taskId}`);
349
+ const timeout = opts?.timeoutSeconds || this.config.timeoutSeconds || 1800;
350
+ const workdir = opts?.cwd ?? WORKSPACE_DIR;
351
+ if (!workdir.startsWith(WORKSPACE_DIR) || !/^[A-Za-z0-9/_.-]+$/.test(workdir)) {
352
+ throw new Error(`Refusing to run command in cwd "${workdir}" — must live under ${WORKSPACE_DIR}`);
353
+ }
354
+ const envFlags = [];
355
+ for (const [k, v] of Object.entries(opts?.sandboxEnv ?? {})) {
356
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(k)) {
357
+ throw new Error(`Refusing sandbox-env key "${k}" — must be UPPER_SNAKE`);
358
+ }
359
+ // Values travel as argv (no shell re-parse), so any value is safe — but a
360
+ // newline would let a value smuggle a second `-e` token in some shells;
361
+ // reject defensively to match runAgent's posture.
362
+ if (/[\n\r]/.test(v)) {
363
+ throw new Error(`Refusing sandbox-env value for "${k}" — contains newline`);
364
+ }
365
+ envFlags.push("-e", `${k}=${v}`);
366
+ }
367
+ const args = ["exec", "--user", "agent", "-w", workdir, ...envFlags, info.containerName, "sh", "-c", command];
368
+ // Bounded tails so a runaway command (e.g. a build dumping MB of logs)
369
+ // can't OOM the harness — keep enough stdout for the downstream
370
+ // `{{phaseOutputs.*}}` reference to be useful.
371
+ const STDOUT_TAIL_BYTES = 256 * 1024;
372
+ const STDERR_TAIL_BYTES = 32 * 1024;
373
+ return await new Promise((resolvePromise, reject) => {
374
+ const child = spawn("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
375
+ let stdoutTail = "";
376
+ let stderrTail = "";
377
+ let buf = "";
378
+ let timedOut = false;
379
+ const timer = setTimeout(() => {
380
+ timedOut = true;
381
+ child.kill("SIGKILL");
382
+ }, timeout * 1000);
383
+ child.stdout.setEncoding("utf-8");
384
+ child.stdout.on("data", (chunk) => {
385
+ stdoutTail += chunk;
386
+ if (stdoutTail.length > STDOUT_TAIL_BYTES)
387
+ stdoutTail = stdoutTail.slice(-STDOUT_TAIL_BYTES);
388
+ if (!opts?.onLine)
389
+ return;
390
+ buf += chunk;
391
+ let nl;
392
+ while ((nl = buf.indexOf("\n")) >= 0) {
393
+ const line = buf.slice(0, nl);
394
+ buf = buf.slice(nl + 1);
395
+ if (line.length > 0) {
396
+ try {
397
+ opts.onLine(line);
398
+ }
399
+ catch { /* swallow listener errors */ }
400
+ }
401
+ }
402
+ });
403
+ child.stderr.setEncoding("utf-8");
404
+ child.stderr.on("data", (chunk) => {
405
+ stderrTail += chunk;
406
+ if (stderrTail.length > STDERR_TAIL_BYTES)
407
+ stderrTail = stderrTail.slice(-STDERR_TAIL_BYTES);
408
+ });
409
+ child.on("error", (err) => {
410
+ clearTimeout(timer);
411
+ reject(new Error(`Sandbox command spawn failed: ${err.message}`));
412
+ });
413
+ child.on("close", (code) => {
414
+ clearTimeout(timer);
415
+ if (buf.length > 0 && opts?.onLine) {
416
+ try {
417
+ opts.onLine(buf);
418
+ }
419
+ catch { /* ignore */ }
420
+ }
421
+ resolvePromise({
422
+ exitCode: timedOut ? 124 : (code ?? 1),
423
+ stdout: stdoutTail,
424
+ stderr: stderrTail,
425
+ timedOut,
426
+ });
427
+ });
428
+ });
429
+ }
325
430
  /**
326
431
  * Remove a sandbox container.
327
432
  */