@openparachute/agent 0.2.2 → 0.2.3-rc.3

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 (58) hide show
  1. package/.parachute/module.json +3 -3
  2. package/package.json +4 -1
  3. package/src/transports/vault.ts +40 -22
  4. package/web/ui/dist/assets/index-5KEwEhfi.js +60 -0
  5. package/web/ui/dist/index.html +1 -1
  6. package/src/_parked/interactive-spawn.test.ts +0 -324
  7. package/src/_parked/interactive-spawn.ts +0 -701
  8. package/src/agent-defs.test.ts +0 -1504
  9. package/src/agent-mcp-config.test.ts +0 -115
  10. package/src/agents.test.ts +0 -360
  11. package/src/auth.test.ts +0 -46
  12. package/src/backends/attached-queue.test.ts +0 -376
  13. package/src/backends/programmatic.test.ts +0 -1715
  14. package/src/backends/registry.test.ts +0 -1494
  15. package/src/backends/stream-json.test.ts +0 -570
  16. package/src/channel-backend-wiring.test.ts +0 -237
  17. package/src/credentials.test.ts +0 -274
  18. package/src/cron.test.ts +0 -342
  19. package/src/daemon-agent-def-api.test.ts +0 -166
  20. package/src/daemon-agent-defs-api.test.ts +0 -953
  21. package/src/daemon-agent-env-api.test.ts +0 -338
  22. package/src/daemon-attached-queue-store.test.ts +0 -65
  23. package/src/daemon-config-api.test.ts +0 -962
  24. package/src/daemon-jobs-api.test.ts +0 -271
  25. package/src/daemon-vault-chat.test.ts +0 -250
  26. package/src/daemon.test.ts +0 -746
  27. package/src/def-vaults.test.ts +0 -136
  28. package/src/delivery-state.test.ts +0 -110
  29. package/src/effective-env.test.ts +0 -114
  30. package/src/grants.test.ts +0 -638
  31. package/src/hub-jwt.test.ts +0 -161
  32. package/src/jobs.test.ts +0 -245
  33. package/src/mcp-http.test.ts +0 -265
  34. package/src/mint-token.test.ts +0 -152
  35. package/src/module-manifest.test.ts +0 -158
  36. package/src/programmatic-wiring.test.ts +0 -838
  37. package/src/registry.test.ts +0 -227
  38. package/src/resolve-port.test.ts +0 -64
  39. package/src/routing.test.ts +0 -184
  40. package/src/runner.test.ts +0 -506
  41. package/src/sandbox/config.test.ts +0 -150
  42. package/src/sandbox/egress.test.ts +0 -113
  43. package/src/sandbox/live-seatbelt.test.ts +0 -277
  44. package/src/sandbox/mounts.test.ts +0 -154
  45. package/src/sandbox/sandbox.test.ts +0 -168
  46. package/src/services-manifest.test.ts +0 -106
  47. package/src/spa-serve.test.ts +0 -116
  48. package/src/spawn-agent-cli.test.ts +0 -172
  49. package/src/spawn-agent.test.ts +0 -1218
  50. package/src/spawn-deps.test.ts +0 -54
  51. package/src/terminal-assets.test.ts +0 -50
  52. package/src/terminal.test.ts +0 -530
  53. package/src/transports/http-ui.test.ts +0 -455
  54. package/src/transports/telegram.test.ts +0 -174
  55. package/src/transports/vault.test.ts +0 -2011
  56. package/src/ui-kit.test.ts +0 -178
  57. package/web/ui/dist/assets/index-VFETBk0a.js +0 -60
  58. package/web/ui/tsconfig.json +0 -21
