@peers-app/peers-sdk 0.25.16 → 0.25.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/contracts/__tests__/contract-proxy.test.js +24 -1
  2. package/dist/contracts/__tests__/contract-resolvers.test.js +3 -0
  3. package/dist/contracts/contract-provider-router.d.ts +16 -2
  4. package/dist/contracts/contract-provider-router.js +4 -0
  5. package/dist/contracts/contract-providers.table.d.ts +2 -2
  6. package/dist/contracts/contract-proxy.d.ts +11 -0
  7. package/dist/contracts/contract-proxy.js +11 -3
  8. package/dist/contracts/contract-resolvers.js +12 -1
  9. package/dist/contracts/index.d.ts +3 -0
  10. package/dist/contracts/index.js +18 -2
  11. package/dist/contracts/system/contract-catalog-contract.d.ts +38 -0
  12. package/dist/contracts/system/contract-catalog-contract.js +51 -0
  13. package/dist/contracts/system/http-contract.d.ts +80 -0
  14. package/dist/contracts/system/http-contract.js +93 -0
  15. package/dist/contracts/system/index.d.ts +3 -0
  16. package/dist/contracts/system/index.js +3 -0
  17. package/dist/contracts/system/scheduler-contract.d.ts +54 -0
  18. package/dist/contracts/system/scheduler-contract.js +70 -0
  19. package/dist/data/tool-access-approvals.d.ts +3 -3
  20. package/dist/package-loader/index.d.ts +1 -0
  21. package/dist/package-loader/index.js +1 -0
  22. package/dist/package-loader/isolated-artifact.d.ts +37 -0
  23. package/dist/package-loader/isolated-artifact.js +79 -2
  24. package/dist/package-loader/isolated-artifact.test.js +25 -0
  25. package/dist/package-loader/isolated-persistent-var-gateway.js +19 -0
  26. package/dist/package-loader/isolated-runtime.d.ts +28 -1
  27. package/dist/package-loader/package-loader.js +4 -0
  28. package/dist/package-loader/package-loader.test.js +50 -2
  29. package/dist/package-loader/package-network-policy.d.ts +52 -0
  30. package/dist/package-loader/package-network-policy.js +62 -0
  31. package/dist/system-ids.d.ts +31 -0
  32. package/dist/system-ids.js +32 -1
  33. package/package.json +1 -1
