@peers-app/peers-sdk 0.25.20 → 0.26.0

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 (94) 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 +649 -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 +91 -0
  47. package/dist/contracts/tool-catalog.js +490 -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-installer/package-creator.d.ts +14 -5
  67. package/dist/package-installer/package-creator.js +92 -37
  68. package/dist/package-installer/package-creator.test.js +308 -23
  69. package/dist/package-installer/types.d.ts +8 -0
  70. package/dist/package-loader/contract-package-loader.d.ts +12 -3
  71. package/dist/package-loader/contract-package-loader.js +54 -50
  72. package/dist/package-loader/contract-package-loader.test.d.ts +1 -1
  73. package/dist/package-loader/contract-package-loader.test.js +172 -13
  74. package/dist/package-loader/host-authority-loading.test.js +16 -3
  75. package/dist/package-loader/isolated-artifact.d.ts +14 -2
  76. package/dist/package-loader/isolated-artifact.js +6 -0
  77. package/dist/package-loader/isolated-artifact.test.js +34 -0
  78. package/dist/package-loader/isolated-runtime.d.ts +14 -2
  79. package/dist/package-loader/isolated-table-gateway.d.ts +3 -1
  80. package/dist/package-loader/isolated-table-gateway.js +54 -5
  81. package/dist/package-loader/isolated-table-gateway.test.js +123 -0
  82. package/dist/package-loader/package-loader.d.ts +14 -2
  83. package/dist/package-loader/package-loader.js +184 -62
  84. package/dist/package-loader/package-loader.test.js +695 -2
  85. package/dist/package-loader/resolve-consumed-contracts.js +1 -1
  86. package/dist/system-ids.d.ts +6 -0
  87. package/dist/system-ids.js +7 -1
  88. package/dist/types/peers-package.d.ts +3 -2
  89. package/dist/types/workflow-run-context.d.ts +12 -2
  90. package/package.json +1 -1
  91. package/dist/tools/index.d.ts +0 -1
  92. package/dist/tools/index.js +0 -17
  93. package/dist/tools/tools-factory.d.ts +0 -5
  94. package/dist/tools/tools-factory.js +0 -34
@@ -1,19 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- jest.mock("../data/tools", () => ({
4
- Tools: () => ({
5
- save: jest.fn().mockResolvedValue(undefined),
6
- }),
7
- }));
8
- jest.mock("../tools", () => ({
9
- registerTool: jest.fn(),
10
- }));
3
+ require("../index");
4
+ const mockBeforeToolSave = jest.fn();
5
+ jest.mock("../data/tools", () => {
6
+ const actual = jest.requireActual("../data/tools");
7
+ const contexts = new WeakMap();
8
+ return {
9
+ ...actual,
10
+ Tools: (context) => {
11
+ let rows = contexts.get(context);
12
+ if (!rows) {
13
+ rows = new Map();
14
+ contexts.set(context, rows);
15
+ }
16
+ const records = rows;
17
+ return {
18
+ list: async () => [...records.values()],
19
+ get: async (id) => records.get(id),
20
+ save: async (row, opts) => {
21
+ await mockBeforeToolSave(row);
22
+ if (!opts?.insertOnly || !records.has(row.toolId))
23
+ records.set(row.toolId, row);
24
+ return records.get(row.toolId);
25
+ },
26
+ update: async (patch) => {
27
+ const row = { ...records.get(patch.toolId), ...patch };
28
+ records.set(patch.toolId, row);
29
+ return row;
30
+ },
31
+ dataChanged: { subscribe: () => ({ unsubscribe: () => true }) },
32
+ };
33
+ },
34
+ };
35
+ });
11
36
  jest.mock("../data/assistants", () => ({
12
37
  Assistants: () => ({
13
38
  save: jest.fn().mockResolvedValue(undefined),
14
39
  }),
15
40
  }));
16
41
  const registry_1 = require("../contracts/registry");
42
+ const tool_catalog_1 = require("../contracts/tool-catalog");
43
+ const tool_catalog_identity_1 = require("../contracts/tool-catalog-identity");
44
+ const table_container_1 = require("../data/orm/table-container");
45
+ const tools_1 = require("../data/tools");
46
+ const field_type_1 = require("../types/field-type");
17
47
  const utils_1 = require("../utils");
18
48
  const contract_package_loader_1 = require("./contract-package-loader");
