@openparachute/agent 0.2.2 → 0.2.3-rc.11

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 (74) 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/programmatic.ts +35 -2
  6. package/src/backends/registry.ts +159 -40
  7. package/src/backends/types.ts +44 -0
  8. package/src/daemon.ts +317 -12
  9. package/src/def-vault-triggers.ts +317 -0
  10. package/src/preflight.ts +139 -0
  11. package/src/spawn-agent.ts +16 -0
  12. package/src/step-up.ts +316 -0
  13. package/src/terminal-ui.ts +73 -0
  14. package/src/transports/http-ui.ts +10 -8
  15. package/src/transports/vault.ts +48 -27
  16. package/src/ui-kit.ts +6 -3
  17. package/src/ui-ticket.ts +121 -0
  18. package/web/ui/dist/assets/index-Dhr5Kl_d.css +1 -0
  19. package/web/ui/dist/assets/index-Di5MmFZR.js +60 -0
  20. package/web/ui/dist/index.html +2 -2
  21. package/src/_parked/interactive-spawn.test.ts +0 -324
  22. package/src/_parked/interactive-spawn.ts +0 -701
  23. package/src/agent-defs.test.ts +0 -1504
  24. package/src/agent-mcp-config.test.ts +0 -115
  25. package/src/agents.test.ts +0 -360
  26. package/src/auth.test.ts +0 -46
  27. package/src/backends/attached-queue.test.ts +0 -376
  28. package/src/backends/programmatic.test.ts +0 -1715
  29. package/src/backends/registry.test.ts +0 -1494
  30. package/src/backends/stream-json.test.ts +0 -570
  31. package/src/channel-backend-wiring.test.ts +0 -237
  32. package/src/credentials.test.ts +0 -274
  33. package/src/cron.test.ts +0 -342
  34. package/src/daemon-agent-def-api.test.ts +0 -166
  35. package/src/daemon-agent-defs-api.test.ts +0 -953
  36. package/src/daemon-agent-env-api.test.ts +0 -338
  37. package/src/daemon-attached-queue-store.test.ts +0 -65
  38. package/src/daemon-config-api.test.ts +0 -962
  39. package/src/daemon-jobs-api.test.ts +0 -271
  40. package/src/daemon-vault-chat.test.ts +0 -250
  41. package/src/daemon.test.ts +0 -746
  42. package/src/def-vaults.test.ts +0 -136
  43. package/src/delivery-state.test.ts +0 -110
  44. package/src/effective-env.test.ts +0 -114
  45. package/src/grants.test.ts +0 -638
  46. package/src/hub-jwt.test.ts +0 -161
  47. package/src/jobs.test.ts +0 -245
  48. package/src/mcp-http.test.ts +0 -265
  49. package/src/mint-token.test.ts +0 -152
  50. package/src/module-manifest.test.ts +0 -158
  51. package/src/programmatic-wiring.test.ts +0 -838
  52. package/src/registry.test.ts +0 -227
  53. package/src/resolve-port.test.ts +0 -64
  54. package/src/routing.test.ts +0 -184
  55. package/src/runner.test.ts +0 -506
  56. package/src/sandbox/config.test.ts +0 -150
  57. package/src/sandbox/egress.test.ts +0 -113
  58. package/src/sandbox/live-seatbelt.test.ts +0 -277
  59. package/src/sandbox/mounts.test.ts +0 -154
  60. package/src/sandbox/sandbox.test.ts +0 -168
  61. package/src/services-manifest.test.ts +0 -106
  62. package/src/spa-serve.test.ts +0 -116
  63. package/src/spawn-agent-cli.test.ts +0 -172
  64. package/src/spawn-agent.test.ts +0 -1218
  65. package/src/spawn-deps.test.ts +0 -54
  66. package/src/terminal-assets.test.ts +0 -50
  67. package/src/terminal.test.ts +0 -530
  68. package/src/transports/http-ui.test.ts +0 -455
  69. package/src/transports/telegram.test.ts +0 -174
  70. package/src/transports/vault.test.ts +0 -2011
  71. package/src/ui-kit.test.ts +0 -178
  72. package/web/ui/dist/assets/index-C-iWdFFV.css +0 -1
  73. package/web/ui/dist/assets/index-VFETBk0a.js +0 -60
  74. package/web/ui/tsconfig.json +0 -21
