@openparachute/hub 0.7.0 → 0.7.2-rc.1
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/README.md +13 -14
- package/package.json +1 -1
- package/src/__tests__/account-setup.test.ts +276 -6
- package/src/__tests__/admin-agent-grants.test.ts +1547 -0
- package/src/__tests__/{admin-channel-token.test.ts → admin-agent-token.test.ts} +32 -32
- package/src/__tests__/admin-connections-credentials.test.ts +1324 -0
- package/src/__tests__/admin-connections.test.ts +211 -57
- package/src/__tests__/admin-csrf-belt.test.ts +7 -7
- package/src/__tests__/admin-lock.test.ts +600 -0
- package/src/__tests__/admin-module-token.test.ts +36 -8
- package/src/__tests__/admin-vaults.test.ts +8 -8
- package/src/__tests__/api-invites.test.ts +166 -6
- package/src/__tests__/api-modules-ops.test.ts +17 -16
- package/src/__tests__/api-modules.test.ts +35 -36
- package/src/__tests__/api-ready.test.ts +2 -2
- package/src/__tests__/audience-gate.test.ts +752 -0
- package/src/__tests__/clients.test.ts +91 -0
- package/src/__tests__/grants-store.test.ts +219 -0
- package/src/__tests__/hub-db.test.ts +36 -0
- package/src/__tests__/hub-server.test.ts +275 -5
- package/src/__tests__/invites.test.ts +64 -1
- package/src/__tests__/lifecycle.test.ts +238 -3
- package/src/__tests__/migrate.test.ts +1 -1
- package/src/__tests__/module-manifest.test.ts +11 -11
- package/src/__tests__/oauth-client.test.ts +446 -0
- package/src/__tests__/oauth-flows-store.test.ts +141 -0
- package/src/__tests__/oauth-handlers.test.ts +124 -26
- package/src/__tests__/operator-token.test.ts +2 -2
- package/src/__tests__/scope-explanations.test.ts +3 -3
- package/src/__tests__/serve-boot.test.ts +14 -14
- package/src/__tests__/serve.test.ts +26 -0
- package/src/__tests__/service-spec-discovery.test.ts +26 -18
- package/src/__tests__/services-manifest.test.ts +60 -48
- package/src/__tests__/setup-gate.test.ts +52 -3
- package/src/__tests__/setup-wizard.test.ts +86 -280
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/upgrade.test.ts +276 -0
- package/src/__tests__/vault-remove.test.ts +393 -0
- package/src/__tests__/ws-bridge.test.ts +573 -0
- package/src/__tests__/ws-connection-caps.test.ts +456 -0
- package/src/account-setup.ts +94 -23
- package/src/admin-agent-grants.ts +1365 -0
- package/src/admin-agent-token.ts +147 -0
- package/src/admin-connections.ts +980 -61
- package/src/admin-host-admin-token.ts +14 -1
- package/src/admin-lock.ts +281 -0
- package/src/admin-login-ui.ts +64 -15
- package/src/admin-module-token.ts +15 -7
- package/src/admin-vault-admin-token.ts +8 -1
- package/src/admin-vaults.ts +21 -12
- package/src/api-admin-lock.ts +335 -0
- package/src/api-invites.ts +92 -12
- package/src/api-modules-ops.ts +3 -2
- package/src/api-modules.ts +9 -9
- package/src/audience-gate.ts +268 -0
- package/src/chrome-strip.ts +8 -1
- package/src/cli.ts +13 -1
- package/src/clients.ts +88 -0
- package/src/commands/install.ts +7 -0
- package/src/commands/lifecycle.ts +187 -47
- package/src/commands/serve-boot.ts +5 -4
- package/src/commands/serve.ts +45 -19
- package/src/commands/setup.ts +4 -3
- package/src/commands/upgrade.ts +118 -2
- package/src/commands/vault-remove.ts +361 -0
- package/src/commands/wizard.ts +4 -4
- package/src/connections-store.ts +35 -5
- package/src/grants-store.ts +272 -0
- package/src/help.ts +17 -7
- package/src/host-admin-token-validation.ts +6 -2
- package/src/hub-db.ts +26 -1
- package/src/hub-server.ts +710 -39
- package/src/hub-settings.ts +23 -8
- package/src/invites.ts +69 -2
- package/src/jwt-sign.ts +11 -1
- package/src/module-manifest.ts +109 -2
- package/src/oauth-client.ts +497 -0
- package/src/oauth-flows-store.ts +163 -0
- package/src/oauth-handlers.ts +40 -13
- package/src/operator-token.ts +1 -1
- package/src/origin-check.ts +13 -5
- package/src/resource-binding.ts +4 -4
- package/src/scope-explanations.ts +3 -3
- package/src/service-spec.ts +56 -43
- package/src/services-manifest.ts +97 -0
- package/src/setup-wizard.ts +56 -240
- package/src/ws-bridge.ts +256 -0
- package/src/ws-connection-caps.ts +170 -0
- package/web/ui/dist/assets/index-B5AUE359.js +61 -0
- package/web/ui/dist/assets/{index-E_9wqjEm.css → index-DR6R8EFf.css} +1 -1
- package/web/ui/dist/index.html +2 -2
- package/src/admin-channel-token.ts +0 -135
- package/web/ui/dist/assets/index-C-XzMVqN.js +0 -61
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* P5) — `GET /api/connections/catalog`, `GET/POST /admin/connections`,
|
|
4
4
|
* `DELETE /admin/connections/:id`.
|
|
5
5
|
*
|
|
6
|
-
* The catalog is built from injected module manifests. The vault +
|
|
6
|
+
* The catalog is built from injected module manifests. The vault + agent HTTP
|
|
7
7
|
* calls are mocked via an injectable `fetchImpl` that records every request
|
|
8
8
|
* (method, URL, decoded bearer, parsed body) and returns scripted responses.
|
|
9
9
|
* Tokens are real (minted by the actual `signAccessToken`), so we can decode the
|
|
10
|
-
* JWT claims (scope/aud) the way
|
|
10
|
+
* JWT claims (scope/aud) the way agent/vault would.
|
|
11
11
|
*
|
|
12
12
|
* The whole point of the engine is GENERALITY: the webhook + scope come from the
|
|
13
13
|
* SINK ACTION's declaration, not hardcoded per module. The tests assert that —
|
|
@@ -39,7 +39,7 @@ import { rotateSigningKey } from "../signing-keys.ts";
|
|
|
39
39
|
import { createUser } from "../users.ts";
|
|
40
40
|
|
|
41
41
|
const HUB_ORIGIN = "https://hub.test";
|
|
42
|
-
const
|
|
42
|
+
const AGENT_ORIGIN = "http://127.0.0.1:1941";
|
|
43
43
|
const VAULT_ORIGIN = "http://127.0.0.1:1940";
|
|
44
44
|
|
|
45
45
|
interface Harness {
|
|
@@ -162,10 +162,10 @@ const VAULT_MANIFEST: ModuleManifest = {
|
|
|
162
162
|
};
|
|
163
163
|
|
|
164
164
|
const CHANNEL_MANIFEST: ModuleManifest = {
|
|
165
|
-
name: "
|
|
166
|
-
manifestName: "parachute-
|
|
165
|
+
name: "agent",
|
|
166
|
+
manifestName: "parachute-agent",
|
|
167
167
|
port: 1941,
|
|
168
|
-
paths: ["/
|
|
168
|
+
paths: ["/agent"],
|
|
169
169
|
health: "/health",
|
|
170
170
|
events: [{ key: "message.received", title: "A message arrived" }],
|
|
171
171
|
actions: [
|
|
@@ -173,24 +173,34 @@ const CHANNEL_MANIFEST: ModuleManifest = {
|
|
|
173
173
|
key: "message.deliver",
|
|
174
174
|
title: "Deliver an inbound message",
|
|
175
175
|
endpoint: "/api/vault/inbound",
|
|
176
|
-
scope: "
|
|
176
|
+
scope: "agent:send",
|
|
177
|
+
provision: { type: "vault-trigger" },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
// A second agent action that needs NO session reply path — a pure inbound
|
|
181
|
+
// webhook. Drives the regression test that the channel-reply prerequisite
|
|
182
|
+
// is gated on the ACTION (message.deliver), not the module (agent#117).
|
|
183
|
+
key: "definition.reload",
|
|
184
|
+
title: "Reload a changed agent definition",
|
|
185
|
+
endpoint: "/api/vault/agent-def",
|
|
186
|
+
scope: "agent:send",
|
|
177
187
|
provision: { type: "vault-trigger" },
|
|
178
188
|
},
|
|
179
189
|
],
|
|
180
|
-
// Mirrors the declaration
|
|
181
|
-
// D2) — drives the catalog `templates` round-trip pin below.
|
|
190
|
+
// Mirrors the declaration the agent module ships in its real module.json
|
|
191
|
+
// (boundary D2) — drives the catalog `templates` round-trip pin below.
|
|
182
192
|
connectionTemplates: [
|
|
183
193
|
{
|
|
184
194
|
key: "link-to-vault",
|
|
185
195
|
title: "Link a channel to a vault",
|
|
186
196
|
description: "Back a channel with a Parachute vault.",
|
|
187
|
-
requestedBy: "
|
|
197
|
+
requestedBy: "agent",
|
|
188
198
|
source: {
|
|
189
199
|
module: "vault",
|
|
190
200
|
event: "note.created",
|
|
191
|
-
filter: { tags: ["#
|
|
201
|
+
filter: { tags: ["#agent-message/inbound"] },
|
|
192
202
|
},
|
|
193
|
-
sink: { module: "
|
|
203
|
+
sink: { module: "agent", action: "message.deliver" },
|
|
194
204
|
parameters: [
|
|
195
205
|
{ key: "vault", target: "source.vault", title: "Vault" },
|
|
196
206
|
{ key: "channel", target: "sink.params.channel", title: "Channel name" },
|
|
@@ -231,7 +241,7 @@ function baseDeps(fetchImpl: typeof fetch, modules: InstalledModuleInfo[]): Conn
|
|
|
231
241
|
hubOrigin: HUB_ORIGIN,
|
|
232
242
|
modules,
|
|
233
243
|
resolveVaultOrigin: (v) => (v === "default" ? VAULT_ORIGIN : null),
|
|
234
|
-
|
|
244
|
+
agentOrigin: AGENT_ORIGIN,
|
|
235
245
|
storePath: harness.storePath,
|
|
236
246
|
fetchImpl,
|
|
237
247
|
};
|
|
@@ -256,14 +266,14 @@ describe("derivation units", () => {
|
|
|
256
266
|
test("whenFromFilter maps filter keys 1:1 to the trigger predicate", () => {
|
|
257
267
|
expect(
|
|
258
268
|
whenFromFilter({
|
|
259
|
-
tags: ["#
|
|
269
|
+
tags: ["#agent-message/inbound"],
|
|
260
270
|
has_metadata: ["channel"],
|
|
261
271
|
missing_metadata: ["channel_inbound_rendered_at"],
|
|
262
272
|
has_content: true,
|
|
263
273
|
ignored: "x",
|
|
264
274
|
}),
|
|
265
275
|
).toEqual({
|
|
266
|
-
tags: ["#
|
|
276
|
+
tags: ["#agent-message/inbound"],
|
|
267
277
|
has_metadata: ["channel"],
|
|
268
278
|
missing_metadata: ["channel_inbound_rendered_at"],
|
|
269
279
|
has_content: true,
|
|
@@ -272,8 +282,8 @@ describe("derivation units", () => {
|
|
|
272
282
|
});
|
|
273
283
|
|
|
274
284
|
test("buildWebhook joins origin + mount + endpoint, trimming slashes", () => {
|
|
275
|
-
expect(buildWebhook(`${HUB_ORIGIN}/`, "/
|
|
276
|
-
`${HUB_ORIGIN}/
|
|
285
|
+
expect(buildWebhook(`${HUB_ORIGIN}/`, "/agent", "/api/vault/inbound")).toBe(
|
|
286
|
+
`${HUB_ORIGIN}/agent/api/vault/inbound`,
|
|
277
287
|
);
|
|
278
288
|
expect(buildWebhook(HUB_ORIGIN, "widget", "hooks/incoming")).toBe(
|
|
279
289
|
`${HUB_ORIGIN}/widget/hooks/incoming`,
|
|
@@ -295,7 +305,7 @@ describe("GET /api/connections/catalog", () => {
|
|
|
295
305
|
filterSchema: { type: "object", properties: { tags: { type: "array" } } },
|
|
296
306
|
});
|
|
297
307
|
expect(cat.actions).toContainEqual({
|
|
298
|
-
module: "
|
|
308
|
+
module: "agent",
|
|
299
309
|
key: "message.deliver",
|
|
300
310
|
title: "Deliver an inbound message",
|
|
301
311
|
inputSchema: null,
|
|
@@ -307,17 +317,17 @@ describe("GET /api/connections/catalog", () => {
|
|
|
307
317
|
const cat = buildCatalog(modulesOf(VAULT_MANIFEST, CHANNEL_MANIFEST));
|
|
308
318
|
expect(cat.templates).toEqual([
|
|
309
319
|
{
|
|
310
|
-
module: "
|
|
320
|
+
module: "agent",
|
|
311
321
|
key: "link-to-vault",
|
|
312
322
|
title: "Link a channel to a vault",
|
|
313
323
|
description: "Back a channel with a Parachute vault.",
|
|
314
|
-
requestedBy: "
|
|
324
|
+
requestedBy: "agent",
|
|
315
325
|
source: {
|
|
316
326
|
module: "vault",
|
|
317
327
|
event: "note.created",
|
|
318
|
-
filter: { tags: ["#
|
|
328
|
+
filter: { tags: ["#agent-message/inbound"] },
|
|
319
329
|
},
|
|
320
|
-
sink: { module: "
|
|
330
|
+
sink: { module: "agent", action: "message.deliver" },
|
|
321
331
|
parameters: [
|
|
322
332
|
{
|
|
323
333
|
key: "vault",
|
|
@@ -371,9 +381,10 @@ describe("GET /api/connections/catalog", () => {
|
|
|
371
381
|
);
|
|
372
382
|
expect(res.status).toBe(200);
|
|
373
383
|
const out = (await res.json()) as { events: unknown[]; actions: unknown[] };
|
|
374
|
-
// vault: 3 events + 1 action;
|
|
384
|
+
// vault: 3 events + 1 action; agent: 1 event + 2 actions (message.deliver +
|
|
385
|
+
// definition.reload).
|
|
375
386
|
expect(out.events.length).toBe(4);
|
|
376
|
-
expect(out.actions.length).toBe(
|
|
387
|
+
expect(out.actions.length).toBe(3);
|
|
377
388
|
});
|
|
378
389
|
});
|
|
379
390
|
|
|
@@ -418,7 +429,7 @@ describe("POST /admin/connections — validation", () => {
|
|
|
418
429
|
headers: { cookie },
|
|
419
430
|
body: JSON.stringify({
|
|
420
431
|
source: { module: "vault", vault: "default", event: "note.imaginary" },
|
|
421
|
-
sink: { module: "
|
|
432
|
+
sink: { module: "agent", action: "message.deliver" },
|
|
422
433
|
}),
|
|
423
434
|
});
|
|
424
435
|
const res = await handleConnections(
|
|
@@ -462,7 +473,7 @@ describe("POST /admin/connections — validation", () => {
|
|
|
462
473
|
headers: { cookie },
|
|
463
474
|
body: JSON.stringify({
|
|
464
475
|
source: { module: "vault", vault: "default", event: "note.created" },
|
|
465
|
-
sink: { module: "
|
|
476
|
+
sink: { module: "agent", action: "message.imaginary" },
|
|
466
477
|
}),
|
|
467
478
|
});
|
|
468
479
|
const res = await handleConnections(
|
|
@@ -648,7 +659,7 @@ describe("POST /admin/connections — provenance (R2)", () => {
|
|
|
648
659
|
headers: { cookie },
|
|
649
660
|
body: JSON.stringify({
|
|
650
661
|
id: "w1",
|
|
651
|
-
requestedBy: "
|
|
662
|
+
requestedBy: "agent",
|
|
652
663
|
source: { module: "vault", vault: "default", event: "note.created" },
|
|
653
664
|
sink: { module: "widget", action: "thing.do" },
|
|
654
665
|
}),
|
|
@@ -659,7 +670,7 @@ describe("POST /admin/connections — provenance (R2)", () => {
|
|
|
659
670
|
expect(res.status).toBe(200);
|
|
660
671
|
// Persisted on the record.
|
|
661
672
|
const stored = readConnections(harness.storePath);
|
|
662
|
-
expect(stored[0]!.requestedBy).toBe("
|
|
673
|
+
expect(stored[0]!.requestedBy).toBe("agent");
|
|
663
674
|
// Returned (snake_case) on the GET wire shape.
|
|
664
675
|
const list = await handleConnections(
|
|
665
676
|
new Request(`${HUB_ORIGIN}/admin/connections`, { method: "GET", headers: { cookie } }),
|
|
@@ -667,7 +678,7 @@ describe("POST /admin/connections — provenance (R2)", () => {
|
|
|
667
678
|
deps,
|
|
668
679
|
);
|
|
669
680
|
const out = (await list.json()) as { connections: Array<{ requested_by?: string }> };
|
|
670
|
-
expect(out.connections[0]!.requested_by).toBe("
|
|
681
|
+
expect(out.connections[0]!.requested_by).toBe("agent");
|
|
671
682
|
});
|
|
672
683
|
|
|
673
684
|
test("defaults requestedBy to custom when the body omits it", async () => {
|
|
@@ -740,12 +751,12 @@ describe("POST /admin/connections — channel-backed (the #624 flow as a connect
|
|
|
740
751
|
vault: "default",
|
|
741
752
|
event: "note.created",
|
|
742
753
|
filter: {
|
|
743
|
-
tags: ["#
|
|
754
|
+
tags: ["#agent-message/inbound"],
|
|
744
755
|
has_metadata: ["channel"],
|
|
745
756
|
missing_metadata: ["channel_inbound_rendered_at"],
|
|
746
757
|
},
|
|
747
758
|
},
|
|
748
|
-
sink: { module: "
|
|
759
|
+
sink: { module: "agent", action: "message.deliver", params: { channel: "eng" } },
|
|
749
760
|
}),
|
|
750
761
|
});
|
|
751
762
|
const res = await handleConnections(
|
|
@@ -761,12 +772,12 @@ describe("POST /admin/connections — channel-backed (the #624 flow as a connect
|
|
|
761
772
|
};
|
|
762
773
|
expect(out.ok).toBe(true);
|
|
763
774
|
// Derived channel id.
|
|
764
|
-
expect(out.connection.id).toBe("
|
|
775
|
+
expect(out.connection.id).toBe("agent-eng");
|
|
765
776
|
// Connect lines (parity with #624).
|
|
766
777
|
expect(out.connect?.mcpAdd).toBe(
|
|
767
|
-
`claude mcp add --transport http --scope user
|
|
778
|
+
`claude mcp add --transport http --scope user agent-eng ${HUB_ORIGIN}/agent/mcp/eng`,
|
|
768
779
|
);
|
|
769
|
-
expect(out.connect?.launch).toContain("server:
|
|
780
|
+
expect(out.connect?.launch).toContain("server:agent-eng");
|
|
770
781
|
|
|
771
782
|
// Channel config POST: vault transport, loopback vaultUrl, real
|
|
772
783
|
// vault:default:write token, NO webhookSecret.
|
|
@@ -780,26 +791,26 @@ describe("POST /admin/connections — channel-backed (the #624 flow as a connect
|
|
|
780
791
|
expect(cfgBody.name).toBe("eng");
|
|
781
792
|
expect(cfgBody.config.vaultUrl).toBe(VAULT_ORIGIN);
|
|
782
793
|
expect(cfgBody.config).not.toHaveProperty("webhookSecret");
|
|
783
|
-
expect(scopeOf(cfgCall!.bearer!)).toEqual(["
|
|
794
|
+
expect(scopeOf(cfgCall!.bearer!)).toEqual(["agent:admin"]);
|
|
784
795
|
expect(scopeOf(cfgBody.config.token)).toEqual(["vault:default:write"]);
|
|
785
796
|
|
|
786
|
-
// Trigger: webhook from
|
|
787
|
-
// carries
|
|
797
|
+
// Trigger: webhook from agent mount + message.deliver endpoint; bearer
|
|
798
|
+
// carries agent:send; predicate from the filter.
|
|
788
799
|
const trigCall = calls.find((c) => c.url.endsWith("/vault/default/api/triggers"));
|
|
789
800
|
const trig = trigCall!.body as {
|
|
790
801
|
name: string;
|
|
791
802
|
when: Record<string, unknown>;
|
|
792
803
|
action: { webhook: string; auth: { bearer: string } };
|
|
793
804
|
};
|
|
794
|
-
expect(trig.action.webhook).toBe(`${HUB_ORIGIN}/
|
|
795
|
-
expect(scopeOf(trig.action.auth.bearer)).toEqual(["
|
|
796
|
-
expect((decodeJwt(trig.action.auth.bearer) as { aud?: string }).aud).toBe("
|
|
805
|
+
expect(trig.action.webhook).toBe(`${HUB_ORIGIN}/agent/api/vault/inbound`);
|
|
806
|
+
expect(scopeOf(trig.action.auth.bearer)).toEqual(["agent:send"]);
|
|
807
|
+
expect((decodeJwt(trig.action.auth.bearer) as { aud?: string }).aud).toBe("agent");
|
|
797
808
|
expect(trig.when).toEqual({
|
|
798
|
-
tags: ["#
|
|
809
|
+
tags: ["#agent-message/inbound"],
|
|
799
810
|
has_metadata: ["channel"],
|
|
800
811
|
missing_metadata: ["channel_inbound_rendered_at"],
|
|
801
812
|
});
|
|
802
|
-
expect(trig.name).toBe("
|
|
813
|
+
expect(trig.name).toBe("conn_agent-eng");
|
|
803
814
|
|
|
804
815
|
// sub = the operator on every minted token.
|
|
805
816
|
expect((decodeJwt(cfgBody.config.token) as { sub?: string }).sub).toBe(userId);
|
|
@@ -809,24 +820,114 @@ describe("POST /admin/connections — channel-backed (the #624 flow as a connect
|
|
|
809
820
|
expect(serialized).not.toContain(trig.action.auth.bearer);
|
|
810
821
|
});
|
|
811
822
|
|
|
812
|
-
test("503 when the
|
|
823
|
+
test("503 when the agent module is not installed (agentOrigin null)", async () => {
|
|
813
824
|
const { cookie } = await adminCookie();
|
|
814
825
|
const { fetchImpl } = mockFetch({});
|
|
815
826
|
const deps = {
|
|
816
827
|
...baseDeps(fetchImpl, modulesOf(VAULT_MANIFEST, CHANNEL_MANIFEST)),
|
|
817
|
-
|
|
828
|
+
agentOrigin: null,
|
|
818
829
|
};
|
|
819
830
|
const req = new Request(`${HUB_ORIGIN}/admin/connections`, {
|
|
820
831
|
method: "POST",
|
|
821
832
|
headers: { cookie },
|
|
822
833
|
body: JSON.stringify({
|
|
823
834
|
source: { module: "vault", vault: "default", event: "note.created" },
|
|
824
|
-
sink: { module: "
|
|
835
|
+
sink: { module: "agent", action: "message.deliver", params: { channel: "eng" } },
|
|
825
836
|
}),
|
|
826
837
|
});
|
|
827
838
|
const res = await handleConnections(req, "", deps);
|
|
828
839
|
expect(res.status).toBe(503);
|
|
829
|
-
expect(((await res.json()) as { error: string }).error).toBe("
|
|
840
|
+
expect(((await res.json()) as { error: string }).error).toBe("agent_unavailable");
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test("definition.reload agent sink provisions WITH NO channel param (agent#117)", async () => {
|
|
844
|
+
// Regression: the channel-reply prerequisite was gated on `sinkModule ===
|
|
845
|
+
// "agent"`, so a non-message.deliver agent sink (no channel param) 400'd
|
|
846
|
+
// with `agent sink requires sink.params.channel`. The def-reload connectors
|
|
847
|
+
// could never provision. Now the gate is action-specific — a pure inbound
|
|
848
|
+
// webhook action provisions the vault trigger and SKIPS the channel config.
|
|
849
|
+
const { cookie } = await adminCookie();
|
|
850
|
+
const { fetchImpl, calls } = mockFetch({
|
|
851
|
+
"POST /vault/default/api/triggers": () => ok({ ok: true }),
|
|
852
|
+
// If the prerequisite wrongly ran, it would POST here — left mocked so a
|
|
853
|
+
// regression surfaces as an unexpected call, not a 599.
|
|
854
|
+
"POST /api/channels": () => ok({ ok: true }),
|
|
855
|
+
});
|
|
856
|
+
const req = new Request(`${HUB_ORIGIN}/admin/connections`, {
|
|
857
|
+
method: "POST",
|
|
858
|
+
headers: { cookie },
|
|
859
|
+
body: JSON.stringify({
|
|
860
|
+
id: "agentdefs-create-default",
|
|
861
|
+
requestedBy: "agent",
|
|
862
|
+
source: {
|
|
863
|
+
module: "vault",
|
|
864
|
+
vault: "default",
|
|
865
|
+
event: "note.created",
|
|
866
|
+
filter: { tags: ["#agent/definition"] },
|
|
867
|
+
},
|
|
868
|
+
sink: { module: "agent", action: "definition.reload" }, // NO params.channel
|
|
869
|
+
}),
|
|
870
|
+
});
|
|
871
|
+
const res = await handleConnections(
|
|
872
|
+
req,
|
|
873
|
+
"",
|
|
874
|
+
baseDeps(fetchImpl, modulesOf(VAULT_MANIFEST, CHANNEL_MANIFEST)),
|
|
875
|
+
);
|
|
876
|
+
expect(res.status).toBe(200);
|
|
877
|
+
const out = (await res.json()) as { ok: boolean; connection: { id: string }; connect?: unknown };
|
|
878
|
+
expect(out.ok).toBe(true);
|
|
879
|
+
expect(out.connection.id).toBe("agentdefs-create-default");
|
|
880
|
+
// No channel → no connect lines (those are message-delivery-specific).
|
|
881
|
+
expect(out.connect).toBeUndefined();
|
|
882
|
+
|
|
883
|
+
// The channel-reply prerequisite was SKIPPED — no /api/channels POST.
|
|
884
|
+
expect(calls.some((c) => c.url.endsWith("/api/channels"))).toBe(false);
|
|
885
|
+
|
|
886
|
+
// The vault trigger WAS registered, from the def-reload action's declaration.
|
|
887
|
+
const trigCall = calls.find((c) => c.url.endsWith("/vault/default/api/triggers"));
|
|
888
|
+
expect(trigCall).toBeDefined();
|
|
889
|
+
const trig = trigCall!.body as {
|
|
890
|
+
events: string[];
|
|
891
|
+
when: Record<string, unknown>;
|
|
892
|
+
action: { webhook: string; auth: { bearer: string } };
|
|
893
|
+
};
|
|
894
|
+
expect(trig.events).toEqual(["created"]);
|
|
895
|
+
expect(trig.action.webhook).toBe(`${HUB_ORIGIN}/agent/api/vault/agent-def`);
|
|
896
|
+
expect(scopeOf(trig.action.auth.bearer)).toEqual(["agent:send"]);
|
|
897
|
+
expect(trig.when).toEqual({ tags: ["#agent/definition"] });
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
test("definition.reload provisions even when agentOrigin is null (no reply path)", async () => {
|
|
901
|
+
// Unlike message.deliver (503 agent_unavailable above), a def-reload sink
|
|
902
|
+
// never touches the agent daemon at provision time — it only registers a
|
|
903
|
+
// vault trigger. So a null agentOrigin must NOT block it.
|
|
904
|
+
const { cookie } = await adminCookie();
|
|
905
|
+
const { fetchImpl } = mockFetch({
|
|
906
|
+
"POST /vault/default/api/triggers": () => ok({ ok: true }),
|
|
907
|
+
});
|
|
908
|
+
const deps = {
|
|
909
|
+
...baseDeps(fetchImpl, modulesOf(VAULT_MANIFEST, CHANNEL_MANIFEST)),
|
|
910
|
+
agentOrigin: null,
|
|
911
|
+
};
|
|
912
|
+
const res = await handleConnections(
|
|
913
|
+
new Request(`${HUB_ORIGIN}/admin/connections`, {
|
|
914
|
+
method: "POST",
|
|
915
|
+
headers: { cookie },
|
|
916
|
+
body: JSON.stringify({
|
|
917
|
+
id: "agentdefs-edit-default",
|
|
918
|
+
source: {
|
|
919
|
+
module: "vault",
|
|
920
|
+
vault: "default",
|
|
921
|
+
event: "note.updated",
|
|
922
|
+
filter: { tags: ["#agent/definition"] },
|
|
923
|
+
},
|
|
924
|
+
sink: { module: "agent", action: "definition.reload" },
|
|
925
|
+
}),
|
|
926
|
+
}),
|
|
927
|
+
"",
|
|
928
|
+
deps,
|
|
929
|
+
);
|
|
930
|
+
expect(res.status).toBe(200);
|
|
830
931
|
});
|
|
831
932
|
});
|
|
832
933
|
|
|
@@ -922,7 +1023,7 @@ describe("DELETE /admin/connections/:id — teardown", () => {
|
|
|
922
1023
|
"POST /api/channels": () => ok({ ok: true }),
|
|
923
1024
|
"POST /vault/default/api/triggers": () => ok({ ok: true }),
|
|
924
1025
|
"DELETE /api/channels/eng": () => ok({ ok: true }),
|
|
925
|
-
"DELETE /vault/default/api/triggers/
|
|
1026
|
+
"DELETE /vault/default/api/triggers/conn_agent-eng": () => ok({ ok: true }),
|
|
926
1027
|
});
|
|
927
1028
|
const deps = baseDeps(fetchImpl, modulesOf(VAULT_MANIFEST, CHANNEL_MANIFEST));
|
|
928
1029
|
await handleConnections(
|
|
@@ -931,18 +1032,18 @@ describe("DELETE /admin/connections/:id — teardown", () => {
|
|
|
931
1032
|
headers: { cookie },
|
|
932
1033
|
body: JSON.stringify({
|
|
933
1034
|
source: { module: "vault", vault: "default", event: "note.created" },
|
|
934
|
-
sink: { module: "
|
|
1035
|
+
sink: { module: "agent", action: "message.deliver", params: { channel: "eng" } },
|
|
935
1036
|
}),
|
|
936
1037
|
}),
|
|
937
1038
|
"",
|
|
938
1039
|
deps,
|
|
939
1040
|
);
|
|
940
1041
|
const res = await handleConnections(
|
|
941
|
-
new Request(`${HUB_ORIGIN}/admin/connections/
|
|
1042
|
+
new Request(`${HUB_ORIGIN}/admin/connections/agent-eng`, {
|
|
942
1043
|
method: "DELETE",
|
|
943
1044
|
headers: { cookie },
|
|
944
1045
|
}),
|
|
945
|
-
"/
|
|
1046
|
+
"/agent-eng",
|
|
946
1047
|
deps,
|
|
947
1048
|
);
|
|
948
1049
|
expect(res.status).toBe(200);
|
|
@@ -952,9 +1053,62 @@ describe("DELETE /admin/connections/:id — teardown", () => {
|
|
|
952
1053
|
expect(
|
|
953
1054
|
calls.some(
|
|
954
1055
|
(c) =>
|
|
955
|
-
c.method === "DELETE" && c.url.endsWith("/vault/default/api/triggers/
|
|
1056
|
+
c.method === "DELETE" && c.url.endsWith("/vault/default/api/triggers/conn_agent-eng"),
|
|
1057
|
+
),
|
|
1058
|
+
).toBe(true);
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
test("definition.reload teardown removes the trigger but NOT a channel (agent#117)", async () => {
|
|
1062
|
+
// Symmetric to the create-side fix: a channel-less agent action created no
|
|
1063
|
+
// channel config entry, so teardown must not issue a spurious
|
|
1064
|
+
// DELETE /api/channels/<id> (the old module-level gate fell back to record.id
|
|
1065
|
+
// as the channel name → a delete against a never-created channel).
|
|
1066
|
+
const { cookie } = await adminCookie();
|
|
1067
|
+
const { fetchImpl, calls } = mockFetch({
|
|
1068
|
+
"POST /vault/default/api/triggers": () => ok({ ok: true }),
|
|
1069
|
+
"DELETE /vault/default/api/triggers/conn_agentdefs-create-default": () => ok({ ok: true }),
|
|
1070
|
+
// Present so a regression surfaces as an unexpected matched call, not a 599.
|
|
1071
|
+
"DELETE /api/channels/agentdefs-create-default": () => ok({ ok: true }),
|
|
1072
|
+
});
|
|
1073
|
+
const deps = baseDeps(fetchImpl, modulesOf(VAULT_MANIFEST, CHANNEL_MANIFEST));
|
|
1074
|
+
await handleConnections(
|
|
1075
|
+
new Request(`${HUB_ORIGIN}/admin/connections`, {
|
|
1076
|
+
method: "POST",
|
|
1077
|
+
headers: { cookie },
|
|
1078
|
+
body: JSON.stringify({
|
|
1079
|
+
id: "agentdefs-create-default",
|
|
1080
|
+
source: {
|
|
1081
|
+
module: "vault",
|
|
1082
|
+
vault: "default",
|
|
1083
|
+
event: "note.created",
|
|
1084
|
+
filter: { tags: ["#agent/definition"] },
|
|
1085
|
+
},
|
|
1086
|
+
sink: { module: "agent", action: "definition.reload" },
|
|
1087
|
+
}),
|
|
1088
|
+
}),
|
|
1089
|
+
"",
|
|
1090
|
+
deps,
|
|
1091
|
+
);
|
|
1092
|
+
const res = await handleConnections(
|
|
1093
|
+
new Request(`${HUB_ORIGIN}/admin/connections/agentdefs-create-default`, {
|
|
1094
|
+
method: "DELETE",
|
|
1095
|
+
headers: { cookie },
|
|
1096
|
+
}),
|
|
1097
|
+
"/agentdefs-create-default",
|
|
1098
|
+
deps,
|
|
1099
|
+
);
|
|
1100
|
+
expect(res.status).toBe(200);
|
|
1101
|
+
// NO channel delete (the channel-reply path was never created).
|
|
1102
|
+
expect(calls.some((c) => c.method === "DELETE" && c.url.includes("/api/channels/"))).toBe(false);
|
|
1103
|
+
// The vault trigger WAS torn down.
|
|
1104
|
+
expect(
|
|
1105
|
+
calls.some(
|
|
1106
|
+
(c) =>
|
|
1107
|
+
c.method === "DELETE" &&
|
|
1108
|
+
c.url.endsWith("/vault/default/api/triggers/conn_agentdefs-create-default"),
|
|
956
1109
|
),
|
|
957
1110
|
).toBe(true);
|
|
1111
|
+
expect(readConnections(harness.storePath)).toHaveLength(0);
|
|
958
1112
|
});
|
|
959
1113
|
|
|
960
1114
|
test("404 deleting an unknown connection", async () => {
|
|
@@ -989,7 +1143,7 @@ describe("B0 — registered connection mints", () => {
|
|
|
989
1143
|
headers: { cookie },
|
|
990
1144
|
body: JSON.stringify({
|
|
991
1145
|
source: { module: "vault", vault: "default", event: "note.created" },
|
|
992
|
-
sink: { module: "
|
|
1146
|
+
sink: { module: "agent", action: "message.deliver", params: { channel: "eng" } },
|
|
993
1147
|
}),
|
|
994
1148
|
}),
|
|
995
1149
|
"",
|
|
@@ -1025,10 +1179,10 @@ describe("B0 — registered connection mints", () => {
|
|
|
1025
1179
|
const webhookRow = findTokenRowByJti(harness.db, webhookJti);
|
|
1026
1180
|
expect(webhookRow).not.toBeNull();
|
|
1027
1181
|
expect(webhookRow!.createdVia).toBe("connection_provision");
|
|
1028
|
-
expect(webhookRow!.scopes).toEqual(["
|
|
1182
|
+
expect(webhookRow!.scopes).toEqual(["agent:send"]);
|
|
1029
1183
|
|
|
1030
1184
|
// The short-lived (60s) provisioning bearers — vault:<v>:admin on the
|
|
1031
|
-
// trigger POST,
|
|
1185
|
+
// trigger POST, agent:admin on the channel-config POST — ride to expiry
|
|
1032
1186
|
// by design (the documented ≤10-min unregistered bound). NOT registered.
|
|
1033
1187
|
const trigCall = calls.find((c) => c.url.endsWith("/vault/default/api/triggers"));
|
|
1034
1188
|
const cfgCall = calls.find((c) => c.url.endsWith("/api/channels"));
|
|
@@ -1051,17 +1205,17 @@ describe("B0 — registered connection mints", () => {
|
|
|
1051
1205
|
"POST /api/channels": () => ok({ ok: true }),
|
|
1052
1206
|
"POST /vault/default/api/triggers": () => ok({ ok: true }),
|
|
1053
1207
|
"DELETE /api/channels/eng": () => ok({ ok: true }),
|
|
1054
|
-
"DELETE /vault/default/api/triggers/
|
|
1208
|
+
"DELETE /vault/default/api/triggers/conn_agent-eng": () => ok({ ok: true }),
|
|
1055
1209
|
});
|
|
1056
1210
|
const deps = baseDeps(fetchImpl, modulesOf(VAULT_MANIFEST, CHANNEL_MANIFEST));
|
|
1057
1211
|
const { replyJti, webhookJti } = await createChannelConnection(cookie, deps, calls);
|
|
1058
1212
|
|
|
1059
1213
|
const res = await handleConnections(
|
|
1060
|
-
new Request(`${HUB_ORIGIN}/admin/connections/
|
|
1214
|
+
new Request(`${HUB_ORIGIN}/admin/connections/agent-eng`, {
|
|
1061
1215
|
method: "DELETE",
|
|
1062
1216
|
headers: { cookie },
|
|
1063
1217
|
}),
|
|
1064
|
-
"/
|
|
1218
|
+
"/agent-eng",
|
|
1065
1219
|
deps,
|
|
1066
1220
|
);
|
|
1067
1221
|
expect(res.status).toBe(200);
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* endpoint's own gate. (The legacy `/admin/channels` wiring was belted
|
|
16
16
|
* here too until boundary D1 retired the endpoint.)
|
|
17
17
|
*
|
|
18
|
-
* The canonical seam consumer is pinned here:
|
|
18
|
+
* The canonical seam consumer is pinned here: the agent module's admin page POSTs
|
|
19
19
|
* `/admin/connections` as a same-origin `fetch()` with
|
|
20
|
-
* `credentials: "include"` (parachute-
|
|
20
|
+
* `credentials: "include"` (parachute-agent src/admin-ui.ts) — i.e.
|
|
21
21
|
* session cookie + browser-sent matching Origin. That shape must keep
|
|
22
22
|
* passing the belt without any token dance.
|
|
23
23
|
*/
|
|
@@ -248,8 +248,8 @@ describe("CSRF belt wiring — /admin/connections", () => {
|
|
|
248
248
|
expect(await errorCode(res)).toBe("invalid_request");
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
test("seam pin — the
|
|
252
|
-
// parachute-
|
|
251
|
+
test("seam pin — the agent link-vault shape (cookie + correct Origin, requestedBy: agent) passes the belt", async () => {
|
|
252
|
+
// parachute-agent/src/admin-ui.ts: fetch(window.location.origin +
|
|
253
253
|
// "/admin/connections", { method: "POST", credentials: "include" }) —
|
|
254
254
|
// same-origin fetch(), so the browser sends Origin = hub origin on the
|
|
255
255
|
// POST. With no modules installed in this harness the engine answers
|
|
@@ -265,10 +265,10 @@ describe("CSRF belt wiring — /admin/connections", () => {
|
|
|
265
265
|
module: "vault",
|
|
266
266
|
vault: "main",
|
|
267
267
|
event: "note.created",
|
|
268
|
-
filter: { tags: ["
|
|
268
|
+
filter: { tags: ["agent-message/inbound"] },
|
|
269
269
|
},
|
|
270
|
-
sink: { module: "
|
|
271
|
-
requestedBy: "
|
|
270
|
+
sink: { module: "agent", action: "message.deliver", params: { channel: "tg" } },
|
|
271
|
+
requestedBy: "agent",
|
|
272
272
|
},
|
|
273
273
|
}),
|
|
274
274
|
);
|