@peers-app/peers-sdk 0.25.18 → 0.25.20
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/README.md +9 -1
- package/dist/contracts/__tests__/action-dispatch.test.d.ts +1 -0
- package/dist/contracts/__tests__/action-dispatch.test.js +339 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.d.ts +1 -0
- package/dist/contracts/__tests__/generic-tool-dispatch.test.js +173 -0
- package/dist/contracts/action-dispatch.d.ts +12 -0
- package/dist/contracts/action-dispatch.js +73 -0
- package/dist/contracts/contract-provider-router.js +7 -1
- package/dist/contracts/contract-proxy.d.ts +47 -0
- package/dist/contracts/contract-proxy.js +152 -16
- package/dist/contracts/index.d.ts +2 -1
- package/dist/contracts/index.js +5 -2
- package/dist/contracts/system/contract-catalog-contract.d.ts +2 -0
- package/dist/data/messages.d.ts +3 -3
- package/dist/data/package-versions.d.ts +10 -10
- package/dist/data/tool-access.d.ts +3 -1
- package/dist/data/tool-access.js +5 -2
- package/dist/data/tool-access.test.js +7 -0
- package/dist/data/tools.d.ts +7 -3
- package/dist/data/tools.js +7 -1
- package/dist/invites/invite-messages.d.ts +2 -2
- package/dist/package-loader/host-authority-loading.test.d.ts +1 -0
- package/dist/package-loader/host-authority-loading.test.js +87 -0
- package/dist/package-loader/host-dispatch-policy.test.d.ts +1 -0
- package/dist/package-loader/host-dispatch-policy.test.js +56 -0
- package/dist/package-loader/isolated-artifact.d.ts +16 -2
- package/dist/package-loader/isolated-artifact.js +40 -3
- package/dist/package-loader/isolated-artifact.test.js +38 -0
- package/dist/package-loader/isolated-runtime.d.ts +15 -0
- package/dist/package-loader/package-loader.d.ts +2 -0
- package/dist/package-loader/package-loader.js +60 -0
- package/dist/package-loader/package-loader.test.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("../index");
|
|
4
|
+
test("host-backed package endpoints honor declared propagation and reread live metadata", async () => {
|
|
5
|
+
const packageId = (0, index_1.newid)();
|
|
6
|
+
const contractId = (0, index_1.newid)();
|
|
7
|
+
const userId = (0, index_1.newid)();
|
|
8
|
+
const groupId = (0, index_1.newid)();
|
|
9
|
+
const dataContext = {
|
|
10
|
+
dataContextId: groupId,
|
|
11
|
+
groupId,
|
|
12
|
+
tableContainer: {
|
|
13
|
+
getTable: (meta) => meta.name === "Groups"
|
|
14
|
+
? { get: async () => ({ founderUserId: (0, index_1.newid)() }) }
|
|
15
|
+
: { findOne: async () => ({ userId, role: index_1.GroupMemberRole.Admin }) },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
Object.assign(dataContext, {
|
|
19
|
+
userContext: {
|
|
20
|
+
userId,
|
|
21
|
+
deviceId: () => (0, index_1.newid)(),
|
|
22
|
+
loadingPromise: Promise.resolve(),
|
|
23
|
+
userDataContext: dataContext,
|
|
24
|
+
getDataContext: () => dataContext,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const loader = new index_1.PackageLoader(dataContext);
|
|
28
|
+
const make = (name, level, fn) => ({
|
|
29
|
+
tool: {
|
|
30
|
+
toolId: (0, index_1.newid)(),
|
|
31
|
+
name,
|
|
32
|
+
accessLevel: level,
|
|
33
|
+
code: "",
|
|
34
|
+
usageDescription: name,
|
|
35
|
+
inputSchema: { type: index_1.IOSchemaType.simple, fields: [] },
|
|
36
|
+
outputSchema: { type: index_1.IOSchemaType.simple, fields: [] },
|
|
37
|
+
},
|
|
38
|
+
toolFn: fn,
|
|
39
|
+
});
|
|
40
|
+
const route = make("route", 20, (_, ctx) => ctx.dispatchAction("act", {}));
|
|
41
|
+
route.tool.authorityPropagation = "preserve";
|
|
42
|
+
const effect = jest.fn(() => "admin target");
|
|
43
|
+
(0, index_1.registerContractResolver)(contractId, 1, () => ({
|
|
44
|
+
contractId,
|
|
45
|
+
version: 1,
|
|
46
|
+
tools: { route },
|
|
47
|
+
actionAliases: { act: "target" },
|
|
48
|
+
actionTools: { target: make("target", 60, effect) },
|
|
49
|
+
}), { providerPackageId: packageId });
|
|
50
|
+
loader.contractRegistry.register(packageId, {
|
|
51
|
+
contractId,
|
|
52
|
+
version: 1,
|
|
53
|
+
name: "Arbitrary",
|
|
54
|
+
description: "",
|
|
55
|
+
tools: [{ name: "route", usageDescription: "route", inputFields: [], outputFields: [] }],
|
|
56
|
+
tables: [],
|
|
57
|
+
observables: [],
|
|
58
|
+
events: [],
|
|
59
|
+
});
|
|
60
|
+
const endpoint = await loader.resolveContractEndpoint({
|
|
61
|
+
contractId,
|
|
62
|
+
version: 1,
|
|
63
|
+
providerPackageId: packageId,
|
|
64
|
+
context: { callerUserId: userId, dataContext },
|
|
65
|
+
notify: async () => undefined,
|
|
66
|
+
});
|
|
67
|
+
const call = {
|
|
68
|
+
contractId,
|
|
69
|
+
version: 1,
|
|
70
|
+
member: "tool",
|
|
71
|
+
name: "route",
|
|
72
|
+
operation: "run",
|
|
73
|
+
args: [{}],
|
|
74
|
+
dataContextId: groupId,
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
await expect(endpoint.handleCall(call)).resolves.toBe("admin target");
|
|
78
|
+
// Metadata is read on every call: an ordinary Reader wrapper caps the Admin at Reader.
|
|
79
|
+
route.tool.authorityPropagation = "cap";
|
|
80
|
+
await expect(endpoint.handleCall(call)).rejects.toThrow("strict delegation");
|
|
81
|
+
expect(effect).toHaveBeenCalledTimes(1);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
endpoint.dispose();
|
|
85
|
+
await loader.dispose();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("../index");
|
|
4
|
+
it("host dispatch honors preferred providers, missing-provider policy, and owner pinning", async () => {
|
|
5
|
+
const dataContext = { dataContextId: (0, index_1.newid)() };
|
|
6
|
+
const loader = new index_1.PackageLoader(dataContext);
|
|
7
|
+
const caller = (0, index_1.newid)();
|
|
8
|
+
const preferred = (0, index_1.newid)();
|
|
9
|
+
const alternate = (0, index_1.newid)();
|
|
10
|
+
const contractId = (0, index_1.newid)();
|
|
11
|
+
const definition = {
|
|
12
|
+
contractId,
|
|
13
|
+
version: 1,
|
|
14
|
+
name: "Target",
|
|
15
|
+
description: "",
|
|
16
|
+
tables: [],
|
|
17
|
+
events: [],
|
|
18
|
+
observables: [],
|
|
19
|
+
tools: [{ name: "target", usageDescription: "", inputFields: [], outputFields: [] }],
|
|
20
|
+
};
|
|
21
|
+
loader.contractRegistry.register(alternate, definition);
|
|
22
|
+
loader.contractRegistry.register(preferred, definition);
|
|
23
|
+
loader.hostConsumes.set(caller, [
|
|
24
|
+
{ contractId, version: 1, providerPackageIds: [preferred], fallbackToDefaultProvider: false },
|
|
25
|
+
]);
|
|
26
|
+
const resolve = jest
|
|
27
|
+
.spyOn(loader, "resolveContractEndpoint")
|
|
28
|
+
.mockImplementation(async (request) => ({ handleCall: async () => request.providerPackageId, dispose: () => undefined }));
|
|
29
|
+
const context = {
|
|
30
|
+
dataContext,
|
|
31
|
+
callerUserId: (0, index_1.newid)(),
|
|
32
|
+
authority: {
|
|
33
|
+
userId: (0, index_1.newid)(),
|
|
34
|
+
userAccessLevel: 60,
|
|
35
|
+
assistantAccessLevel: 40,
|
|
36
|
+
strictActionScope: true,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const dispatch = (providerPackageId) => loader.dispatchToolFromProvider(caller, { contractId, version: 1, toolName: "target", providerPackageId }, {}, context);
|
|
40
|
+
try {
|
|
41
|
+
await expect(dispatch()).resolves.toBe(preferred);
|
|
42
|
+
await expect(dispatch(alternate)).rejects.toThrow("Provider substitution");
|
|
43
|
+
loader.contractRegistry.unregister(preferred);
|
|
44
|
+
await expect(dispatch()).rejects.toThrow("No declared contract provider");
|
|
45
|
+
loader.hostConsumes.get(caller)[0].fallbackToDefaultProvider = true;
|
|
46
|
+
await expect(dispatch()).resolves.toBe(alternate);
|
|
47
|
+
loader.contractRegistry.register(caller, definition);
|
|
48
|
+
loader.hostConsumes.delete(caller);
|
|
49
|
+
await expect(dispatch(alternate)).rejects.toThrow("Provider substitution");
|
|
50
|
+
await expect(dispatch()).resolves.toBe(caller);
|
|
51
|
+
expect(resolve).toHaveBeenCalledTimes(3);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await loader.dispose();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -22,7 +22,8 @@ export interface IIsolatedContractToolRef {
|
|
|
22
22
|
*
|
|
23
23
|
* Shape matches {@link IContractTool}. `suggestedAccessLevel` is the default
|
|
24
24
|
* effective level until install-time approval exists and calls `approveTool`.
|
|
25
|
-
*
|
|
25
|
+
* Omitted admission is None (0) for a preservation request, otherwise fail-closed Self (100).
|
|
26
|
+
* Operator approval changes propagation, never admission.
|
|
26
27
|
*/
|
|
27
28
|
export interface IIsolatedContractTool extends Omit<IContractTool, "inputFields" | "outputFields"> {
|
|
28
29
|
/** Defaults to `[]` during validation when omitted. */
|
|
@@ -33,9 +34,13 @@ export interface IIsolatedContractTool extends Omit<IContractTool, "inputFields"
|
|
|
33
34
|
* Default effective access level for this tool.
|
|
34
35
|
*
|
|
35
36
|
* Used until an administrator explicitly approves the tool at install time.
|
|
36
|
-
* Omit
|
|
37
|
+
* Omit for None (0) when requesting preservation; other tools default to Self (100).
|
|
37
38
|
*/
|
|
38
39
|
suggestedAccessLevel?: number;
|
|
40
|
+
/** Declared outgoing authority mode; `preserve` omits this tool's own level from strict narrowing. */
|
|
41
|
+
authorityPropagation?: "cap" | "preserve";
|
|
42
|
+
/** Checked package-internal target, omitted from the public contract definition. */
|
|
43
|
+
private?: boolean;
|
|
39
44
|
/**
|
|
40
45
|
* Per-invoke deadline in milliseconds for this tool.
|
|
41
46
|
*
|
|
@@ -136,6 +141,11 @@ export interface IIsolatedConsumedContract {
|
|
|
136
141
|
providerPackageIds?: string[];
|
|
137
142
|
fallbackToDefaultProvider?: boolean;
|
|
138
143
|
}
|
|
144
|
+
/** Package-owned catalog selector bound to a registered canonical action. */
|
|
145
|
+
export interface IIsolatedActionAlias {
|
|
146
|
+
name: string;
|
|
147
|
+
target: IIsolatedContractToolRef;
|
|
148
|
+
}
|
|
139
149
|
/**
|
|
140
150
|
* Pure-data package description. Validated on the host; never executed there.
|
|
141
151
|
*/
|
|
@@ -149,6 +159,8 @@ export interface IIsolatedPackageManifest {
|
|
|
149
159
|
/** Host allowlist and credential refs for System HTTP. Omit to deny outbound HTTP. */
|
|
150
160
|
network?: IIsolatedPackageNetwork;
|
|
151
161
|
appNavs?: IAppNav[];
|
|
162
|
+
/** Private checked targets, independent of the frozen Voice Skill catalog. */
|
|
163
|
+
actionAliases?: IIsolatedActionAlias[];
|
|
152
164
|
}
|
|
153
165
|
/**
|
|
154
166
|
* Installable isolated package: declarative manifest plus provider source
|
|
@@ -181,6 +193,8 @@ export interface IIsolatedInstalledPackage {
|
|
|
181
193
|
tables: IIsolatedPackageTable[];
|
|
182
194
|
definitions: IContractDefinition[];
|
|
183
195
|
appNavs?: IAppNav[];
|
|
196
|
+
/** Private checked targets, independent of the frozen Voice Skill catalog. */
|
|
197
|
+
actionAliases?: IIsolatedActionAlias[];
|
|
184
198
|
}
|
|
185
199
|
/** Host-contract member guests use to request a consumed-contract tool call. */
|
|
186
200
|
export declare const ISOLATED_CONSUME_HOST_MEMBER = "contract.consume";
|
|
@@ -241,6 +241,8 @@ function validateProvidedContract(contract, index, packageTables) {
|
|
|
241
241
|
"inputFields",
|
|
242
242
|
"outputFields",
|
|
243
243
|
"suggestedAccessLevel",
|
|
244
|
+
"authorityPropagation",
|
|
245
|
+
"private",
|
|
244
246
|
"remoteAccessLevel",
|
|
245
247
|
"timeoutMs",
|
|
246
248
|
], toolLabel);
|
|
@@ -256,6 +258,12 @@ function validateProvidedContract(contract, index, packageTables) {
|
|
|
256
258
|
}
|
|
257
259
|
tool.inputFields = validateFields(tool.inputFields, `${toolLabel}.inputFields`);
|
|
258
260
|
tool.outputFields = validateFields(tool.outputFields, `${toolLabel}.outputFields`);
|
|
261
|
+
if (tool.authorityPropagation !== undefined &&
|
|
262
|
+
tool.authorityPropagation !== "cap" &&
|
|
263
|
+
tool.authorityPropagation !== "preserve")
|
|
264
|
+
throw new Error(`${toolLabel}.authorityPropagation must be cap or preserve`);
|
|
265
|
+
if (tool.private !== undefined && typeof tool.private !== "boolean")
|
|
266
|
+
throw new Error(`${toolLabel}.private must be a boolean`);
|
|
259
267
|
if (tool.suggestedAccessLevel !== undefined) {
|
|
260
268
|
if (!Number.isInteger(tool.suggestedAccessLevel) ||
|
|
261
269
|
tool.suggestedAccessLevel < access_level_1.MIN_ACCESS_LEVEL ||
|
|
@@ -570,7 +578,16 @@ function validateIsolatedPackageArtifact(artifact) {
|
|
|
570
578
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
571
579
|
throw new Error("Package artifact.manifest is required");
|
|
572
580
|
}
|
|
573
|
-
assertOnlyKeys(manifest, [
|
|
581
|
+
assertOnlyKeys(manifest, [
|
|
582
|
+
"packageId",
|
|
583
|
+
"version",
|
|
584
|
+
"provides",
|
|
585
|
+
"consumes",
|
|
586
|
+
"tables",
|
|
587
|
+
"network",
|
|
588
|
+
"appNavs",
|
|
589
|
+
"actionAliases",
|
|
590
|
+
], "manifest");
|
|
574
591
|
if (typeof providerSource !== "string" || providerSource.trim() === "") {
|
|
575
592
|
throw new Error("Package artifact.providerSource must be a non-empty string");
|
|
576
593
|
}
|
|
@@ -598,6 +615,22 @@ function validateIsolatedPackageArtifact(artifact) {
|
|
|
598
615
|
for (const [index, dep] of manifest.consumes.entries()) {
|
|
599
616
|
validateConsumedContract(dep, index);
|
|
600
617
|
}
|
|
618
|
+
if (manifest.actionAliases !== undefined) {
|
|
619
|
+
if (!Array.isArray(manifest.actionAliases))
|
|
620
|
+
throw new Error("actionAliases must be an array");
|
|
621
|
+
const names = new Set();
|
|
622
|
+
for (const alias of manifest.actionAliases) {
|
|
623
|
+
if (!alias || typeof alias.name !== "string" || !alias.name || names.has(alias.name)) {
|
|
624
|
+
throw new Error("Invalid or duplicate action alias");
|
|
625
|
+
}
|
|
626
|
+
names.add(alias.name);
|
|
627
|
+
const target = alias.target;
|
|
628
|
+
if (!target ||
|
|
629
|
+
!packageProvidesTool(manifest.provides, target.contractId, target.version, target.toolName)) {
|
|
630
|
+
throw new Error(`Action alias ${alias.name} has no registered target`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
601
634
|
manifest.appNavs = validateAppNavs(manifest.appNavs);
|
|
602
635
|
validatePackageNetwork(manifest);
|
|
603
636
|
return manifest;
|
|
@@ -650,12 +683,16 @@ function serializeIsolatedPackageArtifact(artifact) {
|
|
|
650
683
|
* @param packageTables - Package-owned schemas available for contract exposure.
|
|
651
684
|
*/
|
|
652
685
|
function isolatedContractToDefinition(contract, packageTables) {
|
|
653
|
-
const tools = contract.tools
|
|
686
|
+
const tools = contract.tools
|
|
687
|
+
.filter((tool) => !tool.private)
|
|
688
|
+
.map((tool) => ({
|
|
654
689
|
name: tool.name,
|
|
655
690
|
usageDescription: tool.usageDescription,
|
|
656
691
|
inputFields: tool.inputFields ?? [],
|
|
657
692
|
outputFields: tool.outputFields ?? [],
|
|
658
|
-
...(tool.remoteAccessLevel !== undefined
|
|
693
|
+
...(tool.remoteAccessLevel !== undefined
|
|
694
|
+
? { remoteAccessLevel: tool.remoteAccessLevel }
|
|
695
|
+
: {}),
|
|
659
696
|
}));
|
|
660
697
|
const tables = (contract.tables ?? []).map((tableRef) => {
|
|
661
698
|
const table = findPackageTable(packageTables, tableRef.name);
|
|
@@ -40,6 +40,16 @@ function makeArtifact(overrides = {}) {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
describe("isolated package artifact", () => {
|
|
43
|
+
test("round-trips a generic authority request and rejects unknown modes", () => {
|
|
44
|
+
const { artifact } = makeArtifact();
|
|
45
|
+
const tool = artifact.manifest.provides[0].tools[0];
|
|
46
|
+
tool.authorityPropagation = "preserve";
|
|
47
|
+
expect(() => (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).not.toThrow();
|
|
48
|
+
expect((0, isolated_artifact_1.parseIsolatedPackageArtifact)((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).manifest.provides[0]
|
|
49
|
+
.tools[0]).toMatchObject({ authorityPropagation: "preserve" });
|
|
50
|
+
tool.authorityPropagation = "elevate";
|
|
51
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/authorityPropagation/);
|
|
52
|
+
});
|
|
43
53
|
test("serializes and parses a valid envelope", () => {
|
|
44
54
|
const { artifact, contractId } = makeArtifact();
|
|
45
55
|
const bundle = (0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact);
|
|
@@ -62,6 +72,34 @@ describe("isolated package artifact", () => {
|
|
|
62
72
|
const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
|
|
63
73
|
expect(manifest.tables).toEqual([]);
|
|
64
74
|
});
|
|
75
|
+
test("validates private action aliases without extending the public contract", () => {
|
|
76
|
+
const { artifact, contractId } = makeArtifact();
|
|
77
|
+
artifact.manifest.provides[0].tools.push({
|
|
78
|
+
name: "private-write",
|
|
79
|
+
usageDescription: "Internal write",
|
|
80
|
+
private: true,
|
|
81
|
+
suggestedAccessLevel: access_level_1.AccessLevel.Writer,
|
|
82
|
+
});
|
|
83
|
+
artifact.manifest.actionAliases = [
|
|
84
|
+
{
|
|
85
|
+
name: "write",
|
|
86
|
+
target: { contractId, version: 1, toolName: "private-write" },
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
const validated = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
|
|
90
|
+
expect((0, isolated_artifact_1.isolatedContractToDefinition)(validated.provides[0], []).tools.map((tool) => tool.name)).toEqual(["greet"]);
|
|
91
|
+
expect((0, isolated_artifact_1.parseIsolatedPackageArtifact)((0, isolated_artifact_1.serializeIsolatedPackageArtifact)(artifact)).manifest
|
|
92
|
+
.actionAliases).toEqual(artifact.manifest.actionAliases);
|
|
93
|
+
artifact.manifest.actionAliases.push(artifact.manifest.actionAliases[0]);
|
|
94
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow("duplicate action alias");
|
|
95
|
+
artifact.manifest.actionAliases = [
|
|
96
|
+
{
|
|
97
|
+
name: "write",
|
|
98
|
+
target: { contractId, version: 1, toolName: "missing" },
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow("no registered target");
|
|
102
|
+
});
|
|
65
103
|
test("accepts a package-private table without adding it to the contract definition", () => {
|
|
66
104
|
const { artifact } = makeArtifact();
|
|
67
105
|
artifact.manifest.tables = [
|
|
@@ -15,6 +15,14 @@ export type IsolatedContractTableOperation = (typeof ISOLATED_CONTRACT_TABLE_OPE
|
|
|
15
15
|
*/
|
|
16
16
|
export interface IIsolatedCallPrincipal {
|
|
17
17
|
userAccessLevel: number;
|
|
18
|
+
/** Signed identity retained by the host across local hops. */
|
|
19
|
+
userId?: string;
|
|
20
|
+
/** Strict runtime tool-dispatch scope; never supplied by guest arguments. */
|
|
21
|
+
strictActionScope?: boolean;
|
|
22
|
+
/** Effective level of the currently executing ordinary tool. */
|
|
23
|
+
callingToolAccessLevel?: number;
|
|
24
|
+
/** Effective permission from the host, never requested package metadata. */
|
|
25
|
+
authorityPropagation?: "cap" | "preserve";
|
|
18
26
|
/** Omit for a direct user invocation. */
|
|
19
27
|
assistantAccessLevel?: number;
|
|
20
28
|
}
|
|
@@ -86,6 +94,13 @@ export interface IIsolatedPackageRuntime {
|
|
|
86
94
|
* @param principal - Trusted caller authority. Defaults to Self when omitted.
|
|
87
95
|
*/
|
|
88
96
|
invokeOwnedTool?(packageId: string, ref: IIsolatedContractToolRef, args: unknown, principal?: IIsolatedCallPrincipal): Promise<unknown>;
|
|
97
|
+
/** Narrow a projected action turn by the trusted consumer's selected contract consume ceiling. */
|
|
98
|
+
actionDispatchPrincipal?(principal: IIsolatedCallPrincipal, consumerPackageId?: string, ref?: IIsolatedContractToolRef): IIsolatedCallPrincipal;
|
|
99
|
+
/** Project aliases whose wrapper and canonical targets allow the trusted caller. */
|
|
100
|
+
authorizedActionNames?(packageId: string, principal: IIsolatedCallPrincipal, consumerPackageId?: string, selectedContract?: {
|
|
101
|
+
contractId: string;
|
|
102
|
+
version: number;
|
|
103
|
+
}): string[] | Promise<string[]>;
|
|
89
104
|
/** Terminate every worker and drop installed state. */
|
|
90
105
|
dispose(): Promise<void>;
|
|
91
106
|
}
|
|
@@ -28,6 +28,7 @@ export declare class PackageLoader {
|
|
|
28
28
|
static PeersSDK: any;
|
|
29
29
|
/** Host-injected Zod module used as the `zod` require. */
|
|
30
30
|
static Zod: any;
|
|
31
|
+
private hostConsumes;
|
|
31
32
|
private packageInstances;
|
|
32
33
|
/** Packages that were installed via the contract-based path (tools/assistants/tables already saved with packageId). */
|
|
33
34
|
private contractInstalledPackages;
|
|
@@ -64,6 +65,7 @@ export declare class PackageLoader {
|
|
|
64
65
|
isIsolatedInstalled(packageId: string): boolean;
|
|
65
66
|
/** Return whether this host configured an isolated-package runtime. */
|
|
66
67
|
supportsIsolatedPackages(): boolean;
|
|
68
|
+
private dispatchToolFromProvider;
|
|
67
69
|
/**
|
|
68
70
|
* Return the active provider selected by this data context's trusted registry.
|
|
69
71
|
*
|
|
@@ -45,6 +45,7 @@ class PackageLoader {
|
|
|
45
45
|
static PeersSDK;
|
|
46
46
|
/** Host-injected Zod module used as the `zod` require. */
|
|
47
47
|
static Zod;
|
|
48
|
+
hostConsumes = new Map();
|
|
48
49
|
packageInstances = {};
|
|
49
50
|
/** Packages that were installed via the contract-based path (tools/assistants/tables already saved with packageId). */
|
|
50
51
|
contractInstalledPackages = new Set();
|
|
@@ -114,6 +115,62 @@ class PackageLoader {
|
|
|
114
115
|
supportsIsolatedPackages() {
|
|
115
116
|
return this.isolatedRuntime !== undefined;
|
|
116
117
|
}
|
|
118
|
+
async dispatchToolFromProvider(packageId, target, args, context) {
|
|
119
|
+
if (!context.authority?.userId ||
|
|
120
|
+
!context.dataContext ||
|
|
121
|
+
context.dataContext.dataContextId !== this.dataContext.dataContextId)
|
|
122
|
+
throw new Error("Missing trusted execution context");
|
|
123
|
+
if (target.targetDeviceId)
|
|
124
|
+
throw new Error("Strict delegated calls cannot target another device until authority transport is supported");
|
|
125
|
+
if (target.dataContextId && target.dataContextId !== this.dataContext.dataContextId)
|
|
126
|
+
throw new Error("Cross-context action dispatch is denied");
|
|
127
|
+
const own = this.contractRegistry
|
|
128
|
+
.getRegisteredProviderIds(target.contractId, target.version)
|
|
129
|
+
.includes(packageId);
|
|
130
|
+
const consume = this.hostConsumes
|
|
131
|
+
.get(packageId)
|
|
132
|
+
?.find((c) => c.contractId === target.contractId && c.version === target.version);
|
|
133
|
+
if (!own && !consume)
|
|
134
|
+
throw new Error("Undeclared contract consumption");
|
|
135
|
+
if (own && target.providerPackageId && target.providerPackageId !== packageId && !consume)
|
|
136
|
+
throw new Error("Provider substitution is denied for package-owned targets");
|
|
137
|
+
const selected = own && (!target.providerPackageId || target.providerPackageId === packageId)
|
|
138
|
+
? packageId
|
|
139
|
+
: consume
|
|
140
|
+
? this.contractRegistry.selectProvider(consume)?.providerPackageId
|
|
141
|
+
: undefined;
|
|
142
|
+
if (!selected)
|
|
143
|
+
throw new Error("No declared contract provider is available");
|
|
144
|
+
if (target.providerPackageId && target.providerPackageId !== selected)
|
|
145
|
+
throw new Error("Provider substitution is denied");
|
|
146
|
+
const providerPackageId = selected;
|
|
147
|
+
const authority = this.isolatedRuntime?.actionDispatchPrincipal?.(context.authority, packageId, target) ??
|
|
148
|
+
context.authority;
|
|
149
|
+
const callContext = { ...context, authority, callerPackageId: packageId };
|
|
150
|
+
const endpoint = await this.resolveContractEndpoint({
|
|
151
|
+
contractId: target.contractId,
|
|
152
|
+
version: target.version,
|
|
153
|
+
dataContextId: this.dataContext.dataContextId,
|
|
154
|
+
providerPackageId,
|
|
155
|
+
context: callContext,
|
|
156
|
+
notify: async () => undefined,
|
|
157
|
+
});
|
|
158
|
+
try {
|
|
159
|
+
return await endpoint.handleCall({
|
|
160
|
+
contractId: target.contractId,
|
|
161
|
+
version: target.version,
|
|
162
|
+
name: target.toolName,
|
|
163
|
+
member: "tool",
|
|
164
|
+
operation: "run",
|
|
165
|
+
args: [args],
|
|
166
|
+
providerPackageId,
|
|
167
|
+
dataContextId: this.dataContext.dataContextId,
|
|
168
|
+
}, callContext);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
endpoint.dispose();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
117
174
|
/**
|
|
118
175
|
* Return the active provider selected by this data context's trusted registry.
|
|
119
176
|
*
|
|
@@ -205,6 +262,7 @@ class PackageLoader {
|
|
|
205
262
|
}
|
|
206
263
|
: resolution;
|
|
207
264
|
const endpoint = (0, contract_proxy_1.createContractProviderEndpoint)(localResolution, {
|
|
265
|
+
dispatchTool: (target, args, context) => this.dispatchToolFromProvider(providerPackageId, target, args, context),
|
|
208
266
|
context: request.context,
|
|
209
267
|
notify: (notification) => request.notify({
|
|
210
268
|
...notification,
|
|
@@ -318,6 +376,7 @@ class PackageLoader {
|
|
|
318
376
|
* @param packageId - Package to unload.
|
|
319
377
|
*/
|
|
320
378
|
async unloadPackage(packageId) {
|
|
379
|
+
this.hostConsumes.delete(packageId);
|
|
321
380
|
if (this.isolatedRuntime?.isInstalled(packageId)) {
|
|
322
381
|
await this.isolatedRuntime.uninstall(packageId);
|
|
323
382
|
}
|
|
@@ -629,6 +688,7 @@ class PackageLoader {
|
|
|
629
688
|
await (0, contract_package_loader_1.installContractPackage)(this.dataContext, packageDefinition, {
|
|
630
689
|
registry: this.contractRegistry,
|
|
631
690
|
});
|
|
691
|
+
this.hostConsumes.set(packageDefinition.packageId, packageDefinition.consumes);
|
|
632
692
|
this.contractInstalledPackages.add(packageDefinition.packageId);
|
|
633
693
|
await this.correctPackageVersion(pkg, selectedPackageVersion, packageDefinition.version);
|
|
634
694
|
// Build a proper IPeersPackage from the contract definition so callers
|
|
@@ -176,7 +176,7 @@ describe("PackageLoader.contractRegistry", () => {
|
|
|
176
176
|
name: "invoke",
|
|
177
177
|
operation: "run",
|
|
178
178
|
args: [],
|
|
179
|
-
})).rejects.toThrow("missing caller identity for tool 'invoke'");
|
|
179
|
+
})).rejects.toThrow("missing caller identity or trusted context for tool 'invoke'");
|
|
180
180
|
expect(write).not.toHaveBeenCalled();
|
|
181
181
|
}
|
|
182
182
|
finally {
|