@nylorun/runtime 0.9.0-beta → 0.11.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 +81 -0
- package/README.md +110 -37
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/media.d.ts +2 -2
- package/dist/adapters/media.js +2 -2
- package/dist/adapters/observe.d.ts +2 -1
- package/dist/adapters/observe.js +2 -3
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/assets.d.ts +2 -2
- package/dist/assets.js +5 -3
- package/dist/core/executors.d.ts +15 -3
- package/dist/core/executors.js +40 -5
- package/dist/core/flow-host.d.ts +225 -0
- package/dist/core/flow-host.js +518 -0
- package/dist/core/limits.d.ts +40 -0
- package/dist/core/limits.js +76 -0
- package/dist/core/runtime.d.ts +5 -90
- package/dist/core/runtime.js +3 -1447
- package/dist/core/sandbox-routes.d.ts +40 -0
- package/dist/core/sandbox-routes.js +106 -0
- package/dist/core/turn-manifest.d.ts +50 -0
- package/dist/core/turn-manifest.js +66 -0
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +17 -0
- package/dist/host/config-for.js +27 -0
- package/dist/host/config.d.ts +16 -0
- package/dist/host/create-host.d.ts +56 -0
- package/dist/host/create-host.js +420 -0
- package/dist/host/environment.d.ts +13 -0
- package/dist/host/environment.js +53 -0
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +39 -0
- package/dist/host/http.js +142 -0
- package/dist/host/index.d.ts +8 -0
- package/dist/host/index.js +7 -0
- package/dist/host/logger.d.ts +3 -0
- package/dist/host/logger.js +18 -0
- package/dist/host/main.d.ts +3 -0
- package/dist/host/main.js +201 -0
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -2
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/mcp/connect.d.ts +1 -0
- package/dist/mcp/connect.js +2 -0
- package/dist/mcp/pool.d.ts +3 -1
- package/dist/mcp/pool.js +2 -1
- package/dist/mcp/stdio.d.ts +2 -0
- package/dist/mcp/stdio.js +3 -1
- package/dist/model/auth-store.d.ts +1 -1
- package/dist/model/auth-store.js +2 -2
- package/dist/model/models.d.ts +4 -0
- package/dist/model/models.js +22 -26
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +6 -6
- package/dist/model/settings.d.ts +5 -1
- package/dist/model/settings.js +6 -2
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +5 -4
- package/dist/sandbox/select.js +18 -13
- package/dist/sandbox/share.d.ts +23 -0
- package/dist/sandbox/share.js +58 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +7 -0
- package/dist/tenant/envelope.js +22 -0
- package/dist/tenant/ephemeral.d.ts +42 -0
- package/dist/tenant/ephemeral.js +224 -0
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/logger.d.ts +9 -0
- package/dist/tenant/logger.js +25 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +38 -0
- package/dist/tenant/module.js +319 -0
- package/dist/tenant/paths.d.ts +28 -0
- package/dist/tenant/paths.js +91 -0
- package/dist/tenant/pool.d.ts +10 -0
- package/dist/tenant/pool.js +54 -0
- package/dist/tenant/principals.d.ts +13 -0
- package/dist/tenant/principals.js +37 -0
- package/dist/tenant/quarantine-error.d.ts +6 -0
- package/dist/tenant/quarantine-error.js +9 -0
- package/dist/tenant/quarantine.d.ts +27 -0
- package/dist/tenant/quarantine.js +72 -0
- package/dist/tenant/reset.d.ts +22 -0
- package/dist/tenant/reset.js +81 -0
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +81 -0
- package/dist/tenant/runtime.js +298 -0
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +37 -0
- package/dist/tenant/status.js +130 -0
- package/dist/tenant/store-memory.d.ts +16 -0
- package/dist/tenant/store-memory.js +111 -0
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +205 -0
- package/dist/tenant/types.js +23 -0
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/kek.d.ts +5 -2
- package/dist/vault/kek.js +7 -10
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +14 -11
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/main.js +0 -35
- package/dist/core/store.d.ts +0 -30
- package/dist/core/store.js +0 -202
- /package/dist/{core/main.d.ts → host/config.js} +0 -0
package/dist/core/runtime.js
CHANGED
|
@@ -1,1447 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
import { AgentManifestSchema, CreateCredentialRequestSchema, CreateVaultRequestSchema, PutAgentRequestSchema, PutHostModelRequestSchema, SelectHostModelRequestSchema, PutSessionRequestSchema, RegisterExecutorsRequestSchema, RotateCredentialRequestSchema, SessionCommandSchema, ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
|
|
8
|
-
import { createDurableCheckpoint, runDurable, } from "@nylorun/harness/run";
|
|
9
|
-
import { hashManifest } from "@nylorun/core/compatibility";
|
|
10
|
-
import { delegateManifest, schemaFromJSON, } from "@nylorun/core/define";
|
|
11
|
-
import { Store, canonical } from "./store.js";
|
|
12
|
-
import { ExecutorRegistry, assertExecutorCredential, hashToken, } from "./executors.js";
|
|
13
|
-
import { RUNTIME_VERSION } from "../version.js";
|
|
14
|
-
import { hostModelCatalog } from "../model/catalog.js";
|
|
15
|
-
import { piModel } from "../model/pi-model.js";
|
|
16
|
-
import { scriptedModel } from "./provider.js";
|
|
17
|
-
import { scrub } from "../redact.js";
|
|
18
|
-
import { createKekFile, defaultKekPath, readVaultKek } from "../vault/kek.js";
|
|
19
|
-
import { VaultError } from "../vault/error.js";
|
|
20
|
-
import { VaultService } from "../vault/service.js";
|
|
21
|
-
import { McpPool, serversOf } from "../mcp/pool.js";
|
|
22
|
-
import { SandboxManager, sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
23
|
-
import { defaultSandboxBackends } from "../sandbox/select.js";
|
|
24
|
-
class HttpError extends Error {
|
|
25
|
-
status;
|
|
26
|
-
constructor(status, message) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.status = status;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const fail = (status, message) => {
|
|
32
|
-
throw new HttpError(status, message);
|
|
33
|
-
};
|
|
34
|
-
function sessionToolsOf(snapshot) {
|
|
35
|
-
if (!snapshot?.mcpTools.length)
|
|
36
|
-
return undefined;
|
|
37
|
-
return snapshot.mcpTools.map((tool) => ({
|
|
38
|
-
...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
|
|
39
|
-
capabilityId: tool.capabilityId,
|
|
40
|
-
name: tool.name,
|
|
41
|
-
...(tool.description === undefined
|
|
42
|
-
? {}
|
|
43
|
-
: { description: tool.description }),
|
|
44
|
-
inputSchema: tool.inputSchema,
|
|
45
|
-
...(tool.outputSchema === undefined
|
|
46
|
-
? {}
|
|
47
|
-
: { outputSchema: tool.outputSchema }),
|
|
48
|
-
}));
|
|
49
|
-
}
|
|
50
|
-
function mcpToolOf(session, request) {
|
|
51
|
-
return session.mcpSnapshot?.mcpTools.find((tool) => tool.agentId === request.agent?.id &&
|
|
52
|
-
tool.capabilityId === request.capabilityId &&
|
|
53
|
-
tool.name === request.toolName);
|
|
54
|
-
}
|
|
55
|
-
/** The manifest of the agent an effect belongs to: the root, or an agent it uses as a tool. */
|
|
56
|
-
function manifestFor(manifest, agent) {
|
|
57
|
-
return agent ? delegateManifest(manifest, agent.id) : manifest;
|
|
58
|
-
}
|
|
59
|
-
function pinnedTool(manifest, capabilityId, toolName) {
|
|
60
|
-
const capability = manifest.capabilities.find((item) => item.id === capabilityId);
|
|
61
|
-
return capability?.tools?.find((tool) => tool.name === toolName);
|
|
62
|
-
}
|
|
63
|
-
function acceptedToolResult(action, command) {
|
|
64
|
-
if (action.kind !== "tool" || !action.outputSchema)
|
|
65
|
-
return command;
|
|
66
|
-
const value = command.outcome.value;
|
|
67
|
-
if (isFailedToolValue(value))
|
|
68
|
-
return command;
|
|
69
|
-
// Executors wrap successful tool output as `{ kind: "completed", output }`.
|
|
70
|
-
// Validate the tool payload, not the outcome envelope.
|
|
71
|
-
const candidate = completedToolOutput(value);
|
|
72
|
-
let matches = false;
|
|
73
|
-
try {
|
|
74
|
-
matches = schemaFromJSON(action.outputSchema).validate(candidate).ok;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
matches = false;
|
|
78
|
-
}
|
|
79
|
-
if (matches)
|
|
80
|
-
return command;
|
|
81
|
-
return {
|
|
82
|
-
...command,
|
|
83
|
-
outcome: {
|
|
84
|
-
...command.outcome,
|
|
85
|
-
value: {
|
|
86
|
-
kind: "failed",
|
|
87
|
-
code: "tool.invalid-output",
|
|
88
|
-
message: "Tool result does not match the output schema stored on the action",
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
function completedToolOutput(value) {
|
|
94
|
-
if (value &&
|
|
95
|
-
typeof value === "object" &&
|
|
96
|
-
!Array.isArray(value) &&
|
|
97
|
-
value.kind === "completed" &&
|
|
98
|
-
"output" in value) {
|
|
99
|
-
return value.output;
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
103
|
-
function isFailedToolValue(value) {
|
|
104
|
-
return (!!value &&
|
|
105
|
-
typeof value === "object" &&
|
|
106
|
-
!Array.isArray(value) &&
|
|
107
|
-
value.kind === "failed");
|
|
108
|
-
}
|
|
109
|
-
const semantic = (value) => {
|
|
110
|
-
const { requestId: _, ...body } = value;
|
|
111
|
-
return canonical(body);
|
|
112
|
-
};
|
|
113
|
-
const sessionIdentity = (value) => {
|
|
114
|
-
const { requestId: _requestId, vaultIds: _vaultIds, credentialSelections: _selections, ...body } = value ?? {};
|
|
115
|
-
return canonical(body);
|
|
116
|
-
};
|
|
117
|
-
const equals = (a, b) => {
|
|
118
|
-
const aa = Buffer.from(a), bb = Buffer.from(b);
|
|
119
|
-
return aa.length === bb.length && timingSafeEqual(aa, bb);
|
|
120
|
-
};
|
|
121
|
-
export class CoreRuntime {
|
|
122
|
-
options;
|
|
123
|
-
store;
|
|
124
|
-
vault;
|
|
125
|
-
kek;
|
|
126
|
-
kekPath;
|
|
127
|
-
running = new Map();
|
|
128
|
-
pending = new Set();
|
|
129
|
-
observers = new Map();
|
|
130
|
-
// Keyed by token hash so a rotation can end exactly the streams that the replaced token owns.
|
|
131
|
-
executorStreams = new Map();
|
|
132
|
-
registry = new ExecutorRegistry();
|
|
133
|
-
scopeId;
|
|
134
|
-
mcp;
|
|
135
|
-
sandbox;
|
|
136
|
-
server;
|
|
137
|
-
closing = false;
|
|
138
|
-
closed = false;
|
|
139
|
-
lockPath;
|
|
140
|
-
timer;
|
|
141
|
-
constructor(options) {
|
|
142
|
-
this.options = options;
|
|
143
|
-
if (options.leaseMs !== undefined &&
|
|
144
|
-
(!Number.isFinite(options.leaseMs) || options.leaseMs <= 0))
|
|
145
|
-
throw new Error("leaseMs must be finite and positive");
|
|
146
|
-
if (new Set(options.executors.map((e) => e.token)).size !==
|
|
147
|
-
options.executors.length)
|
|
148
|
-
throw new Error("Executor tokens must be unique");
|
|
149
|
-
if (!options.serverToken || options.serverToken.length < 16)
|
|
150
|
-
throw new Error("A server token of at least 16 characters is required");
|
|
151
|
-
for (const executor of options.executors)
|
|
152
|
-
assertExecutorCredential(executor, options.serverToken);
|
|
153
|
-
if (options.sqlitePath !== ":memory:") {
|
|
154
|
-
mkdirSync(dirname(options.sqlitePath), { recursive: true });
|
|
155
|
-
this.lockPath = options.sqlitePath + ".runtime-lock";
|
|
156
|
-
// One local scheduling owner; recover a stale process lock only after checking that its PID is absent.
|
|
157
|
-
try {
|
|
158
|
-
const oldPid = Number(readFileSync(this.lockPath, "utf8"));
|
|
159
|
-
if (!Number.isSafeInteger(oldPid) || oldPid < 1)
|
|
160
|
-
throw new Error("Invalid runtime lock; inspect before removing");
|
|
161
|
-
try {
|
|
162
|
-
process.kill(oldPid, 0);
|
|
163
|
-
throw new Error("Another Runtime owns this SQLite database");
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
if (error.code !== "ESRCH")
|
|
167
|
-
throw error;
|
|
168
|
-
unlinkSync(this.lockPath);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
if (error.code !== "ENOENT")
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
const fd = openSync(this.lockPath, "wx");
|
|
176
|
-
writeFileSync(fd, String(process.pid));
|
|
177
|
-
closeSync(fd);
|
|
178
|
-
}
|
|
179
|
-
this.kekPath = options.vaultKekPath ?? defaultKekPath();
|
|
180
|
-
let store;
|
|
181
|
-
try {
|
|
182
|
-
store = new Store(options.sqlitePath);
|
|
183
|
-
this.kek = readVaultKek({
|
|
184
|
-
vaultKek: options.vaultKek,
|
|
185
|
-
vaultKekPath: this.kekPath,
|
|
186
|
-
});
|
|
187
|
-
if (store.credentialCount() > 0 && !this.kek)
|
|
188
|
-
throw new Error("Vault key-encryption key is required to open this database");
|
|
189
|
-
this.store = store;
|
|
190
|
-
}
|
|
191
|
-
catch (e) {
|
|
192
|
-
store?.db.close();
|
|
193
|
-
if (this.lockPath)
|
|
194
|
-
unlinkSync(this.lockPath);
|
|
195
|
-
throw e;
|
|
196
|
-
}
|
|
197
|
-
// A short digest of the resolved database path identifies this scope to the CLI without
|
|
198
|
-
// disclosing the path itself over the unauthenticated health route.
|
|
199
|
-
this.scopeId =
|
|
200
|
-
options.sqlitePath === ":memory:"
|
|
201
|
-
? "memory"
|
|
202
|
-
: createHash("sha256")
|
|
203
|
-
.update(realpathSync(options.sqlitePath))
|
|
204
|
-
.digest("hex")
|
|
205
|
-
.slice(0, 16);
|
|
206
|
-
// Registrations persisted by PUT /v1/executors load first; scopes supplied through
|
|
207
|
-
// NYLORUN_EXECUTORS_JSON then overlay them for this process only and are never written back.
|
|
208
|
-
this.registry.seed(this.store.allExecutors().map((row) => ({
|
|
209
|
-
agentId: row.agentId,
|
|
210
|
-
implementationVersion: row.implementationVersion,
|
|
211
|
-
...(row.manifestHash === undefined
|
|
212
|
-
? {}
|
|
213
|
-
: { manifestHash: row.manifestHash }),
|
|
214
|
-
tokenHash: row.tokenHash,
|
|
215
|
-
persisted: true,
|
|
216
|
-
updatedAt: row.updatedAt,
|
|
217
|
-
})));
|
|
218
|
-
const now = new Date().toISOString();
|
|
219
|
-
for (const executor of options.executors) {
|
|
220
|
-
const tokenHash = hashToken(executor.token);
|
|
221
|
-
const collision = this.registry.find(tokenHash);
|
|
222
|
-
if (collision && collision.agentId !== executor.agentId) {
|
|
223
|
-
this.store.db.close();
|
|
224
|
-
if (this.lockPath)
|
|
225
|
-
unlinkSync(this.lockPath);
|
|
226
|
-
throw new Error("Executor tokens must be unique");
|
|
227
|
-
}
|
|
228
|
-
this.registry.upsert({
|
|
229
|
-
agentId: executor.agentId,
|
|
230
|
-
implementationVersion: executor.implementationVersion,
|
|
231
|
-
...(executor.manifestHash === undefined
|
|
232
|
-
? {}
|
|
233
|
-
: { manifestHash: executor.manifestHash }),
|
|
234
|
-
tokenHash,
|
|
235
|
-
persisted: false,
|
|
236
|
-
updatedAt: now,
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
this.vault = new VaultService(this.store.db, (fn) => this.store.tx(fn), () => this.ensureKek(), options.vaultFetch ?? globalThis.fetch);
|
|
240
|
-
this.mcp = new McpPool({
|
|
241
|
-
dataDir: dirname(options.sqlitePath),
|
|
242
|
-
authorize: (sessionId, request) => this.authorize(sessionId, request),
|
|
243
|
-
});
|
|
244
|
-
const ephemeral = options.sqlitePath === ":memory:";
|
|
245
|
-
const sandboxRoot = options.sandbox?.root ??
|
|
246
|
-
(ephemeral
|
|
247
|
-
? mkdtempSync(join(tmpdir(), "nylorun-sandboxes-"))
|
|
248
|
-
: join(dirname(options.sqlitePath), "sandboxes"));
|
|
249
|
-
this.sandbox = new SandboxManager({
|
|
250
|
-
scope: ephemeral ? `m${randomUUID().slice(0, 8)}` : this.scopeId,
|
|
251
|
-
store: this.store,
|
|
252
|
-
backends: options.sandbox?.backends ?? defaultSandboxBackends({ root: sandboxRoot }),
|
|
253
|
-
preference: options.sandbox?.backend ?? process.env.NYLORUN_SANDBOX,
|
|
254
|
-
ephemeral,
|
|
255
|
-
emit: (sessionId, turnId, type, payload) => {
|
|
256
|
-
if (this.closed)
|
|
257
|
-
return;
|
|
258
|
-
const event = this.store.tx(() => this.store.event(sessionId, turnId, type, payload));
|
|
259
|
-
this.publish(event);
|
|
260
|
-
},
|
|
261
|
-
});
|
|
262
|
-
if (!ephemeral && this.sandbox.hasRecords())
|
|
263
|
-
void this.sandbox
|
|
264
|
-
.reconcile((id) => !this.closed && !!this.store.get("sessions", id))
|
|
265
|
-
.catch(() => undefined);
|
|
266
|
-
this.store.tx(() => {
|
|
267
|
-
for (const effect of this.store.all("effects"))
|
|
268
|
-
if (effect.status === "invoking") {
|
|
269
|
-
effect.status = "uncertain";
|
|
270
|
-
this.store.put("effects", effect.request.effectId, effect);
|
|
271
|
-
const s = this.store.get("sessions", effect.request.sessionId);
|
|
272
|
-
if (s &&
|
|
273
|
-
s.status !== "cancelled" &&
|
|
274
|
-
s.activeTurnId === effect.request.turnId) {
|
|
275
|
-
s.status = "uncertain";
|
|
276
|
-
this.store.put("sessions", s.id, s);
|
|
277
|
-
this.store.event(s.id, s.activeTurnId, "effect.uncertain", {
|
|
278
|
-
effectId: effect.request.effectId,
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
this.retireLegacyHooks();
|
|
284
|
-
this.expireClaims();
|
|
285
|
-
this.timer = setInterval(() => this.expireClaims(), Math.min(options.leaseMs ?? 30000, 5000));
|
|
286
|
-
this.timer.unref();
|
|
287
|
-
for (const s of this.store.all("sessions"))
|
|
288
|
-
if (s.status === "running" || s.status === "runnable")
|
|
289
|
-
this.schedule(s.id);
|
|
290
|
-
}
|
|
291
|
-
session(id) {
|
|
292
|
-
return (this.store.get("sessions", id) ?? fail(404, "Session not found"));
|
|
293
|
-
}
|
|
294
|
-
publish(event) {
|
|
295
|
-
for (const response of this.observers.get(event.sessionId) ?? [])
|
|
296
|
-
this.send(response, `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
297
|
-
}
|
|
298
|
-
send(response, data) {
|
|
299
|
-
if (!response.write(data))
|
|
300
|
-
response.destroy();
|
|
301
|
-
}
|
|
302
|
-
notify() {
|
|
303
|
-
for (const streams of this.executorStreams.values())
|
|
304
|
-
for (const response of streams)
|
|
305
|
-
this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
|
-
* Manifest v4 replaced beforeModelCall/afterModelCall with scoped hooks. Turns started under
|
|
309
|
-
* an older manifest cannot continue, and their hook actions no longer parse, so end them.
|
|
310
|
-
*/
|
|
311
|
-
retireLegacyHooks() {
|
|
312
|
-
this.store.tx(() => {
|
|
313
|
-
for (const action of this.store.all("actions"))
|
|
314
|
-
if ((action.kind === "beforeModelCall" || action.kind === "afterModelCall") &&
|
|
315
|
-
(action.status === "pending" || action.status === "claimed")) {
|
|
316
|
-
action.status = "cancelled";
|
|
317
|
-
this.store.put("actions", action.actionId, action);
|
|
318
|
-
}
|
|
319
|
-
for (const s of this.store.all("sessions"))
|
|
320
|
-
if (s.activeTurnId && s.manifest?.manifestSchemaVersion !== 4) {
|
|
321
|
-
this.store.event(s.id, s.activeTurnId, "turn.failed", {
|
|
322
|
-
error: {
|
|
323
|
-
code: "execution.incompatible",
|
|
324
|
-
message: "This turn started under manifest schema 3; see MIGRATION.md for before/after hooks",
|
|
325
|
-
},
|
|
326
|
-
});
|
|
327
|
-
s.status = "failed";
|
|
328
|
-
s.activeTurnId = null;
|
|
329
|
-
this.store.put("sessions", s.id, s);
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
expireClaims() {
|
|
334
|
-
const events = [];
|
|
335
|
-
let redeliver = false;
|
|
336
|
-
this.store.tx(() => {
|
|
337
|
-
for (const action of this.store.all("actions"))
|
|
338
|
-
if (action.status === "claimed" &&
|
|
339
|
-
Date.parse(action.leaseExpiresAt) <= Date.now()) {
|
|
340
|
-
if (action.kind === "hook") {
|
|
341
|
-
// Hooks are pure by contract, so a lost claim is simply delivered again.
|
|
342
|
-
// The next claim bumps the generation, which fences any late result.
|
|
343
|
-
action.status = "pending";
|
|
344
|
-
action.claimId = null;
|
|
345
|
-
action.leaseExpiresAt = null;
|
|
346
|
-
this.store.put("actions", action.actionId, action);
|
|
347
|
-
redeliver = true;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
action.status = "uncertain";
|
|
351
|
-
this.store.put("actions", action.actionId, action);
|
|
352
|
-
const effect = this.store.get("effects", action.actionId);
|
|
353
|
-
effect.status = "uncertain";
|
|
354
|
-
this.store.put("effects", action.actionId, effect);
|
|
355
|
-
const s = this.session(action.sessionId);
|
|
356
|
-
if (s.status !== "cancelled" && s.activeTurnId === action.turnId) {
|
|
357
|
-
s.status = "uncertain";
|
|
358
|
-
this.store.put("sessions", s.id, s);
|
|
359
|
-
events.push(this.store.event(s.id, s.activeTurnId, "action.uncertain", {
|
|
360
|
-
actionId: action.actionId,
|
|
361
|
-
...(action.agent ? { agent: action.agent } : {}),
|
|
362
|
-
}));
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
events.forEach((e) => this.publish(e));
|
|
367
|
-
if (redeliver)
|
|
368
|
-
this.notify();
|
|
369
|
-
}
|
|
370
|
-
schedule(id) {
|
|
371
|
-
if (this.closing)
|
|
372
|
-
return;
|
|
373
|
-
this.pending.add(id);
|
|
374
|
-
setImmediate(() => {
|
|
375
|
-
if (!this.running.has(id) && this.pending.delete(id) && !this.closing)
|
|
376
|
-
void this.execute(id);
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
async execute(id) {
|
|
380
|
-
const s = this.session(id);
|
|
381
|
-
if (!s.checkpoint || !["running", "runnable"].includes(s.status))
|
|
382
|
-
return;
|
|
383
|
-
s.status = "running";
|
|
384
|
-
this.store.tx(() => this.store.put("sessions", id, s));
|
|
385
|
-
const controller = new AbortController();
|
|
386
|
-
this.running.set(id, controller);
|
|
387
|
-
try {
|
|
388
|
-
await this.prepareMcp(id, controller.signal);
|
|
389
|
-
const current = this.session(id);
|
|
390
|
-
const result = await runDurable({
|
|
391
|
-
manifest: current.manifest,
|
|
392
|
-
checkpoint: current.checkpoint,
|
|
393
|
-
signal: controller.signal,
|
|
394
|
-
sessionTools: sessionToolsOf(current.mcpSnapshot),
|
|
395
|
-
host: {
|
|
396
|
-
resolveEffect: (e) => this.resolveEffect(e, controller.signal),
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
let event;
|
|
400
|
-
this.store.tx(() => {
|
|
401
|
-
const current = this.session(id);
|
|
402
|
-
if (current.status === "cancelled" ||
|
|
403
|
-
current.activeTurnId !== s.activeTurnId)
|
|
404
|
-
return;
|
|
405
|
-
// A result can arrive during concurrent action persistence. Preserve the runnable marker.
|
|
406
|
-
const resumeRequested = current.status === "runnable";
|
|
407
|
-
if (result.status === "waiting" || result.status === "uncertain") {
|
|
408
|
-
current.status = resumeRequested
|
|
409
|
-
? "runnable"
|
|
410
|
-
: current.status === "uncertain"
|
|
411
|
-
? "uncertain"
|
|
412
|
-
: result.status;
|
|
413
|
-
current.waits = { effectIds: result.effectIds };
|
|
414
|
-
}
|
|
415
|
-
else if ("result" in result) {
|
|
416
|
-
current.state =
|
|
417
|
-
result.status === "failed"
|
|
418
|
-
? current.turnStartState
|
|
419
|
-
: result.result.state;
|
|
420
|
-
current.checkpoint = result.checkpoint;
|
|
421
|
-
this.store.put("checkpoints", JSON.stringify([id, s.activeTurnId, result.checkpoint.segment]), { checkpoint: result.checkpoint, status: result.status });
|
|
422
|
-
current.status = result.status;
|
|
423
|
-
current.waits =
|
|
424
|
-
result.status === "paused"
|
|
425
|
-
? result.result.pending
|
|
426
|
-
: undefined;
|
|
427
|
-
if (result.status !== "paused")
|
|
428
|
-
current.activeTurnId = null;
|
|
429
|
-
event = this.store.event(id, s.activeTurnId, `turn.${result.status}`, result.status === "completed"
|
|
430
|
-
? { output: result.result.output }
|
|
431
|
-
: result.status === "paused"
|
|
432
|
-
? {
|
|
433
|
-
interactions: (result.result.pending ?? []).map((call) => ({
|
|
434
|
-
invocationId: call.invocationId,
|
|
435
|
-
interaction: call.interaction,
|
|
436
|
-
wait: call.wait,
|
|
437
|
-
status: call.status,
|
|
438
|
-
})),
|
|
439
|
-
}
|
|
440
|
-
: result.status === "failed"
|
|
441
|
-
? {
|
|
442
|
-
error: {
|
|
443
|
-
code: result.result.error?.code,
|
|
444
|
-
message: result.result.error?.message,
|
|
445
|
-
},
|
|
446
|
-
}
|
|
447
|
-
: {});
|
|
448
|
-
}
|
|
449
|
-
this.store.put("sessions", id, current);
|
|
450
|
-
});
|
|
451
|
-
if (event)
|
|
452
|
-
this.publish(event);
|
|
453
|
-
}
|
|
454
|
-
catch (error) {
|
|
455
|
-
let event;
|
|
456
|
-
this.store.tx(() => {
|
|
457
|
-
const current = this.session(id);
|
|
458
|
-
if (current.status === "cancelled" ||
|
|
459
|
-
current.activeTurnId !== s.activeTurnId)
|
|
460
|
-
return;
|
|
461
|
-
current.status = "failed";
|
|
462
|
-
current.state = current.turnStartState;
|
|
463
|
-
if (current.checkpoint)
|
|
464
|
-
this.store.put("checkpoints", JSON.stringify([id, s.activeTurnId, current.checkpoint.segment]), { checkpoint: current.checkpoint, status: "failed" });
|
|
465
|
-
current.error = error instanceof Error ? error.message : String(error);
|
|
466
|
-
this.store.put("sessions", id, current);
|
|
467
|
-
event = this.store.event(id, current.activeTurnId, "turn.failed", {
|
|
468
|
-
message: current.error,
|
|
469
|
-
});
|
|
470
|
-
current.activeTurnId = null;
|
|
471
|
-
this.store.put("sessions", id, current);
|
|
472
|
-
});
|
|
473
|
-
if (event)
|
|
474
|
-
this.publish(event);
|
|
475
|
-
}
|
|
476
|
-
finally {
|
|
477
|
-
this.running.delete(id);
|
|
478
|
-
if (!this.closing &&
|
|
479
|
-
(this.pending.has(id) || this.session(id).status === "runnable"))
|
|
480
|
-
this.schedule(id);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
invokeModel(request, signal) {
|
|
484
|
-
if (!this.options.useHostModel)
|
|
485
|
-
return (this.options.model ?? scriptedModel())(request, signal);
|
|
486
|
-
const adapter = piModel({
|
|
487
|
-
readHostModel: () => this.vault.readHostModel(),
|
|
488
|
-
writeHostCredential: (credential) => this.vault.updateHostCredential(credential),
|
|
489
|
-
});
|
|
490
|
-
return adapter(request.input, {
|
|
491
|
-
request: request.context.request,
|
|
492
|
-
invocationId: String(request.context.invocationId),
|
|
493
|
-
signal,
|
|
494
|
-
reportPreparedCall() { },
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
async resolveEffect(request, signal) {
|
|
498
|
-
let invoke;
|
|
499
|
-
let notify = false;
|
|
500
|
-
let event;
|
|
501
|
-
const resolution = this.store.tx(() => {
|
|
502
|
-
const s = this.session(request.sessionId);
|
|
503
|
-
if (s.status === "cancelled" ||
|
|
504
|
-
s.activeTurnId !== request.turnId ||
|
|
505
|
-
signal.aborted)
|
|
506
|
-
throw new Error("Turn cancelled");
|
|
507
|
-
const existing = this.store.get("effects", request.effectId);
|
|
508
|
-
if (existing) {
|
|
509
|
-
if (canonical(existing.request) !== canonical(request))
|
|
510
|
-
throw new Error("Effect identity request drift");
|
|
511
|
-
return existing.status === "completed"
|
|
512
|
-
? { status: "completed", outcome: existing.outcome }
|
|
513
|
-
: {
|
|
514
|
-
status: existing.status === "uncertain" ||
|
|
515
|
-
existing.status === "invoking"
|
|
516
|
-
? "uncertain"
|
|
517
|
-
: "pending",
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
if (request.kind === "delegation") {
|
|
521
|
-
// Lifecycle points of an agent used as a tool: journaled once, so replays never re-emit.
|
|
522
|
-
const outcome = { value: null };
|
|
523
|
-
this.store.put("effects", request.effectId, {
|
|
524
|
-
request,
|
|
525
|
-
status: "completed",
|
|
526
|
-
outcome,
|
|
527
|
-
});
|
|
528
|
-
const settled = request.effectId.endsWith(":settled");
|
|
529
|
-
event = this.store.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", { agent: request.agent, ...request.input });
|
|
530
|
-
return { status: "completed", outcome };
|
|
531
|
-
}
|
|
532
|
-
const agentManifest = manifestFor(s.manifest, request.agent);
|
|
533
|
-
if (!agentManifest)
|
|
534
|
-
throw new Error(`Agent '${request.agent?.id ?? ""}' is not used as a tool`);
|
|
535
|
-
const mcpTool = request.kind === "tool" ? mcpToolOf(s, request) : undefined;
|
|
536
|
-
const sandboxTool = request.kind === "tool" && !mcpTool
|
|
537
|
-
? sandboxCapabilityOf(agentManifest, request.capabilityId, request.toolName)
|
|
538
|
-
: undefined;
|
|
539
|
-
this.store.put("effects", request.effectId, {
|
|
540
|
-
request,
|
|
541
|
-
status: request.kind === "model" || mcpTool || sandboxTool
|
|
542
|
-
? "invoking"
|
|
543
|
-
: "pending",
|
|
544
|
-
});
|
|
545
|
-
if (request.kind === "model" || mcpTool || sandboxTool) {
|
|
546
|
-
invoke = mcpTool ? "mcp" : sandboxTool ? "sandbox" : "model";
|
|
547
|
-
return undefined;
|
|
548
|
-
}
|
|
549
|
-
const tool = request.kind === "tool"
|
|
550
|
-
? pinnedTool(agentManifest, request.capabilityId, request.toolName)
|
|
551
|
-
: undefined;
|
|
552
|
-
const base = {
|
|
553
|
-
actionId: request.effectId,
|
|
554
|
-
sessionId: request.sessionId,
|
|
555
|
-
turnId: request.turnId,
|
|
556
|
-
agentId: request.agentId,
|
|
557
|
-
manifestHash: request.manifestHash,
|
|
558
|
-
implementationVersion: s.implementationVersion,
|
|
559
|
-
input: request.input,
|
|
560
|
-
context: request.context,
|
|
561
|
-
status: "pending",
|
|
562
|
-
generation: 0,
|
|
563
|
-
claimId: null,
|
|
564
|
-
leaseExpiresAt: null,
|
|
565
|
-
...(request.agent ? { agent: request.agent } : {}),
|
|
566
|
-
};
|
|
567
|
-
const action = request.kind === "hook"
|
|
568
|
-
? {
|
|
569
|
-
...base,
|
|
570
|
-
kind: "hook",
|
|
571
|
-
hook: {
|
|
572
|
-
at: request.hook.at,
|
|
573
|
-
scope: request.hook.scope,
|
|
574
|
-
capabilityIds: [...request.hook.capabilityIds],
|
|
575
|
-
},
|
|
576
|
-
}
|
|
577
|
-
: {
|
|
578
|
-
...base,
|
|
579
|
-
kind: "tool",
|
|
580
|
-
capabilityId: request.capabilityId,
|
|
581
|
-
toolName: request.toolName,
|
|
582
|
-
...(tool?.inputSchema ? { inputSchema: tool.inputSchema } : {}),
|
|
583
|
-
...(tool?.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
584
|
-
};
|
|
585
|
-
this.store.put("actions", action.actionId, action);
|
|
586
|
-
event = this.store.event(s.id, s.activeTurnId, "action.pending", {
|
|
587
|
-
actionId: action.actionId,
|
|
588
|
-
kind: action.kind,
|
|
589
|
-
...actionTarget(action),
|
|
590
|
-
input: action.input,
|
|
591
|
-
});
|
|
592
|
-
notify = true;
|
|
593
|
-
return { status: "pending" };
|
|
594
|
-
});
|
|
595
|
-
if (event)
|
|
596
|
-
this.publish(event);
|
|
597
|
-
if (notify)
|
|
598
|
-
this.notify();
|
|
599
|
-
if (!invoke)
|
|
600
|
-
return resolution;
|
|
601
|
-
try {
|
|
602
|
-
const value = invoke === "mcp"
|
|
603
|
-
? await this.callMcpTool(request)
|
|
604
|
-
: invoke === "sandbox"
|
|
605
|
-
? await this.callSandboxTool(request, signal)
|
|
606
|
-
: await this.invokeModel(request, signal);
|
|
607
|
-
return this.store.tx(() => {
|
|
608
|
-
const s = this.session(request.sessionId);
|
|
609
|
-
if (s.status === "cancelled" ||
|
|
610
|
-
s.activeTurnId !== request.turnId ||
|
|
611
|
-
signal.aborted)
|
|
612
|
-
throw new Error("Turn cancelled");
|
|
613
|
-
const effect = this.store.get("effects", request.effectId);
|
|
614
|
-
effect.status = "completed";
|
|
615
|
-
effect.outcome = { value };
|
|
616
|
-
this.store.put("effects", request.effectId, effect);
|
|
617
|
-
return { status: "completed", outcome: effect.outcome };
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
catch (error) {
|
|
621
|
-
let event;
|
|
622
|
-
this.store.tx(() => {
|
|
623
|
-
const effect = this.store.get("effects", request.effectId);
|
|
624
|
-
effect.status = "uncertain";
|
|
625
|
-
effect.error = error instanceof Error ? error.message : String(error);
|
|
626
|
-
this.store.put("effects", request.effectId, effect);
|
|
627
|
-
const s = this.session(request.sessionId);
|
|
628
|
-
if (s.status !== "cancelled" && s.activeTurnId === request.turnId)
|
|
629
|
-
event = this.store.event(s.id, request.turnId, "effect.uncertain", {
|
|
630
|
-
effectId: request.effectId,
|
|
631
|
-
message: effect.error,
|
|
632
|
-
});
|
|
633
|
-
});
|
|
634
|
-
if (event)
|
|
635
|
-
this.publish(event);
|
|
636
|
-
return { status: "uncertain" };
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
scope(request) {
|
|
640
|
-
const header = request.headers.authorization;
|
|
641
|
-
const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
|
|
642
|
-
if (!token)
|
|
643
|
-
return fail(401, "Invalid credentials");
|
|
644
|
-
if (equals(token, this.options.serverToken))
|
|
645
|
-
return "server";
|
|
646
|
-
// The map is keyed by a digest of the presented token, so the lookup reveals nothing the
|
|
647
|
-
// caller does not already hold; the server token keeps its constant-time comparison.
|
|
648
|
-
const executor = this.registry.find(hashToken(token));
|
|
649
|
-
if (!executor)
|
|
650
|
-
return fail(401, "Invalid credentials");
|
|
651
|
-
return executor;
|
|
652
|
-
}
|
|
653
|
-
scoped(scope, action) {
|
|
654
|
-
if (scope === "server" || scope.agentId !== action.agentId)
|
|
655
|
-
fail(403, "Executor scope does not authorize this action");
|
|
656
|
-
}
|
|
657
|
-
async body(request) {
|
|
658
|
-
let data = "";
|
|
659
|
-
for await (const chunk of request) {
|
|
660
|
-
data += chunk;
|
|
661
|
-
if (Buffer.byteLength(data) > 1024 * 1024)
|
|
662
|
-
fail(413, "Request too large");
|
|
663
|
-
}
|
|
664
|
-
try {
|
|
665
|
-
return JSON.parse(data);
|
|
666
|
-
}
|
|
667
|
-
catch {
|
|
668
|
-
return fail(400, "Invalid JSON");
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
command(id, input, scope) {
|
|
672
|
-
let event;
|
|
673
|
-
let schedule = false;
|
|
674
|
-
const response = this.store.tx(() => {
|
|
675
|
-
const s = this.session(id);
|
|
676
|
-
let command = input;
|
|
677
|
-
if (command.type === "action_result") {
|
|
678
|
-
const a = this.store.get("actions", command.actionId) ??
|
|
679
|
-
fail(404, "Action not found");
|
|
680
|
-
if (a.sessionId !== id)
|
|
681
|
-
fail(403, "Action belongs to another session");
|
|
682
|
-
this.scoped(scope, a);
|
|
683
|
-
command = acceptedToolResult(a, command);
|
|
684
|
-
}
|
|
685
|
-
else if (scope !== "server")
|
|
686
|
-
fail(403, "Application credential required");
|
|
687
|
-
const key = JSON.stringify([id, command.idempotencyKey]);
|
|
688
|
-
const existing = this.store.get("commands", key);
|
|
689
|
-
if (existing) {
|
|
690
|
-
if (semantic(existing.command) !== semantic(command))
|
|
691
|
-
fail(409, "Idempotency key already binds another command");
|
|
692
|
-
return existing.response;
|
|
693
|
-
}
|
|
694
|
-
if (command.type === "action_result") {
|
|
695
|
-
const action = this.store.get("actions", command.actionId);
|
|
696
|
-
const prior = this.store.get("effects", command.actionId);
|
|
697
|
-
if (action.status === "completed") {
|
|
698
|
-
if (action.claimId !== command.claimId ||
|
|
699
|
-
action.generation !== command.generation ||
|
|
700
|
-
canonical(prior.outcome) !== canonical(command.outcome))
|
|
701
|
-
fail(409, "Conflicting action result");
|
|
702
|
-
this.store.put("commands", key, { command, response: prior.receipt });
|
|
703
|
-
return prior.receipt;
|
|
704
|
-
}
|
|
705
|
-
if (s.status === "cancelled" ||
|
|
706
|
-
s.activeTurnId !== action.turnId ||
|
|
707
|
-
action.status !== "claimed" ||
|
|
708
|
-
action.claimId !== command.claimId ||
|
|
709
|
-
action.generation !== command.generation ||
|
|
710
|
-
Date.parse(action.leaseExpiresAt) <= Date.now())
|
|
711
|
-
fail(409, "Stale, expired, or cancelled claim");
|
|
712
|
-
action.status = "completed";
|
|
713
|
-
this.store.put("actions", action.actionId, action);
|
|
714
|
-
prior.status = "completed";
|
|
715
|
-
prior.outcome = command.outcome;
|
|
716
|
-
s.status = "runnable";
|
|
717
|
-
schedule = true;
|
|
718
|
-
event = this.store.event(id, s.activeTurnId, "action.completed", {
|
|
719
|
-
actionId: action.actionId,
|
|
720
|
-
...actionTarget(action),
|
|
721
|
-
kind: action.kind,
|
|
722
|
-
result: command.outcome.value,
|
|
723
|
-
});
|
|
724
|
-
const receipt = {
|
|
725
|
-
status: "accepted",
|
|
726
|
-
turnId: s.activeTurnId,
|
|
727
|
-
cursor: event.cursor,
|
|
728
|
-
requestId: command.requestId,
|
|
729
|
-
};
|
|
730
|
-
prior.receipt = receipt;
|
|
731
|
-
this.store.put("effects", action.actionId, prior);
|
|
732
|
-
}
|
|
733
|
-
else if (command.type === "cancel") {
|
|
734
|
-
const cancelledTurnId = s.activeTurnId;
|
|
735
|
-
s.status = "cancelled";
|
|
736
|
-
for (const a of this.store.all("actions"))
|
|
737
|
-
if (a.sessionId === id &&
|
|
738
|
-
a.turnId === cancelledTurnId &&
|
|
739
|
-
["pending", "claimed"].includes(a.status)) {
|
|
740
|
-
// Claimed work may already have an external effect. Preserve it for reconciliation.
|
|
741
|
-
a.status = a.status === "claimed" ? "uncertain" : "cancelled";
|
|
742
|
-
this.store.put("actions", a.actionId, a);
|
|
743
|
-
const effect = this.store.get("effects", a.actionId);
|
|
744
|
-
effect.status = a.status;
|
|
745
|
-
this.store.put("effects", a.actionId, effect);
|
|
746
|
-
}
|
|
747
|
-
for (const effect of this.store.all("effects"))
|
|
748
|
-
if (effect.request.sessionId === id &&
|
|
749
|
-
effect.request.turnId === cancelledTurnId &&
|
|
750
|
-
effect.status === "invoking") {
|
|
751
|
-
effect.status = "uncertain";
|
|
752
|
-
effect.error =
|
|
753
|
-
"Turn cancelled before model outcome was durably recorded";
|
|
754
|
-
this.store.put("effects", effect.request.effectId, effect);
|
|
755
|
-
}
|
|
756
|
-
event = this.store.event(id, cancelledTurnId, "turn.cancelled", {
|
|
757
|
-
reason: command.reason,
|
|
758
|
-
});
|
|
759
|
-
s.activeTurnId = null;
|
|
760
|
-
// The next turn starts from the state preceding the cancelled turn, never its paused plan.
|
|
761
|
-
if (cancelledTurnId !== null)
|
|
762
|
-
s.state = s.turnStartState;
|
|
763
|
-
s.checkpoint = undefined;
|
|
764
|
-
s.waits = undefined;
|
|
765
|
-
s.error = undefined;
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
if (command.type === "message") {
|
|
769
|
-
if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
|
|
770
|
-
fail(409, "Session has active or unresolved work");
|
|
771
|
-
s.turnStartState = s.state;
|
|
772
|
-
s.activeTurnId = randomUUID();
|
|
773
|
-
s.checkpoint = createDurableCheckpoint({
|
|
774
|
-
manifest: s.manifest,
|
|
775
|
-
sessionId: id,
|
|
776
|
-
turnId: s.activeTurnId,
|
|
777
|
-
input: command.content,
|
|
778
|
-
state: s.state,
|
|
779
|
-
info: s.info,
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
if (s.status !== "paused" || !s.state || !s.activeTurnId)
|
|
784
|
-
fail(409, "Session is not awaiting a response");
|
|
785
|
-
const pending = s.state.plan?.calls ?? [];
|
|
786
|
-
if (!pending.some((c) => c.status === "interaction" &&
|
|
787
|
-
c.interaction?.id === command.interactionId &&
|
|
788
|
-
(command.type === "approve") ===
|
|
789
|
-
(c.interaction?.kind === "approval")))
|
|
790
|
-
fail(409, "Unknown interaction");
|
|
791
|
-
const input = command.type === "approve"
|
|
792
|
-
? {
|
|
793
|
-
kind: "approve",
|
|
794
|
-
interactionId: command.interactionId,
|
|
795
|
-
approved: command.approved,
|
|
796
|
-
}
|
|
797
|
-
: {
|
|
798
|
-
kind: "respond",
|
|
799
|
-
interactionId: command.interactionId,
|
|
800
|
-
value: command.value,
|
|
801
|
-
};
|
|
802
|
-
s.checkpoint = createDurableCheckpoint({
|
|
803
|
-
manifest: s.manifest,
|
|
804
|
-
sessionId: id,
|
|
805
|
-
turnId: s.activeTurnId,
|
|
806
|
-
input: input,
|
|
807
|
-
state: s.state,
|
|
808
|
-
info: s.info,
|
|
809
|
-
segment: (s.checkpoint?.segment ?? 0) + 1,
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
this.store.put("checkpoints", JSON.stringify([id, s.activeTurnId, s.checkpoint.segment]), { checkpoint: s.checkpoint, status: "runnable" });
|
|
813
|
-
s.status = "runnable";
|
|
814
|
-
s.waits = undefined;
|
|
815
|
-
schedule = true;
|
|
816
|
-
event = this.store.event(id, s.activeTurnId, `command.${command.type}`, command);
|
|
817
|
-
}
|
|
818
|
-
this.store.put("sessions", id, s);
|
|
819
|
-
const response = {
|
|
820
|
-
status: "accepted",
|
|
821
|
-
turnId: s.activeTurnId,
|
|
822
|
-
cursor: event?.cursor ?? this.store.history(id).cursor,
|
|
823
|
-
requestId: command.requestId,
|
|
824
|
-
};
|
|
825
|
-
this.store.put("commands", key, { command, response });
|
|
826
|
-
return response;
|
|
827
|
-
});
|
|
828
|
-
if (event)
|
|
829
|
-
this.publish(event);
|
|
830
|
-
if (input.type === "cancel")
|
|
831
|
-
this.running.get(id)?.abort();
|
|
832
|
-
if (schedule)
|
|
833
|
-
this.schedule(id);
|
|
834
|
-
return response;
|
|
835
|
-
}
|
|
836
|
-
sse(request, response, set, whenEmpty) {
|
|
837
|
-
response.writeHead(200, {
|
|
838
|
-
"content-type": "text/event-stream",
|
|
839
|
-
"cache-control": "no-cache",
|
|
840
|
-
connection: "keep-alive",
|
|
841
|
-
"x-accel-buffering": "no",
|
|
842
|
-
});
|
|
843
|
-
response.flushHeaders();
|
|
844
|
-
set.add(response);
|
|
845
|
-
const timer = setInterval(() => this.send(response, ": keepalive\n\n"), 15000);
|
|
846
|
-
timer.unref();
|
|
847
|
-
request.on("close", () => {
|
|
848
|
-
clearInterval(timer);
|
|
849
|
-
set.delete(response);
|
|
850
|
-
if (!set.size)
|
|
851
|
-
whenEmpty?.();
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
async handle(request, response) {
|
|
855
|
-
const json = (value, status = 200) => {
|
|
856
|
-
response.writeHead(status, { "content-type": "application/json" });
|
|
857
|
-
response.end(JSON.stringify(value));
|
|
858
|
-
};
|
|
859
|
-
try {
|
|
860
|
-
const url = new URL(request.url ?? "/", "http://runtime");
|
|
861
|
-
const path = url.pathname
|
|
862
|
-
.split("/")
|
|
863
|
-
.filter(Boolean)
|
|
864
|
-
.map(decodeURIComponent);
|
|
865
|
-
const method = request.method;
|
|
866
|
-
if (url.pathname === "/health")
|
|
867
|
-
return json({
|
|
868
|
-
status: "ok",
|
|
869
|
-
service: "oss-runtime",
|
|
870
|
-
version: RUNTIME_VERSION,
|
|
871
|
-
scopeId: this.scopeId,
|
|
872
|
-
pid: process.pid,
|
|
873
|
-
});
|
|
874
|
-
if (url.pathname === "/ready") {
|
|
875
|
-
this.store.db.prepare("SELECT 1").get();
|
|
876
|
-
return json({
|
|
877
|
-
status: this.closing ? "not_ready" : "ready",
|
|
878
|
-
service: "oss-runtime",
|
|
879
|
-
checks: {
|
|
880
|
-
sqlite: true,
|
|
881
|
-
scheduler: !this.closing,
|
|
882
|
-
modelConfigured: true,
|
|
883
|
-
},
|
|
884
|
-
}, this.closing ? 503 : 200);
|
|
885
|
-
}
|
|
886
|
-
const scope = this.scope(request);
|
|
887
|
-
if (path[0] !== "v1")
|
|
888
|
-
fail(404, "Route not found");
|
|
889
|
-
if (path[1] === "executors" &&
|
|
890
|
-
path[2] === "connect" &&
|
|
891
|
-
method === "GET") {
|
|
892
|
-
if (scope === "server")
|
|
893
|
-
fail(403, "Executor credential required");
|
|
894
|
-
const tokenHash = scope.tokenHash;
|
|
895
|
-
let streams = this.executorStreams.get(tokenHash);
|
|
896
|
-
if (!streams)
|
|
897
|
-
this.executorStreams.set(tokenHash, (streams = new Set()));
|
|
898
|
-
this.sse(request, response, streams, () => this.executorStreams.delete(tokenHash));
|
|
899
|
-
this.send(response, 'event: work_available\ndata: {"type":"work_available"}\n\n');
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (path[1] === "actions") {
|
|
903
|
-
if (scope === "server")
|
|
904
|
-
return fail(403, "Executor credential required");
|
|
905
|
-
this.expireClaims();
|
|
906
|
-
if (path.length === 2 && method === "GET")
|
|
907
|
-
return json({
|
|
908
|
-
actions: this.store
|
|
909
|
-
.all("actions")
|
|
910
|
-
.filter((a) => a.status === "pending" && a.agentId === scope.agentId),
|
|
911
|
-
});
|
|
912
|
-
const actionId = path[2];
|
|
913
|
-
if (!actionId)
|
|
914
|
-
fail(404, "Action not found");
|
|
915
|
-
const body = await this.body(request);
|
|
916
|
-
let event;
|
|
917
|
-
const result = this.store.tx(() => {
|
|
918
|
-
const action = this.store.get("actions", actionId) ??
|
|
919
|
-
fail(404, "Action not found");
|
|
920
|
-
this.scoped(scope, action);
|
|
921
|
-
if (method === "POST" && path[3] === "claim") {
|
|
922
|
-
ActionClaimRequestSchema.parse(body);
|
|
923
|
-
if (action.status !== "pending" ||
|
|
924
|
-
this.session(action.sessionId).status === "cancelled" ||
|
|
925
|
-
this.session(action.sessionId).activeTurnId !== action.turnId)
|
|
926
|
-
fail(409, "Action unavailable");
|
|
927
|
-
action.status = "claimed";
|
|
928
|
-
action.generation++;
|
|
929
|
-
action.claimId = randomUUID();
|
|
930
|
-
action.leaseExpiresAt = new Date(Date.now() + (this.options.leaseMs ?? 30000)).toISOString();
|
|
931
|
-
this.store.put("actions", actionId, action);
|
|
932
|
-
event = this.store.event(action.sessionId, action.turnId, "action.claimed", {
|
|
933
|
-
actionId,
|
|
934
|
-
generation: action.generation,
|
|
935
|
-
...(action.agent ? { agent: action.agent } : {}),
|
|
936
|
-
});
|
|
937
|
-
return {
|
|
938
|
-
action,
|
|
939
|
-
claimId: action.claimId,
|
|
940
|
-
generation: action.generation,
|
|
941
|
-
leaseExpiresAt: action.leaseExpiresAt,
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
if (method === "POST" && path[3] === "heartbeat") {
|
|
945
|
-
const beat = ActionHeartbeatRequestSchema.parse(body);
|
|
946
|
-
if (this.session(action.sessionId).activeTurnId !== action.turnId ||
|
|
947
|
-
action.status !== "claimed" ||
|
|
948
|
-
action.claimId !== beat.claimId ||
|
|
949
|
-
action.generation !== beat.generation ||
|
|
950
|
-
Date.parse(action.leaseExpiresAt) <= Date.now())
|
|
951
|
-
fail(409, "Stale or expired claim");
|
|
952
|
-
action.leaseExpiresAt = new Date(Date.now() + (this.options.leaseMs ?? 30000)).toISOString();
|
|
953
|
-
this.store.put("actions", actionId, action);
|
|
954
|
-
return { leaseExpiresAt: action.leaseExpiresAt };
|
|
955
|
-
}
|
|
956
|
-
return fail(404, "Route not found");
|
|
957
|
-
});
|
|
958
|
-
if (event)
|
|
959
|
-
this.publish(event);
|
|
960
|
-
return json(result);
|
|
961
|
-
}
|
|
962
|
-
if (path[1] === "sessions" &&
|
|
963
|
-
path[2] &&
|
|
964
|
-
path[3] === "commands" &&
|
|
965
|
-
method === "POST")
|
|
966
|
-
return json(this.command(path[2], SessionCommandSchema.parse(await this.body(request)), scope));
|
|
967
|
-
if (path[1] === "vaults")
|
|
968
|
-
return json(await this.dispatchVault(scope, method, path, url, request));
|
|
969
|
-
if (path[1] === "host")
|
|
970
|
-
return json(await this.dispatchHost(scope, method, path, request));
|
|
971
|
-
if (scope !== "server")
|
|
972
|
-
fail(403, "Application credential required");
|
|
973
|
-
if (path[1] === "executors" && path.length === 2 && method === "GET")
|
|
974
|
-
return json({
|
|
975
|
-
executors: this.registry.list().map((e) => ({
|
|
976
|
-
agentId: e.agentId,
|
|
977
|
-
implementationVersion: e.implementationVersion,
|
|
978
|
-
...(e.manifestHash === undefined
|
|
979
|
-
? {}
|
|
980
|
-
: { manifestHash: e.manifestHash }),
|
|
981
|
-
connected: (this.executorStreams.get(e.tokenHash)?.size ?? 0) > 0,
|
|
982
|
-
updatedAt: e.updatedAt,
|
|
983
|
-
})),
|
|
984
|
-
});
|
|
985
|
-
// The length guard matters: the connect branch above only matches GET, so without it a
|
|
986
|
-
// PUT to /v1/executors/connect would register an agent literally named "connect".
|
|
987
|
-
if (path[1] === "executors" && path.length === 2 && method === "PUT") {
|
|
988
|
-
const body = RegisterExecutorsRequestSchema.parse(await this.body(request));
|
|
989
|
-
// Credential rules are shared with startup validation, which throws plainly; on the
|
|
990
|
-
// wire a rejected registration is a bad request, not a server fault.
|
|
991
|
-
try {
|
|
992
|
-
for (const executor of body.executors)
|
|
993
|
-
assertExecutorCredential(executor, this.options.serverToken);
|
|
994
|
-
}
|
|
995
|
-
catch (error) {
|
|
996
|
-
fail(400, error instanceof Error ? error.message : String(error));
|
|
997
|
-
}
|
|
998
|
-
if (new Set(body.executors.map((e) => e.agentId)).size !==
|
|
999
|
-
body.executors.length ||
|
|
1000
|
-
new Set(body.executors.map((e) => e.token)).size !==
|
|
1001
|
-
body.executors.length)
|
|
1002
|
-
fail(400, "Executor registrations must be unique per agent and token");
|
|
1003
|
-
const updatedAt = new Date().toISOString();
|
|
1004
|
-
const records = body.executors.map((executor) => ({
|
|
1005
|
-
agentId: executor.agentId,
|
|
1006
|
-
implementationVersion: executor.implementationVersion,
|
|
1007
|
-
...(executor.manifestHash === undefined
|
|
1008
|
-
? {}
|
|
1009
|
-
: { manifestHash: executor.manifestHash }),
|
|
1010
|
-
tokenHash: hashToken(executor.token),
|
|
1011
|
-
persisted: true,
|
|
1012
|
-
updatedAt,
|
|
1013
|
-
}));
|
|
1014
|
-
for (const record of records) {
|
|
1015
|
-
const collision = this.registry.find(record.tokenHash);
|
|
1016
|
-
if (collision && collision.agentId !== record.agentId)
|
|
1017
|
-
fail(409, "Executor tokens must be unique");
|
|
1018
|
-
}
|
|
1019
|
-
// Persist the whole batch first; the in-memory registry must never run ahead of SQLite.
|
|
1020
|
-
this.store.tx(() => {
|
|
1021
|
-
for (const record of records)
|
|
1022
|
-
this.store.putExecutor({
|
|
1023
|
-
agentId: record.agentId,
|
|
1024
|
-
tokenHash: record.tokenHash,
|
|
1025
|
-
implementationVersion: record.implementationVersion,
|
|
1026
|
-
...(record.manifestHash === undefined
|
|
1027
|
-
? {}
|
|
1028
|
-
: { manifestHash: record.manifestHash }),
|
|
1029
|
-
updatedAt,
|
|
1030
|
-
});
|
|
1031
|
-
});
|
|
1032
|
-
const results = records.map((record) => {
|
|
1033
|
-
const { rotated, previousHash } = this.registry.upsert(record);
|
|
1034
|
-
if (rotated && previousHash) {
|
|
1035
|
-
for (const r of this.executorStreams.get(previousHash) ?? [])
|
|
1036
|
-
r.end();
|
|
1037
|
-
this.executorStreams.delete(previousHash);
|
|
1038
|
-
}
|
|
1039
|
-
return {
|
|
1040
|
-
agentId: record.agentId,
|
|
1041
|
-
implementationVersion: record.implementationVersion,
|
|
1042
|
-
rotated,
|
|
1043
|
-
};
|
|
1044
|
-
});
|
|
1045
|
-
return json({ executors: results });
|
|
1046
|
-
}
|
|
1047
|
-
if (path[1] === "executors" &&
|
|
1048
|
-
path.length === 3 &&
|
|
1049
|
-
path[2] &&
|
|
1050
|
-
method === "DELETE") {
|
|
1051
|
-
const agentId = path[2];
|
|
1052
|
-
const existing = this.registry.get(agentId) ?? fail(404, "Executor not found");
|
|
1053
|
-
if (!existing.persisted)
|
|
1054
|
-
fail(409, "Executor is provided by NYLORUN_EXECUTORS_JSON and cannot be deleted");
|
|
1055
|
-
this.store.tx(() => this.store.deleteExecutor(agentId));
|
|
1056
|
-
this.registry.remove(agentId);
|
|
1057
|
-
for (const r of this.executorStreams.get(existing.tokenHash) ?? [])
|
|
1058
|
-
r.end();
|
|
1059
|
-
this.executorStreams.delete(existing.tokenHash);
|
|
1060
|
-
return json({ agentId, deleted: true });
|
|
1061
|
-
}
|
|
1062
|
-
if (path[1] === "agents" && path.length === 2 && method === "GET")
|
|
1063
|
-
return json({
|
|
1064
|
-
agents: this.store.all("definitions").map((d) => ({
|
|
1065
|
-
agentId: d.manifest.id,
|
|
1066
|
-
manifest: d.manifest,
|
|
1067
|
-
manifestHash: d.manifestHash,
|
|
1068
|
-
implementationVersion: d.implementationVersion,
|
|
1069
|
-
})),
|
|
1070
|
-
});
|
|
1071
|
-
if (path[1] === "sessions" && path.length === 2 && method === "GET")
|
|
1072
|
-
return json({
|
|
1073
|
-
sessions: this.store
|
|
1074
|
-
.all("sessions")
|
|
1075
|
-
.filter((s) => !url.searchParams.has("agentId") ||
|
|
1076
|
-
s.agentId === url.searchParams.get("agentId"))
|
|
1077
|
-
.map((s) => ({
|
|
1078
|
-
id: s.id,
|
|
1079
|
-
agentId: s.agentId,
|
|
1080
|
-
ownerUserId: s.ownerUserId,
|
|
1081
|
-
status: s.status,
|
|
1082
|
-
activeTurnId: s.activeTurnId,
|
|
1083
|
-
})),
|
|
1084
|
-
});
|
|
1085
|
-
if (path[1] === "agents" &&
|
|
1086
|
-
path[2] &&
|
|
1087
|
-
path.length === 3 &&
|
|
1088
|
-
method === "PUT") {
|
|
1089
|
-
const body = PutAgentRequestSchema.parse(await this.body(request));
|
|
1090
|
-
if (body.manifest.id !== path[2])
|
|
1091
|
-
fail(400, "Agent id mismatch");
|
|
1092
|
-
AgentManifestSchema.parse(body.manifest);
|
|
1093
|
-
const definition = {
|
|
1094
|
-
...body,
|
|
1095
|
-
manifestHash: hashManifest(body.manifest),
|
|
1096
|
-
};
|
|
1097
|
-
this.store.tx(() => {
|
|
1098
|
-
this.store.put("definitions", path[2], definition);
|
|
1099
|
-
});
|
|
1100
|
-
return json({
|
|
1101
|
-
agentId: path[2],
|
|
1102
|
-
manifestHash: definition.manifestHash,
|
|
1103
|
-
implementationVersion: body.implementationVersion,
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
if (path[1] === "sessions" && path[2]) {
|
|
1107
|
-
const id = path[2];
|
|
1108
|
-
if (method === "PUT" && path.length === 3) {
|
|
1109
|
-
const body = PutSessionRequestSchema.parse(await this.body(request));
|
|
1110
|
-
const vaultIds = body.vaultIds ?? [];
|
|
1111
|
-
const credentialSelections = body.credentialSelections ?? [];
|
|
1112
|
-
const result = this.store.tx(() => {
|
|
1113
|
-
this.vault.assertAttachment(body.ownerUserId, vaultIds, credentialSelections);
|
|
1114
|
-
const prior = this.store.get("sessions", id);
|
|
1115
|
-
if (prior) {
|
|
1116
|
-
if (sessionIdentity(prior.creation) !== sessionIdentity(body))
|
|
1117
|
-
fail(409, "Session already exists with different creation parameters");
|
|
1118
|
-
prior.vaultIds = vaultIds;
|
|
1119
|
-
prior.credentialSelections = credentialSelections;
|
|
1120
|
-
prior.creation = body;
|
|
1121
|
-
this.store.put("sessions", id, prior);
|
|
1122
|
-
this.vault.recordAttachment(id, vaultIds);
|
|
1123
|
-
return prior;
|
|
1124
|
-
}
|
|
1125
|
-
const definition = this.store.get("definitions", body.agentId) ??
|
|
1126
|
-
fail(404, "Definition not found");
|
|
1127
|
-
const s = {
|
|
1128
|
-
id,
|
|
1129
|
-
agentId: body.agentId,
|
|
1130
|
-
ownerUserId: body.ownerUserId,
|
|
1131
|
-
manifest: definition.manifest,
|
|
1132
|
-
manifestHash: definition.manifestHash,
|
|
1133
|
-
implementationVersion: definition.implementationVersion,
|
|
1134
|
-
info: body.info,
|
|
1135
|
-
status: "idle",
|
|
1136
|
-
activeTurnId: null,
|
|
1137
|
-
creation: body,
|
|
1138
|
-
vaultIds,
|
|
1139
|
-
credentialSelections,
|
|
1140
|
-
pluginRoots: definition.pluginRoots ?? {},
|
|
1141
|
-
};
|
|
1142
|
-
this.store.put("sessions", id, s);
|
|
1143
|
-
this.vault.recordAttachment(id, vaultIds);
|
|
1144
|
-
return s;
|
|
1145
|
-
});
|
|
1146
|
-
return json(this.view(result));
|
|
1147
|
-
}
|
|
1148
|
-
const s = this.session(id);
|
|
1149
|
-
if (method === "GET" && path.length === 3)
|
|
1150
|
-
return json(this.view(s));
|
|
1151
|
-
const cursor = url.searchParams.get("cursor") ??
|
|
1152
|
-
(typeof request.headers["last-event-id"] === "string"
|
|
1153
|
-
? request.headers["last-event-id"]
|
|
1154
|
-
: undefined);
|
|
1155
|
-
if (method === "GET" && path[3] === "items")
|
|
1156
|
-
return json(this.store.history(id, cursor, url.searchParams.get("agent") ?? undefined));
|
|
1157
|
-
if (method === "GET" && path[3] === "events") {
|
|
1158
|
-
const history = this.store.history(id, cursor);
|
|
1159
|
-
const set = this.observers.get(id) ?? new Set();
|
|
1160
|
-
this.observers.set(id, set);
|
|
1161
|
-
this.sse(request, response, set);
|
|
1162
|
-
for (const event of history.items)
|
|
1163
|
-
this.send(response, `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
fail(404, "Route not found");
|
|
1168
|
-
}
|
|
1169
|
-
catch (error) {
|
|
1170
|
-
if (response.headersSent) {
|
|
1171
|
-
response.end();
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
const status = error instanceof HttpError || error instanceof VaultError
|
|
1175
|
-
? error.status
|
|
1176
|
-
: error?.name === "ZodError" ||
|
|
1177
|
-
error?.message === "Invalid cursor"
|
|
1178
|
-
? 400
|
|
1179
|
-
: 500;
|
|
1180
|
-
json({
|
|
1181
|
-
status: "rejected",
|
|
1182
|
-
code: status === 500 ? "internal_error" : "request_rejected",
|
|
1183
|
-
message: status === 500
|
|
1184
|
-
? "Runtime request failed"
|
|
1185
|
-
: error.message,
|
|
1186
|
-
}, status);
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
view(s) {
|
|
1190
|
-
return {
|
|
1191
|
-
id: s.id,
|
|
1192
|
-
agentId: s.agentId,
|
|
1193
|
-
ownerUserId: s.ownerUserId,
|
|
1194
|
-
manifestHash: s.manifestHash,
|
|
1195
|
-
implementationVersion: s.implementationVersion,
|
|
1196
|
-
status: s.status,
|
|
1197
|
-
activeTurnId: s.activeTurnId,
|
|
1198
|
-
vaultIds: s.vaultIds ?? [],
|
|
1199
|
-
credentialSelections: s.credentialSelections ?? [],
|
|
1200
|
-
mcpSnapshot: s.mcpSnapshot ?? null,
|
|
1201
|
-
mcpDiagnostics: s.mcpDiagnostics ?? [],
|
|
1202
|
-
waits: Array.isArray(s.waits)
|
|
1203
|
-
? s.waits.map((call) => ({
|
|
1204
|
-
invocationId: call.invocationId,
|
|
1205
|
-
interaction: call.interaction,
|
|
1206
|
-
wait: call.wait,
|
|
1207
|
-
status: call.status,
|
|
1208
|
-
}))
|
|
1209
|
-
: s.waits,
|
|
1210
|
-
error: s.error,
|
|
1211
|
-
actions: this.store
|
|
1212
|
-
.all("actions")
|
|
1213
|
-
.filter((a) => a.sessionId === s.id &&
|
|
1214
|
-
!["completed", "cancelled"].includes(a.status)),
|
|
1215
|
-
uncertainEffects: this.store
|
|
1216
|
-
.all("effects")
|
|
1217
|
-
.filter((e) => e.request.sessionId === s.id && e.status === "uncertain")
|
|
1218
|
-
.map((e) => ({
|
|
1219
|
-
effectId: e.request.effectId,
|
|
1220
|
-
turnId: e.request.turnId,
|
|
1221
|
-
kind: e.request.kind,
|
|
1222
|
-
error: e.error,
|
|
1223
|
-
})),
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
async prepareMcp(id, signal) {
|
|
1227
|
-
const s = this.session(id);
|
|
1228
|
-
if (serversOf(s.manifest).length === 0)
|
|
1229
|
-
return;
|
|
1230
|
-
if (!s.mcpSnapshot) {
|
|
1231
|
-
const found = await this.mcp.discover({
|
|
1232
|
-
sessionId: id,
|
|
1233
|
-
manifest: s.manifest,
|
|
1234
|
-
manifestHash: s.manifestHash,
|
|
1235
|
-
pluginRoots: s.pluginRoots ?? {},
|
|
1236
|
-
signal,
|
|
1237
|
-
});
|
|
1238
|
-
this.store.tx(() => {
|
|
1239
|
-
const current = this.session(id);
|
|
1240
|
-
if (current.mcpSnapshot)
|
|
1241
|
-
return;
|
|
1242
|
-
current.mcpSnapshot = found.snapshot;
|
|
1243
|
-
current.mcpDiagnostics = found.diagnostics;
|
|
1244
|
-
this.store.put("sessions", id, current);
|
|
1245
|
-
});
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const diagnostics = await this.mcp.reconnect({
|
|
1249
|
-
sessionId: id,
|
|
1250
|
-
manifest: s.manifest,
|
|
1251
|
-
pluginRoots: s.pluginRoots ?? {},
|
|
1252
|
-
tools: s.mcpSnapshot.mcpTools,
|
|
1253
|
-
signal,
|
|
1254
|
-
});
|
|
1255
|
-
if (diagnostics.length === 0)
|
|
1256
|
-
return;
|
|
1257
|
-
this.store.tx(() => {
|
|
1258
|
-
const current = this.session(id);
|
|
1259
|
-
const prior = [...(current.mcpDiagnostics ?? [])];
|
|
1260
|
-
for (const item of diagnostics) {
|
|
1261
|
-
const index = prior.findIndex((existing) => existing.capabilityId === item.capabilityId &&
|
|
1262
|
-
existing.serverName === item.serverName);
|
|
1263
|
-
if (index >= 0)
|
|
1264
|
-
prior[index] = item;
|
|
1265
|
-
else
|
|
1266
|
-
prior.push(item);
|
|
1267
|
-
}
|
|
1268
|
-
current.mcpDiagnostics = prior;
|
|
1269
|
-
this.store.put("sessions", id, current);
|
|
1270
|
-
});
|
|
1271
|
-
}
|
|
1272
|
-
async callMcpTool(request) {
|
|
1273
|
-
const s = this.session(request.sessionId);
|
|
1274
|
-
const tool = mcpToolOf(s, request);
|
|
1275
|
-
if (!tool)
|
|
1276
|
-
throw new Error(`MCP tool '${request.toolName ?? ""}' is not in the session snapshot`);
|
|
1277
|
-
return this.mcp.call({
|
|
1278
|
-
sessionId: s.id,
|
|
1279
|
-
...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
|
|
1280
|
-
capabilityId: tool.capabilityId,
|
|
1281
|
-
serverName: tool.serverName,
|
|
1282
|
-
serverToolName: tool.serverToolName,
|
|
1283
|
-
args: request.input,
|
|
1284
|
-
manifest: s.manifest,
|
|
1285
|
-
pluginRoots: s.pluginRoots ?? {},
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
async callSandboxTool(request, signal) {
|
|
1289
|
-
const s = this.session(request.sessionId);
|
|
1290
|
-
// Agents used as tools share the session's sandbox; the tree declares one sandbox spec.
|
|
1291
|
-
const capability = sandboxCapabilityOf(manifestFor(s.manifest, request.agent), request.capabilityId, request.toolName);
|
|
1292
|
-
if (!capability)
|
|
1293
|
-
throw new Error(`'${request.toolName ?? ""}' is not a sandbox tool`);
|
|
1294
|
-
return this.sandbox.run({ id: s.id, activeTurnId: s.activeTurnId, manifest: s.manifest }, capability, request.toolName, request.input, signal);
|
|
1295
|
-
}
|
|
1296
|
-
async authorize(sessionId, request) {
|
|
1297
|
-
const s = this.session(sessionId);
|
|
1298
|
-
const result = await this.vault.authorize({
|
|
1299
|
-
sessionId,
|
|
1300
|
-
vaultIds: s.vaultIds ?? [],
|
|
1301
|
-
credentialSelections: s.credentialSelections ?? [],
|
|
1302
|
-
url: request.url,
|
|
1303
|
-
serverName: request.serverName,
|
|
1304
|
-
});
|
|
1305
|
-
if (result.status === "authorized") {
|
|
1306
|
-
const token = result.headers.authorization.slice("Bearer ".length);
|
|
1307
|
-
scrub({ authorization: result.headers.authorization, url: result.url }, [
|
|
1308
|
-
token,
|
|
1309
|
-
]);
|
|
1310
|
-
}
|
|
1311
|
-
return result;
|
|
1312
|
-
}
|
|
1313
|
-
ensureKek() {
|
|
1314
|
-
if (this.kek)
|
|
1315
|
-
return this.kek;
|
|
1316
|
-
if (this.options.vaultKek === null)
|
|
1317
|
-
throw new Error("Vault key-encryption key is required");
|
|
1318
|
-
this.kek = createKekFile(this.kekPath);
|
|
1319
|
-
return this.kek;
|
|
1320
|
-
}
|
|
1321
|
-
async dispatchHost(scope, method, path, request) {
|
|
1322
|
-
if (scope !== "server") {
|
|
1323
|
-
this.vault.reject(path.join("/"));
|
|
1324
|
-
fail(403, "Application credential required");
|
|
1325
|
-
}
|
|
1326
|
-
if (path[2] === "models" && path.length === 3 && method === "GET")
|
|
1327
|
-
return hostModelCatalog();
|
|
1328
|
-
if (path[2] === "sandbox" && path.length === 3 && method === "GET")
|
|
1329
|
-
return this.sandbox.report();
|
|
1330
|
-
if (path[2] === "providers" && path.length === 3 && method === "GET")
|
|
1331
|
-
return this.vault.listHostProviders();
|
|
1332
|
-
if (path[2] === "model" && path.length === 3 && method === "GET")
|
|
1333
|
-
return this.vault.getHostModel();
|
|
1334
|
-
if (path[2] === "model" && path.length === 3 && method === "PUT")
|
|
1335
|
-
return this.vault.putHostModel(PutHostModelRequestSchema.parse(await this.body(request)));
|
|
1336
|
-
if (path[2] === "model" &&
|
|
1337
|
-
path[3] === "selection" &&
|
|
1338
|
-
path.length === 4 &&
|
|
1339
|
-
method === "PUT")
|
|
1340
|
-
return this.vault.selectHostModel(SelectHostModelRequestSchema.parse(await this.body(request)));
|
|
1341
|
-
fail(404, "Route not found");
|
|
1342
|
-
}
|
|
1343
|
-
async dispatchVault(scope, method, path, url, request) {
|
|
1344
|
-
if (scope !== "server") {
|
|
1345
|
-
this.vault.reject(path.join("/"));
|
|
1346
|
-
fail(403, "Application credential required");
|
|
1347
|
-
}
|
|
1348
|
-
if (path.length === 2 && method === "POST") {
|
|
1349
|
-
const body = CreateVaultRequestSchema.parse(await this.body(request));
|
|
1350
|
-
return this.vault.createVault(body);
|
|
1351
|
-
}
|
|
1352
|
-
if (path.length === 2 && method === "GET") {
|
|
1353
|
-
const ownerUserId = url.searchParams.get("ownerUserId") ??
|
|
1354
|
-
fail(400, "ownerUserId is required");
|
|
1355
|
-
return { vaults: this.vault.listVaults(ownerUserId) };
|
|
1356
|
-
}
|
|
1357
|
-
const vaultId = path[2];
|
|
1358
|
-
if (!vaultId)
|
|
1359
|
-
fail(404, "Vault not found");
|
|
1360
|
-
if (path.length === 3 && method === "GET")
|
|
1361
|
-
return this.vault.getVault(vaultId);
|
|
1362
|
-
if (path.length === 3 && method === "DELETE")
|
|
1363
|
-
return this.vault.deleteVault(vaultId);
|
|
1364
|
-
if (path[3] !== "credentials")
|
|
1365
|
-
fail(404, "Route not found");
|
|
1366
|
-
if (path.length === 4 && method === "POST") {
|
|
1367
|
-
const body = CreateCredentialRequestSchema.parse(await this.body(request));
|
|
1368
|
-
return this.vault.createCredential(vaultId, body);
|
|
1369
|
-
}
|
|
1370
|
-
if (path.length === 4 && method === "GET")
|
|
1371
|
-
return { credentials: this.vault.listCredentials(vaultId) };
|
|
1372
|
-
const credentialId = path[4];
|
|
1373
|
-
if (!credentialId)
|
|
1374
|
-
fail(404, "Credential not found");
|
|
1375
|
-
if (path.length === 5 && method === "GET")
|
|
1376
|
-
return this.vault.getCredential(vaultId, credentialId);
|
|
1377
|
-
if (path.length === 5 && method === "POST") {
|
|
1378
|
-
const body = RotateCredentialRequestSchema.parse(await this.body(request));
|
|
1379
|
-
return this.vault.rotateCredential(vaultId, credentialId, body);
|
|
1380
|
-
}
|
|
1381
|
-
if (path.length === 5 && method === "DELETE")
|
|
1382
|
-
return this.vault.deleteCredential(vaultId, credentialId);
|
|
1383
|
-
fail(404, "Route not found");
|
|
1384
|
-
}
|
|
1385
|
-
async listen(port = 8787, hostname = "127.0.0.1") {
|
|
1386
|
-
if (this.server)
|
|
1387
|
-
throw new Error("Already listening");
|
|
1388
|
-
this.server = createServer((q, s) => {
|
|
1389
|
-
void this.handle(q, s);
|
|
1390
|
-
});
|
|
1391
|
-
await new Promise((resolve, reject) => {
|
|
1392
|
-
this.server.once("error", reject);
|
|
1393
|
-
this.server.listen(port, hostname, resolve);
|
|
1394
|
-
});
|
|
1395
|
-
const address = this.server.address();
|
|
1396
|
-
return {
|
|
1397
|
-
url: `http://${hostname}:${typeof address === "object" && address ? address.port : port}`,
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
async close() {
|
|
1401
|
-
this.closing = true;
|
|
1402
|
-
clearInterval(this.timer);
|
|
1403
|
-
for (const c of this.running.values())
|
|
1404
|
-
c.abort();
|
|
1405
|
-
await this.mcp.close();
|
|
1406
|
-
for (const streams of this.executorStreams.values())
|
|
1407
|
-
for (const r of streams)
|
|
1408
|
-
r.end();
|
|
1409
|
-
for (const set of this.observers.values())
|
|
1410
|
-
for (const r of set)
|
|
1411
|
-
r.end();
|
|
1412
|
-
if (this.server)
|
|
1413
|
-
await new Promise((resolve) => {
|
|
1414
|
-
this.server.close(() => resolve());
|
|
1415
|
-
this.server.closeIdleConnections();
|
|
1416
|
-
});
|
|
1417
|
-
// Providers must honor AbortSignal. Keep storage open until any in-flight journal writes have drained.
|
|
1418
|
-
while (this.running.size)
|
|
1419
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1420
|
-
await this.sandbox.close();
|
|
1421
|
-
this.closed = true;
|
|
1422
|
-
this.store.db.close();
|
|
1423
|
-
if (this.lockPath)
|
|
1424
|
-
unlinkSync(this.lockPath);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
export function createRuntime(options) {
|
|
1428
|
-
return new CoreRuntime(options);
|
|
1429
|
-
}
|
|
1430
|
-
export async function startRuntime(options) {
|
|
1431
|
-
const runtime = createRuntime(options);
|
|
1432
|
-
try {
|
|
1433
|
-
const { url } = await runtime.listen(options.port, options.hostname);
|
|
1434
|
-
return Object.assign(runtime, { url });
|
|
1435
|
-
}
|
|
1436
|
-
catch (error) {
|
|
1437
|
-
await runtime.close();
|
|
1438
|
-
throw error;
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
/** What an action runs, for events: a tool name, or a hook point and its capabilities. */
|
|
1442
|
-
function actionTarget(action) {
|
|
1443
|
-
return {
|
|
1444
|
-
...(action.kind === "hook" ? { hook: action.hook } : { toolName: action.toolName }),
|
|
1445
|
-
...(action.agent ? { agent: action.agent } : {}),
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1
|
+
export { openTenantRuntime, TenantRuntime, } from "../tenant/runtime.js";
|
|
2
|
+
export { startEphemeralRuntime, } from "../tenant/ephemeral.js";
|
|
3
|
+
export { scriptedModel, gatewayModel } from "./provider.js";
|