@@ -0,0 +1,54 @@
1
+ import type { IContractConsumerBase } from "../contract-proxy";
2
+ import type { IContractDefinition } from "../types";
3
+ /** Version of the built-in System Scheduler contract. */
4
+ export declare const SCHEDULER_CONTRACT_VERSION = 1;
5
+ /** Package id of the System Scheduler provider. */
6
+ export declare const schedulerProviderPackageId = "000peers0system0000000001";
7
+ /** Furthest a schedule may sit in the future. */
8
+ export declare const SCHEDULER_MAX_DELAY_MS: number;
9
+ /** Input for scheduling a wake-up of the caller's own tool. */
10
+ export interface IScheduleInput {
11
+ /** Unix epoch milliseconds when the tool should run. */
12
+ at: number;
13
+ contractId: string;
14
+ version: number;
15
+ toolName: string;
16
+ payload?: unknown;
17
+ }
18
+ /** One device-local schedule owned by a package. */
19
+ export interface IScheduleInfo {
20
+ scheduleId: string;
21
+ packageId: string;
22
+ dataContextId: string;
23
+ contractId: string;
24
+ version: number;
25
+ toolName: string;
26
+ at: number;
27
+ }
28
+ /**
29
+ * Consumer shape for System Scheduler.
30
+ *
31
+ * Schedules are stored on this device only. Another device does not run them.
32
+ */
33
+ export interface ISchedulerConsumer extends IContractConsumerBase {
34
+ tools: {
35
+ schedule(input: IScheduleInput): Promise<{
36
+ scheduleId: string;
37
+ }>;
38
+ cancel(input: {
39
+ scheduleId: string;
40
+ }): Promise<{
41
+ cancelled: boolean;
42
+ }>;
43
+ list(): Promise<{
44
+ schedules: IScheduleInfo[];
45
+ }>;
46
+ };
47
+ }
48
+ /**
49
+ * Return the local-only System Scheduler contract shape.
50
+ *
51
+ * A package may schedule a tool that it provides. When `at` arrives, the host
52
+ * starts that package's worker if needed and runs the tool as Self.
53
+ */
54
+ export declare function getSchedulerContractDefinition(): IContractDefinition;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCHEDULER_MAX_DELAY_MS = exports.schedulerProviderPackageId = exports.SCHEDULER_CONTRACT_VERSION = void 0;
4
+ exports.getSchedulerContractDefinition = getSchedulerContractDefinition;
5
+ const system_ids_1 = require("../../system-ids");
6
+ const field_type_1 = require("../../types/field-type");
7
+ const system_contracts_1 = require("../system-contracts");
8
+ /** Version of the built-in System Scheduler contract. */
9
+ exports.SCHEDULER_CONTRACT_VERSION = 1;
10
+ /** Package id of the System Scheduler provider. */
11
+ exports.schedulerProviderPackageId = system_ids_1.peersSystemPackageId;
12
+ /** Furthest a schedule may sit in the future. */
13
+ exports.SCHEDULER_MAX_DELAY_MS = 366 * 24 * 60 * 60 * 1000;
14
+ const field = (name, type, description, opts = {}) => ({ name, type, description, ...opts });
15
+ let definition;
16
+ /**
17
+ * Return the local-only System Scheduler contract shape.
18
+ *
19
+ * A package may schedule a tool that it provides. When `at` arrives, the host
20
+ * starts that package's worker if needed and runs the tool as Self.
21
+ */
22
+ function getSchedulerContractDefinition() {
23
+ definition ??= {
24
+ contractId: system_ids_1.schedulerContractId,
25
+ version: exports.SCHEDULER_CONTRACT_VERSION,
26
+ devTag: "dev",
27
+ name: "System Scheduler",
28
+ description: "Device-local schedules that wake a package's own tool. Schedules do not sync to other devices.",
29
+ tables: [],
30
+ tools: [
31
+ {
32
+ name: "schedule",
33
+ usageDescription: "Run one of this package's tools at a future time on this device. Returns a scheduleId.",
34
+ inputFields: [
35
+ field("at", field_type_1.FieldType.number, "Unix epoch milliseconds when the tool should run."),
36
+ field("contractId", field_type_1.FieldType.id, "Contract this package provides."),
37
+ field("version", field_type_1.FieldType.number, "Contract version."),
38
+ field("toolName", field_type_1.FieldType.string, "Tool on that contract to run."),
39
+ field("payload", field_type_1.FieldType.any, "JSON value passed to the tool.", { optional: true }),
40
+ ],
41
+ outputFields: [field("scheduleId", field_type_1.FieldType.id, "Id to pass to cancel.")],
42
+ },
43
+ {
44
+ name: "cancel",
45
+ usageDescription: "Cancel a schedule this package created.",
46
+ inputFields: [field("scheduleId", field_type_1.FieldType.id, "Schedule to cancel.")],
47
+ outputFields: [
48
+ field("cancelled", field_type_1.FieldType.boolean, "True when the schedule existed and was removed."),
49
+ ],
50
+ },
51
+ {
52
+ name: "list",
53
+ usageDescription: "List schedules owned by the calling package on this device.",
54
+ inputFields: [],
55
+ outputFields: [
56
+ field("schedules", field_type_1.FieldType.object, "Schedules with scheduleId, contractId, version, toolName, and at.", { isArray: true }),
57
+ ],
58
+ },
59
+ ],
60
+ observables: [],
61
+ events: [],
62
+ };
63
+ return definition;
64
+ }
65
+ (0, system_contracts_1.registerSystemContract)({
66
+ providerPackageId: exports.schedulerProviderPackageId,
67
+ contractId: system_ids_1.schedulerContractId,
68
+ version: exports.SCHEDULER_CONTRACT_VERSION,
69
+ getDefinition: getSchedulerContractDefinition,
70
+ });
@@ -52,8 +52,8 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
52
52
  dataContextId: string;
53
53
  createdAt: Date;
54
54
  expiresAt: Date;
55
- toolAccessApprovalId: string;
56
55
  toolName: string;
56
+ toolAccessApprovalId: string;
57
57
  argsHash: string;
58
58
  argsPreview: string;
