@openparachute/agent 0.2.3-rc.2 → 0.2.3-rc.4

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/package.json +4 -1
  2. package/src/transports/vault.ts +19 -1
  3. package/web/ui/dist/assets/{index-5KEwEhfi.js → index-0e7eQymr.js} +1 -1
  4. package/web/ui/dist/assets/index-tvKbxee4.css +1 -0
  5. package/web/ui/dist/index.html +2 -2
  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 -2012
  56. package/src/ui-kit.test.ts +0 -178
  57. package/web/ui/dist/assets/index-C-iWdFFV.css +0 -1
  58. package/web/ui/tsconfig.json +0 -21
@@ -0,0 +1 @@
1
+ :root{--bg: #faf8f4;--bg-soft: #f3f0ea;--fg: #2c2a26;--fg-muted: #6b6860;--fg-dim: #9a9690;--accent: #4a7c59;--accent-soft: rgba(74, 124, 89, .08);--accent-hover: #3d6849;--border: #e4e0d8;--border-light: #ece9e2;--card-bg: #ffffff;--error: #a3392b;--error-soft: rgba(163, 57, 43, .08);--warn: #b08023;--warn-soft: rgba(176, 128, 35, .08);--success: #3d6849;--success-soft: rgba(61, 104, 73, .08);--font-serif: Georgia, "Times New Roman", serif;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;--font-mono: ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", monospace;font-family:var(--font-sans)}*{box-sizing:border-box}html,body{margin:0;padding:0;background:var(--bg);color:var(--fg)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;background:var(--accent);color:#fff;border:0;border-radius:6px;padding:.45rem .9rem;cursor:pointer;transition:background .15s ease}button:hover{background:var(--accent-hover)}button:disabled{opacity:.5;cursor:not-allowed}button.secondary{background:#fff;color:var(--fg);border:1px solid var(--border)}button.secondary:hover{background:var(--bg-soft)}code{font-family:var(--font-mono);font-size:.85em;background:var(--bg-soft);padding:.1em .3em;border-radius:3px}.page{max-width:960px;margin:0 auto;padding:1.5rem 1.5rem 6rem}.nav{display:flex;flex-wrap:wrap;gap:.6rem 1rem;align-items:center;padding-bottom:1rem;border-bottom:1px solid var(--border);margin-bottom:2rem}.nav .brand{font-weight:600;font-family:var(--font-serif);font-size:1.15rem;margin-right:auto;display:inline-flex;align-items:center;gap:.45rem;color:var(--accent);text-decoration:none}.nav .brand:hover{color:var(--accent-hover);text-decoration:none}.nav .brand-wordmark{color:var(--fg);letter-spacing:-.005em}.nav .brand .sub{color:var(--fg-dim);font-size:.78rem;font-weight:400;margin-left:.4rem;font-family:var(--font-sans)}.nav a{color:var(--fg-muted);font-size:.95rem}.nav a:hover{text-decoration:none;color:var(--fg)}.nav a.nav-link-active{color:var(--accent);font-weight:500;text-decoration:underline;text-underline-offset:.3em;text-decoration-thickness:2px}h1{margin:0 0 .5rem;font-family:var(--font-serif);font-size:1.85rem;font-weight:400;letter-spacing:-.01em;line-height:1.2;color:var(--fg)}h2{margin:0 0 1rem;font-size:1.4rem;font-weight:500}h3{margin:0 0 .5rem;font-size:1.05rem;font-weight:600}.muted{color:var(--fg-muted);font-size:.92rem}.dim{color:var(--fg-dim);font-size:.85rem}.lede{color:var(--fg-muted);font-size:.95rem;margin:0 0 1.5rem;max-width:60ch}.error-banner{background:var(--error-soft);border:1px solid var(--error);color:var(--error);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.9rem}.info-banner{background:var(--accent-soft);border:1px solid var(--accent);color:var(--fg);padding:.65rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.88rem}.empty{border:1px dashed var(--border);border-radius:10px;padding:2rem 1.5rem;text-align:center;color:var(--fg-muted);background:var(--card-bg)}.loading{color:var(--fg-muted);padding:1rem 0;font-size:.92rem}.card{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:1.1rem 1.25rem;box-shadow:0 1px 2px #2c2a260a,0 8px 24px #2c2a260d;margin-bottom:1.5rem}.section-head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:.75rem}.section-head .count{color:var(--fg-dim);font-size:.85rem}table{width:100%;border-collapse:collapse;font-size:.88rem}th,td{text-align:left;padding:.6rem .7rem;border-bottom:1px solid var(--border);vertical-align:middle}th{color:var(--fg-muted);font-weight:500;font-size:.78rem;text-transform:uppercase;letter-spacing:.03em}tr.agent-row{cursor:pointer}tr.agent-row:hover{background:var(--bg-soft)}tr.agent-row.selected{background:var(--accent-soft)}.cell-name{font-weight:600;color:var(--fg)}.cell-dim{color:var(--fg-dim)}.pill{display:inline-block;padding:.1rem .5rem;border-radius:999px;font-size:.72rem;font-weight:600;letter-spacing:.02em;border:1px solid var(--border);color:var(--fg-muted);background:var(--bg-soft)}.pill.backend-programmatic{color:var(--accent);background:var(--accent-soft);border-color:transparent}.pill.backend-channel{color:var(--warn);background:var(--warn-soft);border-color:transparent}.pill.status-enabled,.pill.status-idle{color:var(--success);background:var(--success-soft);border-color:transparent}.pill.status-working{color:var(--accent);background:var(--accent-soft);border-color:transparent}.pill.status-pending,.pill.status-queued{color:var(--warn);background:var(--warn-soft);border-color:transparent}.pill.status-error{color:var(--error);background:var(--error-soft);border-color:transparent}.detail{background:var(--card-bg);border:1px solid var(--border);border-radius:12px;padding:1.25rem 1.4rem;margin-bottom:1.5rem;box-shadow:0 1px 2px #2c2a260a,0 8px 24px #2c2a260d}.detail-head{display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;margin-bottom:.9rem}.detail-head h2{margin:0;font-size:1.25rem}.detail-grid{display:grid;grid-template-columns:max-content 1fr;gap:.45rem 1.2rem;font-size:.9rem;margin-bottom:1rem}.detail-grid dt{color:var(--fg-muted);font-weight:500}.detail-grid dd{margin:0;color:var(--fg);word-break:break-word}.detail-prompt{background:var(--bg-soft);border:1px solid var(--border-light);border-radius:8px;padding:.75rem .9rem;font-family:var(--font-mono);font-size:.82rem;white-space:pre-wrap;color:var(--fg);margin:.3rem 0 1rem}.detail-note{font-size:.82rem;color:var(--fg-dim);margin:.5rem 0 0}.detail-close{margin-left:auto;background:#fff;color:var(--fg-muted);border:1px solid var(--border);padding:.3rem .7rem;font-size:.82rem}.detail-close:hover{background:var(--bg-soft);color:var(--fg)}.tag-list{display:flex;flex-wrap:wrap;gap:.35rem}.tag{font-family:var(--font-mono);font-size:.76rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:4px;padding:.05rem .35rem;color:var(--fg-muted)}.section-head-actions{display:inline-flex;align-items:center;gap:.9rem}.button-link{display:inline-block;background:var(--accent);color:#fff;border-radius:6px;padding:.4rem .85rem;font-size:.85rem;font-weight:500;transition:background .15s ease}.button-link:hover{background:var(--accent-hover);color:#fff;text-decoration:none}.field{display:block;border:0;margin:0 0 1.25rem;padding:0}.field>label,.field>legend{display:block;font-weight:500;font-size:.9rem;color:var(--fg);margin-bottom:.35rem;padding:0}.field input[type=text],.field select,.field textarea{width:100%;font:inherit;font-size:.9rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:.5rem .6rem}.field textarea{font-family:var(--font-mono);font-size:.82rem;resize:vertical}.field input[type=text]:focus,.field select:focus,.field textarea:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.field-hint{color:var(--fg-dim);font-size:.8rem;margin:.3rem 0 0}.field-error{color:var(--error);font-size:.8rem;margin:.3rem 0 0}.radio-row{display:flex;align-items:flex-start;gap:.6rem;border:1px solid var(--border);border-radius:8px;padding:.65rem .8rem;margin-bottom:.5rem;cursor:pointer;transition:border-color .15s ease,background .15s ease}.radio-row:hover{background:var(--bg-soft)}.radio-row.selected{border-color:var(--accent);background:var(--accent-soft)}.radio-row input[type=radio]{margin-top:.2rem;accent-color:var(--accent)}.radio-body{display:flex;flex-direction:column;gap:.15rem}.radio-label{font-weight:500;font-size:.9rem;color:var(--fg)}.radio-help{font-size:.8rem;color:var(--fg-muted)}.advanced{margin:0 0 1.25rem}.advanced>summary{cursor:pointer;font-size:.88rem;font-weight:500;color:var(--fg-muted);margin-bottom:.75rem}.advanced>summary:hover{color:var(--fg)}.form-actions{display:flex;align-items:center;gap:1rem}.cancel-link{color:var(--fg-muted);font-size:.9rem}.success-banner{background:var(--success-soft);border:1px solid var(--success);color:var(--success);padding:.75rem 1rem;border-radius:8px;margin-bottom:1rem;font-size:.95rem}.snippet-row{display:flex;align-items:stretch;gap:.6rem;margin:.5rem 0}.snippet{flex:1;font-family:var(--font-mono);font-size:.8rem;background:var(--bg-soft);border:1px solid var(--border-light);border-radius:6px;padding:.6rem .75rem;color:var(--fg);white-space:pre-wrap;word-break:break-all}.detail-actions{display:flex;gap:.6rem;margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border-light)}button.button-danger{background:#fff;color:var(--error);border:1px solid var(--error)}button.button-danger:hover{background:var(--error-soft)}button.button-danger:disabled{opacity:.5;cursor:not-allowed}.confirm-box{margin-top:1.25rem;padding:1rem;border:1px solid var(--error);border-radius:8px;background:var(--error-soft)}.confirm-prompt{margin:0 0 .75rem;font-size:.9rem;color:var(--fg)}.confirm-inline{display:inline-flex;align-items:center;gap:.6rem}.inline-form{margin:.75rem 0 1rem}.schedules,.detail-section{margin-top:1.25rem;padding-top:1rem;border-top:1px solid var(--border-light)}.schedules .section-head h3,.detail-section .section-head h3{margin:0;font-size:1.05rem}.schedule-presets{display:flex;flex-wrap:wrap;gap:.4rem;margin-top:.45rem}.schedule-presets button{font-size:.78rem;padding:.2rem .55rem}.schedule-row-actions{display:inline-flex;align-items:center;gap:.6rem}.schedule-status{font-size:.82rem;color:var(--fg-muted);margin:.5rem 0 0}.chat-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:.75rem 1rem;margin-bottom:1rem}.chat-head h1{margin:0}.chat-picker{display:inline-flex;align-items:center;gap:.5rem}.chat-picker-label{font-size:.82rem;color:var(--fg-muted)}.chat-picker select{font:inherit;font-size:.88rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:6px;padding:.35rem .5rem}.chat-status{font-size:.8rem;color:var(--fg-muted);margin-left:auto}.chat-status.status-live{color:var(--success)}.chat-status.status-err{color:var(--error)}.transcript{display:flex;flex-direction:column;gap:.5rem;height:60vh;min-height:18rem;overflow-y:auto;padding:1rem;background:var(--card-bg);border:1px solid var(--border);border-radius:12px 12px 0 0}.transcript .msg{max-width:78%;padding:.5rem .75rem;border-radius:12px;font-size:.9rem;line-height:1.4;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:anywhere}.transcript .msg.you{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:4px}.transcript .msg.them{align-self:flex-start;background:var(--bg-soft);color:var(--fg);border:1px solid var(--border);border-bottom-left-radius:4px}.transcript .msg.sys{align-self:center;background:transparent;color:var(--fg-muted);font-size:.8rem;font-style:italic;max-width:90%;text-align:center}.transcript .msg.live{border-style:dashed;animation:chatLivePulse 1.4s ease-in-out infinite}@keyframes chatLivePulse{0%,to{opacity:1}50%{opacity:.6}}.transcript .msg.live.errored{border-color:var(--error);color:var(--error);animation:none}.transcript .live-tools{display:flex;flex-wrap:wrap;gap:.25rem;margin-top:.4rem}.transcript .tool-chip{font-family:var(--font-mono);font-size:.72rem;padding:.05rem .45rem;border-radius:10px;background:var(--card-bg);color:var(--fg-muted);border:1px solid var(--border)}.transcript .live-working{margin-top:.4rem;font-size:.75rem;color:var(--fg-muted);font-style:italic}.composer{display:flex;gap:.6rem;padding:.75rem 1rem;background:var(--card-bg);border:1px solid var(--border);border-top:0;border-radius:0 0 12px 12px;margin-bottom:1.5rem}.composer .chat-input{flex:1;font:inherit;font-size:.9rem;color:var(--fg);background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:.55rem .7rem;resize:none;max-height:7.5rem}.composer .chat-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.composer button{flex:0 0 auto;align-self:flex-end}
@@ -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-5KEwEhfi.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-0e7eQymr.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/agent/app/assets/index-tvKbxee4.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
- });