@@ -1,136 +0,0 @@
1
- /**
2
- * Unit tests for the def-vault config (design 2026-06-17-vault-native-agents,
3
- * Phase 4a "Commit 3"). Sandboxed to a throwaway state dir (NEVER the operator's
4
- * ~/.parachute); the mint is driven by an injected fetch — deterministic, no real hub.
5
- */
6
-
7
- import { describe, test, expect, afterEach } from "bun:test";
8
- import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync, statSync } from "fs";
9
- import { join } from "path";
10
- import { tmpdir } from "os";
11
- import {
12
- readDefVaultsFile,
13
- writeDefVaultsFile,
14
- defVaultsFilePath,
15
- resolveDefVaults,
16
- DEFAULT_DEF_VAULT_NAME,
17
- } from "./def-vaults.ts";
18
-
19
- const dirs: string[] = [];
20
- function freshDir(): string {
21
- const d = mkdtempSync(join(tmpdir(), "agent-defvaults-"));
22
- dirs.push(d);
23
- return d;
24
- }
25
- afterEach(() => {
26
- for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
27
- });
28
-
29
- /** A fake hub mint endpoint returning a scripted token. */
30
- function fakeMint(token: string): { fetchFn: typeof fetch; calls: Array<Record<string, unknown>> } {
31
- const calls: Array<Record<string, unknown>> = [];
32
- const fetchFn = (async (_url: string | URL | Request, init?: RequestInit) => {
33
- calls.push(JSON.parse(String(init?.body ?? "{}")));
34
- return new Response(JSON.stringify({ token, jti: "j", expires_at: "", scope: "vault:default:write" }), {
35
- status: 200,
36
- headers: { "content-type": "application/json" },
37
- });
38
- }) as typeof fetch;
39
- return { fetchFn, calls };
40
- }
41
-
42
- describe("agent-vaults.json read/write", () => {
43
- test("readDefVaultsFile returns null when absent", () => {
44
- expect(readDefVaultsFile(freshDir())).toBeNull();
45
- });
46
-
47
- test("write then read round-trips; file is 0600", () => {
48
- const dir = freshDir();
49
- writeDefVaultsFile({ vaults: [{ vault: "default", vaultUrl: "http://x", token: "t" }] }, dir);
50
- const back = readDefVaultsFile(dir);
51
- expect(back?.vaults).toEqual([{ vault: "default", vaultUrl: "http://x", token: "t" }]);
52
- const mode = statSync(defVaultsFilePath(dir)).mode & 0o777;
53
- expect(mode).toBe(0o600);
54
- });
55
-
56
- test("a malformed file throws (operator error, not silently defaulted)", () => {
57
- const dir = freshDir();
58
- writeFileSync(defVaultsFilePath(dir), JSON.stringify({ nope: true }));
59
- expect(() => readDefVaultsFile(dir)).toThrow(/"vaults" array/);
60
- });
61
-
62
- test("an entry missing vault throws", () => {
63
- const dir = freshDir();
64
- writeFileSync(defVaultsFilePath(dir), JSON.stringify({ vaults: [{ token: "t" }] }));
65
- expect(() => readDefVaultsFile(dir)).toThrow(/non-empty "vault"/);
66
- });
67
- });
68
-
69
- describe("resolveDefVaults", () => {
70
- test("explicit agent-vaults.json wins (multi-vault, verbatim tokens, no mint)", async () => {
71
- const dir = freshDir();
72
- writeDefVaultsFile(
73
- {
74
- vaults: [
75
- { vault: "default", vaultUrl: "http://127.0.0.1:1940", token: "tok-default" },
76
- { vault: "research", vaultUrl: "http://127.0.0.1:1940", token: "tok-research" },
77
- ],
78
- },
79
- dir,
80
- );
81
- const { fetchFn, calls } = fakeMint("SHOULD-NOT-BE-USED");
82
- const bindings = await resolveDefVaults({ stateDir: dir, managerBearer: "op", fetchFn });
83
- expect(bindings.map((b) => b.vault)).toEqual(["default", "research"]);
84
- expect(bindings[0]!.token).toBe("tok-default");
85
- expect(bindings[1]!.token).toBe("tok-research");
86
- // No mint when the file carries tokens.
87
- expect(calls).toHaveLength(0);
88
- });
89
-
90
- test("no file + a manager bearer → mints a default vault:default:write token + persists", async () => {
91
- const dir = freshDir();
92
- const { fetchFn, calls } = fakeMint("minted-token");
93
- const bindings = await resolveDefVaults({
94
- stateDir: dir,
95
- hubOrigin: "http://127.0.0.1:1939",
96
- managerBearer: "operator-bearer",
97
- fetchFn,
98
- });
99
- expect(bindings).toHaveLength(1);
100
- expect(bindings[0]).toMatchObject({ vault: DEFAULT_DEF_VAULT_NAME, token: "minted-token" });
101
- // Minted with the vault:default:write scope, attenuated to the operator bearer.
102
- expect(calls[0]!.scope).toBe("vault:default:write");
103
- // Persisted so a restart reuses it (no re-mint).
104
- expect(existsSync(defVaultsFilePath(dir))).toBe(true);
105
- const persisted = readDefVaultsFile(dir);
106
- expect(persisted?.vaults[0]!.token).toBe("minted-token");
107
- });
108
-
109
- test("persist:false → mints but does NOT write the file", async () => {
110
- const dir = freshDir();
111
- const { fetchFn } = fakeMint("minted");
112
- const bindings = await resolveDefVaults({
113
- stateDir: dir,
114
- managerBearer: "op",
115
- fetchFn,
116
- persist: false,
117
- });
118
- expect(bindings).toHaveLength(1);
119
- expect(existsSync(defVaultsFilePath(dir))).toBe(false);
120
- });
121
-
122
- test("no file + no manager bearer → NO bindings (vault-native path idle; channels.json unaffected)", async () => {
123
- const bindings = await resolveDefVaults({ stateDir: freshDir(), managerBearer: null });
124
- expect(bindings).toEqual([]);
125
- });
126
-
127
- test("a mint failure → no bindings (best-effort; boot is not crashed)", async () => {
128
- const dir = freshDir();
129
- const fetchFn = (async () =>
130
- new Response(JSON.stringify({ error: "invalid_scope" }), { status: 400 })) as unknown as typeof fetch;
131
- const bindings = await resolveDefVaults({ stateDir: dir, managerBearer: "op", fetchFn });
132
- expect(bindings).toEqual([]);
133
- // Nothing persisted on a failed mint.
134
- expect(existsSync(defVaultsFilePath(dir))).toBe(false);
135
- });
136
- });
@@ -1,110 +0,0 @@
1
- /**
2
- * DeliveryState tests — the per-channel high-water-mark that gates backlog replay.
3
- *
4
- * Covered:
5
- * - monotonic advance (never rewinds; reports whether it moved);
6
- * - default-to-bootTime for an unknown channel (so a first connect never replays
7
- * ancient history);
8
- * - persist + reload across a simulated restart (a new instance reads the file);
9
- * - blank-ts advance is a no-op (we never mark to an empty ts).
10
- *
11
- * Each test points the store at a throwaway temp dir, so there's no shared global
12
- * state and no touch of the real `~/.parachute/agent/`.
13
- */
14
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
15
- import { mkdtempSync, rmSync, existsSync, readFileSync, writeFileSync } from "fs";
16
- import { join } from "path";
17
- import { tmpdir } from "os";
18
- import { DeliveryState } from "./delivery-state.ts";
19
-
20
- let dir: string;
21
- beforeEach(() => {
22
- dir = mkdtempSync(join(tmpdir(), "channel-delivery-"));
23
- });
24
- afterEach(() => {
25
- rmSync(dir, { recursive: true, force: true });
26
- });
27
-
28
- describe("getLastDelivered default", () => {
29
- test("an unknown channel returns the boot-time default mark, not epoch", () => {
30
- const boot = "2026-06-16T12:00:00.000Z";
31
- const ds = new DeliveryState({ stateDir: dir, defaultMark: boot });
32
- expect(ds.getLastDelivered("never-seen")).toBe(boot);
33
- });
34
-
35
- test("defaultMark defaults to ~now when omitted", () => {
36
- const before = Date.now();
37
- const ds = new DeliveryState({ stateDir: dir });
38
- const mark = Date.parse(ds.getLastDelivered("x"));
39
- const after = Date.now();
40
- expect(mark).toBeGreaterThanOrEqual(before);
41
- expect(mark).toBeLessThanOrEqual(after + 1000);
42
- });
43
- });
44
-
45
- describe("advance is monotonic", () => {
46
- test("moves forward + reports true; a stale/equal ts is a no-op reporting false", () => {
47
- const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
48
- expect(ds.advance("c", "2026-06-16T10:00:00.000Z")).toBe(true);
49
- expect(ds.getLastDelivered("c")).toBe("2026-06-16T10:00:00.000Z");
50
-
51
- // An OLDER ts must not rewind the mark.
52
- expect(ds.advance("c", "2026-06-16T09:00:00.000Z")).toBe(false);
53
- expect(ds.getLastDelivered("c")).toBe("2026-06-16T10:00:00.000Z");
54
-
55
- // The SAME ts is also a no-op (strictly-greater advance).
56
- expect(ds.advance("c", "2026-06-16T10:00:00.000Z")).toBe(false);
57
- expect(ds.getLastDelivered("c")).toBe("2026-06-16T10:00:00.000Z");
58
-
59
- // A NEWER ts advances again.
60
- expect(ds.advance("c", "2026-06-16T11:00:00.000Z")).toBe(true);
61
- expect(ds.getLastDelivered("c")).toBe("2026-06-16T11:00:00.000Z");
62
- });
63
-
64
- test("a blank ts never advances the mark", () => {
65
- const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
66
- expect(ds.advance("c", "")).toBe(false);
67
- expect(ds.getLastDelivered("c")).toBe("2026-01-01T00:00:00.000Z");
68
- });
69
-
70
- test("per-channel marks are independent", () => {
71
- const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
72
- ds.advance("a", "2026-06-16T10:00:00.000Z");
73
- expect(ds.getLastDelivered("a")).toBe("2026-06-16T10:00:00.000Z");
74
- // b is untouched — still the default.
75
- expect(ds.getLastDelivered("b")).toBe("2026-01-01T00:00:00.000Z");
76
- });
77
- });
78
-
79
- describe("persist + reload (simulated restart)", () => {
80
- test("a fresh instance reads the persisted marks (the restart case)", () => {
81
- const ds1 = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
82
- ds1.advance("eng", "2026-06-16T10:00:00.000Z");
83
- ds1.advance("ops", "2026-06-16T11:00:00.000Z");
84
- expect(existsSync(join(dir, "delivery-state.json"))).toBe(true);
85
-
86
- // Simulate a daemon restart: a brand-new instance with a LATER boot default.
87
- // The persisted per-channel marks must win over the new default — that's what
88
- // makes the replay-the-gap behavior work across a bounce.
89
- const ds2 = new DeliveryState({ stateDir: dir, defaultMark: "2026-06-16T12:00:00.000Z" });
90
- expect(ds2.getLastDelivered("eng")).toBe("2026-06-16T10:00:00.000Z");
91
- expect(ds2.getLastDelivered("ops")).toBe("2026-06-16T11:00:00.000Z");
92
- // A channel with no persisted mark still falls back to the new boot default.
93
- expect(ds2.getLastDelivered("brand-new")).toBe("2026-06-16T12:00:00.000Z");
94
- });
95
-
96
- test("the persisted file is valid JSON of channel→ts", () => {
97
- const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-01-01T00:00:00.000Z" });
98
- ds.advance("eng", "2026-06-16T10:00:00.000Z");
99
- const onDisk = JSON.parse(readFileSync(join(dir, "delivery-state.json"), "utf8"));
100
- expect(onDisk).toEqual({ eng: "2026-06-16T10:00:00.000Z" });
101
- });
102
-
103
- test("a corrupt file is tolerated — starts empty, the default covers unknowns", () => {
104
- writeFileSync(join(dir, "delivery-state.json"), "{ not json");
105
- const ds = new DeliveryState({ stateDir: dir, defaultMark: "2026-06-16T12:00:00.000Z" });
106
- expect(ds.getLastDelivered("eng")).toBe("2026-06-16T12:00:00.000Z");
107
- // Still usable: advance + persist overwrites the corrupt file.
108
- expect(ds.advance("eng", "2026-06-16T13:00:00.000Z")).toBe(true);
109
- });
110
- });
@@ -1,114 +0,0 @@
1
- /**
2
- * Unit tests for the PURE effective-env composition (effective-env.ts) — the
3
- * precedence/overridden logic + the no-material grant-name derivation, isolated from
4
- * the daemon route + any I/O. (The route integration lives in daemon-agent-env-api.test.ts.)
5
- */
6
- import { describe, test, expect } from "bun:test";
7
- import {
8
- approvedGrantEnvNames,
9
- composeEffectiveEnv,
10
- resolveEffectiveEnv,
11
- } from "./effective-env.ts";
12
-
13
- describe("approvedGrantEnvNames — names only, approved-service grants only, NO material", () => {
14
- test("maps an approved service grant to its env-var name via serviceEnvVar", () => {
15
- const out = approvedGrantEnvNames([{ kind: "service", target: "github", status: "approved" }]);
16
- expect(out).toEqual([{ name: "GITHUB_TOKEN", source: "grant:github" }]);
17
- });
18
-
19
- test("a non-default service maps to <TARGET>_TOKEN", () => {
20
- const out = approvedGrantEnvNames([{ kind: "service", target: "fireflies", status: "approved" }]);
21
- expect(out).toEqual([{ name: "FIREFLIES_TOKEN", source: "grant:fireflies" }]);
22
- });
23
-
24
- test("ignores pending grants, vault grants, and mcp grants (none inject an env var)", () => {
25
- const out = approvedGrantEnvNames([
26
- { kind: "service", target: "github", status: "pending" }, // not approved
27
- { kind: "vault", target: "research", status: "approved" }, // vault → MCP, not env
28
- { kind: "mcp", target: "https://x/mcp", status: "approved" }, // remote MCP, not env
29
- { kind: "service", target: "cloudflare", status: "approved" }, // the only env one
30
- ]);
31
- expect(out).toEqual([{ name: "CLOUDFLARE_API_TOKEN", source: "grant:cloudflare" }]);
32
- });
33
-
34
- test("undefined connections → empty", () => {
35
- expect(approvedGrantEnvNames(undefined)).toEqual([]);
36
- });
37
- });
38
-
39
- describe("composeEffectiveEnv — precedence channel > default > grant + overridden marking", () => {
40
- const channelEnv = {
41
- default: ["DEFAULT_VAR", "GITHUB_TOKEN"],
42
- channels: { "uni-dev": ["CHANNEL_VAR", "GITHUB_TOKEN"] },
43
- };
44
- const connections = [{ kind: "service", target: "github", status: "approved" }];
45
-
46
- test("a name set in all three layers → channel wins, default + grant marked overridden", () => {
47
- const out = composeEffectiveEnv("uni-dev", channelEnv, connections);
48
- const gh = out.filter((e) => e.name === "GITHUB_TOKEN");
49
- expect(gh).toHaveLength(3);
50
- const winner = gh.find((e) => !e.overridden)!;
51
- expect(winner.source).toBe("channel");
52
- expect(gh.filter((e) => e.overridden).map((e) => e.source).sort()).toEqual(["default", "grant:github"]);
53
- });
54
-
55
- test("single-layer names carry no overridden flag", () => {
56
- const out = composeEffectiveEnv("uni-dev", channelEnv, connections);
57
- expect(out.find((e) => e.name === "DEFAULT_VAR")).toEqual({ name: "DEFAULT_VAR", source: "default" });
58
- expect(out.find((e) => e.name === "CHANNEL_VAR")).toEqual({ name: "CHANNEL_VAR", source: "channel" });
59
- });
60
-
61
- test("default beats grant when no channel override exists", () => {
62
- const out = composeEffectiveEnv(
63
- "uni-dev",
64
- { default: ["GITHUB_TOKEN"], channels: {} },
65
- connections,
66
- );
67
- const gh = out.filter((e) => e.name === "GITHUB_TOKEN");
68
- expect(gh.find((e) => !e.overridden)!.source).toBe("default");
69
- expect(gh.find((e) => e.overridden)!.source).toBe("grant:github");
70
- });
71
-
72
- test("an agent with no env-store entries + no connections → empty", () => {
73
- expect(composeEffectiveEnv("nobody", { default: [], channels: {} }, undefined)).toEqual([]);
74
- });
75
-
76
- test("only the matching agent's channel layer is used (another agent's overrides are ignored)", () => {
77
- const out = composeEffectiveEnv(
78
- "uni-dev",
79
- { default: [], channels: { other: ["OTHER_VAR"], "uni-dev": ["MINE"] } },
80
- undefined,
81
- );
82
- expect(out.map((e) => e.name)).toEqual(["MINE"]);
83
- });
84
- });
85
-
86
- describe("resolveEffectiveEnv — degraded note when no def, never returns values", () => {
87
- test("hasDef:false → attaches a note + still returns env layers", () => {
88
- const res = resolveEffectiveEnv("uni-dev", {
89
- describeEnv: () => ({ default: ["DEFAULT_VAR"], channels: { "uni-dev": ["CHANNEL_VAR"] } }),
90
- hasDef: false,
91
- });
92
- expect(res.note).toBeDefined();
93
- expect(res.env.map((e) => e.name).sort()).toEqual(["CHANNEL_VAR", "DEFAULT_VAR"]);
94
- });
95
-
96
- test("hasDef:true → no note", () => {
97
- const res = resolveEffectiveEnv("uni-dev", {
98
- describeEnv: () => ({ default: [], channels: {} }),
99
- connections: [{ kind: "service", target: "github", status: "approved" }],
100
- hasDef: true,
101
- });
102
- expect(res.note).toBeUndefined();
103
- expect(res.env).toEqual([{ name: "GITHUB_TOKEN", source: "grant:github" }]);
104
- });
105
-
106
- test("the shape carries NO `value` field on any entry", () => {
107
- const res = resolveEffectiveEnv("uni-dev", {
108
- describeEnv: () => ({ default: ["A"], channels: { "uni-dev": ["B"] } }),
109
- connections: [{ kind: "service", target: "github", status: "approved" }],
110
- hasDef: true,
111
- });
112
- for (const e of res.env) expect(Object.keys(e).sort()).not.toContain("value");
113
- });
114
- });