59
59
  frozenArgs: {} & {
@@ -73,8 +73,8 @@ export declare const toolAccessApprovalSchema: z.ZodObject<{
73
73
  toolId: string;
74
74
  dataContextId: string;
75
75
  expiresAt: Date;
76
- toolAccessApprovalId: string;
77
76
  toolName: string;
77
+ toolAccessApprovalId: string;
78
78
  argsHash: string;
79
79
  frozenArgs: {} & {
80
80
  [k: string]: any;
@@ -108,8 +108,8 @@ export declare function ToolAccessApprovals(dataContext?: DataContext): import("
108
108
  dataContextId: string;
109
109
  createdAt: Date;
110
110
  expiresAt: Date;
111
- toolAccessApprovalId: string;
112
111
  toolName: string;
112
+ toolAccessApprovalId: string;
113
113
  argsHash: string;
114
114
  argsPreview: string;
115
115
  frozenArgs: {} & {
@@ -4,3 +4,4 @@ export * from "./isolated-persistent-var-gateway";
4
4
  export * from "./isolated-runtime";
5
5
  export * from "./isolated-table-gateway";
6
6
  export * from "./package-loader";
7
+ export * from "./package-network-policy";
@@ -20,3 +20,4 @@ __exportStar(require("./isolated-persistent-var-gateway"), exports);
20
20
  __exportStar(require("./isolated-runtime"), exports);
21
21
  __exportStar(require("./isolated-table-gateway"), exports);
22
22
  __exportStar(require("./package-loader"), exports);
23
+ __exportStar(require("./package-network-policy"), exports);
@@ -36,6 +36,41 @@ export interface IIsolatedContractTool extends Omit<IContractTool, "inputFields"
36
36
  * Omit to keep the fail-closed Self default.
37
37
  */
38
38
  suggestedAccessLevel?: number;
39
+ /**
40
+ * Per-invoke deadline in milliseconds for this tool.
41
+ *
42
+ * When omitted, the runtime's `invokeTimeoutMs` applies. Capped at
43
+ * {@link ISOLATED_TOOL_TIMEOUT_MAX_MS}. Host calls made during the invoke,
44
+ * including System HTTP, count against this deadline.
45
+ */
46
+ timeoutMs?: number;
47
+ }
48
+ /** Longest per-tool invoke deadline an isolated manifest may declare. */
49
+ export declare const ISOLATED_TOOL_TIMEOUT_MAX_MS = 120000;
50
+ /**
51
+ * Secret the host may attach to an outbound request.
52
+ *
53
+ * The guest passes `ref`. The host reads `secretVar` and sets `header`.
54
+ */
55
+ export interface IIsolatedPackageNetworkCredential {
56
+ ref: string;
57
+ /** Logical persistent-variable name. The host prefixes it with the package id. */
58
+ secretVar: string;
59
+ /** Defaults to `user` when omitted. */
60
+ scope?: "user" | "device";
61
+ header: string;
62
+ /** Must contain the literal `{secret}`. */
63
+ format: string;
64
+ }
65
+ /**
66
+ * Outbound HTTPS this package is allowed to perform through System HTTP.
67
+ *
68
+ * Honored at install the same way `suggestedAccessLevel` is: there is no
69
+ * separate approval step yet. The Packages screen displays the declaration.
70
+ */
71
+ export interface IIsolatedPackageNetwork {
72
+ allowedHosts: string[];
73
+ credentials?: IIsolatedPackageNetworkCredential[];
39
74
  }
40
75
  /**
41
76
  * Private table schema declared by an isolated package.
@@ -111,6 +146,8 @@ export interface IIsolatedPackageManifest {
111
146
  consumes: IIsolatedConsumedContract[];
112
147
  /** Package-owned table schemas. Contract references decide public exposure. */
113
148
  tables?: IIsolatedPackageTable[];
149
+ /** Host allowlist and credential refs for System HTTP. Omit to deny outbound HTTP. */
150
+ network?: IIsolatedPackageNetwork;
114
151
  appNavs?: IAppNav[];
115
152
  }
116
153
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ISOLATED_EVENT_HOST_MEMBER = exports.ISOLATED_PERSISTENT_VAR_HOST_MEMBER = exports.ISOLATED_TABLE_HOST_MEMBER = exports.ISOLATED_CONSUME_HOST_MEMBER = exports.ISOLATED_PACKAGE_MAGIC = void 0;
3
+ exports.ISOLATED_EVENT_HOST_MEMBER = exports.ISOLATED_PERSISTENT_VAR_HOST_MEMBER = exports.ISOLATED_TABLE_HOST_MEMBER = exports.ISOLATED_CONSUME_HOST_MEMBER = exports.ISOLATED_TOOL_TIMEOUT_MAX_MS = exports.ISOLATED_PACKAGE_MAGIC = void 0;
4
4
  exports.looksLikeIsolatedPackageArtifact = looksLikeIsolatedPackageArtifact;
5
5
  exports.validateIsolatedPackageArtifact = validateIsolatedPackageArtifact;
6
6
  exports.parseIsolatedPackageArtifact = parseIsolatedPackageArtifact;
@@ -25,6 +25,8 @@ const utils_1 = require("../utils");
25
25
  * opt-in format without evaluating source.
26
26
  */
27
27
  exports.ISOLATED_PACKAGE_MAGIC = "PEERS_ISOLATED_PACKAGE_V1";
28
+ /** Longest per-tool invoke deadline an isolated manifest may declare. */
29
+ exports.ISOLATED_TOOL_TIMEOUT_MAX_MS = 120_000;
28
30
  /** Host-contract member guests use to request a consumed-contract tool call. */
29
31
  exports.ISOLATED_CONSUME_HOST_MEMBER = "contract.consume";
30
32
  /** Host-contract member guests use to read or write a package-owned table. */
@@ -240,6 +242,7 @@ function validateProvidedContract(contract, index, packageTables) {
240
242
  "outputFields",
241
243
  "suggestedAccessLevel",
242
244
  "remoteAccessLevel",
245
+ "timeoutMs",
243
246
  ], toolLabel);
244
247
  if (typeof tool.name !== "string" || tool.name.trim() === "") {
245
248
  throw new Error(`${toolLabel}.name is required`);
@@ -267,6 +270,13 @@ function validateProvidedContract(contract, index, packageTables) {
267
270
  throw new Error(`${toolLabel}.remoteAccessLevel must be an integer 0–100`);
268
271
  }
269
272
  }
273
+ if (tool.timeoutMs !== undefined) {
274
+ if (!Number.isInteger(tool.timeoutMs) ||
275
+ tool.timeoutMs < 1 ||
276
+ tool.timeoutMs > exports.ISOLATED_TOOL_TIMEOUT_MAX_MS) {
277
+ throw new Error(`${toolLabel}.timeoutMs must be an integer from 1 to ${exports.ISOLATED_TOOL_TIMEOUT_MAX_MS}`);
278
+ }
279
+ }
270
280
  }
271
281
  if (contract.tables === undefined) {
272
282
  contract.tables = [];
@@ -430,6 +440,72 @@ function validateConsumedContract(dep, index) {
430
440
  }
431
441
  }
432
442
  }
