@openparachute/agent 0.2.0 → 0.2.3-rc.10

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 (70) hide show
  1. package/.parachute/module.json +3 -3
  2. package/package.json +8 -1
  3. package/src/agent-defs.ts +9 -0
  4. package/src/auth.ts +182 -14
  5. package/src/backends/registry.ts +65 -27
  6. package/src/daemon.ts +311 -12
  7. package/src/def-vault-triggers.ts +317 -0
  8. package/src/preflight.ts +139 -0
  9. package/src/spawn-agent.ts +16 -0
  10. package/src/step-up.ts +316 -0
  11. package/src/terminal-ui.ts +73 -0
  12. package/src/transports/http-ui.ts +10 -8
  13. package/src/transports/vault.ts +40 -22
  14. package/src/ui-kit.ts +6 -3
  15. package/src/ui-ticket.ts +121 -0
  16. package/web/ui/dist/assets/index-Dhr5Kl_d.css +1 -0
  17. package/web/ui/dist/assets/index-Di5MmFZR.js +60 -0
  18. package/web/ui/dist/index.html +15 -0
  19. package/src/_parked/interactive-spawn.test.ts +0 -324
  20. package/src/_parked/interactive-spawn.ts +0 -701
  21. package/src/agent-defs.test.ts +0 -1504
  22. package/src/agent-mcp-config.test.ts +0 -115
  23. package/src/agents.test.ts +0 -360
  24. package/src/auth.test.ts +0 -46
  25. package/src/backends/attached-queue.test.ts +0 -376
  26. package/src/backends/programmatic.test.ts +0 -1715
  27. package/src/backends/registry.test.ts +0 -1494
  28. package/src/backends/stream-json.test.ts +0 -570
  29. package/src/channel-backend-wiring.test.ts +0 -237
  30. package/src/credentials.test.ts +0 -274
  31. package/src/cron.test.ts +0 -342
  32. package/src/daemon-agent-def-api.test.ts +0 -166
  33. package/src/daemon-agent-defs-api.test.ts +0 -953
  34. package/src/daemon-agent-env-api.test.ts +0 -338
  35. package/src/daemon-attached-queue-store.test.ts +0 -65
  36. package/src/daemon-config-api.test.ts +0 -962
  37. package/src/daemon-jobs-api.test.ts +0 -271
  38. package/src/daemon-vault-chat.test.ts +0 -250
  39. package/src/daemon.test.ts +0 -746
  40. package/src/def-vaults.test.ts +0 -136
  41. package/src/delivery-state.test.ts +0 -110
  42. package/src/effective-env.test.ts +0 -114
  43. package/src/grants.test.ts +0 -638
  44. package/src/hub-jwt.test.ts +0 -161
  45. package/src/jobs.test.ts +0 -245
  46. package/src/mcp-http.test.ts +0 -265
  47. package/src/mint-token.test.ts +0 -152
  48. package/src/module-manifest.test.ts +0 -158
  49. package/src/programmatic-wiring.test.ts +0 -838
  50. package/src/registry.test.ts +0 -227
  51. package/src/resolve-port.test.ts +0 -64
  52. package/src/routing.test.ts +0 -184
  53. package/src/runner.test.ts +0 -506
  54. package/src/sandbox/config.test.ts +0 -150
  55. package/src/sandbox/egress.test.ts +0 -113
  56. package/src/sandbox/live-seatbelt.test.ts +0 -277
  57. package/src/sandbox/mounts.test.ts +0 -154
  58. package/src/sandbox/sandbox.test.ts +0 -168
  59. package/src/services-manifest.test.ts +0 -106
  60. package/src/spa-serve.test.ts +0 -116
  61. package/src/spawn-agent-cli.test.ts +0 -172
  62. package/src/spawn-agent.test.ts +0 -1218
  63. package/src/spawn-deps.test.ts +0 -54
  64. package/src/terminal-assets.test.ts +0 -50
  65. package/src/terminal.test.ts +0 -530
  66. package/src/transports/http-ui.test.ts +0 -455
  67. package/src/transports/telegram.test.ts +0 -174
  68. package/src/transports/vault.test.ts +0 -2011
  69. package/src/ui-kit.test.ts +0 -178
  70. 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
- });