@nookplot/runtime 0.5.142 → 0.5.144

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 (60) hide show
  1. package/dist/__tests__/bdAgentPack.test.d.ts +2 -0
  2. package/dist/__tests__/bdAgentPack.test.d.ts.map +1 -0
  3. package/dist/__tests__/bdAgentPack.test.js +44 -0
  4. package/dist/__tests__/bdAgentPack.test.js.map +1 -0
  5. package/dist/__tests__/externalMcpTools.test.d.ts +2 -0
  6. package/dist/__tests__/externalMcpTools.test.d.ts.map +1 -0
  7. package/dist/__tests__/externalMcpTools.test.js +94 -0
  8. package/dist/__tests__/externalMcpTools.test.js.map +1 -0
  9. package/dist/__tests__/pack.gating.test.d.ts +2 -0
  10. package/dist/__tests__/pack.gating.test.d.ts.map +1 -0
  11. package/dist/__tests__/pack.gating.test.js +134 -0
  12. package/dist/__tests__/pack.gating.test.js.map +1 -0
  13. package/dist/__tests__/pack.test.d.ts +2 -0
  14. package/dist/__tests__/pack.test.d.ts.map +1 -0
  15. package/dist/__tests__/pack.test.js +299 -0
  16. package/dist/__tests__/pack.test.js.map +1 -0
  17. package/dist/__tests__/packLoader.test.d.ts +2 -0
  18. package/dist/__tests__/packLoader.test.d.ts.map +1 -0
  19. package/dist/__tests__/packLoader.test.js +304 -0
  20. package/dist/__tests__/packLoader.test.js.map +1 -0
  21. package/dist/__tests__/presetLoader.test.d.ts +2 -0
  22. package/dist/__tests__/presetLoader.test.d.ts.map +1 -0
  23. package/dist/__tests__/presetLoader.test.js +766 -0
  24. package/dist/__tests__/presetLoader.test.js.map +1 -0
  25. package/dist/actionCatalog.d.ts.map +1 -1
  26. package/dist/actionCatalog.generated.d.ts +1 -1
  27. package/dist/actionCatalog.generated.d.ts.map +1 -1
  28. package/dist/actionCatalog.generated.js +7 -2
  29. package/dist/actionCatalog.generated.js.map +1 -1
  30. package/dist/actionCatalog.js +4 -12
  31. package/dist/actionCatalog.js.map +1 -1
  32. package/dist/autonomous.d.ts +24 -1
  33. package/dist/autonomous.d.ts.map +1 -1
  34. package/dist/autonomous.js +66 -8
  35. package/dist/autonomous.js.map +1 -1
  36. package/dist/index.d.ts +7 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +6 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/pack.d.ts +181 -0
  41. package/dist/pack.d.ts.map +1 -0
  42. package/dist/pack.js +379 -0
  43. package/dist/pack.js.map +1 -0
  44. package/dist/packLoader.d.ts +109 -0
  45. package/dist/packLoader.d.ts.map +1 -0
  46. package/dist/packLoader.js +236 -0
  47. package/dist/packLoader.js.map +1 -0
  48. package/dist/presetLoader.d.ts +132 -0
  49. package/dist/presetLoader.d.ts.map +1 -0
  50. package/dist/presetLoader.js +740 -0
  51. package/dist/presetLoader.js.map +1 -0
  52. package/dist/signalActionMap.d.ts +17 -1
  53. package/dist/signalActionMap.d.ts.map +1 -1
  54. package/dist/signalActionMap.js +37 -2
  55. package/dist/signalActionMap.js.map +1 -1
  56. package/dist/tools.d.ts +23 -7
  57. package/dist/tools.d.ts.map +1 -1
  58. package/dist/tools.js +20 -6
  59. package/dist/tools.js.map +1 -1
  60. package/package.json +2 -2
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bdAgentPack.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bdAgentPack.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/bdAgentPack.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The repo-shipped bd-agent pack (packs/bd-agent.pack.yaml) must always
3
+ * validate against the live Pack schema — it's the reference bundle the
4
+ * onboarding doc points at, so schema drift here means a broken first-run
5
+ * for every BD-agent user.
6
+ */
7
+ import { describe, it, expect } from "vitest";
8
+ import { readFileSync } from "node:fs";
9
+ import { resolve, dirname } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { parsePack, resolvePackActions } from "../pack.js";
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const PACK_PATH = resolve(__dirname, "../../../packs/bd-agent.pack.yaml");
14
+ describe("packs/bd-agent.pack.yaml", () => {
15
+ it("parses and validates against the live Pack schema", () => {
16
+ const result = parsePack(readFileSync(PACK_PATH, "utf-8"));
17
+ expect(result.errors).toEqual([]);
18
+ expect(result.ok).toBe(true);
19
+ expect(result.pack?.name).toBe("bd-agent");
20
+ });
21
+ it("declares the Notion connection its MCP mount references", () => {
22
+ const { pack } = parsePack(readFileSync(PACK_PATH, "utf-8"));
23
+ const mount = pack?.mcpServers?.find((s) => s.name === "notion");
24
+ expect(mount?.connection).toBe("notion");
25
+ expect(pack?.connections?.some((c) => c.service === "notion" && c.owner === "agent")).toBe(true);
26
+ });
27
+ it("keeps chat connectors reactive-only (risk posture: cold outreach is email)", () => {
28
+ const { pack } = parsePack(readFileSync(PACK_PATH, "utf-8"));
29
+ for (const c of pack?.connectors ?? []) {
30
+ if (c.platform === "discord" || c.platform === "telegram") {
31
+ expect(c.mode).toBe("reactive");
32
+ }
33
+ }
34
+ expect(pack?.connectors?.some((c) => c.platform === "email" && c.mode === "outbound")).toBe(true);
35
+ });
36
+ it("resolves a non-empty action set including research + email", () => {
37
+ const { pack } = parsePack(readFileSync(PACK_PATH, "utf-8"));
38
+ const actions = resolvePackActions(pack);
39
+ expect(actions).toContain("search_knowledge");
40
+ expect(actions).toContain("send_email");
41
+ expect(actions).toContain("store_memory");
42
+ });
43
+ });
44
+ //# sourceMappingURL=bdAgentPack.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bdAgentPack.test.js","sourceRoot":"","sources":["../../src/__tests__/bdAgentPack.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=externalMcpTools.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"externalMcpTools.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/externalMcpTools.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * External MCP tool wiring (ROADMAP_external-mcp-connectors Phase 1):
3
+ * mounted servers' tools surface in the available-actions set and dispatch
4
+ * unprefixed as `mcp__<server>__<tool>`.
5
+ */
6
+ import { describe, it, expect, vi } from "vitest";
7
+ import { getAvailableActions, AutonomousAgent } from "../autonomous.js";
8
+ import { getAvailableActionsFromMap, resolveDispatchToolName, CORE_ACTIONS } from "../signalActionMap.js";
9
+ import { ACTION_CATALOG } from "../actionCatalog.js";
10
+ import { ToolManager } from "../tools.js";
11
+ import { createMockRuntime } from "./helpers/mockRuntime.js";
12
+ const EXTERNAL = ["mcp__notion__search", "mcp__notion__create_page"];
13
+ describe("external MCP tools in available actions", () => {
14
+ it("merges mounted tools into getAvailableActionsFromMap", () => {
15
+ const actions = getAvailableActionsFromMap("directive", new Set(), EXTERNAL);
16
+ expect(actions).toContain("mcp__notion__search");
17
+ expect(actions).toContain("mcp__notion__create_page");
18
+ for (const core of CORE_ACTIONS)
19
+ expect(actions).toContain(core);
20
+ });
21
+ it("getAvailableActions forwards externalActions (and omits them by default)", () => {
22
+ expect(getAvailableActions("directive", undefined, EXTERNAL)).toContain("mcp__notion__search");
23
+ expect(getAvailableActions("directive")).not.toContain("mcp__notion__search");
24
+ });
25
+ it("surfaces mounted tools in progressive-disclosure mode too", () => {
26
+ process.env.NOOKPLOT_PROGRESSIVE_DISCLOSURE = "1";
27
+ try {
28
+ const actions = getAvailableActionsFromMap("directive", new Set(), EXTERNAL);
29
+ expect(actions).toContain("mcp__notion__search");
30
+ expect(actions.length).toBe(CORE_ACTIONS.length + EXTERNAL.length);
31
+ }
32
+ finally {
33
+ delete process.env.NOOKPLOT_PROGRESSIVE_DISCLOSURE;
34
+ }
35
+ });
36
+ it("refreshExternalMcpActions maps listMcpTools into mcp__<server>__<tool> names", async () => {
37
+ const { runtime } = createMockRuntime();
38
+ runtime.tools = {
39
+ listMcpTools: vi.fn().mockResolvedValue([
40
+ { name: "search", description: "d", inputSchema: {}, serverName: "notion", serverUrl: "https://mcp.notion.com/mcp", wireName: "mcp__notion__search" },
41
+ { name: "create_page", description: "d", inputSchema: {}, serverName: "notion", serverUrl: "https://mcp.notion.com/mcp", wireName: "mcp__notion__create_page" },
42
+ // No wireName = the gateway couldn't map the name provider-safe —
43
+ // listed for visibility, never exposed as a dispatchable action.
44
+ { name: "x".repeat(80), description: "d", inputSchema: {}, serverName: "notion", serverUrl: "https://mcp.notion.com/mcp" },
45
+ ]),
46
+ };
47
+ const agent = new AutonomousAgent(runtime, { verbose: false, generateResponse: async () => null });
48
+ const actions = await agent.refreshExternalMcpActions();
49
+ expect(actions).toEqual(["mcp__notion__search", "mcp__notion__create_page"]);
50
+ });
51
+ });
52
+ describe("dispatch tool-name resolution", () => {
53
+ it("passes mcp:* through unprefixed and prefixes catalog actions", () => {
54
+ expect(resolveDispatchToolName("mcp__notion__search")).toBe("mcp__notion__search");
55
+ expect(resolveDispatchToolName("send_message")).toBe("nookplot_send_message");
56
+ expect(resolveDispatchToolName("create_post")).toBe("nookplot_create_post");
57
+ });
58
+ });
59
+ describe("connectMcpServer auth contract (Phase 2)", () => {
60
+ it("forwards oauth / workspace auth fields to the gateway", async () => {
61
+ const request = vi.fn().mockResolvedValue({ data: { id: "srv_1" } });
62
+ const tools = new ToolManager({ request });
63
+ await tools.connectMcpServer("https://mcp.notion.com/mcp", "notion", {
64
+ authType: "oauth",
65
+ oauthProvider: "notion",
66
+ });
67
+ expect(request).toHaveBeenLastCalledWith("POST", "/v1/agents/me/mcp/servers", {
68
+ serverUrl: "https://mcp.notion.com/mcp",
69
+ serverName: "notion",
70
+ authType: "oauth",
71
+ oauthProvider: "notion",
72
+ });
73
+ await tools.connectMcpServer("https://mcp.notion.com/mcp", "team-notion", {
74
+ authType: "workspace",
75
+ workspaceId: "11111111-2222-3333-4444-555555555555",
76
+ credentialService: "notion",
77
+ });
78
+ expect(request).toHaveBeenLastCalledWith("POST", "/v1/agents/me/mcp/servers", {
79
+ serverUrl: "https://mcp.notion.com/mcp",
80
+ serverName: "team-notion",
81
+ authType: "workspace",
82
+ credentialService: "notion",
83
+ workspaceId: "11111111-2222-3333-4444-555555555555",
84
+ });
85
+ });
86
+ });
87
+ describe("dead MCP meta-actions removed from the catalog", () => {
88
+ it("no longer lists call_mcp_tool / connect_mcp_server / disconnect_mcp_server", () => {
89
+ expect(ACTION_CATALOG).not.toHaveProperty("call_mcp_tool");
90
+ expect(ACTION_CATALOG).not.toHaveProperty("connect_mcp_server");
91
+ expect(ACTION_CATALOG).not.toHaveProperty("disconnect_mcp_server");
92
+ });
93
+ });
94
+ //# sourceMappingURL=externalMcpTools.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"externalMcpTools.test.js","sourceRoot":"","sources":["../../src/__tests__/externalMcpTools.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pack.gating.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack.gating.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/pack.gating.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Pack gating (ROADMAP_external-mcp-connectors Phase 3): with a pack loaded,
3
+ * the agent's surface resolves to CORE ∪ pack actions ∪ mounted external MCP
4
+ * tools — in prompt building (getAvailableActionsFromMap) and at dispatch
5
+ * (AutonomousAgent.handleActionRequest refuses anything outside the set).
6
+ */
7
+ import { describe, it, expect, vi, beforeEach } from "vitest";
8
+ import { createMockRuntime } from "./helpers/mockRuntime.js";
9
+ import { AutonomousAgent, getAvailableActions } from "../autonomous.js";
10
+ import { getAvailableActionsFromMap, CORE_ACTIONS } from "../signalActionMap.js";
11
+ import { resolvePackActions } from "../pack.js";
12
+ vi.mock("../signing.js", () => ({
13
+ prepareSignRelay: vi.fn().mockResolvedValue({ txHash: "0xRELAY_TX" }),
14
+ signForwardRequest: vi.fn().mockResolvedValue("0xSIGNATURE"),
15
+ }));
16
+ const PACK = {
17
+ name: "notion-research-agent",
18
+ version: "1.0.0",
19
+ tools: ["search_knowledge", "send_email"],
20
+ mcpServers: [{ name: "notion", url: "https://mcp.notion.com/mcp" }],
21
+ };
22
+ const PACK_ACTIONS = resolvePackActions(PACK);
23
+ const EXTERNAL = ["mcp__notion__search", "mcp__notion__create_page"];
24
+ describe("getAvailableActionsFromMap — pack gating", () => {
25
+ it("resolves to CORE ∪ pack ∪ external when a pack is active", () => {
26
+ const actions = getAvailableActionsFromMap("email_received", new Set(), EXTERNAL, PACK_ACTIONS);
27
+ for (const core of CORE_ACTIONS)
28
+ expect(actions).toContain(core);
29
+ expect(actions).toContain("search_knowledge");
30
+ expect(actions).toContain("send_email");
31
+ expect(actions).toContain("mcp__notion__search");
32
+ // email_received's signal-context action reply_email is NOT in the pack → gated away.
33
+ expect(actions).not.toContain("reply_email");
34
+ expect(new Set(actions).size).toBe(new Set([...CORE_ACTIONS, ...PACK_ACTIONS, ...EXTERNAL]).size);
35
+ });
36
+ it("an empty pack action set still gates (CORE + external only)", () => {
37
+ const actions = getAvailableActionsFromMap("email_received", new Set(), EXTERNAL, []);
38
+ expect(actions).not.toContain("reply_email");
39
+ expect(actions).toContain("mcp__notion__search");
40
+ expect(actions.length).toBe(CORE_ACTIONS.length + EXTERNAL.length);
41
+ });
42
+ it("null/undefined packActions leaves behavior unchanged", () => {
43
+ expect(getAvailableActionsFromMap("email_received", new Set(), undefined, null)).toContain("reply_email");
44
+ expect(getAvailableActionsFromMap("email_received", new Set())).toContain("reply_email");
45
+ });
46
+ it("browse_tools categories no longer widen the set under a pack", () => {
47
+ const ungated = getAvailableActionsFromMap("directive", new Set(["bounties"]));
48
+ expect(ungated).toContain("create_bounty");
49
+ const gated = getAvailableActionsFromMap("directive", new Set(["bounties"]), undefined, PACK_ACTIONS);
50
+ expect(gated).not.toContain("create_bounty");
51
+ });
52
+ it("gates identically in progressive-disclosure mode", () => {
53
+ process.env.NOOKPLOT_PROGRESSIVE_DISCLOSURE = "1";
54
+ try {
55
+ const actions = getAvailableActionsFromMap("directive", new Set(), EXTERNAL, PACK_ACTIONS);
56
+ expect(actions).toContain("search_knowledge");
57
+ expect(actions).toContain("mcp__notion__create_page");
58
+ expect(new Set(actions).size).toBe(new Set([...CORE_ACTIONS, ...PACK_ACTIONS, ...EXTERNAL]).size);
59
+ }
60
+ finally {
61
+ delete process.env.NOOKPLOT_PROGRESSIVE_DISCLOSURE;
62
+ }
63
+ });
64
+ it("module-level getAvailableActions forwards packActions", () => {
65
+ expect(getAvailableActions("email_received", undefined, undefined, PACK_ACTIONS)).not.toContain("reply_email");
66
+ expect(getAvailableActions("email_received")).toContain("reply_email");
67
+ });
68
+ });
69
+ describe("AutonomousAgent — dispatch guard", () => {
70
+ let runtime;
71
+ let callbacks;
72
+ let requestMock;
73
+ let rejectMock;
74
+ beforeEach(() => {
75
+ vi.clearAllMocks();
76
+ const mock = createMockRuntime();
77
+ runtime = mock.runtime;
78
+ callbacks = mock.callbacks;
79
+ requestMock = runtime.connection.request;
80
+ requestMock.mockImplementation(async (method, path) => {
81
+ if (method === "POST" && path === "/v1/actions/execute") {
82
+ return { status: "completed", result: { success: true } };
83
+ }
84
+ return { success: true };
85
+ });
86
+ rejectMock = runtime
87
+ .proactive.rejectDelegatedAction;
88
+ });
89
+ async function dispatch(agent, actionType, actionId) {
90
+ callbacks.actionCb({ agentId: "agent_1", actionType, payload: {}, actionId });
91
+ await new Promise((r) => setTimeout(r, 20));
92
+ }
93
+ function executeCalls() {
94
+ return requestMock.mock.calls
95
+ .filter(([m, p]) => m === "POST" && p === "/v1/actions/execute")
96
+ .map(([, , body]) => body.toolName);
97
+ }
98
+ it("refuses an action outside the pack and rejects the delegated action", async () => {
99
+ const agent = new AutonomousAgent(runtime, { verbose: false, pack: PACK });
100
+ agent.start();
101
+ await dispatch(agent, "create_bounty", "act_1");
102
+ expect(executeCalls()).toEqual([]);
103
+ expect(rejectMock).toHaveBeenCalledWith("act_1", "Not allowed by loaded pack");
104
+ });
105
+ it("allows pack-declared and core actions through", async () => {
106
+ const agent = new AutonomousAgent(runtime, { verbose: false, pack: PACK });
107
+ agent.start();
108
+ await dispatch(agent, "search_knowledge");
109
+ await dispatch(agent, "my_profile"); // CORE — always allowed
110
+ expect(executeCalls()).toEqual(["nookplot_search_knowledge", "nookplot_my_profile"]);
111
+ expect(rejectMock).not.toHaveBeenCalled();
112
+ });
113
+ it("allows mounted mcp:* tools and refuses unmounted ones", async () => {
114
+ runtime.tools = {
115
+ listMcpTools: vi.fn().mockResolvedValue([
116
+ { name: "search", description: "d", inputSchema: {}, serverName: "notion", serverUrl: "https://mcp.notion.com/mcp", wireName: "mcp__notion__search" },
117
+ ]),
118
+ };
119
+ const agent = new AutonomousAgent(runtime, { verbose: false, pack: PACK });
120
+ await agent.refreshExternalMcpActions();
121
+ agent.start();
122
+ await dispatch(agent, "mcp__notion__search");
123
+ await dispatch(agent, "mcp__rogue__exfiltrate", "act_2");
124
+ expect(executeCalls()).toEqual(["mcp__notion__search"]);
125
+ expect(rejectMock).toHaveBeenCalledWith("act_2", "Not allowed by loaded pack");
126
+ });
127
+ it("does not gate when no pack is loaded", async () => {
128
+ const agent = new AutonomousAgent(runtime, { verbose: false });
129
+ agent.start();
130
+ await dispatch(agent, "create_bounty");
131
+ expect(executeCalls()).toEqual(["nookplot_create_bounty"]);
132
+ });
133
+ });
134
+ //# sourceMappingURL=pack.gating.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack.gating.test.js","sourceRoot":"","sources":["../../src/__tests__/pack.gating.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pack.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/pack.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Pack schema + validator + preset conversion (ROADMAP_external-mcp-connectors
3
+ * Phase 3). Every schema field maps to a consumer; every reject path is
4
+ * exercised; preset → pack → preset is lossless.
5
+ */
6
+ import { describe, it, expect } from "vitest";
7
+ import { validatePack, parsePack, presetToPack, packToPresetConfig, resolvePackActions, CONNECTOR_PLATFORM_MODES, CONNECTOR_PLATFORMS, PACK_SCHEMA_VERSION, } from "../pack.js";
8
+ import { ACTION_CATALOG } from "../actionCatalog.js";
9
+ const FULL_PACK = {
10
+ name: "notion-research-agent",
11
+ version: "1.0.0",
12
+ description: "Answers team questions from the shared Notion workspace.",
13
+ preset: {
14
+ id: "research-biology",
15
+ version: 2,
16
+ trustLevel: "verified",
17
+ failurePolicy: "continue",
18
+ maxCostNook: 500,
19
+ sources: [
20
+ { type: "mining", label: "Bio traces", config: { domainTags: ["biology"] } },
21
+ { type: "bundle", config: { bundleId: 42 } },
22
+ ],
23
+ },
24
+ tools: ["search_knowledge", "store_knowledge_item", "send_dm"],
25
+ mcpServers: [
26
+ { name: "notion", url: "https://mcp.notion.com/mcp", connection: "notion" },
27
+ { name: "open-docs", url: "https://docs.example.com/mcp" },
28
+ ],
29
+ connections: [{ service: "notion", owner: "workspace" }],
30
+ connectors: [
31
+ { platform: "discord", mode: "reactive" },
32
+ { platform: "email", mode: "outbound" },
33
+ ],
34
+ };
35
+ describe("validatePack — happy path", () => {
36
+ it("accepts a full pack exercising every field", () => {
37
+ const result = validatePack(FULL_PACK);
38
+ expect(result.errors).toEqual([]);
39
+ expect(result.ok).toBe(true);
40
+ expect(result.pack).toEqual(FULL_PACK);
41
+ });
42
+ it("accepts a minimal pack (name + version only)", () => {
43
+ const result = validatePack({ name: "tiny", version: "0.1.0" });
44
+ expect(result.ok).toBe(true);
45
+ });
46
+ it("returns a deep copy — mutating the result does not touch the input", () => {
47
+ const input = JSON.parse(JSON.stringify(FULL_PACK));
48
+ const result = validatePack(input);
49
+ result.pack.tools.push("ignore");
50
+ expect(input.tools).toEqual(FULL_PACK.tools);
51
+ });
52
+ });
53
+ describe("validatePack — reject paths", () => {
54
+ const expectError = (input, fragment) => {
55
+ const result = validatePack(input);
56
+ expect(result.ok).toBe(false);
57
+ expect(result.pack).toBeNull();
58
+ expect(result.errors.some((e) => e.includes(fragment)), `expected an error containing "${fragment}", got: ${JSON.stringify(result.errors)}`).toBe(true);
59
+ };
60
+ it("rejects non-objects", () => {
61
+ expectError("nope", "must be an object");
62
+ expectError(null, "must be an object");
63
+ expectError([1], "must be an object");
64
+ });
65
+ it("rejects missing/bad name and version", () => {
66
+ expectError({ version: "1.0.0" }, "name:");
67
+ expectError({ name: "-bad-start", version: "1.0.0" }, "name:");
68
+ expectError({ name: "ok", version: "1.0" }, "version:");
69
+ expectError({ name: "ok" }, "version:");
70
+ });
71
+ it("rejects unknown top-level keys, with a hint for snake_case spellings", () => {
72
+ expectError({ name: "ok", version: "1.0.0", bogus: 1 }, "bogus: unknown key");
73
+ expectError({ name: "ok", version: "1.0.0", mcp_servers: [] }, 'did you mean "mcpServers"');
74
+ });
75
+ it("rejects an over-long description", () => {
76
+ expectError({ name: "ok", version: "1.0.0", description: "x".repeat(501) }, "description:");
77
+ });
78
+ it("rejects bad preset blocks", () => {
79
+ expectError({ name: "ok", version: "1.0.0", preset: { version: 1 } }, "preset.id");
80
+ expectError({ name: "ok", version: "1.0.0", preset: { id: "p", version: 0 } }, "preset.version");
81
+ expectError({ name: "ok", version: "1.0.0", preset: { id: "p", trustLevel: "wild" } }, "preset.trustLevel");
82
+ expectError({ name: "ok", version: "1.0.0", preset: { id: "p", failurePolicy: "explode" } }, "preset.failurePolicy");
83
+ expectError({ name: "ok", version: "1.0.0", preset: { id: "p", maxCostNook: -1 } }, "preset.maxCostNook");
84
+ expectError({ name: "ok", version: "1.0.0", preset: { id: "p", sources: [{ label: "no type" }] } }, "sources[0].type");
85
+ expectError({ name: "ok", version: "1.0.0", preset: { id: "p", sources: [{ type: "mining", config: "nope" }] } }, "sources[0].config");
86
+ });
87
+ it("rejects unknown, mcp-prefixed, and duplicate tools", () => {
88
+ expectError({ name: "ok", version: "1.0.0", tools: ["definitely_not_a_real_action"] }, "unknown action type");
89
+ expectError({ name: "ok", version: "1.0.0", tools: ["mcp__notion__search"] }, "declare the server under mcpServers");
90
+ expectError({ name: "ok", version: "1.0.0", tools: ["send_dm", "send_dm"] }, "duplicate action type");
91
+ expectError({ name: "ok", version: "1.0.0", tools: "send_dm" }, "tools: must be an array");
92
+ });
93
+ it("rejects bad mcpServers entries", () => {
94
+ expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "bad name!", url: "https://x.com" }] }, "mcpServers[0].name");
95
+ expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "ftp://x.com" }] }, "mcpServers[0].url");
96
+ expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "not a url" }] }, "mcpServers[0].url");
97
+ expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "https://x.com" }, { name: "a", url: "https://y.com" }] }, "duplicate server name");
98
+ expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "https://x.com", connection: "ghost" }] }, 'does not match any connections[].service');
99
+ expectError({ name: "ok", version: "1.0.0", mcpServers: [{ name: "a", url: "https://x.com", extra: 1 }] }, "mcpServers[0].extra: unknown key");
100
+ });
101
+ it("rejects bad connections and dead (unreferenced) connections", () => {
102
+ expectError({ name: "ok", version: "1.0.0", connections: [{ service: "bad service!", owner: "agent" }] }, "connections[0].service");
103
+ expectError({ name: "ok", version: "1.0.0", connections: [{ service: "notion", owner: "guild" }] }, "connections[0].owner");
104
+ expectError({
105
+ name: "ok", version: "1.0.0",
106
+ connections: [{ service: "notion", owner: "agent" }, { service: "notion", owner: "workspace" }],
107
+ mcpServers: [{ name: "n", url: "https://x.com", connection: "notion" }],
108
+ }, "duplicate service");
109
+ // Declared but never referenced by an mcpServer — dead declaration.
110
+ expectError({ name: "ok", version: "1.0.0", connections: [{ service: "notion", owner: "agent" }] }, "not referenced by any mcpServers[].connection");
111
+ });
112
+ it("rejects bad connectors — platform, per-platform mode, duplicates", () => {
113
+ expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "slack", mode: "reactive" }] }, "connectors[0].platform");
114
+ // Risk posture: chat connectors are reactive-only (no outbound/cold DMs).
115
+ expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "discord", mode: "outbound" }] }, "not allowed for discord");
116
+ expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "telegram", mode: "outbound" }] }, "not allowed for telegram");
117
+ expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "email", mode: "reactive" }] }, "not allowed for email");
118
+ expectError({ name: "ok", version: "1.0.0", connectors: [{ platform: "email", mode: "outbound" }, { platform: "email", mode: "outbound" }] }, "duplicate platform");
119
+ });
120
+ it("collects multiple errors in one pass", () => {
121
+ const result = validatePack({ name: "!", version: "x", tools: ["nope_action"] });
122
+ expect(result.ok).toBe(false);
123
+ expect(result.errors.length).toBeGreaterThanOrEqual(3);
124
+ });
125
+ });
126
+ describe("schemaVersion (pack-format gate)", () => {
127
+ it("accepts an explicit current schemaVersion and round-trips it", () => {
128
+ const result = validatePack({ schemaVersion: PACK_SCHEMA_VERSION, name: "ok", version: "1.0.0" });
129
+ expect(result.errors).toEqual([]);
130
+ expect(result.ok).toBe(true);
131
+ expect(result.pack.schemaVersion).toBe(PACK_SCHEMA_VERSION);
132
+ });
133
+ it("treats an omitted schemaVersion as format 1 (every pre-existing pack)", () => {
134
+ const result = validatePack({ name: "ok", version: "1.0.0" });
135
+ expect(result.ok).toBe(true);
136
+ expect(result.pack.schemaVersion).toBeUndefined();
137
+ });
138
+ it("fails closed on a NEWER format with ONE upgrade error — no unknown-key noise", () => {
139
+ const result = validatePack({
140
+ schemaVersion: PACK_SCHEMA_VERSION + 1,
141
+ name: "future", version: "1.0.0",
142
+ someFutureField: { anything: true },
143
+ });
144
+ expect(result.ok).toBe(false);
145
+ expect(result.errors).toHaveLength(1);
146
+ expect(result.errors[0]).toContain(`supports up to ${PACK_SCHEMA_VERSION}`);
147
+ expect(result.errors[0]).toContain("upgrade");
148
+ });
149
+ it("rejects non-positive-integer schemaVersion values", () => {
150
+ for (const bad of [0, -1, 1.5, "1", null]) {
151
+ const result = validatePack({ schemaVersion: bad, name: "ok", version: "1.0.0" });
152
+ expect(result.ok, `schemaVersion ${JSON.stringify(bad)} should be rejected`).toBe(false);
153
+ expect(result.errors[0]).toContain("schemaVersion: must be a positive integer");
154
+ }
155
+ });
156
+ it("hints the camelCase key for the snake_case spelling", () => {
157
+ const result = validatePack({ schema_version: 1, name: "ok", version: "1.0.0" });
158
+ expect(result.ok).toBe(false);
159
+ expect(result.errors.some((e) => e.includes('did you mean "schemaVersion"'))).toBe(true);
160
+ });
161
+ });
162
+ describe("parsePack", () => {
163
+ it("parses a YAML pack document", () => {
164
+ const result = parsePack(`
165
+ name: notion-research-agent
166
+ version: 1.0.0
167
+ tools: [search_knowledge, send_dm]
168
+ mcpServers:
169
+ - name: notion
170
+ url: https://mcp.notion.com/mcp
171
+ connection: notion
172
+ connections:
173
+ - service: notion
174
+ owner: agent
175
+ connectors:
176
+ - platform: email
177
+ mode: outbound
178
+ `);
179
+ expect(result.errors).toEqual([]);
180
+ expect(result.ok).toBe(true);
181
+ expect(result.pack?.mcpServers?.[0]?.connection).toBe("notion");
182
+ });
183
+ it("reports YAML syntax errors instead of throwing", () => {
184
+ const result = parsePack("name: [unclosed");
185
+ expect(result.ok).toBe(false);
186
+ expect(result.errors[0]).toContain("YAML parse error");
187
+ });
188
+ it("validates parsed JSON too (JSON is YAML)", () => {
189
+ const result = parsePack(JSON.stringify({ name: "j", version: "1.0.0" }));
190
+ expect(result.ok).toBe(true);
191
+ });
192
+ });
193
+ describe("preset ↔ pack conversion (lossless round-trip)", () => {
194
+ const roundTrip = (preset) => packToPresetConfig(presetToPack(preset));
195
+ it("round-trips a minimal preset", () => {
196
+ const preset = { id: "defi-trader-v2" };
197
+ expect(roundTrip(preset)).toEqual(preset);
198
+ });
199
+ it("round-trips a full preset with every field", () => {
200
+ const preset = {
201
+ id: "research-biology",
202
+ version: 3,
203
+ trustLevel: "scanned",
204
+ failurePolicy: "abort",
205
+ maxCostNook: 0,
206
+ sources: [
207
+ { type: "mining", label: "Traces", config: { domainTags: ["bio"], minScore: 0.7 } },
208
+ { type: "aggregate", config: { domainTags: ["bio"] } },
209
+ { type: "composite", config: { nested: { deep: [1, 2, 3] } } },
210
+ ],
211
+ };
212
+ expect(roundTrip(preset)).toEqual(preset);
213
+ });
214
+ it("defaults pack version from the preset version", () => {
215
+ expect(presetToPack({ id: "p", version: 4 }).version).toBe("4.0.0");
216
+ expect(presetToPack({ id: "p" }).version).toBe("1.0.0");
217
+ });
218
+ it("honors explicit meta overrides", () => {
219
+ const pack = presetToPack({ id: "p" }, { name: "my-pack", version: "2.1.0", description: "d" });
220
+ expect(pack.name).toBe("my-pack");
221
+ expect(pack.version).toBe("2.1.0");
222
+ expect(pack.description).toBe("d");
223
+ });
224
+ it("throws when the converted pack would be invalid", () => {
225
+ expect(() => presetToPack({ id: "spaces are not a valid pack name" })).toThrow(/presetToPack/);
226
+ });
227
+ it("packToPresetConfig returns null when no preset is embedded", () => {
228
+ expect(packToPresetConfig({ name: "x", version: "1.0.0" })).toBeNull();
229
+ });
230
+ it("conversion deep-copies — mutating the pack does not touch the preset", () => {
231
+ const preset = { id: "p", sources: [{ type: "mining", config: { a: 1 } }] };
232
+ const pack = presetToPack(preset);
233
+ pack.preset.sources[0].config.a = 999;
234
+ expect(preset.sources[0].config.a).toBe(1);
235
+ });
236
+ });
237
+ describe("resolvePackActions", () => {
238
+ it("returns the declared tools, sorted", () => {
239
+ const actions = resolvePackActions({ name: "x", version: "1.0.0", tools: ["send_dm", "search_knowledge"] });
240
+ expect(actions).toEqual(["search_knowledge", "send_dm"]);
241
+ });
242
+ it("an email connector contributes every catalog action in the email category", () => {
243
+ const emailActions = Object.entries(ACTION_CATALOG)
244
+ .filter(([, info]) => info.category === "email")
245
+ .map(([name]) => name);
246
+ expect(emailActions.length).toBeGreaterThan(0);
247
+ const actions = resolvePackActions({
248
+ name: "x", version: "1.0.0",
249
+ connectors: [{ platform: "email", mode: "outbound" }],
250
+ });
251
+ expect(actions).toEqual([...emailActions].sort());
252
+ expect(actions).toContain("send_email");
253
+ });
254
+ it("a discord connector contributes no catalog actions (dispatcher-driven)", () => {
255
+ const actions = resolvePackActions({
256
+ name: "x", version: "1.0.0",
257
+ connectors: [{ platform: "discord", mode: "reactive" }],
258
+ });
259
+ expect(actions).toEqual([]);
260
+ });
261
+ it("dedupes overlap between tools and connector categories", () => {
262
+ const actions = resolvePackActions({
263
+ name: "x", version: "1.0.0",
264
+ tools: ["send_email"],
265
+ connectors: [{ platform: "email", mode: "outbound" }],
266
+ });
267
+ expect(actions.filter((a) => a === "send_email")).toHaveLength(1);
268
+ });
269
+ it("returns [] for a pack with no tools or connectors", () => {
270
+ expect(resolvePackActions({ name: "x", version: "1.0.0" })).toEqual([]);
271
+ });
272
+ });
273
+ describe("CONNECTOR_PLATFORM_MODES (risk posture)", () => {
274
+ it("chat connectors are reactive-only; email is outbound-only", () => {
275
+ expect(CONNECTOR_PLATFORM_MODES.discord).toEqual(["reactive"]);
276
+ expect(CONNECTOR_PLATFORM_MODES.telegram).toEqual(["reactive"]);
277
+ expect(CONNECTOR_PLATFORM_MODES.email).toEqual(["outbound"]);
278
+ });
279
+ it("CONNECTOR_PLATFORMS is the single registration point — modes view derives from it, every descriptor is complete", () => {
280
+ for (const [platform, descriptor] of Object.entries(CONNECTOR_PLATFORMS)) {
281
+ expect(CONNECTOR_PLATFORM_MODES[platform]).toEqual(descriptor.modes);
282
+ expect(descriptor.label.length).toBeGreaterThan(0);
283
+ if (!descriptor.hasBindings) {
284
+ // PackLoader surfaces readyDetail as the whole status line for
285
+ // bindings-less platforms — it must exist.
286
+ expect(descriptor.readyDetail, `${platform}: hasBindings=false requires readyDetail`).toBeTruthy();
287
+ }
288
+ }
289
+ });
290
+ it("accepts a telegram reactive connector (Phase 4b) with no catalog contribution", () => {
291
+ const result = validatePack({
292
+ name: "tg", version: "1.0.0",
293
+ connectors: [{ platform: "telegram", mode: "reactive" }],
294
+ });
295
+ expect(result.ok).toBe(true);
296
+ expect(resolvePackActions(result.pack)).toEqual([]);
297
+ });
298
+ });
299
+ //# sourceMappingURL=pack.test.js.map