@openparachute/agent 0.2.2 → 0.2.3-rc.10

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 (72) hide show
  1. package/.parachute/module.json +3 -3
  2. package/package.json +4 -1
  3. package/src/agent-defs.ts +9 -0
  4. package/src/auth.ts +182 -14
  5. package/src/backends/registry.ts +65 -27
  6. package/src/daemon.ts +311 -12
  7. package/src/def-vault-triggers.ts +317 -0
  8. package/src/preflight.ts +139 -0
  9. package/src/spawn-agent.ts +16 -0
  10. package/src/step-up.ts +316 -0
  11. package/src/terminal-ui.ts +73 -0
  12. package/src/transports/http-ui.ts +10 -8
  13. package/src/transports/vault.ts +40 -22
  14. package/src/ui-kit.ts +6 -3
  15. package/src/ui-ticket.ts +121 -0
  16. package/web/ui/dist/assets/index-Dhr5Kl_d.css +1 -0
  17. package/web/ui/dist/assets/index-Di5MmFZR.js +60 -0
  18. package/web/ui/dist/index.html +2 -2
  19. package/src/_parked/interactive-spawn.test.ts +0 -324
  20. package/src/_parked/interactive-spawn.ts +0 -701
  21. package/src/agent-defs.test.ts +0 -1504
  22. package/src/agent-mcp-config.test.ts +0 -115
  23. package/src/agents.test.ts +0 -360
  24. package/src/auth.test.ts +0 -46
  25. package/src/backends/attached-queue.test.ts +0 -376
  26. package/src/backends/programmatic.test.ts +0 -1715
  27. package/src/backends/registry.test.ts +0 -1494
  28. package/src/backends/stream-json.test.ts +0 -570
  29. package/src/channel-backend-wiring.test.ts +0 -237
  30. package/src/credentials.test.ts +0 -274
  31. package/src/cron.test.ts +0 -342
  32. package/src/daemon-agent-def-api.test.ts +0 -166
  33. package/src/daemon-agent-defs-api.test.ts +0 -953
  34. package/src/daemon-agent-env-api.test.ts +0 -338
  35. package/src/daemon-attached-queue-store.test.ts +0 -65
  36. package/src/daemon-config-api.test.ts +0 -962
  37. package/src/daemon-jobs-api.test.ts +0 -271
  38. package/src/daemon-vault-chat.test.ts +0 -250
  39. package/src/daemon.test.ts +0 -746
  40. package/src/def-vaults.test.ts +0 -136
  41. package/src/delivery-state.test.ts +0 -110
  42. package/src/effective-env.test.ts +0 -114
  43. package/src/grants.test.ts +0 -638
  44. package/src/hub-jwt.test.ts +0 -161
  45. package/src/jobs.test.ts +0 -245
  46. package/src/mcp-http.test.ts +0 -265
  47. package/src/mint-token.test.ts +0 -152
  48. package/src/module-manifest.test.ts +0 -158
  49. package/src/programmatic-wiring.test.ts +0 -838
  50. package/src/registry.test.ts +0 -227
  51. package/src/resolve-port.test.ts +0 -64
  52. package/src/routing.test.ts +0 -184
  53. package/src/runner.test.ts +0 -506
  54. package/src/sandbox/config.test.ts +0 -150
  55. package/src/sandbox/egress.test.ts +0 -113
  56. package/src/sandbox/live-seatbelt.test.ts +0 -277
  57. package/src/sandbox/mounts.test.ts +0 -154
  58. package/src/sandbox/sandbox.test.ts +0 -168
  59. package/src/services-manifest.test.ts +0 -106
  60. package/src/spa-serve.test.ts +0 -116
  61. package/src/spawn-agent-cli.test.ts +0 -172
  62. package/src/spawn-agent.test.ts +0 -1218
  63. package/src/spawn-deps.test.ts +0 -54
  64. package/src/terminal-assets.test.ts +0 -50
  65. package/src/terminal.test.ts +0 -530
  66. package/src/transports/http-ui.test.ts +0 -455
  67. package/src/transports/telegram.test.ts +0 -174
  68. package/src/transports/vault.test.ts +0 -2011
  69. package/src/ui-kit.test.ts +0 -178
  70. package/web/ui/dist/assets/index-C-iWdFFV.css +0 -1
  71. package/web/ui/dist/assets/index-VFETBk0a.js +0 -60
  72. 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
- });