@nylorun/runtime 0.9.0-beta → 0.10.0-beta

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 (129) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +96 -25
  3. package/dist/adapters/media.d.ts +2 -2
  4. package/dist/adapters/media.js +2 -2
  5. package/dist/adapters/observe.d.ts +2 -1
  6. package/dist/adapters/observe.js +2 -3
  7. package/dist/assets.d.ts +2 -2
  8. package/dist/assets.js +5 -3
  9. package/dist/core/executors.d.ts +11 -2
  10. package/dist/core/executors.js +20 -4
  11. package/dist/core/flow-host.d.ts +264 -0
  12. package/dist/core/flow-host.js +783 -0
  13. package/dist/core/limits.d.ts +40 -0
  14. package/dist/core/limits.js +76 -0
  15. package/dist/core/runtime.d.ts +5 -90
  16. package/dist/core/runtime.js +3 -1447
  17. package/dist/core/sandbox-routes.d.ts +35 -0
  18. package/dist/core/sandbox-routes.js +106 -0
  19. package/dist/core/store.d.ts +3 -1
  20. package/dist/core/store.js +14 -73
  21. package/dist/core/turn-manifest.d.ts +50 -0
  22. package/dist/core/turn-manifest.js +66 -0
  23. package/dist/host/config-for.d.ts +16 -0
  24. package/dist/host/config-for.js +45 -0
  25. package/dist/host/config.d.ts +24 -0
  26. package/dist/host/create-host.d.ts +21 -0
  27. package/dist/host/create-host.js +396 -0
  28. package/dist/host/environment.d.ts +13 -0
  29. package/dist/host/environment.js +53 -0
  30. package/dist/host/http.d.ts +35 -0
  31. package/dist/host/http.js +136 -0
  32. package/dist/host/index.d.ts +6 -0
  33. package/dist/host/index.js +5 -0
  34. package/dist/host/logger.d.ts +3 -0
  35. package/dist/host/logger.js +18 -0
  36. package/dist/host/main.d.ts +3 -0
  37. package/dist/host/main.js +142 -0
  38. package/dist/index.d.ts +7 -2
  39. package/dist/index.js +4 -2
  40. package/dist/launcher/commands.d.ts +24 -0
  41. package/dist/launcher/commands.js +260 -0
  42. package/dist/launcher/ensure-config.d.ts +16 -0
  43. package/dist/launcher/ensure-config.js +79 -0
  44. package/dist/launcher/errors.d.ts +12 -0
  45. package/dist/launcher/errors.js +22 -0
  46. package/dist/launcher/host-config.d.ts +17 -0
  47. package/dist/launcher/host-config.js +64 -0
  48. package/dist/launcher/host-state.d.ts +17 -0
  49. package/dist/launcher/host-state.js +61 -0
  50. package/dist/launcher/lifecycle.d.ts +66 -0
  51. package/dist/launcher/lifecycle.js +407 -0
  52. package/dist/launcher/locks.d.ts +26 -0
  53. package/dist/launcher/locks.js +117 -0
  54. package/dist/launcher/logs.d.ts +18 -0
  55. package/dist/launcher/logs.js +117 -0
  56. package/dist/launcher/main.d.ts +2 -0
  57. package/dist/launcher/main.js +66 -0
  58. package/dist/launcher/output.d.ts +11 -0
  59. package/dist/launcher/output.js +68 -0
  60. package/dist/launcher/paths.d.ts +20 -0
  61. package/dist/launcher/paths.js +36 -0
  62. package/dist/launcher/probe.d.ts +23 -0
  63. package/dist/launcher/probe.js +72 -0
  64. package/dist/launcher/protocol.d.ts +62 -0
  65. package/dist/launcher/protocol.js +1 -0
  66. package/dist/launcher/schema-guard.d.ts +6 -0
  67. package/dist/launcher/schema-guard.js +36 -0
  68. package/dist/launcher/spawn.d.ts +22 -0
  69. package/dist/launcher/spawn.js +85 -0
  70. package/dist/launcher/status.d.ts +6 -0
  71. package/dist/launcher/status.js +65 -0
  72. package/dist/mcp/connect.d.ts +1 -0
  73. package/dist/mcp/connect.js +2 -0
  74. package/dist/mcp/pool.d.ts +3 -1
  75. package/dist/mcp/pool.js +2 -1
  76. package/dist/mcp/stdio.d.ts +2 -0
  77. package/dist/mcp/stdio.js +3 -1
  78. package/dist/model/auth-store.d.ts +1 -1
  79. package/dist/model/auth-store.js +2 -2
  80. package/dist/model/models.d.ts +4 -0
  81. package/dist/model/models.js +22 -26
  82. package/dist/model/pi-model.js +1 -1
  83. package/dist/model/settings.d.ts +5 -1
  84. package/dist/model/settings.js +6 -2
  85. package/dist/sandbox/select.d.ts +4 -3
  86. package/dist/sandbox/select.js +15 -9
  87. package/dist/sandbox/share.d.ts +23 -0
  88. package/dist/sandbox/share.js +58 -0
  89. package/dist/tenant/envelope.d.ts +10 -0
  90. package/dist/tenant/envelope.js +54 -0
  91. package/dist/tenant/ephemeral.d.ts +31 -0
  92. package/dist/tenant/ephemeral.js +156 -0
  93. package/dist/tenant/logger.d.ts +9 -0
  94. package/dist/tenant/logger.js +25 -0
  95. package/dist/tenant/migration.d.ts +16 -0
  96. package/dist/tenant/migration.js +116 -0
  97. package/dist/tenant/module.d.ts +17 -0
  98. package/dist/tenant/module.js +315 -0
  99. package/dist/tenant/paths.d.ts +24 -0
  100. package/dist/tenant/paths.js +65 -0
  101. package/dist/tenant/pool.d.ts +10 -0
  102. package/dist/tenant/pool.js +54 -0
  103. package/dist/tenant/principals.d.ts +17 -0
  104. package/dist/tenant/principals.js +44 -0
  105. package/dist/tenant/quarantine-error.d.ts +6 -0
  106. package/dist/tenant/quarantine-error.js +15 -0
  107. package/dist/tenant/quarantine.d.ts +35 -0
  108. package/dist/tenant/quarantine.js +94 -0
  109. package/dist/tenant/reset.d.ts +21 -0
  110. package/dist/tenant/reset.js +125 -0
  111. package/dist/tenant/runtime.d.ts +80 -0
  112. package/dist/tenant/runtime.js +2256 -0
  113. package/dist/tenant/schema.d.ts +23 -0
  114. package/dist/tenant/schema.js +151 -0
  115. package/dist/tenant/status.d.ts +28 -0
  116. package/dist/tenant/status.js +95 -0
  117. package/dist/tenant/store-fs.d.ts +16 -0
  118. package/dist/tenant/store-fs.js +205 -0
  119. package/dist/tenant/store-memory.d.ts +19 -0
  120. package/dist/tenant/store-memory.js +100 -0
  121. package/dist/tenant/types.d.ts +125 -0
  122. package/dist/tenant/types.js +1 -0
  123. package/dist/vault/kek.d.ts +5 -2
  124. package/dist/vault/kek.js +7 -10
  125. package/dist/version.d.ts +1 -1
  126. package/dist/version.js +1 -1
  127. package/package.json +9 -6
  128. package/dist/core/main.js +0 -35
  129. /package/dist/{core/main.d.ts → host/config.js} +0 -0
