@peers-app/peers-sdk 0.25.21 → 0.26.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.
Files changed (87) hide show
  1. package/dist/contracts/__tests__/builder.test.js +0 -2
  2. package/dist/contracts/__tests__/contract-provider-router.test.js +51 -0
  3. package/dist/contracts/__tests__/contract-proxy.test.js +21 -0
  4. package/dist/contracts/__tests__/contract-resolvers.test.js +84 -0
  5. package/dist/contracts/__tests__/legacy-removal.test.d.ts +1 -0
  6. package/dist/contracts/__tests__/legacy-removal.test.js +90 -0
  7. package/dist/contracts/__tests__/system-contracts.test.js +88 -0
  8. package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +1 -0
  9. package/dist/contracts/__tests__/tool-catalog-inventory.test.js +44 -0
  10. package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +1 -0
  11. package/dist/contracts/__tests__/tool-catalog-service.test.js +446 -0
  12. package/dist/contracts/__tests__/tool-catalog.test.d.ts +1 -0
  13. package/dist/contracts/__tests__/tool-catalog.test.js +924 -0
  14. package/dist/contracts/__tests__/workflow-context.test.d.ts +1 -0
  15. package/dist/contracts/__tests__/workflow-context.test.js +61 -0
  16. package/dist/contracts/builder.d.ts +9 -8
  17. package/dist/contracts/builder.js +34 -16
  18. package/dist/contracts/contract-provider-router.d.ts +12 -1
  19. package/dist/contracts/contract-provider-router.js +40 -2
  20. package/dist/contracts/contract-proxy.d.ts +20 -6
  21. package/dist/contracts/contract-proxy.js +23 -12
  22. package/dist/contracts/contract-resolvers.d.ts +22 -3
  23. package/dist/contracts/contract-resolvers.js +86 -7
  24. package/dist/contracts/extract.d.ts +7 -0
  25. package/dist/contracts/extract.js +3 -0
  26. package/dist/contracts/index.d.ts +11 -4
  27. package/dist/contracts/index.js +14 -2
  28. package/dist/contracts/registry.d.ts +3 -0
  29. package/dist/contracts/registry.js +2 -0
  30. package/dist/contracts/system/desktop-tools-contract.d.ts +5 -0
  31. package/dist/contracts/system/desktop-tools-contract.js +88 -0
  32. package/dist/contracts/system/host-tools-contract.d.ts +5 -0
  33. package/dist/contracts/system/host-tools-contract.js +76 -0
  34. package/dist/contracts/system/index.d.ts +3 -0
  35. package/dist/contracts/system/index.js +3 -0
  36. package/dist/contracts/system/tool-catalog-contract.d.ts +73 -0
  37. package/dist/contracts/system/tool-catalog-contract.js +70 -0
  38. package/dist/contracts/system-contracts.d.ts +3 -1
  39. package/dist/contracts/system-contracts.js +20 -6
  40. package/dist/contracts/tool-catalog-identity.d.ts +35 -0
  41. package/dist/contracts/tool-catalog-identity.js +88 -0
  42. package/dist/contracts/tool-catalog-migrations.d.ts +12 -0
  43. package/dist/contracts/tool-catalog-migrations.js +179 -0
  44. package/dist/contracts/tool-catalog-service.d.ts +30 -0
  45. package/dist/contracts/tool-catalog-service.js +287 -0
  46. package/dist/contracts/tool-catalog.d.ts +96 -0
  47. package/dist/contracts/tool-catalog.js +542 -0
  48. package/dist/contracts/types.d.ts +7 -5
  49. package/dist/data/messages.d.ts +3 -3
  50. package/dist/data/orm/sql.data-source.d.ts +5 -0
  51. package/dist/data/orm/sql.data-source.js +28 -10
  52. package/dist/data/orm/sql.data-source.test.d.ts +1 -1
  53. package/dist/data/orm/sql.data-source.test.js +160 -0
  54. package/dist/data/orm/table-container.d.ts +14 -0
  55. package/dist/data/orm/table-container.js +20 -0
  56. package/dist/data/tool-access-approvals.d.ts +6 -6
  57. package/dist/data/tool-catalog-fields.d.ts +31 -0
  58. package/dist/data/tool-catalog-fields.js +67 -0
  59. package/dist/data/tools.d.ts +38 -10
  60. package/dist/data/tools.js +31 -8
  61. package/dist/data/workflow-logs.d.ts +6 -2
  62. package/dist/data/workflow-logs.js +7 -5
  63. package/dist/data/workflow-runs.d.ts +6 -6
  64. package/dist/index.d.ts +0 -2
  65. package/dist/index.js +1 -3
  66. package/dist/package-loader/contract-package-loader.d.ts +12 -3
  67. package/dist/package-loader/contract-package-loader.js +54 -50
  68. package/dist/package-loader/contract-package-loader.test.d.ts +1 -1
  69. package/dist/package-loader/contract-package-loader.test.js +172 -13
  70. package/dist/package-loader/host-authority-loading.test.js +16 -3
  71. package/dist/package-loader/isolated-runtime.d.ts +14 -2
  72. package/dist/package-loader/isolated-table-gateway.d.ts +3 -1
  73. package/dist/package-loader/isolated-table-gateway.js +54 -5
  74. package/dist/package-loader/isolated-table-gateway.test.js +123 -0
  75. package/dist/package-loader/package-loader.d.ts +14 -2
  76. package/dist/package-loader/package-loader.js +184 -62
  77. package/dist/package-loader/package-loader.test.js +674 -1
  78. package/dist/package-loader/resolve-consumed-contracts.js +1 -1
  79. package/dist/system-ids.d.ts +6 -0
  80. package/dist/system-ids.js +7 -1
  81. package/dist/types/peers-package.d.ts +3 -2
  82. package/dist/types/workflow-run-context.d.ts +12 -2
  83. package/package.json +1 -1
  84. package/dist/tools/index.d.ts +0 -1
  85. package/dist/tools/index.js +0 -17
  86. package/dist/tools/tools-factory.d.ts +0 -5
  87. package/dist/tools/tools-factory.js +0 -34
