@openparachute/agent 0.2.3-rc.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 (54) hide show
  1. package/package.json +4 -1
  2. package/src/transports/vault.ts +19 -1
  3. package/src/_parked/interactive-spawn.test.ts +0 -324
  4. package/src/_parked/interactive-spawn.ts +0 -701
  5. package/src/agent-defs.test.ts +0 -1504
  6. package/src/agent-mcp-config.test.ts +0 -115
  7. package/src/agents.test.ts +0 -360
  8. package/src/auth.test.ts +0 -46
  9. package/src/backends/attached-queue.test.ts +0 -376
  10. package/src/backends/programmatic.test.ts +0 -1715
  11. package/src/backends/registry.test.ts +0 -1494
  12. package/src/backends/stream-json.test.ts +0 -570
  13. package/src/channel-backend-wiring.test.ts +0 -237
  14. package/src/credentials.test.ts +0 -274
  15. package/src/cron.test.ts +0 -342
  16. package/src/daemon-agent-def-api.test.ts +0 -166
  17. package/src/daemon-agent-defs-api.test.ts +0 -953
  18. package/src/daemon-agent-env-api.test.ts +0 -338
  19. package/src/daemon-attached-queue-store.test.ts +0 -65
  20. package/src/daemon-config-api.test.ts +0 -962
  21. package/src/daemon-jobs-api.test.ts +0 -271
  22. package/src/daemon-vault-chat.test.ts +0 -250
  23. package/src/daemon.test.ts +0 -746
  24. package/src/def-vaults.test.ts +0 -136
  25. package/src/delivery-state.test.ts +0 -110
  26. package/src/effective-env.test.ts +0 -114
  27. package/src/grants.test.ts +0 -638
  28. package/src/hub-jwt.test.ts +0 -161
  29. package/src/jobs.test.ts +0 -245
  30. package/src/mcp-http.test.ts +0 -265
  31. package/src/mint-token.test.ts +0 -152
  32. package/src/module-manifest.test.ts +0 -158
  33. package/src/programmatic-wiring.test.ts +0 -838
  34. package/src/registry.test.ts +0 -227
  35. package/src/resolve-port.test.ts +0 -64
  36. package/src/routing.test.ts +0 -184
  37. package/src/runner.test.ts +0 -506
  38. package/src/sandbox/config.test.ts +0 -150
  39. package/src/sandbox/egress.test.ts +0 -113
  40. package/src/sandbox/live-seatbelt.test.ts +0 -277
  41. package/src/sandbox/mounts.test.ts +0 -154
  42. package/src/sandbox/sandbox.test.ts +0 -168
  43. package/src/services-manifest.test.ts +0 -106
  44. package/src/spa-serve.test.ts +0 -116
  45. package/src/spawn-agent-cli.test.ts +0 -172
  46. package/src/spawn-agent.test.ts +0 -1218
  47. package/src/spawn-deps.test.ts +0 -54
  48. package/src/terminal-assets.test.ts +0 -50
  49. package/src/terminal.test.ts +0 -530
  50. package/src/transports/http-ui.test.ts +0 -455
  51. package/src/transports/telegram.test.ts +0 -174
  52. package/src/transports/vault.test.ts +0 -2012
  53. package/src/ui-kit.test.ts +0 -178
  54. package/web/ui/tsconfig.json +0 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openparachute/agent",
3
- "version": "0.2.3-rc.2",
3
+ "version": "0.2.3-rc.3",
4
4
  "description": "Vault-native agents for Claude Code — a #agent/definition note + an inbound message becomes a sandboxed claude turn; the reply is written back as a note. Messaging gateway on :1941.",
5
5
  "license": "AGPL-3.0",
6
6
  "type": "module",
@@ -24,6 +24,8 @@
24
24
  "test:spa": "cd web/ui && bun run test",
25
25
  "test:all": "bun run test && bun run test:spa",
26
26
  "test:e2e": "bun e2e/llm/run.ts",
27
+ "lint": "biome check .",
28
+ "lint:fix": "biome check --write .",
27
29
  "typecheck": "tsc --noEmit",
28
30
  "build:spa": "cd web/ui && bun install --frozen-lockfile && bun run build",
29
31
  "prepack": "bun run build:spa"
@@ -39,6 +41,7 @@
39
41
  "typescript": "^5"
40
42
  },
41
43
  "devDependencies": {
44
+ "@biomejs/biome": "^1.9.4",
42
45
  "@types/bun": "latest"
43
46
  },
44
47
  "repository": {
@@ -96,6 +96,17 @@ export interface VaultTransportConfig {
96
96
  webhookSecret?: string;
97
97
  /** Optional path prefix for written notes. Default `channel`. */
98
98
  notePathPrefix?: string;
99
+ /**
100
+ * Whether `start()` fires the best-effort `ensureSchema()` tag-schema upsert
101
+ * against the connected vault. Default `true` (back-compat — the daemon always
102
+ * declares the module's tag inheritance on connect). Tests that construct a
103
+ * transport with a fake token set this `false` so `start()` does NOT hit the
104
+ * live vault on 127.0.0.1:1940 (which 401s the fake token → ~one `console.warn`
105
+ * per schema entry of benign noise). The "tag both parent + child" write floor
106
+ * means a channel works regardless, so skipping the declaration is safe; it's
107
+ * only a setup optimization, not a runtime contract. See #32.
108
+ */
109
+ declareSchemaOnStart?: boolean;
99
110
  }
100
111
 
101
112
  /** The note shape the daemon hands `ingestInbound` (a subset of the trigger payload). */
@@ -605,6 +616,8 @@ export class VaultTransport implements Transport {
605
616
  */
606
617
  readonly webhookSecret?: string;
607
618
  private readonly pathPrefix: string;
619
+ /** See `VaultTransportConfig.declareSchemaOnStart`. Default `true`. */
620
+ private readonly declareSchemaOnStart: boolean;
608
621
 
609
622
  constructor(config: VaultTransportConfig) {
610
623
  if (!config.vault) {
@@ -621,6 +634,7 @@ export class VaultTransport implements Transport {
621
634
  this.token = config.token;
622
635
  this.webhookSecret = config.webhookSecret;
623
636
  this.pathPrefix = (config.notePathPrefix ?? DEFAULT_PATH_PREFIX).replace(/\/$/, "");
637
+ this.declareSchemaOnStart = config.declareSchemaOnStart ?? true;
624
638
  }
625
639
 
626
640
  /**
@@ -642,7 +656,11 @@ export class VaultTransport implements Transport {
642
656
  // "tag both parent + child" floor in the note writes is the fail-safe, so the
643
657
  // channel works even if this declaration never lands. Fire-and-forget — no
644
658
  // reason to delay the channel coming up on a schema upsert.
645
- void this.ensureSchema();
659
+ //
660
+ // Suppressible via `declareSchemaOnStart: false` — tests with a fake token
661
+ // set this so `start()` doesn't 401 against the live vault (benign warn noise,
662
+ // #32). The write floor makes the declaration optional anyway.
663
+ if (this.declareSchemaOnStart) void this.ensureSchema();
646
664
  }
647
665
 
648
666
  // -------------------------------------------------------------------------
@@ -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
- });