@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.
- package/dist/contracts/__tests__/builder.test.js +0 -2
- package/dist/contracts/__tests__/contract-provider-router.test.js +51 -0
- package/dist/contracts/__tests__/contract-proxy.test.js +21 -0
- package/dist/contracts/__tests__/contract-resolvers.test.js +84 -0
- package/dist/contracts/__tests__/legacy-removal.test.d.ts +1 -0
- package/dist/contracts/__tests__/legacy-removal.test.js +90 -0
- package/dist/contracts/__tests__/system-contracts.test.js +88 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-inventory.test.js +44 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog-service.test.js +446 -0
- package/dist/contracts/__tests__/tool-catalog.test.d.ts +1 -0
- package/dist/contracts/__tests__/tool-catalog.test.js +649 -0
- package/dist/contracts/__tests__/workflow-context.test.d.ts +1 -0
- package/dist/contracts/__tests__/workflow-context.test.js +61 -0
- package/dist/contracts/builder.d.ts +9 -8
- package/dist/contracts/builder.js +34 -16
- package/dist/contracts/contract-provider-router.d.ts +12 -1
- package/dist/contracts/contract-provider-router.js +40 -2
- package/dist/contracts/contract-proxy.d.ts +20 -6
- package/dist/contracts/contract-proxy.js +23 -12
- package/dist/contracts/contract-resolvers.d.ts +22 -3
- package/dist/contracts/contract-resolvers.js +86 -7
- package/dist/contracts/extract.d.ts +7 -0
- package/dist/contracts/extract.js +3 -0
- package/dist/contracts/index.d.ts +11 -4
- package/dist/contracts/index.js +14 -2
- package/dist/contracts/registry.d.ts +3 -0
- package/dist/contracts/registry.js +2 -0
- package/dist/contracts/system/desktop-tools-contract.d.ts +5 -0
- package/dist/contracts/system/desktop-tools-contract.js +88 -0
- package/dist/contracts/system/host-tools-contract.d.ts +5 -0
- package/dist/contracts/system/host-tools-contract.js +76 -0
- package/dist/contracts/system/index.d.ts +3 -0
- package/dist/contracts/system/index.js +3 -0
- package/dist/contracts/system/tool-catalog-contract.d.ts +73 -0
- package/dist/contracts/system/tool-catalog-contract.js +70 -0
- package/dist/contracts/system-contracts.d.ts +3 -1
- package/dist/contracts/system-contracts.js +20 -6
- package/dist/contracts/tool-catalog-identity.d.ts +35 -0
- package/dist/contracts/tool-catalog-identity.js +88 -0
- package/dist/contracts/tool-catalog-migrations.d.ts +12 -0
- package/dist/contracts/tool-catalog-migrations.js +179 -0
- package/dist/contracts/tool-catalog-service.d.ts +30 -0
- package/dist/contracts/tool-catalog-service.js +287 -0
- package/dist/contracts/tool-catalog.d.ts +91 -0
- package/dist/contracts/tool-catalog.js +490 -0
- package/dist/contracts/types.d.ts +7 -5
- package/dist/data/messages.d.ts +3 -3
- package/dist/data/orm/sql.data-source.d.ts +5 -0
- package/dist/data/orm/sql.data-source.js +28 -10
- package/dist/data/orm/sql.data-source.test.d.ts +1 -1
- package/dist/data/orm/sql.data-source.test.js +160 -0
- package/dist/data/orm/table-container.d.ts +14 -0
- package/dist/data/orm/table-container.js +20 -0
- package/dist/data/tool-access-approvals.d.ts +6 -6
- package/dist/data/tool-catalog-fields.d.ts +31 -0
- package/dist/data/tool-catalog-fields.js +67 -0
- package/dist/data/tools.d.ts +38 -10
- package/dist/data/tools.js +31 -8
- package/dist/data/workflow-logs.d.ts +6 -2
- package/dist/data/workflow-logs.js +7 -5
- package/dist/data/workflow-runs.d.ts +6 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -3
- package/dist/package-installer/package-creator.d.ts +14 -5
- package/dist/package-installer/package-creator.js +92 -37
- package/dist/package-installer/package-creator.test.js +308 -23
- package/dist/package-installer/types.d.ts +8 -0
- package/dist/package-loader/contract-package-loader.d.ts +12 -3
- package/dist/package-loader/contract-package-loader.js +54 -50
- package/dist/package-loader/contract-package-loader.test.d.ts +1 -1
- package/dist/package-loader/contract-package-loader.test.js +172 -13
- package/dist/package-loader/host-authority-loading.test.js +16 -3
- package/dist/package-loader/isolated-artifact.d.ts +14 -2
- package/dist/package-loader/isolated-artifact.js +6 -0
- package/dist/package-loader/isolated-artifact.test.js +34 -0
- package/dist/package-loader/isolated-runtime.d.ts +14 -2
- package/dist/package-loader/isolated-table-gateway.d.ts +3 -1
- package/dist/package-loader/isolated-table-gateway.js +54 -5
- package/dist/package-loader/isolated-table-gateway.test.js +123 -0
- package/dist/package-loader/package-loader.d.ts +14 -2
- package/dist/package-loader/package-loader.js +184 -62
- package/dist/package-loader/package-loader.test.js +695 -2
- package/dist/package-loader/resolve-consumed-contracts.js +1 -1
- package/dist/system-ids.d.ts +6 -0
- package/dist/system-ids.js +7 -1
- package/dist/types/peers-package.d.ts +3 -2
- package/dist/types/workflow-run-context.d.ts +12 -2
- package/package.json +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -17
- package/dist/tools/tools-factory.d.ts +0 -5
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 === "
|
|
14
|
-
?
|
|
15
|
-
:
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
376
|
-
|
|
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
|
+
});
|