443
+ const NETWORK_HOST_PATTERN = /^(?=.{1,253}$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/;
444
+ const NETWORK_REF_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
445
+ const NETWORK_HEADER_PATTERN = /^[A-Za-z0-9-]{1,64}$/;
446
+ const NETWORK_SECRET_VAR_PATTERN = /^[A-Za-z][A-Za-z0-9_:-]{0,80}$/;
447
+ function validatePackageNetwork(manifest) {
448
+ if (manifest.network === undefined) {
449
+ return;
450
+ }
451
+ const network = manifest.network;
452
+ if (!network || typeof network !== "object" || Array.isArray(network)) {
453
+ throw new Error("manifest.network must be an object");
454
+ }
455
+ assertOnlyKeys(network, ["allowedHosts", "credentials"], "manifest.network");
456
+ if (!Array.isArray(network.allowedHosts) || network.allowedHosts.length === 0) {
457
+ throw new Error("manifest.network.allowedHosts must be a non-empty array");
458
+ }
459
+ const hosts = new Set();
460
+ for (const [index, host] of network.allowedHosts.entries()) {
461
+ if (typeof host !== "string" || !NETWORK_HOST_PATTERN.test(host)) {
462
+ throw new Error(`manifest.network.allowedHosts[${index}] must be a lowercase hostname with no scheme, port, or path`);
463
+ }
464
+ if (host.split(".").every((label) => /^\d+$/.test(label))) {
465
+ throw new Error(`manifest.network.allowedHosts[${index}] must be a hostname, not an IP address`);
466
+ }
467
+ if (hosts.has(host)) {
468
+ throw new Error(`manifest.network declares duplicate host "${host}"`);
469
+ }
470
+ hosts.add(host);
471
+ }
472
+ if (network.credentials === undefined) {
473
+ network.credentials = [];
474
+ }
475
+ if (!Array.isArray(network.credentials)) {
476
+ throw new Error("manifest.network.credentials must be an array");
477
+ }
478
+ const refs = new Set();
479
+ for (const [index, credential] of network.credentials.entries()) {
480
+ const label = `manifest.network.credentials[${index}]`;
481
+ if (!credential || typeof credential !== "object" || Array.isArray(credential)) {
482
+ throw new Error(`${label} must be an object`);
483
+ }
484
+ assertOnlyKeys(credential, ["ref", "secretVar", "scope", "header", "format"], label);
485
+ if (typeof credential.ref !== "string" || !NETWORK_REF_PATTERN.test(credential.ref)) {
486
+ throw new Error(`${label}.ref must be a short identifier`);
487
+ }
488
+ if (refs.has(credential.ref)) {
489
+ throw new Error(`manifest.network declares duplicate credential ref "${credential.ref}"`);
490
+ }
491
+ refs.add(credential.ref);
492
+ if (typeof credential.secretVar !== "string" ||
493
+ !NETWORK_SECRET_VAR_PATTERN.test(credential.secretVar)) {
494
+ throw new Error(`${label}.secretVar must be a logical persistent-variable name`);
495
+ }
496
+ if (credential.scope !== undefined &&
497
+ credential.scope !== "user" &&
498
+ credential.scope !== "device") {
499
+ throw new Error(`${label}.scope must be "user" or "device"`);
500
+ }
501
+ if (typeof credential.header !== "string" || !NETWORK_HEADER_PATTERN.test(credential.header)) {
502
+ throw new Error(`${label}.header must be an HTTP header name`);
503
+ }
504
+ if (typeof credential.format !== "string" || !credential.format.includes("{secret}")) {
505
+ throw new Error(`${label}.format must contain {secret}`);
506
+ }
507
+ }
508
+ }
433
509
  function validateAppNavs(appNavs) {
434
510
  if (appNavs === undefined) {
435
511
  return undefined;
@@ -494,7 +570,7 @@ function validateIsolatedPackageArtifact(artifact) {
494
570
  if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
495
571
  throw new Error("Package artifact.manifest is required");
496
572
  }
497
- assertOnlyKeys(manifest, ["packageId", "version", "provides", "consumes", "tables", "appNavs"], "manifest");
573
+ assertOnlyKeys(manifest, ["packageId", "version", "provides", "consumes", "tables", "network", "appNavs"], "manifest");
498
574
  if (typeof providerSource !== "string" || providerSource.trim() === "") {
499
575
  throw new Error("Package artifact.providerSource must be a non-empty string");
500
576
  }
@@ -523,6 +599,7 @@ function validateIsolatedPackageArtifact(artifact) {
523
599
  validateConsumedContract(dep, index);
524
600
  }
525
601
  manifest.appNavs = validateAppNavs(manifest.appNavs);
602
+ validatePackageNetwork(manifest);
526
603
  return manifest;
527
604
  }
528
605
  /**
@@ -315,4 +315,29 @@ describe("isolated package artifact", () => {
315
315
  };
316
316
  expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/JSON-compatible/);
317
317
  });
318
+ test("accepts a network allowlist and rejects a host with a scheme", () => {
319
+ const { artifact } = makeArtifact();
320
+ artifact.manifest.network = {
321
+ allowedHosts: ["api.openai.com"],
322
+ credentials: [
323
+ {
324
+ ref: "openai",
325
+ secretVar: "openaiApiKey",
326
+ header: "Authorization",
327
+ format: "Bearer {secret}",
328
+ },
329
+ ],
330
+ };
331
+ const manifest = (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact);
332
+ expect(manifest.network?.allowedHosts).toEqual(["api.openai.com"]);
333
+ artifact.manifest.network = { allowedHosts: ["https://api.openai.com"] };
334
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/lowercase hostname/);
335
+ artifact.manifest.network = { allowedHosts: ["127.0.0.1"] };
336
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/hostname, not an IP address/);
337
+ });
338
+ test("rejects a tool timeout above the cap", () => {
339
+ const { artifact } = makeArtifact();
340
+ artifact.manifest.provides[0].tools[0].timeoutMs = 120_001;
341
+ expect(() => (0, isolated_artifact_1.validateIsolatedPackageArtifact)(artifact)).toThrow(/timeoutMs/);
342
+ });
318
343
  });
@@ -7,6 +7,19 @@ const isolated_value_1 = require("./isolated-value");
7
7
  function memoryKey(ref) {
8
8
  return `${ref.packageId}:${ref.scope}:${ref.name}`;
9
9
  }
10
+ async function assertPackagePersistentVarIsNotSecret(dataContext, ref) {
11
+ const userContext = dataContext.userContext;
12
+ if (!userContext) {
13
+ throw new Error("Package persistent variables require a signed-in user");
14
+ }
15
+ const storageContext = ref.scope === "group" ? dataContext : userContext.userDataContext;
16
+ const storageName = (0, persistent_var_identity_1.packageScopedPersistentVarStorageName)(ref.packageId, ref.name, ref.scope, dataContext.dataContextId);
17
+ const { PersistentVars } = await Promise.resolve().then(() => require("../data/persistent-vars"));
18
+ const record = await PersistentVars(storageContext).findOne({ name: storageName });
19
+ if (record?.isSecret) {
20
+ throw new Error("Secret persistent variables are not readable by packages");
21
+ }
22
+ }
10
23
  async function resolvePersistentVar(dataContext, ref) {
11
24
  const { deviceVar, groupDeviceVar, groupUserVar, groupVar, userVar } = await Promise.resolve().then(() => require("../data/persistent-vars"));
12
25
  const name = (0, persistent_var_identity_1.packageScopedPersistentVarName)(ref.packageId, ref.name);
@@ -77,8 +90,10 @@ function createMemoryIsolatedPackagePersistentVarGateway() {
77
90
  function createIsolatedPackagePersistentVarGateway(dataContext) {
78
91
  return {
79
92
  async get(ref) {
93
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
80
94
  const persistentVar = await resolvePersistentVar(dataContext, ref);
81
95
  await persistentVar.loadingPromise;
96
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
82
97
  const value = persistentVar();
83
98
  return value === undefined ? undefined : (0, isolated_value_1.copyIsolatedValue)(value);
84
99
  },
@@ -86,17 +101,21 @@ function createIsolatedPackagePersistentVarGateway(dataContext) {
86
101
  if (value === undefined) {
87
102
  throw new Error("Persistent variable value must be copyable");
88
103
  }
104
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
89
105
  const persistentVar = await resolvePersistentVar(dataContext, ref);
90
106
  const copy = (0, isolated_value_1.copyIsolatedValue)(value);
91
107
  await persistentVar.loadingPromise;
108
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
92
109
  persistentVar(copy);
93
110
  await persistentVar.loadingPromise;
94
111
  const saved = persistentVar();
95
112
  return saved === undefined ? undefined : (0, isolated_value_1.copyIsolatedValue)(saved);
96
113
  },
97
114
  async subscribe(ref, handler) {
115
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
98
116
  const persistentVar = await resolvePersistentVar(dataContext, ref);
99
117
  await persistentVar.loadingPromise;
118
+ await assertPackagePersistentVarIsNotSecret(dataContext, ref);
100
119
  const subscription = persistentVar.subscribe((value) => {
101
120
  handler((0, isolated_value_1.copyIsolatedValue)(value));
102
121
  });
@@ -1,9 +1,10 @@
1
+ import type { DataContext } from "../context/data-context";
1
2
  import type { IContractProviderEndpointRequest } from "../contracts/contract-provider-router";
2
3
  import type { IContractCall, IContractProviderEndpoint } from "../contracts/contract-proxy";
3
4
  import type { ContractRegistry } from "../contracts/registry";
4
5
  import type { PersistentVarScope } from "../data/persistent-vars";
5
6
  import type { IField } from "../types/field-type";
6
- import type { IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
7
+ import type { IIsolatedContractToolRef, IIsolatedInstallResult, IIsolatedPackageArtifact } from "./isolated-artifact";
7
8
  /** Standard table methods exposed through isolated contract-table references. */
8
9
  export declare const ISOLATED_CONTRACT_TABLE_OPERATIONS: readonly ["get", "list", "count", "findOne", "save", "insert", "update", "delete"];
9
10
  /** Consumer-callable operation on an isolated contract table. */
@@ -73,6 +74,18 @@ export interface IIsolatedPackageRuntime {
73
74
  provides(contractId: string, version: number): boolean;
74
75
  /** True when this runtime has installed `packageId`. */
75
76
  isInstalled(packageId: string): boolean;
77
+ /**
78
+ * Run a tool the package provides, starting its worker if needed.
79
+ *
80
+ * Used by System Scheduler. The principal defaults to Self for compatibility,
81
+ * but schedulers should pass the current user's trusted access level.
82
+ *
83
+ * @param packageId - Installed package that declares the tool.
84
+ * @param ref - Contract tool to invoke.
85
+ * @param args - JSON value passed as the tool argument.
86
+ * @param principal - Trusted caller authority. Defaults to Self when omitted.
87
+ */
88
+ invokeOwnedTool?(packageId: string, ref: IIsolatedContractToolRef, args: unknown, principal?: IIsolatedCallPrincipal): Promise<unknown>;
76
89
  /** Terminate every worker and drop installed state. */
77
90
  dispose(): Promise<void>;
78
91
  }
@@ -199,12 +212,26 @@ export interface IIsolatedContractProviderGateway {
199
212
  */
200
213
  resolveEndpoint(request: IIsolatedContractProviderEndpointRequest): Promise<IContractProviderEndpoint | undefined>;
201
214
  }
215
+ /**
216
+ * Resolve the signed-in local user for a runtime's owning data context.
217
+ *
218
+ * Invoked only by the host for guest-originated gateway calls. The user id and
219
+ * data context must come from authenticated host state, never guest payloads.
220
+ * Return `undefined` when no signed-in identity is available; protected tools
221
+ * then retain their existing missing-caller-identity denial.
222
+ */
223
+ export type IsolatedLocalUserContextResolver = () => {
224
+ userId: string;
225
+ dataContext: DataContext;
226
+ } | undefined;
202
227
  /** Options passed to a registered {@link IsolatedPackageRuntimeFactory}. */
203
228
  export interface IIsolatedPackageRuntimeFactoryOptions {
204
229
  /** Shared per-data-context contract registry from {@link PackageLoader}. */
205
230
  registry: ContractRegistry;
206
231
  /** Owning data-context id (personal user id or group id). */
207
232
  dataContextId: string;
233
+ /** Trusted local identity and role-lookup context for guest-to-host calls. */
234
+ getLocalUserContext?: IsolatedLocalUserContextResolver;
208
235
  /**
209
236
  * Host-owned table capability for this data context. Required when an
210
237
  * isolated package declares tables.
@@ -74,6 +74,10 @@ class PackageLoader {
74
74
  this.isolatedRuntime = factory({
75
75
  registry: this.contractRegistry,
76
76
  dataContextId: dataContext.dataContextId,
77
+ getLocalUserContext: () => {
78
+ const userId = this.dataContext.userContext?.userId;
79
+ return userId ? { userId, dataContext: this.dataContext } : undefined;
80
+ },
77
81
  tableGateway: dataContext.tableContainer
78
82
  ? (0, isolated_table_gateway_1.createIsolatedPackageTableGateway)(dataContext)
79
83
  : undefined,
@@ -12,6 +12,7 @@ jest.mock("../data/package-versions", () => ({
12
12
  }));
13
13
  const contract_resolvers_1 = require("../contracts/contract-resolvers");
14
14
  const registry_1 = require("../contracts/registry");
15
+ const access_level_1 = require("../data/access-level");
15
16
  const system_ids_1 = require("../system-ids");
16
17
  const utils_1 = require("../utils");
17
18
  const isolated_artifact_1 = require("./isolated-artifact");
@@ -142,6 +143,46 @@ describe("PackageLoader.contractRegistry", () => {
142
143
  expect(resolver).toHaveBeenCalledWith({ dataContext });
143
144
  endpoint.dispose();
144
145
  });
146
+ it("does not supply local identity to ordinary callers that omit it", async () => {
147
+ const userId = (0, utils_1.newid)();
148
+ const dataContext = {
149
+ dataContextId: userId,
150
+ userContext: { userId, deviceId: () => "local-device" },
151
+ };
152
+ const loader = new package_loader_1.PackageLoader(dataContext);
153
+ const contractId = (0, utils_1.newid)();
154
+ const write = jest.fn();
155
+ (0, contract_resolvers_1.registerContractResolver)(contractId, 1, () => ({
156
+ contractId,
157
+ version: 1,
158
+ tools: {
159
+ invoke: {
160
+ tool: { toolId: (0, utils_1.newid)(), name: "invoke", accessLevel: access_level_1.AccessLevel.Writer },
161
+ toolFn: write,
162
+ },
163
+ },
164
+ }));
165
+ const endpoint = await loader.resolveContractEndpoint({
166
+ contractId,
167
+ version: 1,
168
+ context: {},
169
+ notify: async () => undefined,
170
+ });
171
+ try {
172
+ await expect(endpoint.handleCall({
173
+ contractId,
174
+ version: 1,
175
+ member: "tool",
176
+ name: "invoke",
177
+ operation: "run",
178
+ args: [],
179
+ })).rejects.toThrow("missing caller identity for tool 'invoke'");
180
+ expect(write).not.toHaveBeenCalled();
181
+ }
182
+ finally {
183
+ endpoint.dispose();
184
+ }
185
+ });
145
186
  });
146
187
  const mockGetFileContents = jest.fn();
147
188
  jest.mock("../data/files", () => ({
@@ -364,8 +405,8 @@ describe("PackageLoader isolated artifacts", () => {
364
405
  await expect(loader.loadPackage(pkg)).rejects.toThrow(/JSON is invalid/);
365
406
  expect(globalThis.__shouldNotEvalOnHost).toBeUndefined();
366
407
  });
367
- it("passes table and package-pvar gateways from a complete data context", () => {
368
- const factory = jest.fn(() => ({
408
+ it("passes gateways and a trusted local identity resolver from its data context", () => {
409
+ const factory = jest.fn((_options) => ({
369
410
  install: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),
370
411
  replace: jest.fn(async () => ({ packageId, provided: [], consumes: [] })),
371
412
  uninstall: jest.fn(async () => undefined),
@@ -381,11 +422,13 @@ describe("PackageLoader isolated artifacts", () => {
381
422
  tableContainer,
382
423
  };
383
424
  dataContext.userContext = {
425
+ userId: (0, utils_1.newid)(),
384
426
  userDataContext: dataContext,
385
427
  };
386
428
  new package_loader_1.PackageLoader(dataContext);
387
429
  expect(factory).toHaveBeenCalledWith(expect.objectContaining({
388
430
  dataContextId: "ctx-tables",
431
+ getLocalUserContext: expect.any(Function),
389
432
  providerGateway: expect.objectContaining({
390
433
  resolveEndpoint: expect.any(Function),
391
434
  }),
@@ -399,6 +442,11 @@ describe("PackageLoader isolated artifacts", () => {
399
442
  save: expect.any(Function),
400
443
  }),
401
444
  }));
445
+ const resolveLocalUser = factory.mock.calls[0][0].getLocalUserContext;
446
+ expect(resolveLocalUser?.()).toEqual({ userId: dataContext.userContext.userId, dataContext });
447
+ expect(resolveLocalUser?.()?.dataContext).toBe(dataContext);
448
+ dataContext.userContext = undefined;
449
+ expect(resolveLocalUser?.()).toBeUndefined();
402
450
  });
403
451
  it("unloads an isolated package on force reload and disable", async () => {
404
452
  const runtime = makeRuntime();
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Host-side record of an isolated package's declared outbound HTTP policy.
3
+ *
4
+ * Populated when the isolated runtime installs a manifest and read by System HTTP.
5
+ * Secrets are not stored here; {@link IPackageNetworkCredential.secretVar} is a
6
+ * logical name the host resolves at request time.
7
+ */
8
+ /** How System HTTP injects one secret the guest never receives. */
9
+ export interface IPackageNetworkCredential {
10
+ /** Name the guest passes as `credentialRef`. */
11
+ ref: string;
12
+ /** Logical persistent-variable name. The host prefixes it with the package id. */
13
+ secretVar: string;
14
+ /** Where the secret persistent variable lives. Defaults to `user`. */
15
+ scope?: "user" | "device";
16
+ /** Request header the host sets from the secret. */
17
+ header: string;
18
+ /** Header value template. Must contain the literal `{secret}`. */
19
+ format: string;
20
+ }
21
+ /** Outbound HTTP permission recorded for one installed package in one data context. */
22
+ export interface IPackageNetworkPolicy {
23
+ /** Exact hostnames the package may request. `https` only. */
24
+ allowedHosts: string[];
25
+ /** Secrets the host may attach. The guest names a `ref`, never the value. */
26
+ credentials: IPackageNetworkCredential[];
27
+ }
28
+ /**
29
+ * Record or clear the network policy for one package in one data context.
30
+ *
31
+ * Pass `undefined` when the package is uninstalled or declares no network block.
32
+ * The policy is process memory; install republishes it.
33
+ *
34
+ * @param dataContextId - Personal or group context that installed the package.
35
+ * @param packageId - Trusted package id from the manifest, not from a guest payload.
36
+ * @param policy - Declared allowlist and credential refs, or `undefined` to forget it.
37
+ */
38
+ export declare function setPackageNetworkPolicy(dataContextId: string, packageId: string, policy: IPackageNetworkPolicy | undefined): void;
39
+ /**
40
+ * Return the network policy installed for this package, if it declared one.
41
+ *
42
+ * @param dataContextId - Context whose runtime installed the package.
43
+ * @param packageId - Package that is calling System HTTP.
44
+ * @returns A copy of the policy, or `undefined` when HTTP is not declared.
45
+ */
46
+ export declare function getPackageNetworkPolicy(dataContextId: string, packageId: string): IPackageNetworkPolicy | undefined;
47
+ /**
48
+ * Drop every recorded network policy.
49
+ *
50
+ * @internal Test-only. Production install and uninstall update one package at a time.
51
+ */
52
+ export declare function _clearPackageNetworkPoliciesForTests(): void;