@nylorun/runtime 0.7.0-beta → 0.8.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 (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +7 -5
  3. package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
  4. package/dist/adapters/sandbox/microsandbox.js +238 -0
  5. package/dist/adapters/sandbox/virtual.d.ts +4 -0
  6. package/dist/adapters/sandbox/virtual.js +125 -0
  7. package/dist/configuration.d.ts +1 -1
  8. package/dist/configuration.js +1 -1
  9. package/dist/core/executors.d.ts +32 -0
  10. package/dist/core/executors.js +60 -0
  11. package/dist/core/main.js +7 -12
  12. package/dist/core/runtime.d.ts +24 -1
  13. package/dist/core/runtime.js +322 -30
  14. package/dist/core/store.d.ts +12 -0
  15. package/dist/core/store.js +50 -1
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/model/catalog.d.ts +12 -0
  19. package/dist/model/catalog.js +29 -0
  20. package/dist/model/models.d.ts +3 -1
  21. package/dist/model/models.js +4 -2
  22. package/dist/model/pi-model.d.ts +9 -3
  23. package/dist/model/pi-model.js +47 -11
  24. package/dist/model/settings.d.ts +0 -2
  25. package/dist/model/settings.js +0 -47
  26. package/dist/node/index.d.ts +1 -1
  27. package/dist/node/index.js +1 -1
  28. package/dist/sandbox/manager.d.ts +61 -0
  29. package/dist/sandbox/manager.js +239 -0
  30. package/dist/sandbox/policy.d.ts +13 -0
  31. package/dist/sandbox/policy.js +67 -0
  32. package/dist/sandbox/select.d.ts +35 -0
  33. package/dist/sandbox/select.js +76 -0
  34. package/dist/sandbox/tools.d.ts +25 -0
  35. package/dist/sandbox/tools.js +165 -0
  36. package/dist/sandbox/types.d.ts +66 -0
  37. package/dist/vault/service.d.ts +41 -1
  38. package/dist/vault/service.js +291 -4
  39. package/dist/version.d.ts +1 -0
  40. package/dist/version.js +3 -0
  41. package/package.json +9 -4
  42. package/dist/config.d.ts +0 -20
  43. package/dist/model/defaults.d.ts +0 -17
  44. package/dist/model/defaults.js +0 -21
  45. package/dist/model/http-model.d.ts +0 -12
  46. package/dist/model/http-model.js +0 -299
  47. package/dist/node/local-sessions.d.ts +0 -5
  48. package/dist/node/local-sessions.js +0 -174
  49. package/dist/server/ag-ui.d.ts +0 -8
  50. package/dist/server/ag-ui.js +0 -76
  51. package/dist/server/delivery.d.ts +0 -24
  52. package/dist/server/delivery.js +0 -107
  53. package/dist/server/digests.d.ts +0 -13
  54. package/dist/server/digests.js +0 -39
  55. package/dist/server/host.d.ts +0 -70
  56. package/dist/server/host.js +0 -647
  57. package/dist/session/api.d.ts +0 -10
  58. package/dist/session/api.js +0 -15
  59. package/dist/session/default.d.ts +0 -5
  60. package/dist/session/default.js +0 -30
  61. package/dist/session/handle.d.ts +0 -27
  62. package/dist/session/handle.js +0 -199
  63. package/dist/session/index.d.ts +0 -2
  64. package/dist/session/index.js +0 -2
  65. package/dist/sessions/host.d.ts +0 -39
  66. package/dist/sessions/host.js +0 -359
  67. package/dist/sessions/store.d.ts +0 -41
  68. package/dist/sessions/store.js +0 -33
  69. /package/dist/{config.js → sandbox/types.js} +0 -0
@@ -0,0 +1,35 @@
1
+ import type { SandboxBackend, SandboxBackendName, SandboxIsolation, SandboxProbe } from "./types.js";
2
+ export type SandboxPreference = "auto" | SandboxBackendName;
3
+ export interface SandboxSelection {
4
+ readonly preference: SandboxPreference;
5
+ readonly backend?: SandboxBackend;
6
+ /** Why this backend was chosen, or why none could be. */
7
+ readonly reason: string;
8
+ readonly probes: readonly SandboxProbe[];
9
+ }
10
+ /** Serializable view of a selection for the CLI and `GET /v1/host/sandbox`. */
11
+ export interface SandboxSelectionReport {
12
+ readonly preference: SandboxPreference;
13
+ readonly backend: SandboxBackendName | null;
14
+ readonly isolation: SandboxIsolation | null;
15
+ readonly reason: string;
16
+ readonly probes: readonly SandboxProbe[];
17
+ }
18
+ /** The default order: strongest isolation first, then the backend that always works. */
19
+ export declare function defaultSandboxBackends(options: {
20
+ readonly root: string;
21
+ }): SandboxBackend[];
22
+ export declare function parseSandboxPreference(value: string | undefined): SandboxPreference | undefined;
23
+ /**
24
+ * Probe backends in order and pick one. An explicit preference never falls back, so a
25
+ * production Runtime cannot silently run with weaker isolation than it was configured for.
26
+ */
27
+ export declare function selectSandboxBackend(backends: readonly SandboxBackend[], preference: SandboxPreference): Promise<SandboxSelection>;
28
+ export declare function reportSelection(selection: SandboxSelection): SandboxSelectionReport;
29
+ /**
30
+ * Probe this machine the way a Runtime would at startup. Used by `nylorun doctor sandbox`.
31
+ * Nothing is created or booted.
32
+ */
33
+ export declare function probeSandboxBackends(options?: {
34
+ readonly preference?: string;
35
+ }): Promise<SandboxSelectionReport>;
@@ -0,0 +1,76 @@
1
+ import { tmpdir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { microsandboxBackend } from "../adapters/sandbox/microsandbox.js";
4
+ import { virtualBackend } from "../adapters/sandbox/virtual.js";
5
+ const PREFERENCES = ["auto", "microsandbox", "virtual"];
6
+ /** The default order: strongest isolation first, then the backend that always works. */
7
+ export function defaultSandboxBackends(options) {
8
+ return [microsandboxBackend(), virtualBackend({ root: options.root })];
9
+ }
10
+ export function parseSandboxPreference(value) {
11
+ const normalized = (value ?? "auto").trim().toLowerCase() || "auto";
12
+ return PREFERENCES.includes(normalized)
13
+ ? normalized
14
+ : undefined;
15
+ }
16
+ /**
17
+ * Probe backends in order and pick one. An explicit preference never falls back, so a
18
+ * production Runtime cannot silently run with weaker isolation than it was configured for.
19
+ */
20
+ export async function selectSandboxBackend(backends, preference) {
21
+ const probes = [];
22
+ for (const backend of backends)
23
+ probes.push(await backend.probe());
24
+ const probeOf = (name) => probes.find((probe) => probe.name === name);
25
+ if (preference !== "auto") {
26
+ const backend = backends.find((item) => item.name === preference);
27
+ const probe = probeOf(preference);
28
+ if (backend && probe?.available)
29
+ return { preference, backend, reason: `${preference} selected by NYLORUN_SANDBOX`, probes };
30
+ return {
31
+ 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`,
33
+ probes,
34
+ };
35
+ }
36
+ const index = backends.findIndex((_, i) => probes[i].available);
37
+ if (index < 0)
38
+ return {
39
+ preference,
40
+ reason: `no sandbox backend is available (${probes.map((probe) => `${probe.name}: ${probe.reason}`).join("; ")})`,
41
+ probes,
42
+ };
43
+ const skipped = probes.slice(0, index).map((probe) => `${probe.name} unavailable: ${probe.reason}`);
44
+ return {
45
+ preference,
46
+ backend: backends[index],
47
+ reason: skipped.length ? skipped.join("; ") : probes[index].reason ?? backends[index].name,
48
+ probes,
49
+ };
50
+ }
51
+ export function reportSelection(selection) {
52
+ return {
53
+ preference: selection.preference,
54
+ backend: selection.backend?.name ?? null,
55
+ isolation: selection.backend?.isolation ?? null,
56
+ reason: selection.reason,
57
+ probes: selection.probes,
58
+ };
59
+ }
60
+ /**
61
+ * Probe this machine the way a Runtime would at startup. Used by `nylorun doctor sandbox`.
62
+ * Nothing is created or booted.
63
+ */
64
+ 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") });
67
+ if (!preference)
68
+ return {
69
+ preference: "auto",
70
+ backend: null,
71
+ isolation: null,
72
+ reason: "NYLORUN_SANDBOX must be auto, microsandbox or virtual",
73
+ probes: await Promise.all(backends.map((backend) => backend.probe())),
74
+ };
75
+ return reportSelection(await selectSandboxBackend(backends, preference));
76
+ }
@@ -0,0 +1,25 @@
1
+ import { type SandboxToolName } from "@nylorun/core/define";
2
+ import type { SandboxHandle } from "./types.js";
3
+ export type SandboxToolOutcome = {
4
+ readonly kind: "completed";
5
+ readonly output: unknown;
6
+ } | {
7
+ readonly kind: "failed";
8
+ readonly code: string;
9
+ readonly message: string;
10
+ };
11
+ /** What the Runtime reports about a tool call on the session stream. */
12
+ export interface SandboxToolReport {
13
+ readonly command?: string;
14
+ readonly path?: string;
15
+ readonly exitCode?: number;
16
+ readonly killed?: boolean;
17
+ }
18
+ export declare function quote(value: string): string;
19
+ export declare function resolvePath(path: string): string;
20
+ /** Keep the head and tail of long output so the model sees both the start and the error. */
21
+ export declare function truncateMiddle(value: string, limit?: number): {
22
+ text: string;
23
+ truncated: boolean;
24
+ };
25
+ export declare function runSandboxTool(handle: SandboxHandle, name: SandboxToolName, input: Record<string, any>, signal: AbortSignal, report: (value: SandboxToolReport) => void): Promise<SandboxToolOutcome>;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The six built-in sandbox tools, implemented once over `SandboxHandle` so every backend behaves
3
+ * the same. Expected problems are returned as failed outcomes; only infrastructure errors throw.
4
+ */
5
+ import { posix } from "node:path";
6
+ import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
7
+ const OUTPUT_LIMIT = 30_000;
8
+ const DEFAULT_TIMEOUT_S = 120;
9
+ const DEFAULT_READ_LINES = 2000;
10
+ const LINE_LIMIT = 2000;
11
+ const DEFAULT_GREP_RESULTS = 200;
12
+ const GLOB_LIMIT = 500;
13
+ const GLOB_SCAN_LIMIT = 20_000;
14
+ export function quote(value) {
15
+ return `'${value.replaceAll("'", `'\\''`)}'`;
16
+ }
17
+ export function resolvePath(path) {
18
+ return posix.normalize(path.startsWith("/") ? path : posix.join(SANDBOX_WORKSPACE, path));
19
+ }
20
+ /** Keep the head and tail of long output so the model sees both the start and the error. */
21
+ export function truncateMiddle(value, limit = OUTPUT_LIMIT) {
22
+ if (value.length <= limit)
23
+ return { text: value, truncated: false };
24
+ const half = Math.floor(limit / 2);
25
+ const omitted = value.length - half * 2;
26
+ return {
27
+ text: `${value.slice(0, half)}\n… [${omitted} characters omitted] …\n${value.slice(-half)}`,
28
+ truncated: true,
29
+ };
30
+ }
31
+ const failed = (code, message) => ({
32
+ kind: "failed",
33
+ code,
34
+ message,
35
+ });
36
+ export async function runSandboxTool(handle, name, input, signal, report) {
37
+ const exec = (command, timeoutMs = 60_000) => handle.exec({ command, cwd: SANDBOX_WORKSPACE, timeoutMs }, signal);
38
+ switch (name) {
39
+ case "bash": {
40
+ const command = String(input.command);
41
+ const timeout = (input.timeout ?? DEFAULT_TIMEOUT_S) * 1000;
42
+ const result = await exec(command, timeout);
43
+ report({ command, exitCode: result.exitCode, killed: result.killed });
44
+ const stdout = truncateMiddle(result.stdout);
45
+ const stderr = truncateMiddle(result.stderr);
46
+ return {
47
+ kind: "completed",
48
+ output: {
49
+ exitCode: result.exitCode,
50
+ stdout: stdout.text,
51
+ stderr: result.timedOut
52
+ ? `${stderr.text}${stderr.text && !stderr.text.endsWith("\n") ? "\n" : ""}Command timed out after ${timeout / 1000}s and was killed.`
53
+ : stderr.text,
54
+ ...(stdout.truncated || stderr.truncated ? { truncated: true } : {}),
55
+ ...(result.timedOut ? { timedOut: true } : {}),
56
+ },
57
+ };
58
+ }
59
+ case "read": {
60
+ const path = resolvePath(String(input.path));
61
+ report({ path });
62
+ const content = await handle.readFile(path);
63
+ if (content === undefined)
64
+ return failed("sandbox.not_found", `No file at ${path}`);
65
+ const lines = content.split("\n");
66
+ if (lines.at(-1) === "")
67
+ lines.pop();
68
+ const start = (input.offset ?? 1) - 1;
69
+ const limit = input.limit ?? DEFAULT_READ_LINES;
70
+ const slice = lines.slice(start, start + limit);
71
+ const width = String(start + slice.length).length;
72
+ const text = slice
73
+ .map((line, index) => {
74
+ const clipped = line.length > LINE_LIMIT ? `${line.slice(0, LINE_LIMIT)}…` : line;
75
+ return `${String(start + index + 1).padStart(width, " ")}\t${clipped}`;
76
+ })
77
+ .join("\n");
78
+ const remaining = lines.length - (start + slice.length);
79
+ return {
80
+ kind: "completed",
81
+ output: lines.length === 0
82
+ ? `${path} is empty.`
83
+ : remaining > 0
84
+ ? `${text}\n… ${remaining} more line(s); read again with offset ${start + slice.length + 1}.`
85
+ : text,
86
+ };
87
+ }
88
+ case "write": {
89
+ const path = resolvePath(String(input.path));
90
+ const content = String(input.content);
91
+ report({ path });
92
+ const parent = await exec(`mkdir -p ${quote(posix.dirname(path))}`);
93
+ if (parent.exitCode !== 0)
94
+ return failed("sandbox.write_failed", parent.stderr.trim() || `Could not create ${posix.dirname(path)}`);
95
+ await handle.writeFile(path, content);
96
+ return { kind: "completed", output: { path, bytes: Buffer.byteLength(content) } };
97
+ }
98
+ case "edit": {
99
+ const path = resolvePath(String(input.path));
100
+ report({ path });
101
+ const content = await handle.readFile(path);
102
+ if (content === undefined)
103
+ return failed("sandbox.not_found", `No file at ${path}`);
104
+ const oldString = String(input.old_string);
105
+ const newString = String(input.new_string);
106
+ if (oldString === newString)
107
+ return failed("sandbox.edit_noop", "old_string and new_string are identical");
108
+ const count = content.split(oldString).length - 1;
109
+ if (count === 0)
110
+ return failed("sandbox.edit_no_match", `old_string was not found in ${path}. Read the file and copy the text exactly.`);
111
+ if (count > 1 && input.replace_all !== true)
112
+ return failed("sandbox.edit_not_unique", `old_string appears ${count} times in ${path}. Add surrounding context to make it unique, or set replace_all.`);
113
+ await handle.writeFile(path, content.replaceAll(oldString, () => newString));
114
+ return { kind: "completed", output: { path, replacements: input.replace_all === true ? count : 1 } };
115
+ }
116
+ case "grep": {
117
+ const max = input.max_results ?? DEFAULT_GREP_RESULTS;
118
+ const target = input.path ? String(input.path) : ".";
119
+ const flags = ["-rnE", ...(input.ignore_case ? ["-i"] : [])];
120
+ const include = input.glob ? ` --include=${quote(String(input.glob))}` : "";
121
+ const command = `grep ${flags.join(" ")}${include} -e ${quote(String(input.pattern))} -- ${quote(target)} | head -n ${max + 1}`;
122
+ report({ command });
123
+ const result = await exec(command);
124
+ // The pipeline's exit code is head's, so a grep error shows up as stderr with no matches.
125
+ const lines = result.stdout.split("\n").filter(Boolean);
126
+ if (lines.length === 0)
127
+ return result.stderr.trim()
128
+ ? failed("sandbox.grep_failed", result.stderr.trim())
129
+ : { kind: "completed", output: "No matches." };
130
+ const shown = lines.slice(0, max).map((line) => (line.length > LINE_LIMIT ? `${line.slice(0, LINE_LIMIT)}…` : line));
131
+ return {
132
+ kind: "completed",
133
+ output: lines.length > max ? `${shown.join("\n")}\n… more matches omitted; narrow the search.` : shown.join("\n"),
134
+ };
135
+ }
136
+ case "glob": {
137
+ const root = resolvePath(input.path ? String(input.path) : ".");
138
+ const pattern = String(input.pattern);
139
+ const command = `find ${quote(root)} -type f -not -path '*/.git/*' 2>/dev/null | head -n ${GLOB_SCAN_LIMIT}`;
140
+ report({ command });
141
+ const result = await exec(command);
142
+ const byBasename = !pattern.includes("/");
143
+ const matches = [];
144
+ for (const file of result.stdout.split("\n")) {
145
+ if (!file)
146
+ continue;
147
+ const relative = posix.relative(root, file);
148
+ if (posix.matchesGlob(byBasename ? posix.basename(relative) : relative, pattern)) {
149
+ matches.push(file);
150
+ if (matches.length > GLOB_LIMIT)
151
+ break;
152
+ }
153
+ }
154
+ if (matches.length === 0)
155
+ return { kind: "completed", output: "No files matched." };
156
+ matches.sort();
157
+ return {
158
+ kind: "completed",
159
+ output: matches.length > GLOB_LIMIT
160
+ ? `${matches.slice(0, GLOB_LIMIT).join("\n")}\n… more files omitted; narrow the pattern.`
161
+ : matches.join("\n"),
162
+ };
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Backend contract for Runtime-owned sandboxes. Only files under `adapters/sandbox/` implement it
3
+ * and import a substrate SDK; everything else in the Runtime talks to this interface.
4
+ */
5
+ import type { SandboxNetworkPreset } from "@nylorun/core/define";
6
+ export type SandboxBackendName = "microsandbox" | "virtual";
7
+ export type SandboxIsolation = "vm" | "process";
8
+ export interface SandboxProbe {
9
+ readonly name: SandboxBackendName;
10
+ readonly available: boolean;
11
+ readonly isolation: SandboxIsolation;
12
+ /** Why the backend is unavailable, or a short description when it is. */
13
+ readonly reason?: string;
14
+ readonly version?: string;
15
+ }
16
+ /** Egress policy after presets are expanded. Always-blocked ranges are the backend's job. */
17
+ export interface ResolvedNetwork {
18
+ readonly preset: SandboxNetworkPreset;
19
+ /** Exact host names allowed over HTTPS/HTTP. */
20
+ readonly hosts: readonly string[];
21
+ /** Domain suffixes such as ".pythonhosted.org", from "*.pythonhosted.org". */
22
+ readonly suffixes: readonly string[];
23
+ }
24
+ export interface SandboxSpec {
25
+ /** Stable backend name for this sandbox; unique per Runtime scope and session. */
26
+ readonly key: string;
27
+ readonly image: string;
28
+ readonly cpus: number;
29
+ readonly memoryMiB: number;
30
+ readonly network: ResolvedNetwork;
31
+ }
32
+ export interface ExecRequest {
33
+ readonly command: string;
34
+ readonly cwd: string;
35
+ readonly timeoutMs: number;
36
+ }
37
+ export interface ExecResult {
38
+ readonly exitCode: number;
39
+ readonly stdout: string;
40
+ readonly stderr: string;
41
+ /** The process was actually stopped (timeout or cancellation). */
42
+ readonly killed: boolean;
43
+ readonly timedOut: boolean;
44
+ }
45
+ export interface SandboxHandle {
46
+ exec(request: ExecRequest, signal: AbortSignal): Promise<ExecResult>;
47
+ /** Returns undefined when the file does not exist. */
48
+ readFile(path: string): Promise<string | undefined>;
49
+ /** Parent directories must already exist. */
50
+ writeFile(path: string, content: string): Promise<void>;
51
+ /** Release compute; files persist. */
52
+ stop(): Promise<void>;
53
+ }
54
+ export interface SandboxBackend {
55
+ readonly name: SandboxBackendName;
56
+ readonly isolation: SandboxIsolation;
57
+ probe(): Promise<SandboxProbe>;
58
+ /** A reason this backend cannot meet the spec, or undefined when it can. */
59
+ unmet(spec: SandboxSpec): string | undefined;
60
+ /** Reattach to an existing sandbox (starting it if stopped) or create a new one. */
61
+ open(spec: SandboxSpec): Promise<SandboxHandle>;
62
+ /** Delete compute and files. Missing sandboxes are ignored. */
63
+ remove(key: string): Promise<void>;
64
+ /** Keys of sandboxes this backend holds whose key starts with the prefix. */
65
+ list(prefix: string): Promise<readonly string[]>;
66
+ }
@@ -1,5 +1,20 @@
1
1
  import type { DatabaseSync } from "node:sqlite";
2
- import type { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, CredentialSelection, RotateCredentialRequest, VaultInfo } from "@nylorun/core/contracts";
2
+ import type { CreateCredentialRequest, CreateVaultRequest, CredentialInfo, CredentialSelection, HostModelProviderInfo, HostModelView, PutHostModelRequest, RotateCredentialRequest, SelectHostModelRequest, VaultInfo } from "@nylorun/core/contracts";
3
+ export type HostModelSecret = {
4
+ provider: string;
5
+ model: string;
6
+ baseUrl?: string;
7
+ authType: "api_key" | "oauth";
8
+ credential: {
9
+ type: "api_key" | "oauth";
10
+ key?: string;
11
+ env?: Record<string, string>;
12
+ refresh?: string;
13
+ access?: string;
14
+ expires?: number;
15
+ [key: string]: unknown;
16
+ };
17
+ };
3
18
  export type AuthorizeResult = {
4
19
  status: "unauthenticated";
5
20
  url: string;
@@ -44,6 +59,21 @@ export declare class VaultService {
44
59
  url: string;
45
60
  serverName?: string;
46
61
  }): Promise<AuthorizeResult>;
62
+ getHostModel(): HostModelView;
63
+ listHostProviders(): {
64
+ providers: HostModelProviderInfo[];
65
+ };
66
+ putHostModel(body: PutHostModelRequest): HostModelView;
67
+ selectHostModel(body: SelectHostModelRequest): HostModelView;
68
+ readHostModel(): HostModelSecret | undefined;
69
+ updateHostCredential(credential: {
70
+ type: "api_key" | "oauth";
71
+ key?: string;
72
+ env?: Record<string, string>;
73
+ refresh?: string;
74
+ access?: string;
75
+ expires?: number;
76
+ }): void;
47
77
  reject(route: string): void;
48
78
  private refresh;
49
79
  private replay;
@@ -52,6 +82,16 @@ export declare class VaultService {
52
82
  private readPayload;
53
83
  private vaultInfo;
54
84
  private vaultRow;
85
+ private ensureHostVault;
86
+ private validateHostModel;
87
+ private insertHostCredential;
88
+ private readHostPayload;
89
+ private hostModelRows;
90
+ private hostCredentialRowFor;
91
+ private activeHostCredentialRow;
92
+ private activeProviderId;
93
+ private setActiveProvider;
94
+ private deleteHostProviderRows;
55
95
  private credentialInfo;
56
96
  private credentialRow;
57
97
  private rowsForVault;