@@ -0,0 +1,65 @@
1
+ import { LAUNCHER_PROTOCOL } from "@nylorun/core/compatibility";
2
+ import { readHostConfig } from "./host-config.js";
3
+ import { readHostState } from "./host-state.js";
4
+ import { processAlive } from "./locks.js";
5
+ import { hostUrl } from "./paths.js";
6
+ import { portBindable, probeHostHealth } from "./probe.js";
7
+ /**
8
+ * Status with no lock and no mutation (D§9.3).
9
+ */
10
+ export async function status(paths, launcherVersion) {
11
+ const config = await readHostConfig(paths);
12
+ const stateFile = await readHostState(paths);
13
+ const runtimeVersion = config && typeof config.runtimeVersion === "string"
14
+ ? config.runtimeVersion
15
+ : undefined;
16
+ const base = {
17
+ launcherProtocol: LAUNCHER_PROTOCOL,
18
+ home: paths.root,
19
+ state: "absent",
20
+ launcherVersion,
21
+ ...(runtimeVersion ? { runtimeVersion } : {}),
22
+ };
23
+ if (!config) {
24
+ return base;
25
+ }
26
+ const url = hostUrl(config);
27
+ const withConfig = {
28
+ ...base,
29
+ state: "stopped",
30
+ url,
31
+ hostId: config.hostId,
32
+ ...(stateFile?.pid ? { pid: stateFile.pid } : {}),
33
+ ...(stateFile?.version ? { version: stateFile.version } : {}),
34
+ };
35
+ const probe = await probeHostHealth(url);
36
+ const ours = probe.health !== undefined &&
37
+ (probe.health.hostId === undefined ||
38
+ probe.health.hostId === config.hostId);
39
+ if (probe.foreign) {
40
+ return { ...withConfig, state: "foreign-port" };
41
+ }
42
+ if (probe.health && ours) {
43
+ return {
44
+ ...withConfig,
45
+ state: "running",
46
+ ...(probe.health.version ? { version: probe.health.version } : {}),
47
+ ...(probe.health.hostId ? { hostId: probe.health.hostId } : {}),
48
+ ...(probe.health.pid ? { pid: probe.health.pid } : {}),
49
+ };
50
+ }
51
+ if (probe.health && !ours) {
52
+ return {
53
+ ...withConfig,
54
+ state: "foreign-port",
55
+ ...(probe.health.hostId ? { hostId: probe.health.hostId } : {}),
56
+ };
57
+ }
58
+ if (stateFile && processAlive(stateFile.pid)) {
59
+ return { ...withConfig, state: "unresponsive", pid: stateFile.pid };
60
+ }
61
+ if (!(await portBindable(config.host, config.port))) {
62
+ return { ...withConfig, state: "foreign-port" };
63
+ }
64
+ return withConfig;
65
+ }
@@ -11,6 +11,7 @@ export declare function openMcpServer(input: {
11
11
  authorize?: (url: string) => Promise<AuthorizeResult>;
12
12
  pluginRoot?: string;
13
13
  pluginData: string;
14
+ childEnv?: Readonly<Record<string, string>>;
14
15
  }): Promise<LiveConnection>;