19
49
  describe("installContractPackage registry wiring", () => {
@@ -40,7 +70,6 @@ describe("installContractPackage registry wiring", () => {
40
70
  alsoImplements: new Map(),
41
71
  assistants: [],
42
72
  appNavs: [],
43
- toolInstances: [],
44
73
  tableDefinitions: [],
45
74
  ...overrides,
46
75
  };
@@ -49,7 +78,7 @@ describe("installContractPackage registry wiring", () => {
49
78
  return {
50
79
  dataContextId: "test-ctx",
51
80
  tableContainer: {
52
- registerTableDefinition: jest.fn(),
81
+ registerTableDefinitions: jest.fn(),
53
82
  },
54
83
  };
55
84
  }
@@ -62,9 +91,59 @@ describe("installContractPackage registry wiring", () => {
62
91
  expect(registry.getProviderPackageId(contractId, 1)).toBe(packageId);
63
92
  expect(registry.getContractKeysForPackage(packageId)).toEqual([`${contractId}@1`]);
64
93
  });
94
+ it("persists CLI runner metadata without executable registration", async () => {
95
+ const registry = new registry_1.ContractRegistry();
96
+ const context = mockDataContext();
97
+ const tool = {
98
+ toolId: "00mlwl2km0iymo1hwhtvqij2n",
99
+ name: "cli_assistant_runner",
100
+ usageDescription: "CLI",
101
+ code: "",
102
+ inputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
103
+ outputSchema: { type: tools_1.IOSchemaType.none, fields: [] },
104
+ isAssistantRunner: true,
105
+ configSchema: { type: tools_1.IOSchemaType.simple, fields: [] },
106
+ };
107
+ const definition = makeDefinition({
108
+ toolMetadata: [
109
+ {
110
+ contractId: "00mp287gdomtx12ald8oelyp0",
111
+ version: 1,
112
+ toolName: tool.name,
113
+ isAssistantRunner: true,
114
+ configSchema: tool.configSchema,
115
+ },
116
+ ],
117
+ });
118
+ definition.contracts[0] = {
119
+ ...definition.contracts[0],
120
+ contractId: "00mp287gdomtx12ald8oelyp0",
121
+ tools: [{ name: tool.name, usageDescription: "CLI", inputFields: [], outputFields: [] }],
122
+ };
123
+ await (0, contract_package_loader_1.installContractPackage)(context, definition, { registry });
124
+ expect(await (0, tools_1.Tools)(context).get(tool.toolId)).toMatchObject({
125
+ isAssistantRunner: true,
126
+ configSchema: tool.configSchema,
127
+ catalogState: "resolved",
128
+ });
129
+ await registry.toolCatalog?.dispose();
130
+ jest.clearAllMocks();
131
+ });
65
132
  it("does not register providers when no registry is provided", async () => {
66
133
  const dataContext = mockDataContext();
67
- await expect((0, contract_package_loader_1.installContractPackage)(dataContext, makeDefinition())).resolves.toBeUndefined();
134
+ const dispose = jest.spyOn(tool_catalog_1.ToolCatalog.prototype, "dispose");
135
+ try {
136
+ await expect((0, contract_package_loader_1.installContractPackage)(dataContext, makeDefinition())).resolves.toBeUndefined();
137
+ // The throwaway registry's catalog must not outlive the call.
138
+ expect(dispose).toHaveBeenCalledTimes(1);
139
+ const registry = new registry_1.ContractRegistry();
140
+ await (0, contract_package_loader_1.installContractPackage)(dataContext, makeDefinition(), { registry });
141
+ expect(dispose).toHaveBeenCalledTimes(1);
142
+ await registry.toolCatalog?.dispose();
143
+ }
144
+ finally {
145
+ dispose.mockRestore();
146
+ }
68
147
  });
69
148
  it("keeps the previous provider active when package persistence fails", async () => {
70
149
  const registry = new registry_1.ContractRegistry();
@@ -72,7 +151,7 @@ describe("installContractPackage registry wiring", () => {
72
151
  previous.contracts[0] = { ...previous.contracts[0], name: "Previous Contract" };
73
152
  await (0, contract_package_loader_1.installContractPackage)(mockDataContext(), previous, { registry });
74
153
  const dataContext = mockDataContext();
75
- dataContext.tableContainer.registerTableDefinition.mockImplementation(() => {
154
+ dataContext.tableContainer.registerTableDefinitions.mockImplementation(() => {
76
155
  throw new Error("table registration failed");
77
156
  });
78
157
  const replacement = makeDefinition({
@@ -82,6 +161,87 @@ describe("installContractPackage registry wiring", () => {
82
161
  expect(registry.resolve(contractId, 1)?.name).toBe("Previous Contract");
83
162
  expect(registry.getProviderPackageId(contractId, 1)).toBe(packageId);
84
163
  });
164
+ it.each([
165
+ "catalog-write",
166
+ "invalid-table",
167
+ ])("keeps active host tables through %s failure and permits retry", async (failure) => {
168
+ const registry = new registry_1.ContractRegistry();
169
+ const container = new table_container_1.TableContainer((metaData) => ({ metaData }));
170
+ const context = { dataContextId: (0, utils_1.newid)(), tableContainer: container };
171
+ const definition = makeDefinition();
172
+ definition.contracts[0].tools = [
173
+ { name: "read", usageDescription: "Read", inputFields: [], outputFields: [] },
174
+ ];
175
+ const tableId = (0, utils_1.newid)();
176
+ const tableDefinition = (primaryKeyName) => ({
177
+ metaData: {
178
+ tableId,
179
+ name: "State",
180
+ description: "State",
181
+ primaryKeyName,
182
+ fields: [{ name: primaryKeyName, type: field_type_1.FieldType.id }],
183
+ },
184
+ });
185
+ definition.tableDefinitions = [tableDefinition("oldId")];
186
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
187
+ try {
188
+ await (0, contract_package_loader_1.installContractPackage)(context, definition, { registry });
189
+ const previous = registry.resolve(contractId, 1);
190
+ const oldTable = container.getTableByName(`State_${tableId}`);
191
+ let saves = 0;
192
+ mockBeforeToolSave.mockImplementation(async (row) => {
193
+ if (failure === "catalog-write" && row.contractId === contractId && ++saves === 2) {
194
+ expect(container.getTableByName(`State_${tableId}`)).toBe(oldTable);
195
+ throw new Error("final catalog disk failure");
196
+ }
197
+ });
198
+ const replacement = { ...definition, tableDefinitions: [tableDefinition("newId")] };
199
+ if (failure === "invalid-table") {
200
+ const invalid = tableDefinition("invalidId");
201
+ invalid.metaData.tableId = "invalid";
202
+ replacement.tableDefinitions.push(invalid);
203
+ }
204
+ await expect((0, contract_package_loader_1.installContractPackage)(context, replacement, { registry })).rejects.toThrow(failure === "catalog-write" ? "final catalog disk failure" : "Invalid tableId");
205
+ expect(registry.resolve(contractId, 1)).toBe(previous);
206
+ expect(container.getTableByName(`State_${tableId}`)).toBe(oldTable);
207
+ expect(oldTable.metaData.primaryKeyName).toBe("oldId");
208
+ mockBeforeToolSave.mockReset();
209
+ replacement.tableDefinitions = [tableDefinition("newId")];
210
+ await (0, contract_package_loader_1.installContractPackage)(context, replacement, { registry });
211
+ expect(container.getTableByName(`State_${tableId}`).metaData.primaryKeyName).toBe("newId");
212
+ }
213
+ finally {
214
+ mockBeforeToolSave.mockReset();
215
+ warn.mockRestore();
216
+ await registry.toolCatalog?.dispose();
217
+ }
218
+ });
219
+ it("indexes public host definitions without instances and activates only after loader preparation", async () => {
220
+ const registry = new registry_1.ContractRegistry();
221
+ const dataContext = mockDataContext();
222
+ const definition = makeDefinition();
223
+ definition.contracts[0].tools = [
224
+ { name: "read", usageDescription: "Read", inputFields: [], outputFields: [] },
225
+ ];
226
+ const toolId = (0, tool_catalog_identity_1.deriveToolCatalogId)({ contractId, version: 1, toolName: "read" });
227
+ const activated = jest.fn();
228
+ const prepare = jest.fn(async () => {
229
+ expect(await (0, tools_1.Tools)(dataContext).get(toolId)).toMatchObject({ catalogState: "resolved" });
230
+ expect(registry.resolve(contractId, 1)).toBeUndefined();
231
+ throw new Error("loader preparation failed");
232
+ });
233
+ await expect((0, contract_package_loader_1.installContractPackage)(dataContext, definition, {
234
+ registry,
235
+ prepare,
236
+ onActivated: activated,
237
+ })).rejects.toThrow(/loader preparation failed/);
238
+ expect(activated).not.toHaveBeenCalled();
239
+ expect(registry.resolve(contractId, 1)).toBeUndefined();
240
+ await (0, contract_package_loader_1.installContractPackage)(dataContext, definition, { registry, onActivated: activated });
241
+ expect(activated).toHaveBeenCalledTimes(1);
242
+ expect((await (0, tools_1.Tools)(dataContext).list()).filter((row) => row.contractId === contractId)).toHaveLength(1);
243
+ await registry.toolCatalog?.dispose();
244
+ });
85
245
  });
86
246
  describe("extractPackageDefinition", () => {
87
247
  it("returns a package definition when packageId and contracts are present", () => {
@@ -93,7 +253,6 @@ describe("extractPackageDefinition", () => {
93
253
  alsoImplements: new Map(),
94
254
  assistants: [],
95
255
  appNavs: [],
96
- toolInstances: [],
97
256
  tableDefinitions: [],
98
257
  };
99
258
  expect((0, contract_package_loader_1.extractPackageDefinition)({ packageDefinition: def })).toBe(def);
@@ -6,13 +6,26 @@ test("host-backed package endpoints honor declared propagation and reread live m
6
6
  const contractId = (0, index_1.newid)();
7
7
  const userId = (0, index_1.newid)();
8
8
  const groupId = (0, index_1.newid)();
9
+ const rows = new Map();
10
+ const tools = {
11
+ list: async () => [...rows.values()],
12
+ get: async (id) => rows.get(id),
13
+ save: async (row, opts) => {
14
+ if (!opts?.insertOnly || !rows.has(row.toolId))
15
+ rows.set(row.toolId, row);
16
+ return rows.get(row.toolId);
17
+ },
18
+ dataChanged: { subscribe: () => ({ unsubscribe: () => true }) },
19
+ };
9
20
  const dataContext = {
10
21
  dataContextId: groupId,
11
22
  groupId,
12
23
  tableContainer: {
13
- getTable: (meta) => meta.name === "Groups"
14
- ? { get: async () => ({ founderUserId: (0, index_1.newid)() }) }
15
- : { findOne: async () => ({ userId, role: index_1.GroupMemberRole.Admin }) },
24
+ getTable: (meta) => meta.name === "Tools"
25
+ ? tools
26
+ : meta.name === "Groups"
27
+ ? { get: async () => ({ founderUserId: (0, index_1.newid)() }) }
28
+ : { findOne: async () => ({ userId, role: index_1.GroupMemberRole.Admin }) },
16
29
  },
17
30
  };
18
31
  Object.assign(dataContext, {
@@ -72,6 +72,8 @@ export interface IIsolatedPackageNetworkCredential {
72
72
  *
73
73
  * Honored at install the same way `suggestedAccessLevel` is: there is no
74
74
  * separate approval step yet. The Packages screen displays the declaration.
75
+ * This constrains the System HTTP bridge in both execution modes. Privileged
76
+ * providers can also use Node's network APIs directly, outside this policy.
75
77
  */
76
78
  export interface IIsolatedPackageNetwork {
77
79
  allowedHosts: string[];
@@ -152,11 +154,21 @@ export interface IIsolatedActionAlias {
152
154
  export interface IIsolatedPackageManifest {
153
155
  packageId: string;
154
156
  version: string;
157
+ /**
158
+ * Provider execution backend. Omission selects the SES sandbox. Privileged
159
+ * providers run as trusted code in a separate Node process with the host's
160
+ * OS account, environment, working directory, and unrestricted OS access;
161
+ * this does not elevate to root/admin and does not apply to UI code.
162
+ */
163
+ executionMode?: "sandbox" | "privileged";
155
164
  provides: IIsolatedProvidedContract[];
156
165
  consumes: IIsolatedConsumedContract[];
157
166
  /** Package-owned table schemas. Contract references decide public exposure. */
158
167
  tables?: IIsolatedPackageTable[];
159
- /** Host allowlist and credential refs for System HTTP. Omit to deny outbound HTTP. */
168
+ /**
169
+ * System HTTP allowlist and credentials. Omit to deny requests through that bridge.
170
+ * Privileged providers retain direct OS/network access.
171
+ */
160
172
  network?: IIsolatedPackageNetwork;
161
173
  appNavs?: IAppNav[];
162
174
  /** Private checked targets, independent of the frozen Voice Skill catalog. */
@@ -164,7 +176,7 @@ export interface IIsolatedPackageManifest {
164
176
  }
165
177
  /**
166
178
  * Installable isolated package: declarative manifest plus provider source
167
- * that may run only inside a supervised worker.
179
+ * that runs only inside a supervised sandbox worker or privileged Node process.
168
180
  */
169
181
  export interface IIsolatedPackageArtifact {
170
182
  manifest: IIsolatedPackageManifest;
@@ -587,10 +587,16 @@ function validateIsolatedPackageArtifact(artifact) {
587
587
  "network",
588
588
  "appNavs",
589
589
  "actionAliases",
590
+ "executionMode",
590
591
  ], "manifest");
591
592
  if (typeof providerSource !== "string" || providerSource.trim() === "") {
592
593
  throw new Error("Package artifact.providerSource must be a non-empty string");
593
594
  }
595
+ if (manifest.executionMode !== undefined &&
596
+ manifest.executionMode !== "sandbox" &&
597
+ manifest.executionMode !== "privileged") {
598
+ throw new Error('manifest.executionMode must be "sandbox" or "privileged"');
599
+ }
594
600
  assertPeerId(manifest.packageId, "packageId");
595
601
  if (typeof manifest.version !== "string" || manifest.version.trim() === "") {
596
602
  throw new Error("package version is required");
@@ -40,6 +40,40 @@ function makeArtifact(overrides = {}) {
40
40
  };
41
41
  }
42
42
  describe("isolated package artifact", () => {
43
+ test.each([
44
+ undefined,
45
+ "sandbox",
46
+ "privileged",
47
+ ])("round-trips executionMode=%s without changing the legacy envelope", (executionMode) => {
48
+ const { artifact } = makeArtifact();
49
+ if (executionMode !== undefined)
50
+ artifact.manifest.executionMode = executionMode;
51
+ const bundle = (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact);
52
+ const parsed = (0, isolated_artifact_1.parseIsolatedPackageArtifact)(bundle);
53
+ expect(parsed.manifest.executionMode).toBe(executionMode);
54
+ expect((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(parsed)).toBe(bundle);
55
+ if (executionMode === undefined)
56
+ expect(bundle).not.toContain("executionMode");
57
+ });
58
+ test.each([
59
+ null,
60
+ true,
61
+ false,
62
+ 1,
63
+ "",
64
+ "node",
65
+ "Privileged",
66
+ {},
67
+ [],
68
+ ])("rejects malformed executionMode=%p at validation and parse time", (executionMode) => {
69
+ const { artifact } = makeArtifact();
70
+ Object.assign(artifact.manifest, { executionMode });
71
+ const bundle = `${isolated_artifact_1.ISOLATED_PACKAGE_MAGIC}\n${JSON.stringify(artifact)}\n`;
72
+ expect((0, isolated_artifact_1.looksLikeIsolatedPackageArtifact)(bundle)).toBe(true);
73
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/executionMode/);
74
+ expect(() => (0, isolated_artifact_1.parseIsolatedPackageArtifact)(bundle)).toThrow(/executionMode/);
75
+ expect(() => (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).toThrow(/executionMode/);
76
+ });
43
77
  test("round-trips a generic authority request and rejects unknown modes", () => {
44
78
  const { artifact } = makeArtifact();
45
79
  const tool = artifact.manifest.provides[0].tools[0];
@@ -28,6 +28,8 @@ export interface IIsolatedCallPrincipal {
28
28
  }
29
29
  /** Trusted lifecycle metadata supplied when an isolated artifact is installed. */
30
30
  export interface IIsolatedPackageInstallOptions {
31
+ /** Host load-generation guard, rechecked before publication after asynchronous preparation. */
32
+ assertCurrent?: () => void;
31
33
  /**
32
34
  * Contract maturity derived from the trusted active PackageVersion.
33
35
  * Guest artifacts cannot select or change this value.
@@ -82,6 +84,12 @@ export interface IIsolatedPackageRuntime {
82
84
  provides(contractId: string, version: number): boolean;
83
85
  /** True when this runtime has installed `packageId`. */
84
86
  isInstalled(packageId: string): boolean;
87
+ /** Read effective policy without starting a worker. */
88
+ getEffectiveToolLevel?(ref: {
89
+ contractId: string;
90
+ version: number;
91
+ toolName: string;
92
+ }, providerPackageId?: string): number;
85
93
  /**
86
94
  * Run a tool the package provides, starting its worker if needed.
87
95
  *
@@ -142,9 +150,13 @@ export interface IIsolatedPackageTableGateway {
142
150
  register(declaration: IIsolatedPackageTableDeclaration): Promise<void>;
143
151
  /**
144
152
  * Validate and bind a package's complete table set as one registration.
145
- * Validation failure must not publish any declaration from the batch.
153
+ * Run `activate` synchronously after all awaited schema persistence, immediately
154
+ * before publishing the prepared bindings. The callback must check cancellation
155
+ * before changing provider state and must not return a promise. No awaited work
156
+ * may separate it from binding publication. If preparation or the callback fails,
157
+ * restore persisted definitions and retain all previous bindings before rejecting.
146
158
  */
147
- registerBatch(declarations: readonly IIsolatedPackageTableDeclaration[]): Promise<void>;
159
+ registerBatch(declarations: readonly IIsolatedPackageTableDeclaration[], activate?: () => void): Promise<void>;
148
160
  /** Return a copied record, or `undefined` when it does not exist. */
149
161
  get(ref: IIsolatedPackageTableRef, recordId: string): Promise<Record<string, unknown> | undefined>;
150
162
  /** Insert or replace a copied record and return the stored copy. */
@@ -9,7 +9,9 @@ import { type IIsolatedPackageTableGateway } from "./isolated-runtime";
9
9
  export declare function createMemoryIsolatedPackageTableGateway(): IIsolatedPackageTableGateway;
10
10
  /**
11
11
  * Create a per-data-context gateway that registers isolated table schemas in
12
- * `TableContainer` and performs host-owned `get` / `save`.
12
+ * `TableContainer` and performs host-owned `get` / `save`. Registration serializes
13
+ * schema persistence with synchronous provider activation; a rejected activation
14
+ * restores the stored definitions before subsequent gateway calls can proceed.
13
15
  *
14
16
  * @param dataContext - Owning personal or group data context.
15
17
  */
@@ -190,12 +190,13 @@ function createMemoryIsolatedPackageTableGateway() {
190
190
  const emitChange = (key, op, dataObject) => {
191
191
  void emitterFor(key).emit({ dataObject, op, source: "local" });
192
192
  };
193
- const registerBatch = async (batch) => {
193
+ const registerBatch = async (batch, activate) => {
194
194
  const candidateDeclarations = new Map(declarations);
195
195
  const candidateByTableId = new Map(byTableId);
196
196
  for (const declaration of batch) {
197
197
  registerDeclaration(candidateDeclarations, candidateByTableId, declaration);
198
198
  }
199
+ activate?.();
199
200
  declarations.clear();
200
201
  byTableId.clear();
201
202
  for (const [key, declaration] of candidateDeclarations) {
@@ -317,7 +318,9 @@ function createMemoryIsolatedPackageTableGateway() {
317
318
  }
318
319
  /**
319
320
  * Create a per-data-context gateway that registers isolated table schemas in
320
- * `TableContainer` and performs host-owned `get` / `save`.
321
+ * `TableContainer` and performs host-owned `get` / `save`. Registration serializes
322
+ * schema persistence with synchronous provider activation; a rejected activation
323
+ * restores the stored definitions before subsequent gateway calls can proceed.
321
324
  *
322
325
  * @param dataContext - Owning personal or group data context.
323
326
  */
@@ -357,7 +360,7 @@ function createIsolatedPackageTableGateway(dataContext) {
357
360
  return dataContext.tableContainer.getTable(activeMetaData);
358
361
  };
359
362
  const ensureTable = async (declaration) => activateTable(await prepareTable(declaration));
360
- const registerBatch = (batch) => {
363
+ const registerBatch = (batch, activate) => {
361
364
  const registration = registrationQueue.then(async () => {
362
365
  const candidateDeclarations = new Map(declarations);
363
366
  const candidateByTableId = new Map(byTableId);
@@ -372,8 +375,54 @@ function createIsolatedPackageTableGateway(dataContext) {
372
375
  plans.push(await prepareTable(declaration));
373
376
  }
374
377
  }
375
- for (const plan of plans) {
376
- await activateTable(plan);
378
+ // Prepare schemas before touching durable or live definitions. Do not create
379
+ // Table instances here: that also schedules unguarded definition persistence.
380
+ const definitions = plans.map(({ activeMetaData }) => ({
381
+ metaData: activeMetaData,
382
+ schema: (0, types_1.fieldsToSchema)(activeMetaData.fields),
383
+ }));
384
+ const definitionsTable = (0, table_definitions_table_1.TableDefinitions)(dataContext);
385
+ const attempted = [];
386
+ try {
387
+ for (const plan of plans) {
388
+ if (!plan.existingRecord || plan.activeMetaData === plan.incomingMetaData) {
389
+ // Include the current write in rollback even if storage writes then rejects.
390
+ attempted.push(plan);
391
+ await definitionsTable.save({
392
+ tableId: requiredTableId(plan.activeMetaData),
393
+ name: (0, package_table_identity_1.packageScopedTableName)(requiredPackageId(plan.activeMetaData), plan.activeMetaData.name),
394
+ metaData: plan.activeMetaData,
395
+ versionNumber: plan.activeMetaData.versionNumber ?? 0,
396
+ }, { restoreIfDeleted: true });
397
+ }
398
+ }
399
+ // Cancellation/registry validation and provider activation happen in the
400
+ // same synchronous turn as binding publication. An unload cannot slip
401
+ // between a completed registration promise and the runtime's guard.
402
+ activate?.();
403
+ }
404
+ catch (error) {
405
+ const failures = [];
406
+ for (const plan of attempted.reverse()) {
407
+ try {
408
+ if (plan.existingRecord) {
409
+ await definitionsTable.save(plan.existingRecord, { restoreIfDeleted: true });
410
+ }
411
+ else {
412
+ await definitionsTable.delete(requiredTableId(plan.activeMetaData));
413
+ }
414
+ }
415
+ catch (rollbackError) {
416
+ failures.push(rollbackError);
417
+ }
418
+ }
419
+ if (failures.length) {
420
+ throw new AggregateError([error, ...failures], "Isolated table registration rollback failed");
421
+ }
422
+ throw error;
423
+ }
424
+ for (const definition of definitions) {
425
+ dataContext.tableContainer.registerTableDefinition(definition, { overwrite: true });
377
426
  }
378
427
  declarations.clear();
379
428
  byTableId.clear();
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const SQLiteDB = require("better-sqlite3");
4
+ const sql_data_source_1 = require("../data/orm/sql.data-source");
5
+ const table_container_1 = require("../data/orm/table-container");
3
6
  const package_table_identity_1 = require("../data/package-table-identity");
7
+ const table_definitions_table_1 = require("../data/table-definitions-table");
4
8
  const field_type_1 = require("../types/field-type");
5
9
  const utils_1 = require("../utils");
6
10
  const isolated_table_gateway_1 = require("./isolated-table-gateway");
@@ -265,3 +269,122 @@ describe("isolated package table gateway", () => {
265
269
  expect(handlers.size).toBe(0);
266
270
  });
267
271
  });
272
+ describe("guarded isolated schema activation", () => {
273
+ test("memory bindings remain unchanged when activation rejects", async () => {
274
+ const gateway = (0, isolated_table_gateway_1.createMemoryIsolatedPackageTableGateway)();
275
+ const old = makeDeclaration({ versionNumber: 1 });
276
+ const next = {
277
+ ...old,
278
+ versionNumber: 2,
279
+ primaryKeyName: "newId",
280
+ fields: [{ name: "newId", type: field_type_1.FieldType.id }],
281
+ };
282
+ await gateway.register(old);
283
+ await expect(gateway.registerBatch([next], () => {
284
+ throw new Error("load invalidated");
285
+ })).rejects.toThrow("load invalidated");
286
+ const record = { stateId: (0, utils_1.newid)(), count: 1 };
287
+ await expect(gateway.save(old, record)).resolves.toEqual(record);
288
+ await expect(gateway.register(old)).resolves.toBeUndefined();
289
+ });
290
+ test.each([
291
+ "invalidation",
292
+ "storage",
293
+ ])("restores SQLite definitions after %s during registration, including fresh tables and restart", async (failure) => {
294
+ const db = new SQLiteDB(":memory:");
295
+ const sql = {
296
+ async close() {
297
+ db.close();
298
+ },
299
+ async get(query, params = []) {
300
+ return db.prepare(query).get(params);
301
+ },
302
+ async all(query, params = []) {
303
+ return db.prepare(query).all(params);
304
+ },
305
+ async exec(query, params = []) {
306
+ return { changes: db.prepare(query).run(params).changes };
307
+ },
308
+ };
309
+ const context = () => ({
310
+ dataContextId: (0, utils_1.newid)(),
311
+ tableContainer: new table_container_1.TableContainer((metaData, schema) => new sql_data_source_1.SQLDataSource(sql, metaData, schema)),
312
+ });
313
+ const dataContext = context();
314
+ const gateway = (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext);
315
+ const old = makeDeclaration({ versionNumber: 1 });
316
+ const next = {
317
+ ...old,
318
+ versionNumber: 2,
319
+ primaryKeyName: "newId",
320
+ fields: [{ name: "newId", type: field_type_1.FieldType.id }],
321
+ };
322
+ const added = makeDeclaration({
323
+ packageId: old.packageId,
324
+ tableName: "Added",
325
+ versionNumber: 1,
326
+ });
327
+ const definitions = (0, table_definitions_table_1.TableDefinitions)(dataContext);
328
+ const oldId = (0, package_table_identity_1.packageScopedTableId)(old.packageId, old.tableName);
329
+ const addedId = (0, package_table_identity_1.packageScopedTableId)(added.packageId, added.tableName);
330
+ const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
331
+ let release;
332
+ let arrived;
333
+ const gate = new Promise((resolve) => {
334
+ release = resolve;
335
+ });
336
+ const saving = new Promise((resolve) => {
337
+ arrived = resolve;
338
+ });
339
+ let saveSpy;
340
+ try {
341
+ await gateway.register(old);
342
+ const record = { stateId: (0, utils_1.newid)(), count: 1 };
343
+ await gateway.save(old, record);
344
+ const original = await definitions.get(oldId);
345
+ const oldTable = dataContext.tableContainer.getTableByName(`${old.tableName}_${old.packageId}`);
346
+ const save = definitions.save.bind(definitions);
347
+ let current = true;
348
+ saveSpy = jest.spyOn(definitions, "save").mockImplementation(async (row, opts) => {
349
+ const result = await save(row, opts);
350
+ if (row.tableId === addedId) {
351
+ arrived();
352
+ await gate;
353
+ if (failure === "storage")
354
+ throw new Error("storage failed after write");
355
+ }
356
+ return result;
357
+ });
358
+ const activated = jest.fn(() => {
359
+ if (!current)
360
+ throw new Error("load invalidated");
361
+ });
362
+ const registering = gateway.registerBatch([next, added], activated);
363
+ const rejected = expect(registering).rejects.toThrow(failure === "storage" ? "storage failed after write" : "load invalidated");
364
+ await saving;
365
+ expect(dataContext.tableContainer.getTableByName(`${old.tableName}_${old.packageId}`)).toBe(oldTable);
366
+ current = false;
367
+ release();
368
+ await rejected;
369
+ expect(activated).toHaveBeenCalledTimes(failure === "storage" ? 0 : 1);
370
+ expect(await definitions.get(oldId)).toEqual(original);
371
+ expect(await definitions.get(addedId)).toBeUndefined();
372
+ expect(dataContext.tableContainer.getTableByName(`${old.tableName}_${old.packageId}`)).toBe(oldTable);
373
+ await expect(gateway.save(old, record)).resolves.toEqual(record);
374
+ await expect(gateway.get(added, (0, utils_1.newid)())).rejects.toThrow(/Undeclared/);
375
+ // A cold gateway reads the restored durable version, so v1 can reinstall.
376
+ const restarted = (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(context());
377
+ await expect(restarted.register(old)).resolves.toBeUndefined();
378
+ await expect(restarted.get(old, record.stateId)).resolves.toEqual(record);
379
+ saveSpy.mockRestore();
380
+ await expect(gateway.registerBatch([next, added])).resolves.toBeUndefined();
381
+ expect((await definitions.get(oldId))?.versionNumber).toBe(2);
382
+ }
383
+ finally {
384
+ release();
385
+ saveSpy?.mockRestore();
386
+ warn.mockRestore();
387
+ db.close();
388
+ }
389
+ });
390
+ });