@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.
- package/package.json +4 -1
- package/src/transports/vault.ts +19 -1
- package/src/_parked/interactive-spawn.test.ts +0 -324
- package/src/_parked/interactive-spawn.ts +0 -701
- package/src/agent-defs.test.ts +0 -1504
- package/src/agent-mcp-config.test.ts +0 -115
- package/src/agents.test.ts +0 -360
- package/src/auth.test.ts +0 -46
- package/src/backends/attached-queue.test.ts +0 -376
- package/src/backends/programmatic.test.ts +0 -1715
- package/src/backends/registry.test.ts +0 -1494
- package/src/backends/stream-json.test.ts +0 -570
- package/src/channel-backend-wiring.test.ts +0 -237
- package/src/credentials.test.ts +0 -274
- package/src/cron.test.ts +0 -342
- package/src/daemon-agent-def-api.test.ts +0 -166
- package/src/daemon-agent-defs-api.test.ts +0 -953
- package/src/daemon-agent-env-api.test.ts +0 -338
- package/src/daemon-attached-queue-store.test.ts +0 -65
- package/src/daemon-config-api.test.ts +0 -962
- package/src/daemon-jobs-api.test.ts +0 -271
- package/src/daemon-vault-chat.test.ts +0 -250
- package/src/daemon.test.ts +0 -746
- package/src/def-vaults.test.ts +0 -136
- package/src/delivery-state.test.ts +0 -110
- package/src/effective-env.test.ts +0 -114
- package/src/grants.test.ts +0 -638
- package/src/hub-jwt.test.ts +0 -161
- package/src/jobs.test.ts +0 -245
- package/src/mcp-http.test.ts +0 -265
- package/src/mint-token.test.ts +0 -152
- package/src/module-manifest.test.ts +0 -158
- package/src/programmatic-wiring.test.ts +0 -838
- package/src/registry.test.ts +0 -227
- package/src/resolve-port.test.ts +0 -64
- package/src/routing.test.ts +0 -184
- package/src/runner.test.ts +0 -506
- package/src/sandbox/config.test.ts +0 -150
- package/src/sandbox/egress.test.ts +0 -113
- package/src/sandbox/live-seatbelt.test.ts +0 -277
- package/src/sandbox/mounts.test.ts +0 -154
- package/src/sandbox/sandbox.test.ts +0 -168
- package/src/services-manifest.test.ts +0 -106
- package/src/spa-serve.test.ts +0 -116
- package/src/spawn-agent-cli.test.ts +0 -172
- package/src/spawn-agent.test.ts +0 -1218
- package/src/spawn-deps.test.ts +0 -54
- package/src/terminal-assets.test.ts +0 -50
- package/src/terminal.test.ts +0 -530
- package/src/transports/http-ui.test.ts +0 -455
- package/src/transports/telegram.test.ts +0 -174
- package/src/transports/vault.test.ts +0 -2012
- package/src/ui-kit.test.ts +0 -178
- package/web/ui/tsconfig.json +0 -21
package/src/mint-token.test.ts
DELETED
|
@@ -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
|
-
});
|