@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
@@ -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
- });