@@ -6,8 +6,8 @@
6
6
  <meta name="referrer" content="no-referrer" />
7
7
  <title>Parachute Agent</title>
8
8
  <meta name="description" content="Manage Parachute agents — every backend, one surface." />
9
- <script type="module" crossorigin src="/agent/app/assets/index-VFETBk0a.js"></script>
10
- <link rel="stylesheet" crossorigin href="/agent/app/assets/index-C-iWdFFV.css">
9
+ <script type="module" crossorigin src="/agent/app/assets/index-Di5MmFZR.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/agent/app/assets/index-Dhr5Kl_d.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -1,324 +0,0 @@
1
- /**
2
- * PARKED unit tests for the interactive (tmux) spawner + session admin
3
- * (`src/_parked/interactive-spawn.ts`). The interactive backend retired 2026-06-19
4
- * (design 2026-06-19-retire-interactive-backend.md); these tests keep the parked
5
- * code provably buildable for the future terminal/process-mgmt revival. They are
6
- * pure over their inputs (an injected `TmuxAdmin` / `TmuxLauncher` recorder) — no
7
- * hub, no sandbox, no real tmux server.
8
- *
9
- * Spawner-internals coverage (buildAgentClaudeArgs, buildLaunchScript,
10
- * realTmuxLauncher, confirmDevChannelsPrompt, spawnAgent) lives in
11
- * `src/spawn-agent.test.ts`, which imports the spawner from the parked module too.
12
- */
13
- import { describe, test, expect } from "bun:test";
14
- import { mkdtempSync, rmSync } from "node:fs";
15
- import { tmpdir } from "node:os";
16
- import { join } from "node:path";
17
- import {
18
- parseTmuxSessions,
19
- agentInfoFromSessions,
20
- redactSpawnResult,
21
- createRealAgentOps,
22
- SpawnRequestError,
23
- type TmuxAdmin,
24
- type TmuxLauncher,
25
- type SpawnAgentDeps,
26
- type SpawnAgentResult,
27
- } from "./interactive-spawn.ts";
28
- import { persistSpec, sessionWorkspace } from "../spawn-agent.ts";
29
- import type { SandboxEngine } from "../sandbox/index.ts";
30
- import type { AgentSpec } from "../sandbox/types.ts";
31
-
32
- describe("parseTmuxSessions", () => {
33
- test("parses `<name> <attachedCount>` lines; attached>0 → true", () => {
34
- const out = parseTmuxSessions("aaron-agent 1\nweaver-agent 0\nmisc 2\n");
35
- expect(out).toEqual([
36
- { name: "aaron-agent", attached: true },
37
- { name: "weaver-agent", attached: false },
38
- { name: "misc", attached: true },
39
- ]);
40
- });
41
- test("empty / blank input → empty list", () => {
42
- expect(parseTmuxSessions("")).toEqual([]);
43
- expect(parseTmuxSessions("\n \n")).toEqual([]);
44
- });
45
- });
46
-
47
- describe("agentInfoFromSessions", () => {
48
- test("keeps only *-agent sessions, strips the suffix, sorts by name", () => {
49
- const infos = agentInfoFromSessions(
50
- [
51
- { name: "weaver-agent", attached: false },
52
- { name: "scratch", attached: true }, // not an agent session — dropped
53
- { name: "aaron-agent", attached: true },
54
- ],
55
- "/tmp/sessions",
56
- );
57
- expect(infos.map((i) => i.name)).toEqual(["aaron", "weaver"]);
58
- expect(infos[0]).toMatchObject({ name: "aaron", session: "aaron-agent", attached: true });
59
- expect(infos[0]!.workspace).toBe("/tmp/sessions/aaron");
60
- expect(infos[0]!.hasWorkspace).toBe(false);
61
- });
62
- test("a bare `-agent` (empty slug) is dropped", () => {
63
- expect(agentInfoFromSessions([{ name: "-agent", attached: false }], "/tmp/s")).toEqual([]);
64
- });
65
- test("surfaces systemPromptMode from the persisted spec when a prompt is set; absent otherwise", () => {
66
- const dir = mkdtempSync(join(tmpdir(), "agent-info-sysprompt-"));
67
- try {
68
- persistSpec(sessionWorkspace(dir, "withprompt"), {
69
- name: "withprompt",
70
- channels: ["withprompt"],
71
- systemPrompt: "You are a focused bot.",
72
- systemPromptMode: "replace",
73
- } as AgentSpec);
74
- persistSpec(sessionWorkspace(dir, "noprompt"), { name: "noprompt", channels: ["noprompt"] } as AgentSpec);
75
- const infos = agentInfoFromSessions(
76
- [
77
- { name: "withprompt-agent", attached: false },
78
- { name: "noprompt-agent", attached: false },
79
- ],
80
- dir,
81
- );
82
- const byName = Object.fromEntries(infos.map((i) => [i.name, i]));
83
- expect(byName.withprompt!.systemPromptMode).toBe("replace");
84
- expect(byName.noprompt!.systemPromptMode).toBeUndefined();
85
- } finally {
86
- rmSync(dir, { recursive: true, force: true });
87
- }
88
- });
89
- test("surfaces workingDir from the persisted spec when the workspace is set AND exists; absent otherwise", () => {
90
- const dir = mkdtempSync(join(tmpdir(), "agent-info-workdir-"));
91
- const workdir = mkdtempSync(join(tmpdir(), "agent-info-real-workdir-"));
92
- try {
93
- persistSpec(sessionWorkspace(dir, "withdir"), {
94
- name: "withdir",
95
- channels: ["withdir"],
96
- workspace: workdir,
97
- } as AgentSpec);
98
- persistSpec(sessionWorkspace(dir, "nodir"), { name: "nodir", channels: ["nodir"] } as AgentSpec);
99
- persistSpec(sessionWorkspace(dir, "gonedir"), {
100
- name: "gonedir",
101
- channels: ["gonedir"],
102
- workspace: "/Users/op/Code/deleted-repo",
103
- } as AgentSpec);
104
- const infos = agentInfoFromSessions(
105
- [
106
- { name: "withdir-agent", attached: false },
107
- { name: "nodir-agent", attached: false },
108
- { name: "gonedir-agent", attached: false },
109
- ],
110
- dir,
111
- );
112
- const byName = Object.fromEntries(infos.map((i) => [i.name, i]));
113
- expect(byName.withdir!.workingDir).toBe(workdir);
114
- expect(byName.nodir!.workingDir).toBeUndefined();
115
- expect(byName.gonedir!.workingDir).toBeUndefined();
116
- } finally {
117
- rmSync(dir, { recursive: true, force: true });
118
- rmSync(workdir, { recursive: true, force: true });
119
- }
120
- });
121
- });
122
-
123
- describe("redactSpawnResult", () => {
124
- const result: SpawnAgentResult = {
125
- session: "aaron-agent",
126
- workspace: "/s/aaron",
127
- alreadyRunning: false,
128
- tokens: {
129
- aaron: { jti: "j1", token: "SECRET-AGENT-TOKEN", expiresAt: "2026-07-01T00:00:00Z", scope: "agent:read agent:write" },
130
- "vault:default": { jti: "j2", token: "SECRET-VAULT-TOKEN", expiresAt: "2026-07-01T00:00:00Z", scope: "vault:default:read" },
131
- },
132
- mcpConfigJson: JSON.stringify({ mcpServers: { "agent-aaron": {}, "vault-default": {} } }),
133
- wrapped: {
134
- argv: ["/bin/bash", "-c", "..."],
135
- env: {},
136
- config: { network: { allowedDomains: ["api.anthropic.com:443"], deniedDomains: [] }, filesystem: { denyRead: ["/Users"], allowWrite: [], denyWrite: [] } },
137
- },
138
- };
139
- test("surfaces scopes + mcp servers + posture + egress, NEVER the token values", () => {
140
- const red = redactSpawnResult(result);
141
- expect(red.session).toBe("aaron-agent");
142
- expect(red.tokens).toEqual([
143
- { resource: "aaron", scope: "agent:read agent:write", expiresAt: "2026-07-01T00:00:00Z" },
144
- { resource: "vault:default", scope: "vault:default:read", expiresAt: "2026-07-01T00:00:00Z" },
145
- ]);
146
- expect(red.mcpServers).toEqual(["agent-aaron", "vault-default"]);
147
- expect(red.egress).toEqual(["api.anthropic.com:443"]);
148
- expect(red.network).toBe("restricted");
149
- expect(red.filesystem).toBe("workspace");
150
- const wire = JSON.stringify(red);
151
- expect(wire).not.toContain("SECRET-AGENT-TOKEN");
152
- expect(wire).not.toContain("SECRET-VAULT-TOKEN");
153
- });
154
- test("an open result (no allowedDomains, no denyRead) → network 'open' + filesystem 'full', egress []", () => {
155
- const open: SpawnAgentResult = {
156
- ...result,
157
- wrapped: {
158
- ...result.wrapped,
159
- config: { network: { deniedDomains: [] }, filesystem: { denyRead: [], allowWrite: [], denyWrite: [] } } as unknown as SpawnAgentResult["wrapped"]["config"],
160
- },
161
- };
162
- const red = redactSpawnResult(open);
163
- expect(red.network).toBe("open");
164
- expect(red.filesystem).toBe("full");
165
- expect(red.egress).toEqual([]);
166
- });
167
- });
168
-
169
- describe("createRealAgentOps — list + kill", () => {
170
- function recorder(sessions: { name: string; attached: boolean }[]): { tmux: TmuxAdmin; killed: string[] } {
171
- const killed: string[] = [];
172
- const tmux: TmuxAdmin = {
173
- async listSessions() {
174
- return sessions;
175
- },
176
- async killSession(name: string) {
177
- killed.push(name);
178
- return sessions.some((s) => s.name === name);
179
- },
180
- };
181
- return { tmux, killed };
182
- }
183
-
184
- test("list maps tmux sessions to agent infos under the sessions dir", async () => {
185
- const { tmux } = recorder([{ name: "aaron-agent", attached: true }, { name: "other", attached: false }]);
186
- const ops = createRealAgentOps({ tmux, sessionsDirPath: "/tmp/s" });
187
- const list = await ops.list();
188
- expect(list.map((a) => a.name)).toEqual(["aaron"]);
189
- });
190
-
191
- test("kill targets `<name>-agent` and reports whether it existed", async () => {
192
- const { tmux, killed } = recorder([{ name: "aaron-agent", attached: false }]);
193
- const ops = createRealAgentOps({ tmux, sessionsDirPath: "/tmp/s" });
194
- expect(await ops.kill("aaron")).toEqual({ killed: true });
195
- expect(killed).toEqual(["aaron-agent"]);
196
- expect(await ops.kill("ghost")).toEqual({ killed: false });
197
- });
198
-
199
- test("kill rejects a non-slug name before touching tmux", async () => {
200
- const { tmux, killed } = recorder([]);
201
- const ops = createRealAgentOps({ tmux, sessionsDirPath: "/tmp/s" });
202
- await expect(ops.kill("../escape")).rejects.toThrow(SpawnRequestError);
203
- expect(killed).toEqual([]);
204
- });
205
- });
206
-
207
- describe("createRealAgentOps — restart (param recovery via persisted spec)", () => {
208
- function spawnDeps(sessionsDirPath: string): {
209
- deps: SpawnAgentDeps;
210
- launched: Array<{ name: string }>;
211
- } {
212
- const launched: Array<{ name: string }> = [];
213
- const launcher: TmuxLauncher = {
214
- async hasSession() {
215
- return false;
216
- },
217
- async newSession(opts) {
218
- launched.push({ name: opts.name });
219
- },
220
- async confirmDevChannelsPrompt() {
221
- return "already-running";
222
- },
223
- };
224
- const engine: SandboxEngine = {
225
- isSupportedPlatform: () => true,
226
- isSandboxingEnabled: () => true,
227
- async initialize() {},
228
- async wrapWithSandboxArgv(command: string) {
229
- return { argv: ["/bin/bash", "-c", command], env: {} };
230
- },
231
- async reset() {},
232
- };
233
- const fetchFn = (async (_u: string | URL | Request, init?: RequestInit) => {
234
- const body = JSON.parse(String(init?.body ?? "{}")) as { scope: string };
235
- return new Response(
236
- JSON.stringify({ jti: "j", token: "TOK", expires_at: "2026-09-01T00:00:00Z", scope: body.scope }),
237
- { status: 200, headers: { "content-type": "application/json" } },
238
- );
239
- }) as unknown as typeof fetch;
240
- const deps: SpawnAgentDeps = {
241
- hubOrigin: "https://hub.example.com",
242
- managerBearer: "MANAGER",
243
- channelUrl: "http://127.0.0.1:1941",
244
- vaultUrl: "http://127.0.0.1:1940",
245
- sessionsDir: sessionsDirPath,
246
- runtimeReadOnly: [],
247
- resolveClaudeToken: () => "OAUTH-PLACEHOLDER",
248
- resolveChannelEnv: () => ({ GH_TOKEN: "ghp_FROM-STORE" }),
249
- sandboxEngine: engine,
250
- tmux: launcher,
251
- fetchFn,
252
- parentEnv: { PATH: "/usr/bin" },
253
- claudeBin: "claude",
254
- };
255
- return { deps, launched };
256
- }
257
-
258
- test("recovers the persisted spec, kills the old session, re-spawns it", async () => {
259
- const sessionsDirPath = mkdtempSync(join(tmpdir(), "restart-ops-"));
260
- try {
261
- const spec: AgentSpec = { name: "aaron", channels: ["aaron"], network: "open" };
262
- persistSpec(sessionWorkspace(sessionsDirPath, "aaron"), spec);
263
-
264
- const killed: string[] = [];
265
- const tmux: TmuxAdmin = {
266
- async listSessions() {
267
- return [{ name: "aaron-agent", attached: false }];
268
- },
269
- async killSession(name) {
270
- killed.push(name);
271
- return true;
272
- },
273
- };
274
- const { deps, launched } = spawnDeps(sessionsDirPath);
275
- const ops = createRealAgentOps({ tmux, sessionsDirPath, depsFactory: () => deps });
276
-
277
- const result = await ops.restart("aaron");
278
- expect(killed).toEqual(["aaron-agent"]);
279
- expect(launched).toEqual([{ name: "aaron-agent" }]);
280
- expect(result.killed).toBe(true);
281
- expect(result.session).toBe("aaron-agent");
282
- expect(JSON.stringify(result)).not.toContain("TOK");
283
- } finally {
284
- rmSync(sessionsDirPath, { recursive: true, force: true });
285
- }
286
- });
287
-
288
- test("a missing persisted spec → SpawnRequestError (kill not attempted)", async () => {
289
- const sessionsDirPath = mkdtempSync(join(tmpdir(), "restart-nospec-"));
290
- try {
291
- const killed: string[] = [];
292
- const tmux: TmuxAdmin = {
293
- async listSessions() {
294
- return [];
295
- },
296
- async killSession(name) {
297
- killed.push(name);
298
- return true;
299
- },
300
- };
301
- const { deps } = spawnDeps(sessionsDirPath);
302
- const ops = createRealAgentOps({ tmux, sessionsDirPath, depsFactory: () => deps });
303
- await expect(ops.restart("ghost")).rejects.toThrow(SpawnRequestError);
304
- await expect(ops.restart("ghost")).rejects.toThrow(/no persisted spec/);
305
- expect(killed).toEqual([]);
306
- } finally {
307
- rmSync(sessionsDirPath, { recursive: true, force: true });
308
- }
309
- });
310
-
311
- test("restart rejects a non-slug name before touching anything", async () => {
312
- const tmux: TmuxAdmin = {
313
- async listSessions() {
314
- return [];
315
- },
316
- async killSession() {
317
- return false;
318
- },
319
- };
320
- const { deps } = spawnDeps("/tmp/s");
321
- const ops = createRealAgentOps({ tmux, sessionsDirPath: "/tmp/s", depsFactory: () => deps });
322
- await expect(ops.restart("../escape")).rejects.toThrow(SpawnRequestError);
323
- });
324
- });