@peers-app/peers-sdk 0.25.21 → 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-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-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 +674 -1
- 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
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToolCatalog = void 0;
|
|
4
|
+
exports.toolCatalogTarget = toolCatalogTarget;
|
|
5
|
+
exports.retainToolCatalogFields = retainToolCatalogFields;
|
|
6
|
+
exports.validateToolCatalogWrite = validateToolCatalogWrite;
|
|
7
|
+
const lodash_1 = require("lodash");
|
|
8
|
+
const tools_1 = require("../data/tools");
|
|
9
|
+
const system_contracts_1 = require("./system-contracts");
|
|
10
|
+
const tool_catalog_identity_1 = require("./tool-catalog-identity");
|
|
11
|
+
const tool_catalog_migrations_1 = require("./tool-catalog-migrations");
|
|
12
|
+
/** Read a complete canonical target; partial mappings are invalid, not legacy tools. */
|
|
13
|
+
function toolCatalogTarget(row) {
|
|
14
|
+
if (row.contractId === undefined &&
|
|
15
|
+
row.contractVersion === undefined &&
|
|
16
|
+
row.toolName === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
if (row.contractId === undefined ||
|
|
19
|
+
row.contractVersion === undefined ||
|
|
20
|
+
row.toolName === undefined)
|
|
21
|
+
throw new Error("Incomplete tool catalog target");
|
|
22
|
+
const target = {
|
|
23
|
+
contractId: row.contractId,
|
|
24
|
+
version: row.contractVersion,
|
|
25
|
+
toolName: row.toolName,
|
|
26
|
+
};
|
|
27
|
+
(0, tool_catalog_identity_1.validateToolCatalogTarget)(target);
|
|
28
|
+
return target;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Mixed-version sync: runtimes that predate the catalog columns reconstruct and write
|
|
32
|
+
* full rows without them. When a synced candidate carries none of the catalog-owned
|
|
33
|
+
* fields, keep the established row's identity and state rather than rejecting the
|
|
34
|
+
* write as a retarget. A candidate carrying any catalog field is returned unchanged
|
|
35
|
+
* and validated as written, so partial stripping and retargeting still fail.
|
|
36
|
+
*
|
|
37
|
+
* @param existing - Materialized row, if any.
|
|
38
|
+
* @param candidate - Reconstructed row about to replace it.
|
|
39
|
+
* @returns The row to validate and write.
|
|
40
|
+
*/
|
|
41
|
+
function retainToolCatalogFields(existing, candidate) {
|
|
42
|
+
if (!existing || tools_1.toolCatalogIdentityFields.some((field) => candidate[field] !== undefined))
|
|
43
|
+
return candidate;
|
|
44
|
+
const retained = { ...candidate };
|
|
45
|
+
for (const field of tools_1.toolCatalogIdentityFields) {
|
|
46
|
+
if (existing[field] !== undefined)
|
|
47
|
+
Object.assign(retained, { [field]: existing[field] });
|
|
48
|
+
}
|
|
49
|
+
return retained;
|
|
50
|
+
}
|
|
51
|
+
/** Reviewed aliases and deterministic IDs are catalog identities even before their rows exist. */
|
|
52
|
+
function isReservedToolCatalogId(toolId) {
|
|
53
|
+
return ((0, tool_catalog_migrations_1.resolveLegacyToolId)(toolId) !== undefined ||
|
|
54
|
+
((0, tool_catalog_identity_1.isCatalogToolId)(toolId) && toolId.startsWith("000tool0")));
|
|
55
|
+
}
|
|
56
|
+
/** Whether a stored row carries catalog identity or quarantine evidence. */
|
|
57
|
+
function hasToolCatalogEvidence(row) {
|
|
58
|
+
return (row.catalogState === "resolved" ||
|
|
59
|
+
row.catalogState === "quarantined" ||
|
|
60
|
+
row.catalogIdentity !== undefined ||
|
|
61
|
+
row.contractId !== undefined ||
|
|
62
|
+
row.contractVersion !== undefined ||
|
|
63
|
+
row.toolName !== undefined);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Shared pre-persistence identity guard for local writes and reconstructed sync rows.
|
|
67
|
+
* Existing mappings cannot be retargeted, stripped or deleted. Rejected sync evidence
|
|
68
|
+
* belongs in the sync recovery queue; it must not replace the established row.
|
|
69
|
+
* Pass recordId for deletes so reserved identities are protected before materialization.
|
|
70
|
+
*
|
|
71
|
+
* Catalog identities are permanent. Unresolved legacy rows are a holdover until their
|
|
72
|
+
* tools move to contracts: they stay ordinary rows that local writers and older
|
|
73
|
+
* runtimes may delete.
|
|
74
|
+
*/
|
|
75
|
+
function validateToolCatalogWrite(existing, candidate, recordId = candidate?.toolId ?? existing?.toolId) {
|
|
76
|
+
if (!candidate) {
|
|
77
|
+
if ((recordId !== undefined && isReservedToolCatalogId(recordId)) ||
|
|
78
|
+
(existing && hasToolCatalogEvidence(existing)))
|
|
79
|
+
throw new Error("Tools catalog rows are permanent");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!candidate.toolId ||
|
|
83
|
+
!(0, tool_catalog_identity_1.isCatalogToolId)(candidate.toolId) ||
|
|
84
|
+
(existing && existing.toolId !== candidate.toolId))
|
|
85
|
+
throw new Error("Tool catalog primary key mismatch");
|
|
86
|
+
if (existing?.catalogState === "quarantined" && candidate.catalogState !== "quarantined")
|
|
87
|
+
throw new Error(`Quarantined tool identity: ${candidate.toolId}`);
|
|
88
|
+
// Quarantine preserves the original evidence byte-for-byte; it cannot retarget it.
|
|
89
|
+
if (existing &&
|
|
90
|
+
candidate.catalogState === "quarantined" &&
|
|
91
|
+
existing.contractId === candidate.contractId &&
|
|
92
|
+
existing.contractVersion === candidate.contractVersion &&
|
|
93
|
+
existing.toolName === candidate.toolName &&
|
|
94
|
+
existing.catalogIdentity === candidate.catalogIdentity)
|
|
95
|
+
return;
|
|
96
|
+
const target = toolCatalogTarget(candidate);
|
|
97
|
+
const legacy = (0, tool_catalog_migrations_1.resolveLegacyToolId)(candidate.toolId);
|
|
98
|
+
const previous = existing && toolCatalogTarget(existing);
|
|
99
|
+
if (previous && (!target || (0, tool_catalog_identity_1.toolCatalogTargetKey)(previous) !== (0, tool_catalog_identity_1.toolCatalogTargetKey)(target)))
|
|
100
|
+
throw new Error(`Tool catalog identity cannot change: ${candidate.toolId}`);
|
|
101
|
+
if (target) {
|
|
102
|
+
if (existing && !previous && !legacy)
|
|
103
|
+
throw new Error(`Unresolved tool identity cannot be claimed: ${candidate.toolId}`);
|
|
104
|
+
if (!(0, tool_catalog_migrations_1.toolCatalogIds)(target).includes(candidate.toolId))
|
|
105
|
+
throw new Error(`Unknown tool catalog mapping: ${candidate.toolId}`);
|
|
106
|
+
if (legacy && (0, tool_catalog_identity_1.toolCatalogTargetKey)(legacy) !== (0, tool_catalog_identity_1.toolCatalogTargetKey)(target))
|
|
107
|
+
throw new Error(`Conflicting legacy tool mapping: ${candidate.toolId}`);
|
|
108
|
+
const identity = legacy ? "legacy-alias" : "deterministic";
|
|
109
|
+
if (candidate.catalogIdentity !== undefined && candidate.catalogIdentity !== identity)
|
|
110
|
+
throw new Error(`Incorrect catalog identity kind: ${candidate.toolId}`);
|
|
111
|
+
}
|
|
112
|
+
else if (candidate.catalogState === "resolved" ||
|
|
113
|
+
candidate.catalogIdentity ||
|
|
114
|
+
candidate.toolId.startsWith("000tool0")) {
|
|
115
|
+
throw new Error(`Missing tool catalog target: ${candidate.toolId}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** JSON-equivalent comparison: key order and undefined members are not changes. */
|
|
119
|
+
function sameValue(a, b) {
|
|
120
|
+
const normalize = (value) => value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
121
|
+
return (0, lodash_1.isEqual)(normalize(a), normalize(b));
|
|
122
|
+
}
|
|
123
|
+
function resolvedFields(target, toolId) {
|
|
124
|
+
return {
|
|
125
|
+
contractId: target.contractId,
|
|
126
|
+
contractVersion: target.version,
|
|
127
|
+
toolName: target.toolName,
|
|
128
|
+
catalogIdentity: (0, tool_catalog_migrations_1.resolveLegacyToolId)(toolId) ? "legacy-alias" : "deterministic",
|
|
129
|
+
catalogState: "resolved",
|
|
130
|
+
catalogReason: undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Per-context runtime owner of permanent Tools identities. Construction is inert;
|
|
135
|
+
* ready() explicitly initializes storage after UserContext construction completes.
|
|
136
|
+
* No worker, resolver or executable ToolInstance is needed to index public members.
|
|
137
|
+
*/
|
|
138
|
+
class ToolCatalog {
|
|
139
|
+
dataContext;
|
|
140
|
+
registry;
|
|
141
|
+
tail = Promise.resolve();
|
|
142
|
+
initialization;
|
|
143
|
+
subscription;
|
|
144
|
+
disposed = false;
|
|
145
|
+
reconciliationQueued = false;
|
|
146
|
+
/** Bind to the registry shared by host and isolated providers in this context. */
|
|
147
|
+
constructor(dataContext, registry) {
|
|
148
|
+
this.dataContext = dataContext;
|
|
149
|
+
this.registry = registry;
|
|
150
|
+
if (registry.toolCatalog)
|
|
151
|
+
throw new Error("Registry already owns a tool catalog");
|
|
152
|
+
registry.toolCatalog = this;
|
|
153
|
+
}
|
|
154
|
+
enqueue(work) {
|
|
155
|
+
const result = this.tail.then(() => {
|
|
156
|
+
if (this.disposed)
|
|
157
|
+
throw new Error("Tool catalog disposed");
|
|
158
|
+
return work();
|
|
159
|
+
});
|
|
160
|
+
this.tail = result.catch(() => undefined);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
/** Await successful backfill and system publication. Failed initialization can be retried. */
|
|
164
|
+
ready() {
|
|
165
|
+
if (this.disposed)
|
|
166
|
+
return Promise.reject(new Error("Tool catalog disposed"));
|
|
167
|
+
if (!this.initialization) {
|
|
168
|
+
this.initialization = this.enqueue(async () => {
|
|
169
|
+
await this.reconcile();
|
|
170
|
+
const groups = new Map();
|
|
171
|
+
for (const entry of (0, system_contracts_1.listSystemContracts)()) {
|
|
172
|
+
const entries = groups.get(entry.providerPackageId) ?? [];
|
|
173
|
+
entries.push({ definition: entry.getDefinition() });
|
|
174
|
+
groups.set(entry.providerPackageId, entries);
|
|
175
|
+
}
|
|
176
|
+
for (const [provider, registrations] of groups) {
|
|
177
|
+
this.validate(provider, registrations);
|
|
178
|
+
}
|
|
179
|
+
// System upgrades refresh contract-owned descriptions on every start.
|
|
180
|
+
for (const [provider, registrations] of groups)
|
|
181
|
+
await this.materialize(registrations, true, provider);
|
|
182
|
+
// Guard the complete system snapshot before publishing any provider.
|
|
183
|
+
// A later save/read may have invalidated an earlier provider's row.
|
|
184
|
+
await this.guardActivation(groups, () => {
|
|
185
|
+
this.assertLive();
|
|
186
|
+
for (const [provider, registrations] of groups) {
|
|
187
|
+
this.registry.replaceBatch(provider, registrations);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
// dispose() may have run while initialization awaited storage; it has
|
|
191
|
+
// already unsubscribed, so a late subscription would never be released.
|
|
192
|
+
this.assertLive();
|
|
193
|
+
this.subscription ??= (0, tools_1.Tools)(this.dataContext).dataChanged.subscribe(() => {
|
|
194
|
+
if (this.reconciliationQueued || this.disposed)
|
|
195
|
+
return;
|
|
196
|
+
this.reconciliationQueued = true;
|
|
197
|
+
void this.enqueue(async () => {
|
|
198
|
+
this.reconciliationQueued = false;
|
|
199
|
+
await this.reconcile();
|
|
200
|
+
}).catch((error) => console.error("Tool catalog reconciliation failed", error));
|
|
201
|
+
});
|
|
202
|
+
}).catch((error) => {
|
|
203
|
+
this.initialization = undefined;
|
|
204
|
+
throw error;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return this.initialization;
|
|
208
|
+
}
|
|
209
|
+
assertLive() {
|
|
210
|
+
if (this.disposed)
|
|
211
|
+
throw new Error("Tool catalog disposed");
|
|
212
|
+
}
|
|
213
|
+
validate(provider, registrations) {
|
|
214
|
+
this.assertLive();
|
|
215
|
+
const result = this.registry.validateBatch(provider, registrations);
|
|
216
|
+
if (!result.valid)
|
|
217
|
+
throw new Error(`Contract registration failed: ${result.errors.map((e) => e.message).join("; ")}`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Serialize validation, catalog persistence, package preparation and activation.
|
|
221
|
+
* A failed preparation never publishes the candidate or changes live tool policy.
|
|
222
|
+
* Successful catalog additions remain historical even if later preparation fails.
|
|
223
|
+
* Descriptive metadata of existing rows is refreshed only after preparation
|
|
224
|
+
* succeeds, so a failed replacement leaves the active provider's rows unchanged.
|
|
225
|
+
* Async activation must call the supplied guard in its final synchronous commit
|
|
226
|
+
* turn, before publishing providers or changing live bindings. The guard observes
|
|
227
|
+
* catalog changes immediately, independently of queued reconciliation.
|
|
228
|
+
*/
|
|
229
|
+
async publish(provider, registrations, prepare, activate, toolMetadata = []) {
|
|
230
|
+
if (toolMetadata.length) {
|
|
231
|
+
const members = new Set(this.definitions(registrations).flatMap((definition) => definition.tools.map((tool) => (0, tool_catalog_identity_1.toolCatalogTargetKey)({
|
|
232
|
+
contractId: definition.contractId,
|
|
233
|
+
version: definition.version,
|
|
234
|
+
toolName: tool.name,
|
|
235
|
+
}))));
|
|
236
|
+
for (const metadata of toolMetadata) {
|
|
237
|
+
(0, tool_catalog_identity_1.validateToolCatalogTarget)(metadata);
|
|
238
|
+
if (!members.has((0, tool_catalog_identity_1.toolCatalogTargetKey)(metadata)))
|
|
239
|
+
throw new Error(`Tool metadata must target a member of this publication: ${metadata.toolName}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await this.ready();
|
|
243
|
+
return this.enqueue(async () => {
|
|
244
|
+
this.validate(provider, registrations);
|
|
245
|
+
await this.materialize(registrations, false);
|
|
246
|
+
await prepare();
|
|
247
|
+
this.validate(provider, registrations);
|
|
248
|
+
await this.materialize(registrations, true, provider);
|
|
249
|
+
for (const metadata of toolMetadata) {
|
|
250
|
+
for (const toolId of (0, tool_catalog_migrations_1.toolCatalogIds)(metadata)) {
|
|
251
|
+
const stored = await (0, tools_1.Tools)(this.dataContext).get(toolId);
|
|
252
|
+
if (!stored || stored.catalogState !== "resolved")
|
|
253
|
+
throw new Error(`Missing or quarantined tool identity: ${toolId}`);
|
|
254
|
+
await this.refresh({ ...metadata, toolId }, stored, tools_1.toolCatalogSupplementalFields);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return this.guardActivation(new Map([[provider, registrations]]), activate);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
async guardActivation(groups, activate) {
|
|
261
|
+
const expected = new Map();
|
|
262
|
+
for (const definition of this.definitions([...groups.values()].flat())) {
|
|
263
|
+
for (const tool of definition.tools) {
|
|
264
|
+
const target = {
|
|
265
|
+
contractId: definition.contractId,
|
|
266
|
+
version: definition.version,
|
|
267
|
+
toolName: tool.name,
|
|
268
|
+
};
|
|
269
|
+
for (const toolId of (0, tool_catalog_migrations_1.toolCatalogIds)(target))
|
|
270
|
+
expected.set(toolId, { toolId, ...resolvedFields(target, toolId) });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const table = (0, tools_1.Tools)(this.dataContext);
|
|
274
|
+
const validateRow = (toolId, row) => {
|
|
275
|
+
if (!row || row.catalogState === "quarantined")
|
|
276
|
+
throw new Error(`Missing or quarantined tool identity: ${toolId}`);
|
|
277
|
+
validateToolCatalogWrite(expected.get(toolId), row);
|
|
278
|
+
};
|
|
279
|
+
let conflict;
|
|
280
|
+
// Subscribe before reading so a later read cannot hide conflicting evidence
|
|
281
|
+
// seen during the snapshot or while the table gateway persists its schemas.
|
|
282
|
+
const subscription = table.dataChanged.subscribe((event) => {
|
|
283
|
+
const toolId = event.dataObject.toolId;
|
|
284
|
+
if (!expected.has(toolId))
|
|
285
|
+
return;
|
|
286
|
+
try {
|
|
287
|
+
validateRow(toolId, event.op === "delete" ? undefined : event.dataObject);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
conflict = error;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
const assertCatalogCurrent = () => {
|
|
294
|
+
if (conflict)
|
|
295
|
+
throw conflict;
|
|
296
|
+
for (const [provider, registrations] of groups)
|
|
297
|
+
this.validate(provider, registrations);
|
|
298
|
+
};
|
|
299
|
+
try {
|
|
300
|
+
for (const toolId of expected.keys())
|
|
301
|
+
validateRow(toolId, await table.get(toolId));
|
|
302
|
+
assertCatalogCurrent();
|
|
303
|
+
return await activate(assertCatalogCurrent);
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
subscription.unsubscribe();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
definitions(registrations) {
|
|
310
|
+
const definitions = new Map(registrations.map(({ definition }) => [
|
|
311
|
+
`${definition.contractId}@${definition.version}`,
|
|
312
|
+
definition,
|
|
313
|
+
]));
|
|
314
|
+
for (const { alsoImplements = [] } of registrations) {
|
|
315
|
+
for (const declaration of alsoImplements) {
|
|
316
|
+
const range = declaration.version;
|
|
317
|
+
const from = typeof range === "number" ? range : range.from;
|
|
318
|
+
const to = typeof range === "number" ? range : range.to;
|
|
319
|
+
for (let version = from; version <= to; version++) {
|
|
320
|
+
const key = `${declaration.contractId}@${version}`;
|
|
321
|
+
const definition = definitions.get(key) ?? this.registry.getDefinition(declaration.contractId, version);
|
|
322
|
+
if (!definition)
|
|
323
|
+
throw new Error(`Missing alsoImplements definition: ${key}`);
|
|
324
|
+
definitions.set(key, definition);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return [...definitions.values()];
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Insert every public member's catalog rows, validating the batch first. With
|
|
332
|
+
* `refresh`, existing rows with absent or matching provider provenance also take the
|
|
333
|
+
* definition's name, usage description and IO schemas.
|
|
334
|
+
*/
|
|
335
|
+
async materialize(registrations, refresh, provider) {
|
|
336
|
+
const table = (0, tools_1.Tools)(this.dataContext);
|
|
337
|
+
const existing = new Map((await table.list()).map((row) => [row.toolId, row]));
|
|
338
|
+
const identities = new Map();
|
|
339
|
+
// Reserve the entire reviewed inventory, even on an empty database.
|
|
340
|
+
for (const entry of tool_catalog_migrations_1.toolCatalogMigrations)
|
|
341
|
+
(0, tool_catalog_identity_1.validateToolCatalogCollision)(identities, entry.toolId, entry);
|
|
342
|
+
const candidates = new Map();
|
|
343
|
+
for (const definition of this.definitions(registrations)) {
|
|
344
|
+
const names = new Set();
|
|
345
|
+
for (const tool of definition.tools) {
|
|
346
|
+
if (names.has(tool.name))
|
|
347
|
+
throw new Error(`Duplicate contract tool: ${tool.name}`);
|
|
348
|
+
names.add(tool.name);
|
|
349
|
+
const target = {
|
|
350
|
+
contractId: definition.contractId,
|
|
351
|
+
version: definition.version,
|
|
352
|
+
toolName: tool.name,
|
|
353
|
+
};
|
|
354
|
+
(0, tool_catalog_identity_1.validateToolCatalogTarget)(target);
|
|
355
|
+
for (const toolId of (0, tool_catalog_migrations_1.toolCatalogIds)(target)) {
|
|
356
|
+
(0, tool_catalog_identity_1.validateToolCatalogCollision)(identities, toolId, target);
|
|
357
|
+
const row = {
|
|
358
|
+
toolId,
|
|
359
|
+
name: tool.name,
|
|
360
|
+
usageDescription: tool.usageDescription,
|
|
361
|
+
code: "",
|
|
362
|
+
inputSchema: { type: tools_1.IOSchemaType.simple, fields: tool.inputFields },
|
|
363
|
+
outputSchema: { type: tools_1.IOSchemaType.simple, fields: tool.outputFields },
|
|
364
|
+
...resolvedFields(target, toolId),
|
|
365
|
+
};
|
|
366
|
+
const stored = existing.get(toolId);
|
|
367
|
+
if (stored?.catalogState === "quarantined")
|
|
368
|
+
throw new Error(`Quarantined tool identity: ${toolId}`);
|
|
369
|
+
validateToolCatalogWrite(stored, row);
|
|
370
|
+
candidates.set(toolId, row);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Validate the entire batch before the first write. Catalog metadata never
|
|
375
|
+
// overwrites provider-specific policy or another provider's descriptions.
|
|
376
|
+
for (const row of candidates.values()) {
|
|
377
|
+
// Older runtimes allowed provider removal to leave tombstones. Only a
|
|
378
|
+
// validated public target may restore one; insert-only still preserves any
|
|
379
|
+
// row that arrived concurrently, and the returned mapping is checked below.
|
|
380
|
+
const stored = await table.save(row, { insertOnly: true, restoreIfDeleted: true });
|
|
381
|
+
validateToolCatalogWrite(row, stored);
|
|
382
|
+
if (!stored || stored.catalogState === "quarantined")
|
|
383
|
+
throw new Error(`Catalog insert did not succeed: ${row.toolId}`);
|
|
384
|
+
if (refresh && (stored.packageId === undefined || stored.packageId === provider))
|
|
385
|
+
await this.refresh(row, stored, tools_1.toolCatalogContractDescriptiveFields);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Patch the listed descriptive fields whose stored values differ. A marked patch
|
|
390
|
+
* leaves every other column, including concurrently synced policy, untouched.
|
|
391
|
+
*/
|
|
392
|
+
async refresh(desired, stored, fields) {
|
|
393
|
+
const changed = fields.filter((field) => !sameValue(stored[field], desired[field]));
|
|
394
|
+
if (!changed.length)
|
|
395
|
+
return;
|
|
396
|
+
const patch = { toolId: desired.toolId };
|
|
397
|
+
for (const field of changed)
|
|
398
|
+
Object.assign(patch, { [field]: desired[field] });
|
|
399
|
+
const refreshed = await (0, tools_1.Tools)(this.dataContext).update((0, tools_1.toolCatalogPatch)(patch));
|
|
400
|
+
validateToolCatalogWrite(stored, refreshed);
|
|
401
|
+
if (refreshed.catalogState === "quarantined")
|
|
402
|
+
throw new Error(`Quarantined tool identity: ${desired.toolId}`);
|
|
403
|
+
}
|
|
404
|
+
async reconcile() {
|
|
405
|
+
const table = (0, tools_1.Tools)(this.dataContext);
|
|
406
|
+
for (const row of await table.list()) {
|
|
407
|
+
let fields;
|
|
408
|
+
// Quarantine is sticky: conflicting evidence is never silently rehabilitated.
|
|
409
|
+
if (row.catalogState === "quarantined")
|
|
410
|
+
continue;
|
|
411
|
+
try {
|
|
412
|
+
validateToolCatalogWrite(undefined, row);
|
|
413
|
+
const target = (0, tool_catalog_migrations_1.resolveLegacyToolId)(row.toolId) ?? toolCatalogTarget(row);
|
|
414
|
+
fields = target ? resolvedFields(target, row.toolId) : { catalogState: "unresolved" };
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
fields = { catalogState: "quarantined", catalogReason: String(error) };
|
|
418
|
+
}
|
|
419
|
+
if (Object.entries(fields).some(([key, value]) => row[key] !== value)) {
|
|
420
|
+
// Update only catalog-owned fields. A sync write may have changed legacy
|
|
421
|
+
// metadata since list() returned; writing the snapshot back would lose it.
|
|
422
|
+
await table.update((0, tools_1.toolCatalogPatch)({ toolId: row.toolId, ...fields }));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Patch supplemental metadata of existing system members, preserving identity,
|
|
428
|
+
* historical source and provenance, descriptions and all authorization policy.
|
|
429
|
+
* @throws If any entry is unresolved, quarantined or outside the system inventory.
|
|
430
|
+
*/
|
|
431
|
+
async saveToolMetadata(entries) {
|
|
432
|
+
await this.ready();
|
|
433
|
+
await this.enqueue(async () => {
|
|
434
|
+
const members = (0, system_contracts_1.listSystemContracts)().flatMap((entry) => {
|
|
435
|
+
const definition = entry.getDefinition();
|
|
436
|
+
return definition.tools.map((tool) => ({
|
|
437
|
+
contractId: definition.contractId,
|
|
438
|
+
version: definition.version,
|
|
439
|
+
toolName: tool.name,
|
|
440
|
+
}));
|
|
441
|
+
});
|
|
442
|
+
const rows = [];
|
|
443
|
+
for (const entry of entries) {
|
|
444
|
+
const target = (0, tool_catalog_migrations_1.resolveLegacyToolId)(entry.toolId) ??
|
|
445
|
+
members.find((member) => (0, tool_catalog_migrations_1.toolCatalogIds)(member).includes(entry.toolId));
|
|
446
|
+
const stored = await (0, tools_1.Tools)(this.dataContext).get(entry.toolId);
|
|
447
|
+
if (!target ||
|
|
448
|
+
!members.some((member) => (0, tool_catalog_identity_1.toolCatalogTargetKey)(member) === (0, tool_catalog_identity_1.toolCatalogTargetKey)(target)) ||
|
|
449
|
+
!stored ||
|
|
450
|
+
stored.catalogState !== "resolved")
|
|
451
|
+
throw new Error(`Tool metadata requires a resolved system member: ${entry.toolId}`);
|
|
452
|
+
validateToolCatalogWrite(undefined, stored);
|
|
453
|
+
rows.push({ entry, stored });
|
|
454
|
+
}
|
|
455
|
+
for (const { entry, stored } of rows)
|
|
456
|
+
await this.refresh(entry, stored, tools_1.toolCatalogSupplementalFields);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
/** Reject quarantined or conflicting retained evidence before a contract tool call. */
|
|
460
|
+
async assertTargetUsable(target) {
|
|
461
|
+
await this.ready();
|
|
462
|
+
for (const toolId of (0, tool_catalog_migrations_1.toolCatalogIds)(target)) {
|
|
463
|
+
const row = await (0, tools_1.Tools)(this.dataContext).get(toolId);
|
|
464
|
+
// Bare registry users may predate runtime catalog installation. Availability
|
|
465
|
+
// and authorization still come from the live registry and selected provider.
|
|
466
|
+
if (!row)
|
|
467
|
+
continue;
|
|
468
|
+
if (row.catalogState === "quarantined")
|
|
469
|
+
throw new Error(`Quarantined tool identity: ${toolId}`);
|
|
470
|
+
validateToolCatalogWrite({ toolId, ...resolvedFields(target, toolId) }, row);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** Serialize host removal with pending publications while retaining all catalog rows. */
|
|
474
|
+
async removeProvider(provider, deactivate) {
|
|
475
|
+
// Join the same readiness boundary as publish(), including on a cold context,
|
|
476
|
+
// so an earlier install cannot enqueue its activation behind this removal.
|
|
477
|
+
await this.ready();
|
|
478
|
+
await this.enqueue(async () => {
|
|
479
|
+
this.registry.unregister(provider);
|
|
480
|
+
deactivate();
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/** Stop subscriptions and queued lifecycle work; permanent rows are retained. */
|
|
484
|
+
async dispose() {
|
|
485
|
+
this.disposed = true;
|
|
486
|
+
this.subscription?.unsubscribe();
|
|
487
|
+
await this.tail;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
exports.ToolCatalog = ToolCatalog;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Table } from "../data/orm/table";
|
|
2
2
|
import type { ITableDefinition } from "../data/orm/table-definitions.type";
|
|
3
|
-
import type { IToolInstance } from "../data/tools";
|
|
4
3
|
import type { Event } from "../events";
|
|
5
4
|
import type { Observable } from "../observable";
|
|
6
5
|
import type { FieldType, IField } from "../types/field-type";
|
|
7
|
-
import type { IContractConsumer, IContractConsumerBase } from "./contract-proxy";
|
|
6
|
+
import type { IContractConsumer, IContractConsumerBase, IContractToolImplementation } from "./contract-proxy";
|
|
7
|
+
import type { IToolCatalogMetadata } from "./tool-catalog";
|
|
8
8
|
/**
|
|
9
9
|
* A versioned interface definition declaring the shape of tables, tools,
|
|
10
10
|
* observables, and events that a package provides. Pure data — no executable code.
|
|
@@ -52,6 +52,8 @@ export interface IContractTool {
|
|
|
52
52
|
* account) are never gated by this value. Policy only — not part of shape validation.
|
|
53
53
|
*/
|
|
54
54
|
remoteAccessLevel?: number;
|
|
55
|
+
/** Host-only invocation policy, excluded from shape identity. Requires an in-process workflow. */
|
|
56
|
+
requiresWorkflowRun?: boolean;
|
|
55
57
|
}
|
|
56
58
|
/** Observable value exposed by a contract. */
|
|
57
59
|
export interface IContractObservable {
|
|
@@ -200,8 +202,8 @@ export interface IPackageDefinitionResult {
|
|
|
200
202
|
/** Package-level items not part of any contract. */
|
|
201
203
|
assistants: unknown[];
|
|
202
204
|
appNavs: unknown[];
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
+
/** Host descriptive metadata only; does not register executable tools or affect contract shape. */
|
|
206
|
+
toolMetadata?: IToolCatalogMetadata[];
|
|
205
207
|
/** All table definitions across all contracts in this package. */
|
|
206
208
|
tableDefinitions: ITableDefinition[];
|
|
207
209
|
}
|
|
@@ -211,7 +213,7 @@ export interface IResolvedContract {
|
|
|
211
213
|
version: number;
|
|
212
214
|
providerPackageId: string;
|
|
213
215
|
tables: Record<string, Table<any>>;
|
|
214
|
-
tools: Record<string,
|
|
216
|
+
tools: Record<string, IContractToolImplementation>;
|
|
215
217
|
observables: Record<string, Observable<any>>;
|
|
216
218
|
/** Live events the provider forwards from, keyed by contract event name. */
|
|
217
219
|
events?: Record<string, Event<any>>;
|
package/dist/data/messages.d.ts
CHANGED
|
@@ -19,9 +19,9 @@ export declare const messageSchema: z.ZodObject<{
|
|
|
19
19
|
messageId: string;
|
|
20
20
|
assistantId?: string | undefined;
|
|
21
21
|
messageParentId?: string | undefined;
|
|
22
|
-
vars?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
23
22
|
editedAt?: Date | undefined;
|
|
24
23
|
fileIds?: string[] | undefined;
|
|
24
|
+
vars?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
25
25
|
}, {
|
|
26
26
|
message: string;
|
|
27
27
|
userId: string;
|
|
@@ -30,9 +30,9 @@ export declare const messageSchema: z.ZodObject<{
|
|
|
30
30
|
messageId: string;
|
|
31
31
|
assistantId?: string | undefined;
|
|
32
32
|
messageParentId?: string | undefined;
|
|
33
|
-
vars?: z.objectInputType<{}, z.ZodAny, "strip"> | undefined;
|
|
34
33
|
editedAt?: Date | undefined;
|
|
35
34
|
fileIds?: string[] | undefined;
|
|
35
|
+
vars?: z.objectInputType<{}, z.ZodAny, "strip"> | undefined;
|
|
36
36
|
}>;
|
|
37
37
|
export type IMessage = z.infer<typeof messageSchema>;
|
|
38
38
|
export declare function Messages(dataContext?: DataContext): import("..").Table<{
|
|
@@ -43,9 +43,9 @@ export declare function Messages(dataContext?: DataContext): import("..").Table<
|
|
|
43
43
|
messageId: string;
|
|
44
44
|
assistantId?: string | undefined;
|
|
45
45
|
messageParentId?: string | undefined;
|
|
46
|
-
vars?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
47
46
|
editedAt?: Date | undefined;
|
|
48
47
|
fileIds?: string[] | undefined;
|
|
48
|
+
vars?: z.objectOutputType<{}, z.ZodAny, "strip"> | undefined;
|
|
49
49
|
}>;
|
|
50
50
|
interface ISendMessageArgs {
|
|
51
51
|
channelOrThreadIdOrWorkflowRunId: string;
|
|
@@ -5,6 +5,7 @@ import { type DataFilter, type ICursorIterable, type IDataQueryParams, type IDat
|
|
|
5
5
|
import type { ISqlDb } from "./sql-db";
|
|
6
6
|
import type { IDataChangedEvent } from "./subscribable.data-source";
|
|
7
7
|
import { type ITableMetaData } from "./types";
|
|
8
|
+
/** SQLite-backed table storage with additive schema migration and serialized initialization. */
|
|
8
9
|
export declare class SQLDataSource<T extends {
|
|
9
10
|
[key: string]: any;
|
|
10
11
|
}> implements IDataSource<T> {
|
|
@@ -19,6 +20,7 @@ export declare class SQLDataSource<T extends {
|
|
|
19
20
|
private ensureTableAndIndexesExist;
|
|
20
21
|
private ensureTableIsNewest;
|
|
21
22
|
private tableBuild;
|
|
23
|
+
/** Share schema initialization; a failed attempt is evicted so later calls can retry. */
|
|
22
24
|
initTable(force?: boolean): Promise<boolean>;
|
|
23
25
|
get(id: string): Promise<T | undefined>;
|
|
24
26
|
list(filter?: DataFilter<T>, opts?: IDataQueryParams<T>): Promise<T[]>;
|
|
@@ -85,6 +87,9 @@ export declare class SQLDataSource<T extends {
|
|
|
85
87
|
*/
|
|
86
88
|
bulkSave(records: T[]): Promise<T[]>;
|
|
87
89
|
}
|
|
90
|
+
/** Encode a JavaScript field for SQLite parameters or an escaped migration literal. */
|
|
88
91
|
export declare function jsValueToSqlValue(value: any, escapeQuote?: boolean): any;
|
|
92
|
+
/** Restore a SQLite field according to its declared Peers type. */
|
|
89
93
|
export declare function sqlValueToJsValue(value: any, fieldType: FieldType, isArray: boolean): any;
|
|
94
|
+
/** Default value used when adding a required field to an existing table. */
|
|
90
95
|
export declare function globalDefaultValues(field: IField): {};
|