@nylorun/runtime 0.6.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.
- package/CHANGELOG.md +48 -0
- package/README.md +9 -5
- package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
- package/dist/adapters/sandbox/microsandbox.js +238 -0
- package/dist/adapters/sandbox/virtual.d.ts +4 -0
- package/dist/adapters/sandbox/virtual.js +125 -0
- package/dist/configuration.d.ts +1 -1
- package/dist/configuration.js +1 -1
- package/dist/core/executors.d.ts +32 -0
- package/dist/core/executors.js +60 -0
- package/dist/core/main.js +7 -12
- package/dist/core/runtime.d.ts +41 -1
- package/dist/core/runtime.js +594 -53
- package/dist/core/store.d.ts +13 -0
- package/dist/core/store.js +95 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/mcp/connect.d.ts +31 -0
- package/dist/mcp/connect.js +190 -0
- package/dist/mcp/pool.d.ts +45 -0
- package/dist/mcp/pool.js +143 -0
- package/dist/mcp/snapshot.d.ts +25 -0
- package/dist/mcp/snapshot.js +10 -0
- package/dist/mcp/stdio.d.ts +15 -0
- package/dist/mcp/stdio.js +80 -0
- package/dist/model/catalog.d.ts +12 -0
- package/dist/model/catalog.js +29 -0
- package/dist/model/models.d.ts +3 -1
- package/dist/model/models.js +4 -2
- package/dist/model/pi-model.d.ts +9 -3
- package/dist/model/pi-model.js +47 -11
- package/dist/model/settings.d.ts +0 -2
- package/dist/model/settings.js +0 -47
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/sandbox/manager.d.ts +61 -0
- package/dist/sandbox/manager.js +239 -0
- package/dist/sandbox/policy.d.ts +13 -0
- package/dist/sandbox/policy.js +67 -0
- package/dist/sandbox/select.d.ts +35 -0
- package/dist/sandbox/select.js +76 -0
- package/dist/sandbox/tools.d.ts +25 -0
- package/dist/sandbox/tools.js +165 -0
- package/dist/sandbox/types.d.ts +66 -0
- package/dist/vault/crypto.d.ts +12 -0
- package/dist/vault/crypto.js +80 -0
- package/dist/vault/error.d.ts +4 -0
- package/dist/vault/error.js +7 -0
- package/dist/vault/kek.d.ts +6 -0
- package/dist/vault/kek.js +26 -0
- package/dist/vault/service.d.ts +99 -0
- package/dist/vault/service.js +825 -0
- package/dist/vault/url.d.ts +3 -0
- package/dist/vault/url.js +27 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/package.json +12 -6
- package/dist/config.d.ts +0 -20
- package/dist/model/defaults.d.ts +0 -17
- package/dist/model/defaults.js +0 -21
- package/dist/model/http-model.d.ts +0 -12
- package/dist/model/http-model.js +0 -299
- package/dist/node/local-sessions.d.ts +0 -5
- package/dist/node/local-sessions.js +0 -174
- package/dist/server/ag-ui.d.ts +0 -8
- package/dist/server/ag-ui.js +0 -76
- package/dist/server/delivery.d.ts +0 -24
- package/dist/server/delivery.js +0 -107
- package/dist/server/digests.d.ts +0 -13
- package/dist/server/digests.js +0 -39
- package/dist/server/host.d.ts +0 -70
- package/dist/server/host.js +0 -647
- package/dist/session/api.d.ts +0 -10
- package/dist/session/api.js +0 -15
- package/dist/session/default.d.ts +0 -5
- package/dist/session/default.js +0 -30
- package/dist/session/handle.d.ts +0 -27
- package/dist/session/handle.js +0 -199
- package/dist/session/index.d.ts +0 -2
- package/dist/session/index.js +0 -2
- package/dist/sessions/host.d.ts +0 -39
- package/dist/sessions/host.js +0 -359
- package/dist/sessions/store.d.ts +0 -41
- package/dist/sessions/store.js +0 -33
- /package/dist/{config.js → sandbox/types.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b8d822a: Studio Model Settings lists multiple vault-stored providers, adds credentials through a sheet, and switches the active provider/model via Runtime host vault APIs.
|
|
8
|
+
- b8d822a: Let a running Runtime learn its executors instead of receiving them all at startup.
|
|
9
|
+
`PUT /v1/executors` registers or rotates scoped executor credentials with the application
|
|
10
|
+
credential, `DELETE /v1/executors/:agentId` removes one, and `GET /v1/executors` lists them
|
|
11
|
+
without disclosing secrets. Registrations persist in SQLite with the token hashed at rest and
|
|
12
|
+
are restored on the next start; scopes supplied through `NYLORUN_EXECUTORS_JSON` still apply
|
|
13
|
+
to that process, take precedence for their agent, and are never written to the database. An
|
|
14
|
+
unchanged registration is idempotent and keeps open streams, while a rotation ends the
|
|
15
|
+
replaced token's work stream and stops it authorizing. Unauthenticated `/health` now also
|
|
16
|
+
reports the Runtime version, a non-reversible scope digest of the database path, and the
|
|
17
|
+
process id; all three are optional in the contract so an older host still parses. Token
|
|
18
|
+
hashing is adequate only because these credentials are high-entropy values minted by the
|
|
19
|
+
host; it is not a password derivation.
|
|
20
|
+
- b8d822a: Remove the legacy in-process host. `httpModel`, `HttpModelOptions`, and `ModelEnvironment` are no longer exported from `@nylorun/runtime`, `localSessions` is no longer exported from `@nylorun/runtime/node`, and `modelSelection` is no longer exported from `@nylorun/runtime/configuration`. The internal `Runtime`, `serveAgents`, `openSession`, and AG-UI route are deleted. Host agents with `nylorun dev` / `nylorun serve` and the standalone Runtime.
|
|
21
|
+
- b8d822a: Add `sandbox()`: one `.use(sandbox())` gives an agent Runtime-executed `bash`, `read`, `write`, `edit`, `grep` and `glob` tools on an isolated machine with a persistent `/workspace`. The Runtime selects a microsandbox microVM where available, otherwise an in-process virtual shell, enforces deny-by-default egress presets, owns sandbox lifecycle, and reports its choice through `GET /v1/host/sandbox`, the `nylorun dev` banner and `nylorun doctor sandbox`.
|
|
22
|
+
- b8d822a: Breaking beta: replace `beforeModelCall` / `afterModelCall` with scoped hooks. Register `before("turn" | "step", fn)` and `after("step" | "turn", fn)` on the agent, or `before: { turn, step }` / `after: { step, turn }` on a capability. `before("turn")` runs once per turn and its `Patch` applies to every model call in the turn; the new `after("turn")` returns a `TurnDecision` for the final answer. `after` hooks take one argument and receive `attempt`, and `retry` now retries instead of failing the run. The manifest moves to `manifestSchemaVersion: 4` with `capabilities[].hooks`, and `BeforeModelCallFn`, `AfterModelCallFn` and the `beforeModelCall` / `afterModelCall` action kinds are removed. Every capability registered at a hook point now runs in one `hook` executor action, an expired hook claim is offered again instead of becoming uncertain, and the durable engine version is `hosted-2`. Hook toggles now hide a capability's tools, or one tool of a multi-tool capability, instead of having no effect or failing. Studio lists each capability's hooks with how often they run and labels hook actions. See MIGRATION.md.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Pin core to the tested release.
|
|
27
|
+
- Pin harness to the tested release.
|
|
28
|
+
- Updated dependencies [b8d822a]
|
|
29
|
+
- Updated dependencies [b8d822a]
|
|
30
|
+
- Updated dependencies [b8d822a]
|
|
31
|
+
- Updated dependencies [b8d822a]
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
- @nylorun/core@0.3.0-beta
|
|
34
|
+
- @nylorun/harness@0.17.0-beta
|
|
35
|
+
|
|
36
|
+
## 0.7.0-beta
|
|
37
|
+
|
|
38
|
+
### Minor Changes
|
|
39
|
+
|
|
40
|
+
- 3a88f51: Ship Agent-Plugins (`plugin()` / `loadPlugin`), Skills (`load_skill` / skill resources), Runtime MCP pool + vault credentials, and manifest v3 capability fields. Validate completed tool `output` against the tool output schema so ordinary tools with `outputSchema` no longer false-fail as `tool.invalid-output`.
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- Pin core to the tested release.
|
|
45
|
+
- Pin harness to the tested release.
|
|
46
|
+
- Updated dependencies [3a88f51]
|
|
47
|
+
- Updated dependencies
|
|
48
|
+
- @nylorun/core@0.2.0-beta
|
|
49
|
+
- @nylorun/harness@0.16.0-beta
|
|
50
|
+
|
|
3
51
|
## 0.6.0-beta
|
|
4
52
|
|
|
5
53
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ NYLORUN_SERVER_KEY='<separate-secret-at-least-16-characters>' npm start --worksp
|
|
|
14
14
|
|
|
15
15
|
Default address: `http://127.0.0.1:8787`. `HOST`, `PORT`, and `NYLORUN_SQLITE_PATH` configure binding and storage. Missing server credentials fail startup. The default model is a credential-free scripted development response. It does not simulate conversations, tools or provider compatibility.
|
|
16
16
|
|
|
17
|
-
`NYLORUN_EXECUTORS_JSON` is an array of `{token, agentId,
|
|
17
|
+
`NYLORUN_EXECUTORS_JSON` is an array of `{token, agentId, implementationVersion}`. Each secret must differ from the application server key and every other executor token. An optional `manifestHash` on an existing record is ignored and does not hide or reject work. SDK clients use `NYLORUN_RUNTIME_URL`, `NYLORUN_SERVER_KEY`, and `NYLORUN_EXECUTOR_KEY`; runtime executor scope is the agent id, never broadened by client input.
|
|
18
18
|
|
|
19
19
|
For a model gateway, set `NYLORUN_MODEL_GATEWAY_URL`, `NYLORUN_MODEL_GATEWAY_KEY`, and `NYLORUN_MODEL`. The gateway receives `{model, call, context}` and returns a harness ModelCandidate or string. Provider credentials remain host configuration. The generic gateway requires an implementation of this envelope; it does not claim arbitrary provider wire compatibility. Alternatively supply a `ModelProvider` to `startRuntime`.
|
|
20
20
|
|
|
@@ -36,12 +36,16 @@ SQLite transactions persist session checkpoints, command receipts, individual ef
|
|
|
36
36
|
|
|
37
37
|
One process owns each SQLite database. A local PID lock rejects concurrent owners and recovers only when the recorded process no longer exists. Shared/network filesystems and multiple replicas are unsupported. Keep the database and its WAL/journal together; do not delete persisted effects separately from checkpoints. Invalid lock contents or PID reuse fail closed and require operator inspection. TLS and external ingress belong to deployment infrastructure.
|
|
38
38
|
|
|
39
|
+
The vault holds two scopes. End-user vaults each belong to one `ownerUserId` and store URL-bound credentials for outbound calls such as MCP. The host scope stores model provider credentials the Runtime uses when it calls a provider; Studio and `nylorun configure` can keep multiple providers and select the active provider/model. Secret values are encrypted with AES-256-GCM under a key-encryption key. Set `NYLORUN_VAULT_KEK` to 32 bytes encoded as base64, or allow the host to create `.nylorun/vault-kek` (mode 0600) on the first write. A database that already holds vault ciphertext will not open without that key. Reads return metadata only.
|
|
40
|
+
|
|
41
|
+
Agents that declare `.use(sandbox())` get Runtime-executed `bash`, `read`, `write`, `edit`, `grep` and `glob` tools. The Runtime owns each sandbox: one per session, created on the first sandbox tool call, stopped after its idle timeout, and reattached with its files on the next call, including after a restart. On first use the Runtime probes its backends in order and keeps the choice for its lifetime: a microsandbox microVM (macOS on Apple Silicon, or Linux with `/dev/kvm`), then the in-process virtual shell (`just-bash`). Set `NYLORUN_SANDBOX=microsandbox` or `virtual` to force one; a forced backend never falls back. `GET /v1/host/sandbox` reports the selection, and `nylorun doctor sandbox` shows the same probe. Virtual workspaces live in `sandboxes/` beside the SQLite file. Tool calls emit `sandbox.state` and `sandbox.exec` session events. A sandbox call interrupted by a crash becomes uncertain and is never re-run. `microsandbox` is an optional dependency pinned to an exact version. See [the sandbox design](../docs/design/sandboxes.md) for guarantees and limitations.
|
|
42
|
+
|
|
39
43
|
## Local project workflow
|
|
40
44
|
|
|
41
|
-
Install `@nylorun/cli` (generated projects already include it). Use `
|
|
45
|
+
Install `@nylorun/cli` (generated projects already include it). Use `nylorun up` and `nylorun dev` in a generated project. It loads the exported `agents` registry from `agents/index.ts`, starts this Runtime in a separate process, and, when no host model credential is stored, prompts in an interactive terminal and writes it to the vault. It then registers manifests, connects scoped SDK executors, and opens Studio. Use `--no-studio` or `--no-open` as needed. Runtime binds loopback on port 8787 (`PORT` override). The CLI creates separate server and executor credentials with mode 0600 in `.nylorun/local-credentials.json`; SQLite defaults to `.nylorun/runtime.sqlite`.
|
|
42
46
|
|
|
43
|
-
`nylorun
|
|
47
|
+
`nylorun serve [entry]` loads `dist/agents/index.js` by default and runs without Studio/watch, attaching to the Runtime that `nylorun up` started. It uses the same first-start prompt. `nylorun configure` replaces the host credential against a Runtime that is already listening. `nylorun studio` attaches using the project's local server credential (or `NYLORUN_SERVER_KEY`). Studio can replace an API-key credential. If `MODEL_PROVIDER`, `MODEL`, and `MODEL_PROVIDER_API_KEY` are already set, the first start seeds the vault from them and does not prompt. Existing `.env` model settings and `.nylorun/auth.json` are imported once the same way. No provider request occurs during that setup. `NYLORUN_DEV_MODEL=fixture` and `NYLORUN_MODEL_GATEWAY_URL` remain separate from the vault.
|
|
44
48
|
|
|
45
|
-
Definitions have no `agent.run()`; applications use `@nylorun/agents`.
|
|
49
|
+
Definitions have no `agent.run()`; applications use `@nylorun/agents`. The previous in-process host (`Runtime`, `serveAgents`, `openSession`, `httpModel`, `localSessions`, and the AG-UI route) has been removed; remove any remaining usage from applications.
|
|
46
50
|
|
|
47
|
-
Source changes restart the local stack. Start a new session after definition or implementation edits; live upgrades are deferred. `GET /v1/agents` and `GET /v1/sessions?agentId=...` are server-authenticated local discovery conveniences.
|
|
51
|
+
Source changes restart the local stack. Start a new session after definition or implementation edits; live upgrades are deferred. `GET /v1/agents` and `GET /v1/sessions?agentId=...` are server-authenticated local discovery conveniences.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/** microsandbox backend: a hardware-isolated microVM per sandbox (libkrun). */
|
|
2
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
|
|
6
|
+
/** Releases that are known not to work; the probe refuses them and selection falls back. */
|
|
7
|
+
const KNOWN_BAD_VERSIONS = new Set(["0.7.0"]);
|
|
8
|
+
/** Keep at most this much of each output stream in memory while a command runs. */
|
|
9
|
+
const CAPTURE_LIMIT = 4 * 1024 * 1024;
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
/** The package's exports map hides package.json, so walk up from its entry point. */
|
|
12
|
+
function installedVersion() {
|
|
13
|
+
try {
|
|
14
|
+
let directory = dirname(require.resolve("microsandbox"));
|
|
15
|
+
for (let depth = 0; depth < 4; depth += 1, directory = dirname(directory)) {
|
|
16
|
+
try {
|
|
17
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
18
|
+
if (manifest.name === "microsandbox")
|
|
19
|
+
return manifest.version;
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function platformReason() {
|
|
30
|
+
if (process.platform === "darwin")
|
|
31
|
+
return process.arch === "arm64" ? undefined : "requires Apple Silicon on macOS";
|
|
32
|
+
if (process.platform === "linux") {
|
|
33
|
+
try {
|
|
34
|
+
accessSync("/dev/kvm", constants.R_OK | constants.W_OK);
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "no usable /dev/kvm (enable KVM or nested virtualisation)";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (process.platform === "win32")
|
|
42
|
+
return "not yet enabled on Windows";
|
|
43
|
+
return `unsupported platform ${process.platform}`;
|
|
44
|
+
}
|
|
45
|
+
export function microsandboxBackend() {
|
|
46
|
+
let sdk;
|
|
47
|
+
const load = () => (sdk ??= import("microsandbox"));
|
|
48
|
+
const handleFor = (sandbox) => ({
|
|
49
|
+
async exec(request, signal) {
|
|
50
|
+
const execution = await sandbox.execStreamWith("bash", (options) => options
|
|
51
|
+
.args(["-c", request.command])
|
|
52
|
+
.cwd(request.cwd)
|
|
53
|
+
.envs({ LANG: "C.UTF-8", TERM: "dumb" })
|
|
54
|
+
.stdinNull());
|
|
55
|
+
const stdout = new Capture();
|
|
56
|
+
const stderr = new Capture();
|
|
57
|
+
let killed = false;
|
|
58
|
+
let timedOut = false;
|
|
59
|
+
const kill = () => {
|
|
60
|
+
if (killed)
|
|
61
|
+
return;
|
|
62
|
+
killed = true;
|
|
63
|
+
void execution.kill().catch(() => undefined);
|
|
64
|
+
};
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
timedOut = true;
|
|
67
|
+
kill();
|
|
68
|
+
}, request.timeoutMs);
|
|
69
|
+
const onAbort = () => kill();
|
|
70
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
if (signal.aborted)
|
|
72
|
+
kill();
|
|
73
|
+
let exitCode = -1;
|
|
74
|
+
try {
|
|
75
|
+
for await (const event of execution) {
|
|
76
|
+
if (event.kind === "stdout")
|
|
77
|
+
stdout.push(event.data);
|
|
78
|
+
else if (event.kind === "stderr")
|
|
79
|
+
stderr.push(event.data);
|
|
80
|
+
else if (event.kind === "exited") {
|
|
81
|
+
exitCode = event.code;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
signal.removeEventListener("abort", onAbort);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
exitCode: killed && exitCode < 0 ? (timedOut ? 124 : 130) : exitCode,
|
|
92
|
+
stdout: stdout.text(),
|
|
93
|
+
stderr: stderr.text(),
|
|
94
|
+
killed,
|
|
95
|
+
timedOut,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
async readFile(path) {
|
|
99
|
+
const fs = sandbox.fs();
|
|
100
|
+
if (!(await fs.exists(path)))
|
|
101
|
+
return undefined;
|
|
102
|
+
return fs.readToString(path);
|
|
103
|
+
},
|
|
104
|
+
async writeFile(path, content) {
|
|
105
|
+
await sandbox.fs().write(path, content);
|
|
106
|
+
},
|
|
107
|
+
async stop() {
|
|
108
|
+
await sandbox.stop();
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
name: "microsandbox",
|
|
113
|
+
isolation: "vm",
|
|
114
|
+
async probe() {
|
|
115
|
+
const base = { name: "microsandbox", isolation: "vm" };
|
|
116
|
+
const version = installedVersion();
|
|
117
|
+
const platform = platformReason();
|
|
118
|
+
if (platform)
|
|
119
|
+
return { ...base, available: false, reason: platform, ...(version ? { version } : {}) };
|
|
120
|
+
if (!version)
|
|
121
|
+
return {
|
|
122
|
+
...base,
|
|
123
|
+
available: false,
|
|
124
|
+
reason: "the optional microsandbox package is not installed (reinstall with optional dependencies enabled)",
|
|
125
|
+
};
|
|
126
|
+
if (KNOWN_BAD_VERSIONS.has(version))
|
|
127
|
+
return { ...base, available: false, version, reason: `microsandbox ${version} is a known-bad release` };
|
|
128
|
+
try {
|
|
129
|
+
const msb = await load();
|
|
130
|
+
if (!(await msb.isRuntimeInstalled()))
|
|
131
|
+
return { ...base, available: false, version, reason: "the microsandbox runtime (msb, libkrunfw) is not installed" };
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
available: false,
|
|
137
|
+
version,
|
|
138
|
+
reason: `could not load microsandbox: ${error instanceof Error ? error.message : String(error)}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { ...base, available: true, version, reason: "hardware-isolated microVM" };
|
|
142
|
+
},
|
|
143
|
+
unmet() {
|
|
144
|
+
return undefined;
|
|
145
|
+
},
|
|
146
|
+
async open(spec) {
|
|
147
|
+
const msb = await load();
|
|
148
|
+
const { Sandbox, SandboxNotFoundError } = msb;
|
|
149
|
+
try {
|
|
150
|
+
const existing = await Sandbox.get(spec.key);
|
|
151
|
+
return handleFor(await existing.connectOrStart());
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (!(error instanceof SandboxNotFoundError))
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
const sandbox = await Sandbox.builder(spec.key)
|
|
158
|
+
.image(spec.image)
|
|
159
|
+
.cpus(spec.cpus)
|
|
160
|
+
.memory(spec.memoryMiB)
|
|
161
|
+
.patch((patch) => patch.mkdir(SANDBOX_WORKSPACE, { mode: 0o755 }))
|
|
162
|
+
.workdir(SANDBOX_WORKSPACE)
|
|
163
|
+
.label("nylorun", "sandbox")
|
|
164
|
+
.network((network) => network.policy(policyFor(msb, spec)))
|
|
165
|
+
.create();
|
|
166
|
+
return handleFor(sandbox);
|
|
167
|
+
},
|
|
168
|
+
async remove(key) {
|
|
169
|
+
const { Sandbox, SandboxNotFoundError } = await load();
|
|
170
|
+
try {
|
|
171
|
+
const handle = await Sandbox.get(key);
|
|
172
|
+
await handle.destroy({ force: true });
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (!(error instanceof SandboxNotFoundError))
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async list(prefix) {
|
|
180
|
+
const { Sandbox } = await load();
|
|
181
|
+
const keys = [];
|
|
182
|
+
let page = await Sandbox.list();
|
|
183
|
+
for (;;) {
|
|
184
|
+
for (const handle of page.sandboxes)
|
|
185
|
+
if (handle.name.startsWith(prefix))
|
|
186
|
+
keys.push(handle.name);
|
|
187
|
+
if (!page.nextCursor)
|
|
188
|
+
break;
|
|
189
|
+
const cursor = page.nextCursor;
|
|
190
|
+
page = await Sandbox.listWith((list) => list.cursor(cursor));
|
|
191
|
+
}
|
|
192
|
+
return keys;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Deny by default. Private, loopback, link-local, metadata and host ranges are always denied. */
|
|
197
|
+
function policyFor(msb, spec) {
|
|
198
|
+
const { Destination, Rule } = msb;
|
|
199
|
+
const always = ["private", "loopback", "link-local", "metadata", "multicast"];
|
|
200
|
+
// The host is denied for TCP only: its resolver answers DNS for the sandbox and filters
|
|
201
|
+
// names by this same policy, so a UDP deny would break every allowed host.
|
|
202
|
+
const host = {
|
|
203
|
+
direction: "egress",
|
|
204
|
+
destination: Destination.group("host"),
|
|
205
|
+
protocols: ["tcp"],
|
|
206
|
+
ports: [],
|
|
207
|
+
action: "deny",
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
defaultEgress: "deny",
|
|
211
|
+
defaultIngress: "deny",
|
|
212
|
+
rules: [
|
|
213
|
+
...always.map((group) => Rule.denyEgress(Destination.group(group))),
|
|
214
|
+
host,
|
|
215
|
+
...(spec.network.preset === "open" ? [Rule.allowEgress(Destination.group("public"))] : []),
|
|
216
|
+
...spec.network.hosts.map((host) => Rule.allowEgress(Destination.domain(host))),
|
|
217
|
+
...spec.network.suffixes.map((suffix) => Rule.allowEgress(Destination.domainSuffix(suffix))),
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
class Capture {
|
|
222
|
+
chunks = [];
|
|
223
|
+
size = 0;
|
|
224
|
+
dropped = 0;
|
|
225
|
+
push(data) {
|
|
226
|
+
const chunk = typeof data === "string" ? Buffer.from(data) : Buffer.from(data);
|
|
227
|
+
if (this.size >= CAPTURE_LIMIT) {
|
|
228
|
+
this.dropped += chunk.length;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.chunks.push(chunk);
|
|
232
|
+
this.size += chunk.length;
|
|
233
|
+
}
|
|
234
|
+
text() {
|
|
235
|
+
const text = Buffer.concat(this.chunks).toString("utf8");
|
|
236
|
+
return this.dropped > 0 ? `${text}\n… [${this.dropped} bytes not captured]` : text;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual backend: an emulated bash with a virtual filesystem, running in the Runtime process.
|
|
3
|
+
* `/workspace` is backed by a host directory so files survive stop and Runtime restarts.
|
|
4
|
+
* It is not a VM boundary; it exists so the first run and CI work on any machine.
|
|
5
|
+
*/
|
|
6
|
+
import { mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
|
|
9
|
+
const ENV = Object.freeze({
|
|
10
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
11
|
+
HOME: SANDBOX_WORKSPACE,
|
|
12
|
+
LANG: "C.UTF-8",
|
|
13
|
+
TERM: "dumb",
|
|
14
|
+
});
|
|
15
|
+
const METHODS = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
|
|
16
|
+
export function virtualBackend(options) {
|
|
17
|
+
let sdk;
|
|
18
|
+
const load = () => (sdk ??= import("just-bash"));
|
|
19
|
+
const directory = (key) => join(options.root, key, "workspace");
|
|
20
|
+
return {
|
|
21
|
+
name: "virtual",
|
|
22
|
+
isolation: "process",
|
|
23
|
+
async probe() {
|
|
24
|
+
const base = { name: "virtual", isolation: "process" };
|
|
25
|
+
try {
|
|
26
|
+
await load();
|
|
27
|
+
return { ...base, available: true, reason: "emulated shell in the Runtime process (not a VM)" };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return {
|
|
31
|
+
...base,
|
|
32
|
+
available: false,
|
|
33
|
+
reason: `could not load just-bash: ${error instanceof Error ? error.message : String(error)}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
unmet(spec) {
|
|
38
|
+
if (spec.network.suffixes.length > 0)
|
|
39
|
+
return "the virtual backend cannot allow wildcard hosts; list exact host names in network.allow or run with microsandbox";
|
|
40
|
+
return undefined;
|
|
41
|
+
},
|
|
42
|
+
async open(spec) {
|
|
43
|
+
const { Bash, InMemoryFs, MountableFs, ReadWriteFs } = await load();
|
|
44
|
+
const root = directory(spec.key);
|
|
45
|
+
mkdirSync(root, { recursive: true });
|
|
46
|
+
const base = new InMemoryFs();
|
|
47
|
+
await base.mkdir("/tmp", { recursive: true });
|
|
48
|
+
const fs = new MountableFs({
|
|
49
|
+
base,
|
|
50
|
+
mounts: [{ mountPoint: SANDBOX_WORKSPACE, filesystem: new ReadWriteFs({ root }) }],
|
|
51
|
+
});
|
|
52
|
+
const network = spec.network.preset === "open"
|
|
53
|
+
? { dangerouslyAllowFullInternetAccess: true, denyPrivateRanges: true }
|
|
54
|
+
: spec.network.hosts.length > 0
|
|
55
|
+
? {
|
|
56
|
+
allowedUrlPrefixes: spec.network.hosts.flatMap((host) => [`https://${host}`, `http://${host}`]),
|
|
57
|
+
allowedMethods: [...METHODS],
|
|
58
|
+
denyPrivateRanges: true,
|
|
59
|
+
}
|
|
60
|
+
: undefined;
|
|
61
|
+
const bash = new Bash({
|
|
62
|
+
fs,
|
|
63
|
+
cwd: SANDBOX_WORKSPACE,
|
|
64
|
+
env: { ...ENV },
|
|
65
|
+
python: true,
|
|
66
|
+
...(network ? { network } : {}),
|
|
67
|
+
});
|
|
68
|
+
return {
|
|
69
|
+
async exec(request, signal) {
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
timedOut = true;
|
|
74
|
+
controller.abort();
|
|
75
|
+
}, request.timeoutMs);
|
|
76
|
+
const onAbort = () => controller.abort();
|
|
77
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
if (signal.aborted)
|
|
79
|
+
controller.abort();
|
|
80
|
+
try {
|
|
81
|
+
const result = await bash.exec(request.command, {
|
|
82
|
+
cwd: request.cwd,
|
|
83
|
+
env: { ...ENV },
|
|
84
|
+
replaceEnv: true,
|
|
85
|
+
rawScript: true,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
});
|
|
88
|
+
const killed = controller.signal.aborted;
|
|
89
|
+
return {
|
|
90
|
+
exitCode: killed ? (timedOut ? 124 : 130) : result.exitCode,
|
|
91
|
+
stdout: result.stdout,
|
|
92
|
+
stderr: result.stderr,
|
|
93
|
+
killed,
|
|
94
|
+
timedOut,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
signal.removeEventListener("abort", onAbort);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
async readFile(path) {
|
|
103
|
+
if (!(await fs.exists(path)))
|
|
104
|
+
return undefined;
|
|
105
|
+
return fs.readFile(path, "utf8");
|
|
106
|
+
},
|
|
107
|
+
async writeFile(path, content) {
|
|
108
|
+
await fs.writeFile(path, content, "utf8");
|
|
109
|
+
},
|
|
110
|
+
async stop() { },
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
async remove(key) {
|
|
114
|
+
rmSync(join(options.root, key), { recursive: true, force: true });
|
|
115
|
+
},
|
|
116
|
+
async list(prefix) {
|
|
117
|
+
try {
|
|
118
|
+
return readdirSync(options.root).filter((name) => name.startsWith(prefix));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/configuration.d.ts
CHANGED
package/dist/configuration.js
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ExecutorScope } from "@nylorun/core/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Executor bearer tokens are 256-bit CSPRNG values minted by the CLI and the wire schema
|
|
4
|
+
* requires at least 16 characters, so an unsalted digest is adequate to keep them out of
|
|
5
|
+
* SQLite at rest. This is not a password KDF and must not be used for one.
|
|
6
|
+
*/
|
|
7
|
+
export declare const hashToken: (token: string) => string;
|
|
8
|
+
export interface ExecutorRecord extends ExecutorScope {
|
|
9
|
+
readonly tokenHash: string;
|
|
10
|
+
/** False for scopes supplied through NYLORUN_EXECUTORS_JSON, which are never written to SQLite. */
|
|
11
|
+
readonly persisted: boolean;
|
|
12
|
+
readonly updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function assertExecutorCredential(executor: {
|
|
15
|
+
agentId: string;
|
|
16
|
+
implementationVersion: string;
|
|
17
|
+
token: string;
|
|
18
|
+
}, serverToken: string): void;
|
|
19
|
+
export declare class ExecutorRegistry {
|
|
20
|
+
private readonly byHash;
|
|
21
|
+
private readonly byAgent;
|
|
22
|
+
seed(records: readonly ExecutorRecord[]): void;
|
|
23
|
+
find(tokenHash: string): ExecutorRecord | undefined;
|
|
24
|
+
get(agentId: string): ExecutorRecord | undefined;
|
|
25
|
+
list(): readonly ExecutorRecord[];
|
|
26
|
+
upsert(record: ExecutorRecord): {
|
|
27
|
+
rotated: boolean;
|
|
28
|
+
previousHash?: string;
|
|
29
|
+
};
|
|
30
|
+
remove(agentId: string): ExecutorRecord | undefined;
|
|
31
|
+
private write;
|
|
32
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const equals = (a, b) => {
|
|
3
|
+
const aa = Buffer.from(a), bb = Buffer.from(b);
|
|
4
|
+
return aa.length === bb.length && timingSafeEqual(aa, bb);
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Executor bearer tokens are 256-bit CSPRNG values minted by the CLI and the wire schema
|
|
8
|
+
* requires at least 16 characters, so an unsalted digest is adequate to keep them out of
|
|
9
|
+
* SQLite at rest. This is not a password KDF and must not be used for one.
|
|
10
|
+
*/
|
|
11
|
+
export const hashToken = (token) => createHash("sha256").update(token, "utf8").digest("hex");
|
|
12
|
+
export function assertExecutorCredential(executor, serverToken) {
|
|
13
|
+
if (executor.token.length < 16 ||
|
|
14
|
+
equals(executor.token, serverToken) ||
|
|
15
|
+
!executor.agentId ||
|
|
16
|
+
!executor.implementationVersion)
|
|
17
|
+
throw new Error("Executor tokens require independent credentials and an agent id");
|
|
18
|
+
}
|
|
19
|
+
export class ExecutorRegistry {
|
|
20
|
+
byHash = new Map();
|
|
21
|
+
byAgent = new Map();
|
|
22
|
+
seed(records) {
|
|
23
|
+
for (const record of records)
|
|
24
|
+
this.write(record);
|
|
25
|
+
}
|
|
26
|
+
find(tokenHash) {
|
|
27
|
+
return this.byHash.get(tokenHash);
|
|
28
|
+
}
|
|
29
|
+
get(agentId) {
|
|
30
|
+
return this.byAgent.get(agentId);
|
|
31
|
+
}
|
|
32
|
+
list() {
|
|
33
|
+
return [...this.byAgent.values()];
|
|
34
|
+
}
|
|
35
|
+
upsert(record) {
|
|
36
|
+
const previous = this.byAgent.get(record.agentId);
|
|
37
|
+
const collision = this.byHash.get(record.tokenHash);
|
|
38
|
+
if (collision && collision.agentId !== record.agentId)
|
|
39
|
+
throw new Error("Executor tokens must be unique");
|
|
40
|
+
this.write(record);
|
|
41
|
+
if (!previous || previous.tokenHash === record.tokenHash)
|
|
42
|
+
return { rotated: false };
|
|
43
|
+
return { rotated: true, previousHash: previous.tokenHash };
|
|
44
|
+
}
|
|
45
|
+
remove(agentId) {
|
|
46
|
+
const record = this.byAgent.get(agentId);
|
|
47
|
+
if (!record)
|
|
48
|
+
return undefined;
|
|
49
|
+
this.byAgent.delete(agentId);
|
|
50
|
+
this.byHash.delete(record.tokenHash);
|
|
51
|
+
return record;
|
|
52
|
+
}
|
|
53
|
+
write(record) {
|
|
54
|
+
const previous = this.byAgent.get(record.agentId);
|
|
55
|
+
if (previous)
|
|
56
|
+
this.byHash.delete(previous.tokenHash);
|
|
57
|
+
this.byAgent.set(record.agentId, record);
|
|
58
|
+
this.byHash.set(record.tokenHash, record);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/core/main.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import { piModel } from "../model/pi-model.js";
|
|
2
1
|
import { startRuntime } from "./runtime.js";
|
|
3
2
|
import { gatewayModel, scriptedModel, toolFixtureModel } from "./provider.js";
|
|
4
3
|
const serverToken = process.env.NYLORUN_SERVER_KEY;
|
|
5
4
|
if (!serverToken)
|
|
6
5
|
throw new Error("Set NYLORUN_SERVER_KEY (at least 16 characters)");
|
|
7
6
|
const executors = JSON.parse(process.env.NYLORUN_EXECUTORS_JSON ?? "[]");
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
15
|
-
const model = process.env.NYLORUN_DEV_MODEL === "fixture"
|
|
16
|
-
? toolFixtureModel()
|
|
17
|
-
: process.env.NYLORUN_PROJECT_PROVIDER === "1"
|
|
18
|
-
? projectModel
|
|
7
|
+
const useHostModel = process.env.NYLORUN_DEV_MODEL !== "fixture" &&
|
|
8
|
+
process.env.NYLORUN_PROJECT_PROVIDER === "1";
|
|
9
|
+
const model = useHostModel
|
|
10
|
+
? undefined
|
|
11
|
+
: process.env.NYLORUN_DEV_MODEL === "fixture"
|
|
12
|
+
? toolFixtureModel()
|
|
19
13
|
: process.env.NYLORUN_MODEL_GATEWAY_URL
|
|
20
14
|
? gatewayModel({
|
|
21
15
|
url: process.env.NYLORUN_MODEL_GATEWAY_URL,
|
|
@@ -28,6 +22,7 @@ const runtime = await startRuntime({
|
|
|
28
22
|
serverToken,
|
|
29
23
|
executors,
|
|
30
24
|
model,
|
|
25
|
+
useHostModel,
|
|
31
26
|
port: Number(process.env.PORT ?? 8787),
|
|
32
27
|
hostname: process.env.HOST ?? "127.0.0.1",
|
|
33
28
|
});
|