15
16
  export declare function listMcpTools(client: Client, input: {
16
17
  capabilityId: string;
@@ -10,11 +10,13 @@ export async function openMcpServer(input) {
10
10
  const launch = prepareStdioLaunch(input.server, {
11
11
  pluginRoot: input.pluginRoot,
12
12
  pluginData: input.pluginData,
13
+ childEnv: input.childEnv,
13
14
  });
14
15
  const transport = new StdioClientTransport({
15
16
  command: launch.command,
16
17
  args: [...launch.args],
17
18
  cwd: launch.cwd,
19
+ // Full explicit env so HOME/TMPDIR from the Tenant override getDefaultEnvironment().
18
20
  env: { ...launch.env },
19
21
  stderr: "pipe",
20
22
  });
@@ -6,7 +6,9 @@ export declare class McpPool {
6
6
  private readonly options;
7
7
  private readonly live;
8
8
  constructor(options: {
9
- readonly dataDir: string;
9
+ /** Absolute Tenant `plugin-data` directory. */
10
+ readonly pluginData: string;
11
+ readonly childEnv: Readonly<Record<string, string>>;
10
12
  readonly authorize: (sessionId: string, request: {
11
13
  url: string;
12
14
  serverName: string;
package/dist/mcp/pool.js CHANGED
@@ -122,7 +122,8 @@ export class McpPool {
122
122
  const connection = await openMcpServer({
123
123
  server: declared.server,
124
124
  pluginRoot: input.pluginRoots[pluginKey(declared)],
125
- pluginData: join(this.options.dataDir, "plugin-data", ...pluginKey(declared).split("/")),
125
+ pluginData: join(this.options.pluginData, ...pluginKey(declared).split("/")),
126
+ childEnv: this.options.childEnv,
126
127
  authorize: declared.server.type === "stdio"
127
128
  ? undefined
128
129
  : (url) => this.options.authorize(input.sessionId, {
@@ -12,4 +12,6 @@ export declare function prepareStdioLaunch(server: Extract<McpServerManifest, {
12
12
  }>, options: {
13
13
  readonly pluginRoot?: string;
14
14
  readonly pluginData: string;
15
+ /** Allowlisted base env (Tenant HOME/TMPDIR/PATH). Manifest env overlays it (A9). */
16
+ readonly childEnv?: Readonly<Record<string, string>>;
15
17
  }): StdioLaunch;
package/dist/mcp/stdio.js CHANGED
@@ -38,7 +38,9 @@ export function prepareStdioLaunch(server, options) {
38
38
  const cwd = server.cwd
39
39
  ? resolveContainedCwd(server.cwd, pluginRoot, pluginData)
40
40
  : (pluginRoot ?? pluginData);
41
- const env = {};
41
+ // childEnv under manifest env; HOME/TMPDIR from childEnv override Host defaults
42
+ // that StdioClientTransport merges via getDefaultEnvironment() (R2).
43
+ const env = { ...(options.childEnv ?? {}) };
42
44
  for (const [key, value] of Object.entries(server.env ?? {}))
43
45
  env[key] = expand(value, pluginRoot, pluginData);
44
46
  if (pluginRoot)
@@ -3,7 +3,7 @@ export declare class ProjectCredentialStore implements CredentialStore {
3
3
  #private;
4
4
  private readonly file;
5
5
  private readonly legacyFile?;
6
- constructor(file?: string, legacyFile?: string | undefined);
6
+ constructor(file: string, legacyFile?: string | undefined);
7
7
  read(providerId: string): Promise<Credential | undefined>;
8
8
  list(): Promise<readonly CredentialInfo[]>;
9
9
  modify(providerId: string, fn: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
@@ -1,11 +1,11 @@
1
1
  var _a;
2
2
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
3
+ import { dirname } from "node:path";
4
4
  export class ProjectCredentialStore {
5
5
  file;
6
6
  legacyFile;
7
7
  #chain = Promise.resolve();
8
- constructor(file = join(process.cwd(), ".nylorun", "auth.json"), legacyFile) {
8
+ constructor(file, legacyFile) {
9
9
  this.file = file;
10
10
  this.legacyFile = legacyFile;
11
11
  }
@@ -6,6 +6,10 @@ export type Selection = Readonly<{
6
6
  baseUrl: string;
7
7
  }>;
8
8
  }>;
9
+ /**
10
+ * Build the pi-ai model registry from explicit credentials only.
11
+ * Ambient process environment / provider env auth is never consulted (A7, A10).
12
+ */
9
13
  export declare function modelsFor(selection: Selection, credentials: CredentialStore, options?: {
10
14
  environment?: boolean;
11
15
  }): import("@earendil-works/pi-ai").MutableModels;
@@ -1,31 +1,15 @@
1
- import { createProvider, defaultProviderAuthContext, envApiKeyAuth, } from "@earendil-works/pi-ai";
1
+ import { createProvider, } from "@earendil-works/pi-ai";
2
2
  import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
3
3
  import { builtinModels } from "@earendil-works/pi-ai/providers/all";
4
+ /**
5
+ * Build the pi-ai model registry from explicit credentials only.
6
+ * Ambient process environment / provider env auth is never consulted (A7, A10).
7
+ */
4
8
  export function modelsFor(selection, credentials, options = {}) {
5
- const environmentFirst = {
6
- async read(providerId, options) {
7
- const explicit = process.env.MODEL_PROVIDER_API_KEY;
8
- if (explicit &&
9
- (!selection.provider || selection.provider === providerId))
10
- return { type: "api_key", key: explicit };
11
- const provider = models
12
- .getProviders()
13
- .find((item) => item.id === providerId);
14
- const ambient = await provider?.auth.apiKey?.resolve({
15
- ctx: defaultProviderAuthContext(),
16
- signal: options?.signal ?? new AbortController().signal,
17
- });
18
- if (ambient)
19
- return undefined;
20
- return credentials.read(providerId, options);
21
- },
22
- list: (options) => credentials.list(options),
23
- modify: (id, fn, options) => credentials.modify(id, fn, options),
24
- delete: (id, options) => credentials.delete(id, options),
25
- };
26
- const models = builtinModels({
27
- credentials: options.environment === false ? credentials : environmentFirst,
28
- });
9
+ if (options.environment) {
10
+ throw new Error("Ambient model environment is not supported; pass credentials explicitly");
11
+ }
12
+ const models = builtinModels({ credentials });
29
13
  if (!selection.custom)
30
14
  return models;
31
15
  const model = {
@@ -45,7 +29,19 @@ export function modelsFor(selection, credentials, options = {}) {
45
29
  name: "Custom OpenAI-compatible",
46
30
  baseUrl: selection.custom.baseUrl,
47
31
  auth: {
48
- apiKey: envApiKeyAuth("Custom API key", ["MODEL_PROVIDER_API_KEY"]),
32
+ apiKey: {
33
+ label: "Custom API key",
34
+ schemas: [],
35
+ async resolve() {
36
+ const credential = await credentials.read("custom");
37
+ if (!credential || credential.type !== "api_key")
38
+ return undefined;
39
+ const key = credential.key;
40
+ if (!key)
41
+ return undefined;
42
+ return { type: "api_key", auth: { apiKey: key } };
43
+ },
44
+ },
49
45
  },
50
46
  models: [model],
51
47
  api: { stream, streamSimple },
@@ -13,7 +13,7 @@ const emptyUsage = () => ({
13
13
  export function piModel(options = {}) {
14
14
  return async (call, context) => {
15
15
  context.signal.throwIfAborted();
16
- const root = options.root ?? process.cwd();
16
+ const root = options.root ?? "";
17
17
  const stored = options.readHostModel?.();
18
18
  if (!stored)
19
19
  throw new Error("Model provider is not configured. Start nylorun dev in a terminal, or set it in Studio.");
@@ -1 +1,5 @@
1
- export declare function projectSecrets(root?: string): readonly string[];
1
+ /**
2
+ * Collect likely secret strings from an explicit env map and optional auth.json
3
+ * under `root`. Callers must pass values; no ambient environment or cwd (A7).
4
+ */
5
+ export declare function projectSecrets(root: string, env?: Readonly<Record<string, string | undefined>>): readonly string[];
@@ -1,7 +1,11 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- export function projectSecrets(root = process.cwd()) {
4
- const values = Object.entries(process.env)
3
+ /**
4
+ * Collect likely secret strings from an explicit env map and optional auth.json
5
+ * under `root`. Callers must pass values; no ambient environment or cwd (A7).
6
+ */
7
+ export function projectSecrets(root, env = {}) {
8
+ const values = Object.entries(env)
5
9
  .filter(([key]) => /key|token|secret|password|credential/i.test(key))
6
10
  .flatMap(([, value]) => (value ? [value] : []));
7
11
  for (const directory of [".nylorun", ".env"])
@@ -7,7 +7,7 @@ export interface SandboxSelection {
7
7
  readonly reason: string;
8
8
  readonly probes: readonly SandboxProbe[];
9
9
  }
10
- /** Serializable view of a selection for the CLI and `GET /v1/host/sandbox`. */
10
+ /** Serializable view of a selection for the CLI and `GET /v1/tenant/sandbox`. */
11
11
  export interface SandboxSelectionReport {
12
12
  readonly preference: SandboxPreference;
13
13
  readonly backend: SandboxBackendName | null;
@@ -27,9 +27,10 @@ export declare function parseSandboxPreference(value: string | undefined): Sandb
27
27
  export declare function selectSandboxBackend(backends: readonly SandboxBackend[], preference: SandboxPreference): Promise<SandboxSelection>;
28
28
  export declare function reportSelection(selection: SandboxSelection): SandboxSelectionReport;
29
29
  /**
30
- * Probe this machine the way a Runtime would at startup. Used by `nylorun doctor sandbox`.
31
- * Nothing is created or booted.
30
+ * Probe backends the way a Tenant Runtime would at open.
31
+ * Callers must pass an explicit probe root; no ambient env or OS temp (A7).
32
32
  */
33
33
  export declare function probeSandboxBackends(options?: {
34
34
  readonly preference?: string;
35
+ readonly root?: string;
35
36
  }): Promise<SandboxSelectionReport>;
@@ -1,5 +1,3 @@
1
- import { tmpdir } from "node:os";
2
- import { join } from "node:path";
3
1
  import { microsandboxBackend } from "../adapters/sandbox/microsandbox.js";
4
2
  import { virtualBackend } from "../adapters/sandbox/virtual.js";
5
3
  const PREFERENCES = ["auto", "microsandbox", "virtual"];
@@ -26,10 +24,15 @@ export async function selectSandboxBackend(backends, preference) {
26
24
  const backend = backends.find((item) => item.name === preference);
27
25
  const probe = probeOf(preference);
28
26
  if (backend && probe?.available)
29
- return { preference, backend, reason: `${preference} selected by NYLORUN_SANDBOX`, probes };
27
+ return {
28
+ preference,
29
+ backend,
30
+ reason: `${preference} selected by TenantConfig.sandbox.backend`,
31
+ probes,
32
+ };
30
33
  return {
31
34
  preference,
32
- reason: `NYLORUN_SANDBOX=${preference} but ${preference} is unavailable: ${probe?.reason ?? "unknown backend"}. Fix it, or unset NYLORUN_SANDBOX to use the best available backend`,
35
+ reason: `sandbox.backend=${preference} but ${preference} is unavailable: ${probe?.reason ?? "unknown backend"}. Fix it, or set backend to auto to use the best available backend`,
33
36
  probes,
34
37
  };
35
38
  }
@@ -58,18 +61,21 @@ export function reportSelection(selection) {
58
61
  };
59
62
  }
60
63
  /**
61
- * Probe this machine the way a Runtime would at startup. Used by `nylorun doctor sandbox`.
62
- * Nothing is created or booted.
64
+ * Probe backends the way a Tenant Runtime would at open.
65
+ * Callers must pass an explicit probe root; no ambient env or OS temp (A7).
63
66
  */
64
67
  export async function probeSandboxBackends(options = {}) {
65
- const preference = parseSandboxPreference(options.preference ?? process.env.NYLORUN_SANDBOX);
66
- const backends = defaultSandboxBackends({ root: join(tmpdir(), "nylorun-sandbox-probe") });
68
+ if (!options.root) {
69
+ throw new Error("probeSandboxBackends requires an explicit root (Tenant or Host tmp path)");
70
+ }
71
+ const preference = parseSandboxPreference(options.preference ?? "auto");
72
+ const backends = defaultSandboxBackends({ root: options.root });
67
73
  if (!preference)
68
74
  return {
69
75
  preference: "auto",
70
76
  backend: null,
71
77
  isolation: null,
72
- reason: "NYLORUN_SANDBOX must be auto, microsandbox or virtual",
78
+ reason: "sandbox.backend must be auto, microsandbox or virtual",
73
79
  probes: await Promise.all(backends.map((backend) => backend.probe())),
74
80
  };
75
81
  return reportSelection(await selectSandboxBackend(backends, preference));
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared workflow sandbox: one sandbox per owning session, attached via
3
+ * PutSession.sandbox = { session }. Specs must match; keys use the owner id.
4
+ */
5
+ import { type AgentManifest, type CapabilityManifest, type SandboxManifest, type WorkflowManifest } from "@nylorun/core/define";
6
+ export type SessionSandboxRef = {
7
+ readonly id: string;
8
+ readonly ownerUserId: string;
9
+ readonly manifest: AgentManifest | WorkflowManifest | Record<string, unknown>;
10
+ /** Session whose id keys the sandbox. Absent means this session owns its own. */
11
+ readonly sandboxOwnerId?: string | null;
12
+ };
13
+ /** Sandbox spec declared on an agent capability or a workflow manifest. */
14
+ export declare function sandboxSpecOf(manifest: AgentManifest | WorkflowManifest | Record<string, unknown> | undefined): SandboxManifest | undefined;
15
+ export declare function sandboxSpecsEqual(a: SandboxManifest | undefined, b: SandboxManifest | undefined): boolean;
16
+ /** Synthetic capability so SandboxManager.run can use a workflow (or shared) spec. */
17
+ export declare function capabilityForSandbox(sandbox: SandboxManifest): CapabilityManifest;
18
+ /**
19
+ * Resolve the session id that keys the sandbox. Walks one hop via sandboxOwnerId;
20
+ * cycles fall back to the starting session.
21
+ */
22
+ export declare function owningSandboxSessionId(session: SessionSandboxRef, lookup: (id: string) => SessionSandboxRef | undefined): string;
23
+ export declare function sessionHasSandbox(session: SessionSandboxRef, lookup: (id: string) => SessionSandboxRef | undefined): boolean;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared workflow sandbox: one sandbox per owning session, attached via
3
+ * PutSession.sandbox = { session }. Specs must match; keys use the owner id.
4
+ */
5
+ import { canonical, } from "@nylorun/core/define";
6
+ /** Sandbox spec declared on an agent capability or a workflow manifest. */
7
+ export function sandboxSpecOf(manifest) {
8
+ if (!manifest || typeof manifest !== "object")
9
+ return undefined;
10
+ if (manifest.kind === "workflow")
11
+ return manifest.sandbox;
12
+ const capabilities = manifest.capabilities;
13
+ if (!Array.isArray(capabilities))
14
+ return undefined;
15
+ for (const capability of capabilities)
16
+ if (capability.sandbox)
17
+ return capability.sandbox;
18
+ return undefined;
19
+ }
20
+ export function sandboxSpecsEqual(a, b) {
21
+ if (a === undefined && b === undefined)
22
+ return true;
23
+ if (a === undefined || b === undefined)
24
+ return false;
25
+ return canonical(a) === canonical(b);
26
+ }
27
+ /** Synthetic capability so SandboxManager.run can use a workflow (or shared) spec. */
28
+ export function capabilityForSandbox(sandbox) {
29
+ return {
30
+ id: "sandbox",
31
+ type: "agent",
32
+ sandbox,
33
+ tools: [],
34
+ };
35
+ }
36
+ /**
37
+ * Resolve the session id that keys the sandbox. Walks one hop via sandboxOwnerId;
38
+ * cycles fall back to the starting session.
39
+ */
40
+ export function owningSandboxSessionId(session, lookup) {
41
+ const seen = new Set();
42
+ let current = session;
43
+ while (current) {
44
+ if (seen.has(current.id))
45
+ return session.id;
46
+ seen.add(current.id);
47
+ const ownerId = current.sandboxOwnerId;
48
+ if (!ownerId || ownerId === current.id)
49
+ return current.id;
50
+ current = lookup(ownerId);
51
+ }
52
+ return session.id;
53
+ }
54
+ export function sessionHasSandbox(session, lookup) {
55
+ const ownerId = owningSandboxSessionId(session, lookup);
56
+ const owner = ownerId === session.id ? session : lookup(ownerId) ?? session;
57
+ return sandboxSpecOf(owner.manifest) !== undefined || sandboxSpecOf(session.manifest) !== undefined;
58
+ }
@@ -0,0 +1,10 @@
1
+ import { type TenantEnvelope } from "@nylorun/core/contracts";
2
+ export declare function parseEnvelope(raw: unknown, expectedId?: string): TenantEnvelope;
3
+ export declare function readEnvelopeFile(path: string, expectedId?: string): TenantEnvelope;
4
+ /** Write envelope via temp sibling then rename (atomic on the same filesystem). */
5
+ export declare function writeEnvelopeFile(envelopePath: string, envelope: TenantEnvelope): void;
6
+ export declare function envelopeNow(input: {
7
+ id: string;
8
+ name: string;
9
+ schemaVersion: number;
10
+ }, now?: Date): TenantEnvelope;
@@ -0,0 +1,54 @@
1
+ import { readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
4
+ import { quarantine } from "./quarantine.js";
5
+ export function parseEnvelope(raw, expectedId) {
6
+ const parsed = TenantEnvelopeSchema.safeParse(raw);
7
+ if (!parsed.success) {
8
+ throw quarantine("envelope-invalid", "tenant.json is missing or not a valid Tenant envelope", expectedId ? { tenantId: expectedId } : {});
9
+ }
10
+ if (expectedId !== undefined && parsed.data.id !== expectedId) {
11
+ throw quarantine("envelope-invalid", `tenant.json id ${parsed.data.id} does not match directory ${expectedId}`, { tenantId: expectedId });
12
+ }
13
+ return parsed.data;
14
+ }
15
+ export function readEnvelopeFile(path, expectedId) {
16
+ let text;
17
+ try {
18
+ text = readFileSync(path, "utf8");
19
+ }
20
+ catch (error) {
21
+ const code = error.code;
22
+ if (code === "ENOENT") {
23
+ throw quarantine("envelope-invalid", "tenant.json is missing", expectedId ? { tenantId: expectedId } : {});
24
+ }
25
+ throw quarantine("corrupt", "tenant.json could not be read", expectedId ? { tenantId: expectedId } : {});
26
+ }
27
+ let json;
28
+ try {
29
+ json = JSON.parse(text);
30
+ }
31
+ catch {
32
+ throw quarantine("envelope-invalid", "tenant.json is not valid JSON", expectedId ? { tenantId: expectedId } : {});
33
+ }
34
+ return parseEnvelope(json, expectedId);
35
+ }
36
+ /** Write envelope via temp sibling then rename (atomic on the same filesystem). */
37
+ export function writeEnvelopeFile(envelopePath, envelope) {
38
+ const validated = parseEnvelope(envelope, envelope.id);
39
+ const tmp = join(envelopePath + `.tmp-${process.pid}-${Date.now()}`);
40
+ writeFileSync(tmp, `${JSON.stringify(validated, null, 2)}\n`, {
41
+ mode: 0o600,
42
+ });
43
+ renameSync(tmp, envelopePath);
44
+ }
45
+ export function envelopeNow(input, now = new Date()) {
46
+ const iso = now.toISOString();
47
+ return {
48
+ id: input.id,
49
+ name: input.name,
50
+ createdAt: iso,
51
+ updatedAt: iso,
52
+ schemaVersion: input.schemaVersion,
53
+ };
54
+ }
@@ -0,0 +1,31 @@
1
+ import type { Logger, TenantModelConfig } from "./types.js";
2
+ export interface StartEphemeralRuntimeOptions {
3
+ /** Absolute Host root. Caller creates any temporary directory. */
4
+ hostRoot: string;
5
+ tenantId?: string;
6
+ name?: string;
7
+ applicationKey?: string;
8
+ adminKey?: string;
9
+ principalId?: string;
10
+ /** Allowlisted baseline for childEnv (e.g. PATH). Never read from ambient here. */
11
+ baseline?: Readonly<Record<string, string>>;
12
+ model?: TenantModelConfig;
13
+ sandboxBackend?: "auto" | "microsandbox" | "virtual";
14
+ /** When true, close() leaves hostRoot on disk. */
15
+ retainRoot?: boolean;
16
+ logger?: Logger;
17
+ }
18
+ export interface EphemeralRuntime {
19
+ url: string;
20
+ tenantId: string;
21
+ applicationKey: string;
22
+ adminKey: string;
23
+ principalId: string;
24
+ hostRoot: string;
25
+ close(): Promise<void>;
26
+ }
27
+ /**
28
+ * Private Host on port 0 with one Tenant (D9 / A19).
29
+ * Exported from `@nylorun/runtime/core`.
30
+ */
31
+ export declare function startEphemeralRuntime(options: StartEphemeralRuntimeOptions): Promise<EphemeralRuntime>;