@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
@@ -1,152 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- mintScopedToken,
4
- agentScope,
5
- vaultScope,
6
- MintError,
7
- type MintTokenDeps,
8
- } from "./mint-token.ts";
9
-
10
- /** A fake hub mint endpoint. Records the request; returns a scripted response. */
11
- function fakeHub(
12
- handler: (body: Record<string, unknown>, headers: Headers) => { status: number; json: unknown },
13
- ): { fetchFn: typeof fetch; calls: Array<{ body: Record<string, unknown>; auth: string | null }> } {
14
- const calls: Array<{ body: Record<string, unknown>; auth: string | null }> = [];
15
- const fetchFn = (async (url: string | URL | Request, init?: RequestInit) => {
16
- const headers = new Headers(init?.headers);
17
- const body = JSON.parse(String(init?.body ?? "{}")) as Record<string, unknown>;
18
- calls.push({ body, auth: headers.get("authorization") });
19
- const { status, json } = handler(body, headers);
20
- return new Response(JSON.stringify(json), {
21
- status,
22
- headers: { "content-type": "application/json" },
23
- });
24
- }) as unknown as typeof fetch;
25
- return { fetchFn, calls };
26
- }
27
-
28
- function depsWith(fetchFn: typeof fetch): MintTokenDeps {
29
- return { hubOrigin: "https://hub.example.com", managerBearer: "MANAGER-BEARER", fetchFn };
30
- }
31
-
32
- describe("scope string helpers", () => {
33
- test("agentScope", () => {
34
- expect(agentScope({ write: false })).toBe("agent:read");
35
- expect(agentScope({ write: true })).toBe("agent:read agent:write");
36
- });
37
- test("vaultScope", () => {
38
- expect(vaultScope("default", "read")).toBe("vault:default:read");
39
- expect(vaultScope("work", "write")).toBe("vault:work:write");
40
- });
41
- });
42
-
43
- describe("mintScopedToken — happy path", () => {
44
- test("POSTs to /api/auth/mint-token with the manager bearer + scope, returns the token", async () => {
45
- const hub = fakeHub((body) => ({
46
- status: 200,
47
- json: {
48
- jti: "j1",
49
- token: "MINTED-TOKEN",
50
- expires_at: "2026-09-01T00:00:00Z",
51
- scope: body.scope,
52
- },
53
- }));
54
- const res = await mintScopedToken({ scope: "agent:read agent:write" }, depsWith(hub.fetchFn));
55
- expect(res.token).toBe("MINTED-TOKEN");
56
- expect(res.jti).toBe("j1");
57
- // Presented the MANAGER's bearer (the attenuation principal).
58
- expect(hub.calls[0]!.auth).toBe("Bearer MANAGER-BEARER");
59
- expect(hub.calls[0]!.body.scope).toBe("agent:read agent:write");
60
- });
61
-
62
- test("passes audience + permissions (scoped_tags) through to the hub", async () => {
63
- const hub = fakeHub(() => ({
64
- status: 200,
65
- json: { jti: "j", token: "T", expires_at: "", scope: "vault:default:read" },
66
- }));
67
- await mintScopedToken(
68
- {
69
- scope: "vault:default:read",
70
- audience: "vault.default",
71
- permissions: { scoped_tags: ["agent/message"] },
72
- },
73
- depsWith(hub.fetchFn),
74
- );
75
- expect(hub.calls[0]!.body.audience).toBe("vault.default");
76
- expect(hub.calls[0]!.body.permissions).toEqual({ scoped_tags: ["agent/message"] });
77
- });
78
- });
79
-
80
- describe("mintScopedToken — attenuation + error surfacing", () => {
81
- test("CAPABILITY ATTENUATION: the hub's 400 invalid_scope (over-broad request) becomes a MintError", async () => {
82
- // Simulate the hub's canGrant guard: a vault:default:read manager bearer
83
- // requests vault:default:write → the hub refuses 400 invalid_scope. The
84
- // attenuation bound lives in the hub; this asserts the client surfaces the
85
- // refusal as a hard error (never launches a session with an ungrantable cred).
86
- const hub = fakeHub((body) => {
87
- // Manager bearer in this scenario only holds vault:default:read; a write
88
- // request exceeds it → hub returns 400.
89
- if (String(body.scope).includes(":write")) {
90
- return {
91
- status: 400,
92
- json: {
93
- error: "invalid_scope",
94
- error_description:
95
- "scope vault:default:write is not grantable by this bearer; use OAuth flow or operator rotation",
96
- },
97
- };
98
- }
99
- return { status: 200, json: { jti: "j", token: "ok", expires_at: "", scope: body.scope } };
100
- });
101
-
102
- // The grantable read mint succeeds.
103
- const ok = await mintScopedToken({ scope: "vault:default:read" }, depsWith(hub.fetchFn));
104
- expect(ok.token).toBe("ok");
105
-
106
- // The over-broad write mint is refused — surfaced as a MintError carrying the code.
107
- let err: unknown;
108
- try {
109
- await mintScopedToken({ scope: "vault:default:write" }, depsWith(hub.fetchFn));
110
- } catch (e) {
111
- err = e;
112
- }
113
- expect(err).toBeInstanceOf(MintError);
114
- expect((err as MintError).status).toBe(400);
115
- expect((err as MintError).code).toBe("invalid_scope");
116
- });
117
-
118
- test("a 403 insufficient_scope (no minting authority) becomes a MintError", async () => {
119
- const hub = fakeHub(() => ({
120
- status: 403,
121
- json: { error: "insufficient_scope", error_description: "bearer holds no minting authority" },
122
- }));
123
- let err: unknown;
124
- try {
125
- await mintScopedToken({ scope: "agent:read" }, depsWith(hub.fetchFn));
126
- } catch (e) {
127
- err = e;
128
- }
129
- expect(err).toBeInstanceOf(MintError);
130
- expect((err as MintError).status).toBe(403);
131
- expect((err as MintError).code).toBe("insufficient_scope");
132
- });
133
-
134
- test("a 200 with no token becomes a MintError (never returns a credential-less success)", async () => {
135
- const hub = fakeHub(() => ({ status: 200, json: { jti: "j", expires_at: "" } }));
136
- await expect(mintScopedToken({ scope: "agent:read" }, depsWith(hub.fetchFn))).rejects.toBeInstanceOf(MintError);
137
- });
138
-
139
- test("a network failure to reach the hub becomes a MintError (status 0)", async () => {
140
- const fetchFn = (async () => {
141
- throw new Error("ECONNREFUSED");
142
- }) as unknown as typeof fetch;
143
- let err: unknown;
144
- try {
145
- await mintScopedToken({ scope: "agent:read" }, depsWith(fetchFn));
146
- } catch (e) {
147
- err = e;
148
- }
149
- expect(err).toBeInstanceOf(MintError);
150
- expect((err as MintError).status).toBe(0);
151
- });
152
- });
@@ -1,158 +0,0 @@
1
- /**
2
- * `.parachute/module.json` manifest contract tests.
3
- *
4
- * Moved out of the (now-deleted) admin-ui.test.ts in Phase 4c — the admin PAGE
5
- * retired into the SPA, but the manifest declarations it validated (modular-UI
6
- * fields, channel events, vault-trigger actions, connectionTemplates, the
7
- * existing name/port/scopes contract) are still load-bearing for the hub.
8
- */
9
- import { describe, expect, test } from "bun:test";
10
- import { join } from "node:path";
11
-
12
- describe("module.json — modular-UI declaration", () => {
13
- // The manifest sits at <repo>/.parachute/module.json; this test file is in
14
- // <repo>/src, so go up one.
15
- const manifestPath = join(import.meta.dir, "..", ".parachute", "module.json");
16
-
17
- test("parses as JSON and carries the modular-UI fields (SPA mount after Phase 4c)", async () => {
18
- const raw = await Bun.file(manifestPath).text();
19
- const m = JSON.parse(raw) as Record<string, unknown>;
20
- // Phase 4c retired the server-rendered config page; configUiUrl now points
21
- // at the SPA app mount the hub frames.
22
- expect(m.configUiUrl).toBe("/agent/app/");
23
- expect(m.focus).toBe("experimental");
24
- expect(m.adminCapabilities).toEqual(["config"]);
25
- });
26
-
27
- test("declares the channel events (message.received / message.sent)", async () => {
28
- const m = JSON.parse(await Bun.file(manifestPath).text()) as {
29
- events?: Array<{ key: string; title: string }>;
30
- };
31
- const keys = (m.events ?? []).map((e) => e.key);
32
- expect(keys).toContain("message.received");
33
- expect(keys).toContain("message.sent");
34
- for (const e of m.events ?? []) expect(typeof e.title).toBe("string");
35
- });
36
-
37
- test("declares the message.deliver action with a vault-trigger provision", async () => {
38
- const m = JSON.parse(await Bun.file(manifestPath).text()) as {
39
- actions?: Array<{ key: string; title: string; provision?: { type?: string } }>;
40
- };
41
- const deliver = (m.actions ?? []).find((a) => a.key === "message.deliver");
42
- expect(deliver).toBeDefined();
43
- expect(deliver?.provision?.type).toBe("vault-trigger");
44
- });
45
-
46
- test("message.deliver declares the hub-connection wiring (endpoint + scope)", async () => {
47
- // The hub's general Connections engine (P5) wires a `vault-trigger` action
48
- // GENERICALLY: the webhook the vault calls back on is derived from the sink
49
- // action's `endpoint` (hub-proxied under the module's mount), and the bearer
50
- // the vault re-presents is minted at the action's declared `scope` — NOT a
51
- // channel-hardcoded path in hub code. So the deliver action must carry both.
52
- const m = JSON.parse(await Bun.file(manifestPath).text()) as {
53
- actions?: Array<{ key: string; endpoint?: string; scope?: string }>;
54
- };
55
- const deliver = (m.actions ?? []).find((a) => a.key === "message.deliver");
56
- expect(deliver?.endpoint).toBe("/api/vault/inbound");
57
- expect(deliver?.scope).toBe("agent:send");
58
- });
59
-
60
- test("declares a connectionTemplate for the parameterized link-to-vault connection (R2)", async () => {
61
- const m = JSON.parse(await Bun.file(manifestPath).text()) as {
62
- connectionTemplates?: Array<{
63
- key: string;
64
- requestedBy?: string;
65
- source?: { module?: string; event?: string; filter?: { tags?: string[] } };
66
- sink?: { module?: string; action?: string };
67
- parameters?: Array<{ key: string; target: string }>;
68
- }>;
69
- };
70
- const tmpl = (m.connectionTemplates ?? []).find((t) => t.key === "link-to-vault");
71
- expect(tmpl).toBeDefined();
72
- // The module declares WHAT it wants: vault.note.created (inbound tag) →
73
- // agent.message.deliver, labeled module-initiated.
74
- expect(tmpl?.requestedBy).toBe("agent");
75
- expect(tmpl?.source?.module).toBe("vault");
76
- expect(tmpl?.source?.event).toBe("note.created");
77
- expect(tmpl?.source?.filter?.tags).toContain("agent/message/inbound");
78
- expect(tmpl?.sink?.module).toBe("agent");
79
- expect(tmpl?.sink?.action).toBe("message.deliver");
80
- // It's PARAMETERIZED — the operator picks the vault + names the channel.
81
- const paramKeys = (tmpl?.parameters ?? []).map((p) => p.key);
82
- expect(paramKeys).toContain("vault");
83
- expect(paramKeys).toContain("channel");
84
- // The parameters point at the connection-body targets the UI fills in.
85
- const vaultParam = (tmpl?.parameters ?? []).find((p) => p.key === "vault");
86
- expect(vaultParam?.target).toBe("source.vault");
87
- const channelParam = (tmpl?.parameters ?? []).find((p) => p.key === "channel");
88
- expect(channelParam?.target).toBe("sink.params.channel");
89
- });
90
-
91
- test("declares the definition.reload action with a vault-trigger provision (Connector 1)", async () => {
92
- // Make a vault #agent/definition change flow LIVE into the registry: the
93
- // hub provisions a vault trigger that webhooks /api/vault/agent-def with an
94
- // agent:send bearer, and the daemon re-reads + re-instantiates that one def.
95
- const m = JSON.parse(await Bun.file(manifestPath).text()) as {
96
- actions?: Array<{ key: string; provision?: { type?: string }; endpoint?: string; scope?: string }>;
97
- };
98
- const reload = (m.actions ?? []).find((a) => a.key === "definition.reload");
99
- expect(reload).toBeDefined();
100
- expect(reload?.provision?.type).toBe("vault-trigger");
101
- // Wiring mirrors message.deliver: the webhook endpoint the daemon already
102
- // serves, and the agent:send scope that endpoint gates on (daemon.ts).
103
- expect(reload?.endpoint).toBe("/api/vault/agent-def");
104
- expect(reload?.scope).toBe("agent:send");
105
- });
106
-
107
- test("declares TWO def-reload templates (created + updated) — the hub binds one event per connection", async () => {
108
- // A connection carries a single `source.event` (admin-connections.ts:
109
- // eventsForSourceEvent maps one note.<verb> → one trigger verb), so create
110
- // and edit reactivity are two connections / two templates. note.deleted is
111
- // deliberately ABSENT — the hub rejects it (Connector 2, platform-blocked).
112
- const m = JSON.parse(await Bun.file(manifestPath).text()) as {
113
- connectionTemplates?: Array<{
114
- key: string;
115
- requestedBy?: string;
116
- source?: { module?: string; event?: string; filter?: { tags?: string[] } };
117
- sink?: { module?: string; action?: string };
118
- parameters?: Array<{ key: string; target: string }>;
119
- }>;
120
- };
121
- const templates = m.connectionTemplates ?? [];
122
- const onCreate = templates.find((t) => t.key === "reload-defs-on-create");
123
- const onEdit = templates.find((t) => t.key === "reload-defs-on-edit");
124
- expect(onCreate).toBeDefined();
125
- expect(onEdit).toBeDefined();
126
-
127
- for (const tmpl of [onCreate, onEdit]) {
128
- expect(tmpl?.requestedBy).toBe("agent");
129
- expect(tmpl?.source?.module).toBe("vault");
130
- // Filters on the def tag — and ONLY that tag (no inbound-message keys).
131
- expect(tmpl?.source?.filter?.tags).toEqual(["agent/definition"]);
132
- expect(tmpl?.sink?.module).toBe("agent");
133
- expect(tmpl?.sink?.action).toBe("definition.reload");
134
- // Parameterized: the operator picks which def-vault. No channel param
135
- // (a def-vault connection has no reply path — it's read-driven reload).
136
- const paramKeys = (tmpl?.parameters ?? []).map((p) => p.key);
137
- expect(paramKeys).toEqual(["vault"]);
138
- const vaultParam = (tmpl?.parameters ?? []).find((p) => p.key === "vault");
139
- expect(vaultParam?.target).toBe("source.vault");
140
- }
141
-
142
- // The two halves differ ONLY in the source event.
143
- expect(onCreate?.source?.event).toBe("note.created");
144
- expect(onEdit?.source?.event).toBe("note.updated");
145
- // No template subscribes deleted (would 400 at the hub).
146
- expect(templates.some((t) => t.source?.event === "note.deleted")).toBe(false);
147
- });
148
-
149
- test("preserves the existing manifest contract (name, port, uiUrl, scopes)", async () => {
150
- const m = JSON.parse(await Bun.file(manifestPath).text()) as Record<string, unknown>;
151
- expect(m.name).toBe("agent");
152
- expect(m.port).toBe(1941);
153
- // Phase 4c: uiUrl points at the SPA app root (the retired /home page is gone).
154
- expect(m.uiUrl).toBe("/agent/app/");
155
- const scopes = m.scopes as { defines?: string[] } | undefined;
156
- expect(scopes?.defines).toContain("agent:admin");
157
- });
158
- });