@@ -0,0 +1,446 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ require("../../index");
4
+ const tools_1 = require("../../data/tools");
5
+ const system_ids_1 = require("../../system-ids");
6
+ const field_type_1 = require("../../types/field-type");
7
+ const utils_1 = require("../../utils");
8
+ const contract_provider_router_1 = require("../contract-provider-router");
9
+ const contract_resolvers_1 = require("../contract-resolvers");
10
+ const registry_1 = require("../registry");
11
+ const tool_catalog_identity_1 = require("../tool-catalog-identity");
12
+ const tool_catalog_service_1 = require("../tool-catalog-service");
13
+ function fixture() {
14
+ const target = { contractId: (0, utils_1.newid)(), version: 1, toolName: "run" };
15
+ const provider = (0, utils_1.newid)();
16
+ const registry = new registry_1.ContractRegistry();
17
+ const definition = {
18
+ contractId: target.contractId,
19
+ version: 1,
20
+ devTag: "dev",
21
+ name: "Test",
22
+ description: "",
23
+ tables: [],
24
+ observables: [],
25
+ events: [],
26
+ tools: [
27
+ {
28
+ name: "run",
29
+ usageDescription: "run",
30
+ inputFields: [{ name: "value", type: field_type_1.FieldType.string }],
31
+ outputFields: [],
32
+ },
33
+ ],
34
+ };
35
+ const row = {
36
+ toolId: (0, tool_catalog_identity_1.deriveToolCatalogId)(target),
37
+ name: "run",
38
+ usageDescription: "run",
39
+ code: "",
40
+ inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
41
+ outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
42
+ contractId: target.contractId,
43
+ contractVersion: 1,
44
+ toolName: "run",
45
+ catalogState: "resolved",
46
+ catalogIdentity: "deterministic",
47
+ };
48
+ const rows = new Map([[row.toolId, row]]);
49
+ const table = {
50
+ get: jest.fn(async (id) => rows.get(id)),
51
+ list: jest.fn(async (filter = {}, opts = {}) => [...rows.values()]
52
+ .filter((r) => (!filter.name || r.name === filter.name) &&
53
+ (!opts.textSearch || r.name.includes(opts.textSearch)))
54
+ .slice(0, opts.pageSize)),
55
+ };
56
+ const endpoint = {
57
+ handleCall: jest.fn(async () => "ok"),
58
+ dispose: jest.fn(),
59
+ };
60
+ const loader = {
61
+ catalogReady: Promise.resolve(),
62
+ contractRegistry: registry,
63
+ toolCatalog: { assertTargetUsable: jest.fn(async () => { }) },
64
+ resolveContractEndpoint: jest.fn(async (_request, _options) => endpoint),
65
+ isolatedRuntime: {
66
+ isInstalled: jest.fn(() => false),
67
+ getEffectiveToolLevel: jest.fn(() => 40),
68
+ resolveEndpoint: jest.fn(),
69
+ },
70
+ };
71
+ const dataContext = {
72
+ dataContextId: (0, utils_1.newid)(),
73
+ tableContainer: {
74
+ getTable: (meta) => (meta.name === "Tools" ? table : { get: async () => undefined }),
75
+ },
76
+ packageLoader: loader,
77
+ };
78
+ const userId = (0, utils_1.newid)();
79
+ Object.assign(dataContext, {
80
+ userContext: {
81
+ userId,
82
+ loadingPromise: Promise.resolve(),
83
+ getDataContext: () => dataContext,
84
+ userDataContext: dataContext,
85
+ },
86
+ });
87
+ const context = { callerUserId: userId, localUserId: userId, dataContext };
88
+ const host = (packageId = provider, version = 1, accessLevel = 40) => {
89
+ (0, contract_resolvers_1.stagePackageContractResolvers)(dataContext, packageId, () => (0, contract_resolvers_1.registerContractResolver)(target.contractId, version, () => ({
90
+ contractId: target.contractId,
91
+ version,
92
+ tools: { run: { tool: { ...row, accessLevel }, toolFn: () => "ok" } },
93
+ }), { providerPackageId: packageId }))();
94
+ };
95
+ const install = () => {
96
+ expect(registry.register(provider, definition).valid).toBe(true);
97
+ host();
98
+ };
99
+ return {
100
+ target,
101
+ provider,
102
+ registry,
103
+ definition,
104
+ row,
105
+ rows,
106
+ table,
107
+ endpoint,
108
+ loader,
109
+ dataContext,
110
+ context,
111
+ host,
112
+ install,
113
+ };
114
+ }
115
+ test("classifies missing providers, missing members, quarantine, missing implementations and removed legacy execution", async () => {
116
+ const f = fixture();
117
+ const describe = () => (0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, f.row);
118
+ expect(describe().availability.reason).toBe("no-provider");
119
+ f.registry.register(f.provider, f.definition);
120
+ expect(describe().availability.reason).toBe("no-implementation");
121
+ f.host();
122
+ expect(describe().availability.dispatch).toBe("contract");
123
+ f.definition.tools = [];
124
+ expect(describe().availability.reason).toBe("member-removed");
125
+ f.row.catalogState = "quarantined";
126
+ expect(describe().availability.reason).toBe("quarantined");
127
+ f.row.contractId = (0, utils_1.newid)();
128
+ expect(describe().availability.reason).toBe("invalid-identity");
129
+ const legacy = {
130
+ ...f.row,
131
+ toolId: (0, utils_1.newid)(),
132
+ contractId: undefined,
133
+ contractVersion: undefined,
134
+ toolName: undefined,
135
+ catalogIdentity: undefined,
136
+ catalogState: "unresolved",
137
+ };
138
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, legacy).availability.reason).toBe("legacy-execution-removed");
139
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, legacy).availability.message).toContain("contract resolver");
140
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, { ...legacy, packageId: f.provider }).availability).toMatchObject({
141
+ dispatch: "unavailable",
142
+ reason: "legacy-package-upgrade-required",
143
+ message: expect.stringContaining(f.provider),
144
+ });
145
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, legacy).availability.dispatch).toBe("unavailable");
146
+ });
147
+ test("projects aliases including the shipped Scheduler ID and groups equal name matches", async () => {
148
+ const f = fixture();
149
+ const alias = {
150
+ ...f.row,
151
+ toolId: "00muxgugn661g0qmpuwafrmsi4",
152
+ contractId: system_ids_1.schedulerContractId,
153
+ toolName: "list",
154
+ catalogIdentity: "legacy-alias",
155
+ };
156
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, alias).aliases).toEqual([alias.toolId]);
157
+ f.rows.clear();
158
+ const aliases = ["00muxoa8xeet2lpi8bdq55q03", "00muxob3hbo3f6g49mipy1hyt"];
159
+ for (const toolId of aliases)
160
+ f.rows.set(toolId, {
161
+ ...alias,
162
+ toolId,
163
+ contractId: "00muxgugn6xvs8f2r80ru205t",
164
+ toolName: "invoke",
165
+ name: "invoke",
166
+ });
167
+ expect((await (0, tool_catalog_service_1.findToolCatalogEntry)(f.dataContext, { toolIdOrName: "invoke" }))?.aliases).toEqual(aliases);
168
+ });
169
+ test.each([
170
+ "authority",
171
+ "assistantId",
172
+ "workflowVars",
173
+ "callerPackageId",
174
+ ])("rejects payload key %s", async (key) => {
175
+ const f = fixture();
176
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId, [key]: { userAccessLevel: 100 } }, f.context)).rejects.toThrow("Unexpected invokeTool input key");
177
+ expect(f.loader.resolveContractEndpoint).not.toHaveBeenCalled();
178
+ });
179
+ test.each([
180
+ { callerPackageId: system_ids_1.peersSystemPackageId },
181
+ { callerDeviceId: (0, utils_1.newid)() },
182
+ { callerUserId: undefined, localUserId: undefined },
183
+ { dataContext: {} },
184
+ ])("rejects unauthenticated, package, device and cross-context callers: %j", async (override) => {
185
+ const f = fixture();
186
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId }, {
187
+ ...f.context,
188
+ ...override,
189
+ })).rejects.toThrow();
190
+ expect(f.loader.resolveContractEndpoint).not.toHaveBeenCalled();
191
+ });
192
+ test("validates arguments, converts dates, and forwards original nested objects and unknown fields", async () => {
193
+ const f = fixture();
194
+ f.install();
195
+ const invoke = (args) => (0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId, args }, f.context);
196
+ for (const args of [undefined, [], null, "bad", { value: 4 }])
197
+ await expect(invoke(args)).rejects.toThrow();
198
+ f.definition.tools[0].inputFields.push({ name: "when", type: field_type_1.FieldType.Date }, { name: "object", type: field_type_1.FieldType.object }, { name: "defaulted", type: field_type_1.FieldType.string, defaultValue: "default" });
199
+ const object = { nested: "retained" };
200
+ const args = { value: "hello", when: "2026-10-16", object, extra: 1 };
201
+ await expect(invoke(args)).resolves.toBe("ok");
202
+ const forwarded = f.endpoint.handleCall.mock.calls[0][0].args[0];
203
+ expect(forwarded).toEqual({ ...args, when: new Date(args.when) });
204
+ expect(forwarded.object).toBe(object);
205
+ expect(forwarded).not.toHaveProperty("defaulted");
206
+ expect(args.when).toBe("2026-10-16");
207
+ await expect(invoke({ ...args, when: "not-a-date" })).rejects.toThrow("argument validation");
208
+ expect(f.endpoint.dispose).toHaveBeenCalledTimes(1);
209
+ });
210
+ test("selects strict, preferred and fallback providers and observes active changes under the same ID", async () => {
211
+ const f = fixture();
212
+ f.install();
213
+ const second = (0, utils_1.newid)();
214
+ f.registry.register(second, f.definition);
215
+ f.host(second);
216
+ const invoke = (input = {}) => (0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId, args: { value: "ok" }, ...input }, f.context);
217
+ await expect(invoke({ providerPackageId: (0, utils_1.newid)() })).rejects.toThrow("Strict provider selection");
218
+ await invoke({ providerPackageId: second });
219
+ expect(f.loader.resolveContractEndpoint.mock.calls.at(-1)?.[0]).toMatchObject({
220
+ providerPackageId: second,
221
+ });
222
+ await invoke({ providerPackageId: (0, utils_1.newid)(), fallbackToDefaultProvider: true });
223
+ expect(f.loader.resolveContractEndpoint.mock.calls.at(-1)?.[0]).toMatchObject({
224
+ providerPackageId: f.provider,
225
+ });
226
+ f.registry.setActiveProvider(f.target.contractId, 1, second);
227
+ await invoke();
228
+ expect(f.loader.resolveContractEndpoint.mock.calls.at(-1)?.[0]).toMatchObject({
229
+ providerPackageId: second,
230
+ context: f.context,
231
+ });
232
+ expect(f.endpoint.handleCall).toHaveBeenLastCalledWith(expect.objectContaining({ args: [{ value: "ok" }] }), f.context);
233
+ });
234
+ test("host alsoImplements validates the requested v1 schema and resolves v2 code", async () => {
235
+ const f = fixture();
236
+ f.registry.register((0, utils_1.newid)(), f.definition);
237
+ const newer = {
238
+ ...f.definition,
239
+ version: 2,
240
+ tools: [
241
+ {
242
+ ...f.definition.tools[0],
243
+ inputFields: [
244
+ ...f.definition.tools[0].inputFields,
245
+ { name: "new", type: field_type_1.FieldType.number, optional: true },
246
+ ],
247
+ },
248
+ ],
249
+ };
250
+ expect(f.registry.register(f.provider, newer, [{ contractId: f.target.contractId, version: 1 }]).valid).toBe(true);
251
+ f.registry.setActiveProvider(f.target.contractId, 1, f.provider);
252
+ f.host(f.provider, 2);
253
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, f.row).provider?.implementation?.version).toBe(2);
254
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId, args: { value: "v1", new: "not a v2 number" } }, f.context)).resolves.toBe("ok");
255
+ });
256
+ test("rejects self targets, unresolved and quarantined rows, and never uses legacy invocation", async () => {
257
+ const f = fixture();
258
+ for (const row of [
259
+ { ...f.row, catalogState: "quarantined" },
260
+ {
261
+ ...f.row,
262
+ toolId: (0, utils_1.newid)(),
263
+ contractId: undefined,
264
+ contractVersion: undefined,
265
+ toolName: undefined,
266
+ catalogIdentity: undefined,
267
+ catalogState: "unresolved",
268
+ },
269
+ {
270
+ ...f.row,
271
+ toolId: (0, tool_catalog_identity_1.deriveToolCatalogId)({
272
+ contractId: system_ids_1.toolCatalogContractId,
273
+ version: 1,
274
+ toolName: "invokeTool",
275
+ }),
276
+ contractId: system_ids_1.toolCatalogContractId,
277
+ toolName: "invokeTool",
278
+ },
279
+ ]) {
280
+ f.rows.set(row.toolId, row);
281
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: row.toolId }, f.context)).rejects.toThrow();
282
+ }
283
+ f.rows.set(f.row.toolId, f.row);
284
+ f.registry.register(f.provider, f.definition);
285
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId, args: { value: "ok" } }, f.context)).rejects.toThrow("no-implementation");
286
+ expect(f.loader.resolveContractEndpoint).not.toHaveBeenCalled();
287
+ });
288
+ test.each([
289
+ "own",
290
+ "downstream",
291
+ "failure",
292
+ "twice",
293
+ ])("disposes endpoints and retries only its own first retirement: %s", async (mode) => {
294
+ const f = fixture();
295
+ f.install();
296
+ const next = { handleCall: jest.fn(async () => "next"), dispose: jest.fn() };
297
+ const replacement = (0, utils_1.newid)();
298
+ f.registry.register(replacement, f.definition);
299
+ f.host(replacement);
300
+ if (mode === "own")
301
+ f.loader.resolveContractEndpoint.mockImplementationOnce(async () => {
302
+ f.registry.setActiveProvider(f.target.contractId, 1, replacement);
303
+ return f.endpoint;
304
+ });
305
+ if (mode !== "own")
306
+ f.loader.resolveContractEndpoint.mockResolvedValueOnce(f.endpoint);
307
+ f.loader.resolveContractEndpoint.mockResolvedValueOnce(next);
308
+ f.endpoint.handleCall.mockRejectedValueOnce(mode === "failure"
309
+ ? new Error("failed")
310
+ : new contract_provider_router_1.RetiredContractProviderEndpointError(mode === "downstream" ? next : f.endpoint));
311
+ if (mode === "twice")
312
+ next.handleCall.mockRejectedValueOnce(new contract_provider_router_1.RetiredContractProviderEndpointError(next));
313
+ const invoking = (0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: f.row.toolId, args: { value: "ok" } }, f.context);
314
+ if (mode === "own") {
315
+ await expect(invoking).resolves.toBe("next");
316
+ expect(f.loader.resolveContractEndpoint.mock.calls.at(-1)?.[0].providerPackageId).toBe(replacement);
317
+ }
318
+ else
319
+ await expect(invoking).rejects.toThrow();
320
+ expect(f.endpoint.dispose).toHaveBeenCalledTimes(1);
321
+ expect(f.loader.resolveContractEndpoint).toHaveBeenCalledTimes(mode === "own" || mode === "twice" ? 2 : 1);
322
+ expect(next.dispose).toHaveBeenCalledTimes(mode === "own" || mode === "twice" ? 1 : 0);
323
+ });
324
+ test("waits for readiness, bounds discovery, and projects live access without resolving an endpoint", async () => {
325
+ const f = fixture();
326
+ f.install();
327
+ await (0, tool_catalog_service_1.listToolCatalogEntries)(f.dataContext, { limit: 900, query: "ru" });
328
+ expect(f.table.list).toHaveBeenCalledWith({}, { pageSize: 500, textSearch: "ru" });
329
+ expect((await (0, tool_catalog_service_1.findToolCatalogEntry)(f.dataContext, { toolIdOrName: f.row.toolId }, f.context))?.access).toEqual({ accessLevel: 40, callerAccessLevel: 100, allowed: true });
330
+ f.loader.isolatedRuntime.isInstalled.mockReturnValue(true);
331
+ expect((await (0, tool_catalog_service_1.findToolCatalogEntry)(f.dataContext, { toolIdOrName: "run" }, f.context))?.access
332
+ ?.accessLevel).toBe(40);
333
+ expect(f.loader.isolatedRuntime.resolveEndpoint).not.toHaveBeenCalled();
334
+ expect(f.loader.resolveContractEndpoint).not.toHaveBeenCalled();
335
+ let ready;
336
+ f.loader.catalogReady = new Promise((resolve) => {
337
+ ready = resolve;
338
+ });
339
+ f.table.list.mockClear();
340
+ const pending = (0, tool_catalog_service_1.listToolCatalogEntries)(f.dataContext);
341
+ await Promise.resolve();
342
+ expect(f.table.list).not.toHaveBeenCalled();
343
+ ready();
344
+ await pending;
345
+ });
346
+ test("an old bundle declaring a member without a resolver reports no implementation", () => {
347
+ const f = fixture();
348
+ f.registry.register(f.provider, f.definition);
349
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, f.row).availability).toEqual({
350
+ dispatch: "unavailable",
351
+ reason: "no-implementation",
352
+ message: expect.stringContaining(`Provider ${f.provider} declares run but registers no implementation`),
353
+ });
354
+ });
355
+ test("workflow requirement is projected and enforced in the matching host context", async () => {
356
+ const f = fixture();
357
+ f.definition.tools[0].requiresWorkflowRun = true;
358
+ f.install();
359
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, f.row)).toMatchObject({
360
+ workflowInvocable: true,
361
+ availability: { requiresWorkflowRun: true },
362
+ });
363
+ const input = { toolId: f.row.toolId, args: { value: "x" } };
364
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, f.context)).rejects.toThrow("requires a host workflow");
365
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, {
366
+ ...f.context,
367
+ workflow: { dataContextId: "elsewhere" },
368
+ })).rejects.toThrow("does not match");
369
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, {
370
+ ...f.context,
371
+ workflow: { dataContextId: f.dataContext.dataContextId },
372
+ })).resolves.toBe("ok");
373
+ });
374
+ test("remote catalog callers require host opt-in and retain identity; local-only targets reject", async () => {
375
+ const f = fixture();
376
+ f.install();
377
+ const remote = { ...f.context, callerDeviceId: (0, utils_1.newid)(), callerUserId: (0, utils_1.newid)() };
378
+ const input = { toolId: f.row.toolId, args: { value: "x" } };
379
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, remote)).rejects.toThrow("local user");
380
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, remote, { allowRemoteCaller: true })).resolves.toBe("ok");
381
+ expect(f.endpoint.handleCall).toHaveBeenCalledWith(expect.anything(), remote);
382
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, { ...remote, callerPackageId: (0, utils_1.newid)() }, { allowRemoteCaller: true })).rejects.toThrow("local user");
383
+ const { hostToolsContractId, getHostToolsContractDefinition } = require("../../index");
384
+ const { toolCatalogIds } = require("../tool-catalog-migrations");
385
+ const target = { contractId: hostToolsContractId, version: 1, toolName: "create-group" };
386
+ const row = {
387
+ ...f.row,
388
+ toolId: toolCatalogIds(target)[0],
389
+ contractId: target.contractId,
390
+ toolName: target.toolName,
391
+ catalogIdentity: "legacy-alias",
392
+ };
393
+ f.rows.set(row.toolId, row);
394
+ f.registry.register(system_ids_1.peersSystemPackageId, getHostToolsContractDefinition());
395
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, { toolId: row.toolId }, remote, {
396
+ allowRemoteCaller: true,
397
+ })).rejects.toThrow("local-only");
398
+ });
399
+ test("workflow invocability excludes administrative systems and unavailable tools", () => {
400
+ const f = fixture();
401
+ f.install();
402
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, f.row).workflowInvocable).toBe(true);
403
+ f.registry.unregister(f.provider);
404
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, f.row).workflowInvocable).toBe(false);
405
+ const sdk = require("../../index");
406
+ const ids = require("../tool-catalog-migrations");
407
+ for (const [definition, name, allowed] of [
408
+ [sdk.getHostToolsContractDefinition(), "tool-search", true],
409
+ [sdk.getToolAccessControlContractDefinition(), "approve", false],
410
+ [sdk.getIdentityContractDefinition(), "signIn", false],
411
+ ]) {
412
+ f.registry.register(system_ids_1.peersSystemPackageId, definition);
413
+ const target = { contractId: definition.contractId, version: 1, toolName: name };
414
+ const row = {
415
+ ...f.row,
416
+ toolId: ids.toolCatalogIds(target)[0],
417
+ contractId: target.contractId,
418
+ toolName: name,
419
+ catalogIdentity: "legacy-alias",
420
+ };
421
+ f.loader.isolatedRuntime.isInstalled.mockReturnValue(true);
422
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, row).workflowInvocable).toBe(allowed);
423
+ }
424
+ const legacy = {
425
+ ...f.row,
426
+ toolId: (0, utils_1.newid)(),
427
+ contractId: undefined,
428
+ contractVersion: undefined,
429
+ toolName: undefined,
430
+ catalogState: "unresolved",
431
+ catalogIdentity: undefined,
432
+ };
433
+ expect((0, tool_catalog_service_1.describeToolCatalogRow)(f.dataContext, legacy).workflowInvocable).toBe(false);
434
+ });
435
+ test("workflow admission rejects a newly raised provider level and strips workflow from isolates", async () => {
436
+ const f = fixture();
437
+ f.install();
438
+ const context = { ...f.context, workflow: { dataContextId: f.dataContext.dataContextId } };
439
+ const input = { toolId: f.row.toolId, args: { value: "x" } };
440
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, context, { maxAccessLevel: 20 })).rejects.toThrow("exceeds the authorized");
441
+ f.loader.isolatedRuntime.isInstalled.mockReturnValue(true);
442
+ await (0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, context, { maxAccessLevel: 40 });
443
+ expect(f.loader.resolveContractEndpoint.mock.calls[0][0].context.workflow).toBeUndefined();
444
+ expect(f.endpoint.handleCall.mock.calls[0][1].workflow).toBeUndefined();
445
+ await expect((0, tool_catalog_service_1.invokeToolCatalogTool)(f.dataContext, input, { ...f.context, callerUserId: undefined, callerDeviceId: (0, utils_1.newid)() }, { allowRemoteCaller: true })).rejects.toThrow("caller user identity");
446
+ });
@@ -0,0 +1 @@
1
+ import "../../index";