@nylorun/runtime 0.17.0-beta → 0.19.1-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 +155 -0
- package/README.md +89 -58
- package/dist/adapters/execution/restate.d.ts +1 -3
- package/dist/adapters/execution/restate.js +4 -31
- package/dist/adapters/replication/pgoutput.js +1 -1
- package/dist/adapters/sandbox/local.js +1 -1
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/api/ag-ui/routes.js +1 -1
- package/dist/api/ag-ui/translate.d.ts +1 -3
- package/dist/api/ag-ui/translate.js +2 -17
- package/dist/api/components.d.ts +4871 -2614
- package/dist/api/components.js +12 -11
- package/dist/api/gate/routes.d.ts +7 -8
- package/dist/api/gate/routes.js +58 -52
- package/dist/api/http/app.d.ts +6 -0
- package/dist/api/http/app.js +18 -14
- package/dist/api/http/define.d.ts +14 -2
- package/dist/api/http/define.js +34 -15
- package/dist/api/http/routes/access.d.ts +3 -3
- package/dist/api/http/routes/access.js +18 -17
- package/dist/api/http/routes/artifacts.js +2 -4
- package/dist/api/http/routes/files.d.ts +8 -0
- package/dist/api/http/routes/files.js +77 -0
- package/dist/api/http/routes/keys.d.ts +10 -0
- package/dist/api/http/routes/keys.js +64 -0
- package/dist/api/http/routes/me.js +5 -2
- package/dist/api/http/routes/reads.d.ts +66 -0
- package/dist/api/http/routes/reads.js +127 -0
- package/dist/api/http/routes/sandboxes.js +7 -4
- package/dist/api/http/routes/sessions.js +25 -8
- package/dist/api/http/routes/tenant.js +15 -18
- package/dist/api/http/routes/vaults.d.ts +10 -9
- package/dist/api/http/routes/vaults.js +27 -23
- package/dist/api/openapi.d.ts +10 -11
- package/dist/api/openapi.js +288 -63
- package/dist/artifacts/links.js +1 -1
- package/dist/core/flow-host.d.ts +44 -23
- package/dist/core/flow-host.js +88 -106
- package/dist/core/provider.d.ts +4 -1
- package/dist/core/provider.js +6 -1
- package/dist/core/sandbox-routes.d.ts +0 -9
- package/dist/core/sandbox-routes.js +2 -21
- package/dist/execution/memory.d.ts +4 -5
- package/dist/execution/memory.js +11 -25
- package/dist/execution/types.d.ts +3 -33
- package/dist/execution/types.js +2 -12
- package/dist/gates/http-tool.d.ts +86 -0
- package/dist/gates/http-tool.js +197 -0
- package/dist/gates/mcp-handler.d.ts +3 -0
- package/dist/gates/mcp-handler.js +1 -3
- package/dist/gates/meter.d.ts +2 -2
- package/dist/gates/meter.js +6 -0
- package/dist/gates/tenant-vaults.d.ts +5 -9
- package/dist/gates/tool-calls.d.ts +7 -15
- package/dist/gates/tool-calls.js +4 -4
- package/dist/gates/tool-client.d.ts +1 -1
- package/dist/gates/tool-client.js +23 -47
- package/dist/gates/tool-contract.d.ts +27 -39
- package/dist/gates/tool-contract.js +30 -21
- package/dist/gates/tool-gate.d.ts +25 -24
- package/dist/gates/tool-gate.js +11 -7
- package/dist/harness/calls.d.ts +10 -4
- package/dist/harness/calls.js +15 -6
- package/dist/harness/executors.d.ts +1 -1
- package/dist/harness/executors.js +17 -12
- package/dist/harness/main.d.ts +2 -2
- package/dist/harness/main.js +5 -13
- package/dist/harness/service.d.ts +4 -5
- package/dist/harness/service.js +9 -2
- package/dist/harness-api/in-process.js +2 -2
- package/dist/harness-api/record.d.ts +6 -3
- package/dist/harness-api/record.js +65 -72
- package/dist/harness-api/server.d.ts +1 -9
- package/dist/harness-api/server.js +19 -17
- package/dist/harness-api/start.js +1 -5
- package/dist/harness-api/workspace.js +2 -3
- package/dist/harness-api/ws-server.js +1 -1
- package/dist/host/app.d.ts +2 -9
- package/dist/host/app.js +15 -25
- package/dist/host/config-for.d.ts +1 -5
- package/dist/host/config-for.js +0 -3
- package/dist/host/config.d.ts +0 -5
- package/dist/host/create-host.d.ts +3 -33
- package/dist/host/create-host.js +18 -94
- package/dist/host/environment.d.ts +0 -3
- package/dist/host/environment.js +0 -9
- package/dist/host/gates.d.ts +4 -1
- package/dist/host/gates.js +3 -2
- package/dist/host/http.d.ts +1 -2
- package/dist/host/http.js +1 -7
- package/dist/host/index.d.ts +2 -2
- package/dist/host/index.js +2 -2
- package/dist/host/main.js +27 -34
- package/dist/host/operate.d.ts +29 -0
- package/dist/host/operate.js +223 -0
- package/dist/host/stack-config.d.ts +12 -12
- package/dist/host/stack-config.js +5 -32
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/infra/workers.d.ts +1 -1
- package/dist/keys/keys.js +2 -2
- package/dist/management-openapi.json +6491 -0
- package/dist/mcp/connect.d.ts +8 -4
- package/dist/mcp/connect.js +6 -26
- package/dist/mcp/pool.d.ts +5 -9
- package/dist/mcp/pool.js +8 -19
- package/dist/mcp/snapshot.d.ts +8 -2
- package/dist/mcp/snapshot.js +29 -2
- package/dist/media.d.ts +1 -1
- package/dist/media.js +1 -1
- package/dist/model/pi-model.js +3 -0
- package/dist/openapi.json +9372 -17030
- package/dist/reads/access.d.ts +4 -0
- package/dist/reads/access.js +12 -0
- package/dist/reads/cursor.d.ts +11 -0
- package/dist/reads/cursor.js +80 -0
- package/dist/reads/types.d.ts +25 -0
- package/dist/reads/types.js +1 -0
- package/dist/sandbox/manager.d.ts +16 -0
- package/dist/sandbox/manager.js +40 -0
- package/dist/sandbox/session-sandbox.js +13 -3
- package/dist/sandbox/skills.d.ts +13 -0
- package/dist/sandbox/skills.js +46 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/ownership.d.ts +9 -0
- package/dist/store/ownership.js +13 -0
- package/dist/store/postgres/connect.d.ts +2 -0
- package/dist/store/postgres/connect.js +29 -0
- package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
- package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
- package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
- package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/reads.d.ts +3 -0
- package/dist/store/postgres/reads.js +272 -0
- package/dist/store/postgres/schema.d.ts +294 -489
- package/dist/store/postgres/schema.js +41 -76
- package/dist/store/postgres/store.js +51 -154
- package/dist/store/postgres/tenant.d.ts +34 -2
- package/dist/store/postgres/tenant.js +16 -32
- package/dist/store/types.d.ts +27 -96
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +0 -2
- package/dist/streams/control.js +0 -5
- package/dist/streams/types.d.ts +1 -10
- package/dist/tenant/advance.d.ts +3 -3
- package/dist/tenant/advance.js +10 -7
- package/dist/tenant/auth.d.ts +14 -9
- package/dist/tenant/auth.js +24 -28
- package/dist/tenant/cause.d.ts +3 -3
- package/dist/tenant/cause.js +1 -1
- package/dist/tenant/commands.d.ts +2 -18
- package/dist/tenant/commands.js +35 -172
- package/dist/tenant/context.d.ts +14 -24
- package/dist/tenant/definition-files.d.ts +24 -0
- package/dist/tenant/definition-files.js +103 -0
- package/dist/tenant/effects.d.ts +18 -20
- package/dist/tenant/effects.js +56 -119
- package/dist/tenant/ephemeral.d.ts +7 -6
- package/dist/tenant/ephemeral.js +3 -11
- package/dist/tenant/host-token.js +1 -1
- package/dist/tenant/jwt.js +1 -1
- package/dist/tenant/module.d.ts +1 -4
- package/dist/tenant/module.js +3 -12
- package/dist/tenant/operator-keys.d.ts +34 -8
- package/dist/tenant/operator-keys.js +50 -18
- package/dist/tenant/outbound.d.ts +10 -1
- package/dist/tenant/outbound.js +44 -27
- package/dist/tenant/paths.d.ts +1 -1
- package/dist/tenant/paths.js +1 -3
- package/dist/tenant/principals.d.ts +11 -2
- package/dist/tenant/principals.js +27 -8
- package/dist/tenant/run-token.js +3 -3
- package/dist/tenant/runtime.d.ts +4 -6
- package/dist/tenant/runtime.js +19 -18
- package/dist/tenant/sandboxes.d.ts +4 -1
- package/dist/tenant/sandboxes.js +11 -0
- package/dist/tenant/scheduler.d.ts +1 -5
- package/dist/tenant/scheduler.js +2 -5
- package/dist/tenant/session-streams.d.ts +2 -1
- package/dist/tenant/session-streams.js +39 -5
- package/dist/tenant/session.d.ts +8 -41
- package/dist/tenant/session.js +0 -37
- package/dist/tenant/sessions.js +16 -8
- package/dist/tenant/signing-keys.d.ts +3 -3
- package/dist/tenant/signing-keys.js +5 -5
- package/dist/tenant/skill-tool.d.ts +20 -0
- package/dist/tenant/skill-tool.js +72 -0
- package/dist/tenant/slim.d.ts +1 -1
- package/dist/tenant/slim.js +1 -1
- package/dist/tenant/status.js +7 -12
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +7 -3
- package/dist/tenant/streams.d.ts +3 -10
- package/dist/tenant/streams.js +1 -24
- package/dist/tenant/sweep.js +0 -5
- package/dist/tenant/transcript.d.ts +2 -2
- package/dist/tenant/transcript.js +2 -2
- package/dist/tenant/types.d.ts +15 -22
- package/dist/tenant/worker.d.ts +1 -3
- package/dist/tenant/worker.js +3 -8
- package/dist/vault/sources.d.ts +7 -1
- package/dist/vault/sources.js +22 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/dist/admin-openapi.json +0 -904
- package/dist/api/http/routes/actions.d.ts +0 -8
- package/dist/api/http/routes/actions.js +0 -75
- package/dist/api/http/routes/endpoints.d.ts +0 -7
- package/dist/api/http/routes/endpoints.js +0 -100
- package/dist/host/admin-api.d.ts +0 -30
- package/dist/host/admin-api.js +0 -140
- package/dist/mcp/stdio.d.ts +0 -17
- package/dist/mcp/stdio.js +0 -82
- package/dist/tenant/actions.d.ts +0 -14
- package/dist/tenant/actions.js +0 -29
- package/dist/tenant/delivery-token.d.ts +0 -41
- package/dist/tenant/delivery-token.js +0 -104
- package/dist/tenant/delivery.d.ts +0 -62
- package/dist/tenant/delivery.js +0 -481
- package/dist/tenant/endpoints.d.ts +0 -67
- package/dist/tenant/endpoints.js +0 -90
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AuthScope, TenantContext } from "../tenant/context.js";
|
|
2
|
+
import type { ReadAccess } from "./types.js";
|
|
3
|
+
export declare function readAccess(scope: AuthScope): ReadAccess;
|
|
4
|
+
export declare function readStoreOf(ctx: TenantContext): import("./types.js").ReadStore;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { accessOf } from "../tenant/auth.js";
|
|
2
|
+
import { fail } from "../tenant/http.js";
|
|
3
|
+
export function readAccess(scope) {
|
|
4
|
+
const access = accessOf(scope);
|
|
5
|
+
return {
|
|
6
|
+
...(access ? { owner: access.owner } : {}),
|
|
7
|
+
...(access?.agents ? { agents: [...access.agents] } : {}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function readStoreOf(ctx) {
|
|
11
|
+
return ctx.reads ?? fail(503, "Session reads are unavailable", { code: "reads_unavailable" });
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Cursors carry position, not authority. Authorization is reapplied at every read. */
|
|
2
|
+
export declare function readCursor(tenant: string, route: string, filters: Record<string, unknown>): {
|
|
3
|
+
encode: (key: (string | null)[]) => string;
|
|
4
|
+
decode(value: string | undefined): (string | null)[] | undefined;
|
|
5
|
+
};
|
|
6
|
+
/** Page cursors also resume SSE. SSE accepts the filter captured by the history page. */
|
|
7
|
+
export declare function historyCursor(tenant: string, sessionId: string, agent?: string): {
|
|
8
|
+
encode: (eventCursor: string) => string;
|
|
9
|
+
decode(value: string | undefined): string | undefined;
|
|
10
|
+
};
|
|
11
|
+
export declare function historyResume(tenant: string, sessionId: string, value: string | undefined): string | undefined;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { decodeCursor } from "../record/index.js";
|
|
4
|
+
import { fail } from "../tenant/http.js";
|
|
5
|
+
const schema = z
|
|
6
|
+
.object({
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
binding: z.string(),
|
|
9
|
+
key: z.array(z.string().nullable()).max(3),
|
|
10
|
+
})
|
|
11
|
+
.strict();
|
|
12
|
+
/** Cursors carry position, not authority. Authorization is reapplied at every read. */
|
|
13
|
+
export function readCursor(tenant, route, filters) {
|
|
14
|
+
const binding = createHash("sha256")
|
|
15
|
+
.update(JSON.stringify([
|
|
16
|
+
tenant,
|
|
17
|
+
route,
|
|
18
|
+
Object.entries(filters).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
19
|
+
]))
|
|
20
|
+
.digest("hex");
|
|
21
|
+
return {
|
|
22
|
+
encode: (key) => Buffer.from(JSON.stringify({ version: 1, binding, key })).toString("base64url"),
|
|
23
|
+
decode(value) {
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (value.length > 4096 || !/^[A-Za-z0-9_-]+$/.test(value))
|
|
27
|
+
return fail(400, "Invalid cursor", { code: "cursor_invalid" });
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = schema.safeParse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return fail(400, "Invalid cursor", { code: "cursor_invalid" });
|
|
34
|
+
}
|
|
35
|
+
if (!parsed.success)
|
|
36
|
+
return fail(400, "Invalid cursor", { code: "cursor_invalid" });
|
|
37
|
+
if (parsed.data.binding !== binding)
|
|
38
|
+
return fail(400, "Cursor belongs to another query", { code: "cursor_mismatch" });
|
|
39
|
+
return parsed.data.key;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Page cursors also resume SSE. SSE accepts the filter captured by the history page. */
|
|
44
|
+
export function historyCursor(tenant, sessionId, agent) {
|
|
45
|
+
const codec = readCursor(tenant, "history", { sessionId, agent });
|
|
46
|
+
return {
|
|
47
|
+
encode: (eventCursor) => "h1." + codec.encode([eventCursor, agent ?? null]),
|
|
48
|
+
decode(value) {
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!value.startsWith("h1."))
|
|
52
|
+
return fail(400, "Invalid history page cursor", { code: "cursor_invalid" });
|
|
53
|
+
const key = codec.decode(value.slice(3));
|
|
54
|
+
if (!key || key.length !== 2 || key[0] === null || key[1] !== (agent ?? null))
|
|
55
|
+
return fail(400, "Invalid history page cursor", { code: "cursor_invalid" });
|
|
56
|
+
try {
|
|
57
|
+
if (!Number.isSafeInteger(decodeCursor(sessionId, key[0])))
|
|
58
|
+
throw new Error("Invalid cursor");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return fail(400, "Invalid history page cursor", { code: "cursor_invalid" });
|
|
62
|
+
}
|
|
63
|
+
return key[0];
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function historyResume(tenant, sessionId, value) {
|
|
68
|
+
if (!value?.startsWith("h1."))
|
|
69
|
+
return value;
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = schema.safeParse(JSON.parse(Buffer.from(value.slice(3), "base64url").toString("utf8")));
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return fail(400, "Invalid history cursor", { code: "cursor_invalid" });
|
|
76
|
+
}
|
|
77
|
+
if (!parsed.success || parsed.data.key.length !== 2)
|
|
78
|
+
return fail(400, "Invalid history cursor", { code: "cursor_invalid" });
|
|
79
|
+
return historyCursor(tenant, sessionId, parsed.data.key[1] ?? undefined).decode(value);
|
|
80
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModelCallExportPage, ModelCallsPage, SandboxPage, SessionManifestView, SessionPage, SessionListItem, SessionUsageTotals } from "@nylorun/core/contracts";
|
|
2
|
+
export type ReadAccess = {
|
|
3
|
+
owner?: string;
|
|
4
|
+
agents?: readonly string[];
|
|
5
|
+
};
|
|
6
|
+
export type SessionFilters = {
|
|
7
|
+
agentId?: string;
|
|
8
|
+
status?: SessionListItem["status"];
|
|
9
|
+
sandboxId?: string;
|
|
10
|
+
ownerUserId?: string;
|
|
11
|
+
};
|
|
12
|
+
export type PageOptions = {
|
|
13
|
+
limit: number;
|
|
14
|
+
cursor?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Public projections only. No execution-state methods or mutable transactions. */
|
|
17
|
+
export interface ReadStore {
|
|
18
|
+
sessions(filters: SessionFilters, page: PageOptions, access: ReadAccess): Promise<SessionPage>;
|
|
19
|
+
manifest(id: string, access: ReadAccess): Promise<SessionManifestView>;
|
|
20
|
+
usage(id: string, turnId: string | undefined, access: ReadAccess): Promise<SessionUsageTotals>;
|
|
21
|
+
modelCalls(id: string, turnId: string | undefined, page: PageOptions, access: ReadAccess): Promise<ModelCallsPage>;
|
|
22
|
+
exportModel(after: string | undefined, limit: number): Promise<ModelCallExportPage>;
|
|
23
|
+
sandboxes(labels: Record<string, string>, page: PageOptions, access: ReadAccess, grants?: readonly string[]): Promise<SandboxPage>;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* past their timeout and marks records of compute this process no longer holds as stopped
|
|
12
12
|
* (after a restart); `reconcile` removes workspaces whose owner is gone. `sweep` does both, the
|
|
13
13
|
* reconcile once per process, as the Tenant sweep does in core's process.
|
|
14
|
+
*
|
|
15
|
+
* A session's skills (track R2 M4) are mounted on its workspace, read-only under
|
|
16
|
+
* `/skills/<name>/`, before the first call that opens it (`skills.ts`); their bytes come
|
|
17
|
+
* through the `definitionFile` port.
|
|
14
18
|
*/
|
|
15
19
|
import type { EventPayload } from "@nylorun/core/contracts";
|
|
16
20
|
import { type AgentManifest, type CapabilityManifest, type SandboxToolName } from "@nylorun/core/define";
|
|
@@ -61,6 +65,12 @@ export interface SandboxManagerOptions {
|
|
|
61
65
|
readonly preference: string | undefined;
|
|
62
66
|
/** Delete sandboxes on close (the Runtime's store does not outlive the process). */
|
|
63
67
|
readonly ephemeral: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* A definition file's bytes by `sha256:<hex>`, for the skills a session's workspace mounts:
|
|
70
|
+
* the Tenant's Object store in core's process, core's answer in a harness (for the run that
|
|
71
|
+
* made the call, `session.claim`). Without it no skills are mounted.
|
|
72
|
+
*/
|
|
73
|
+
readonly definitionFile?: (sha256: string, session: SandboxSessionRef) => Promise<Uint8Array>;
|
|
64
74
|
/** Writes a session event (in core, in its own transaction; in a harness, as a claim). */
|
|
65
75
|
readonly emit: <T extends SandboxEventType>(sessionId: string, turnId: string | null, type: T, payload: EventPayload<T>, meta: SandboxEventMeta) => void | Promise<void>;
|
|
66
76
|
}
|
|
@@ -135,6 +145,12 @@ export declare class SandboxManager {
|
|
|
135
145
|
private prepare;
|
|
136
146
|
private execute;
|
|
137
147
|
private executeOpen;
|
|
148
|
+
/**
|
|
149
|
+
* Mounts the session's skills the workspace does not hold as they are: on a handle just opened
|
|
150
|
+
* all of them, later only those another session brought or changed. A failed outcome when
|
|
151
|
+
* one cannot be mounted; nothing is marked mounted then, so the next call tries again.
|
|
152
|
+
*/
|
|
153
|
+
private mountSkills;
|
|
138
154
|
private open;
|
|
139
155
|
private stop;
|
|
140
156
|
private record;
|
package/dist/sandbox/manager.js
CHANGED
|
@@ -2,6 +2,7 @@ import { SANDBOX_WORKSPACE, } from "@nylorun/core/define";
|
|
|
2
2
|
import { sandboxWorkspaceKey, sessionWorkspaceKey, workspacePrefix, } from "./records.js";
|
|
3
3
|
import { DEFAULT_SANDBOX_CPUS, DEFAULT_SANDBOX_IMAGE, describeNetwork, idleMsOf, memoryMiBOf, resolveNetwork, } from "./policy.js";
|
|
4
4
|
import { parseSandboxPreference, reportSelection, selectSandboxBackend, } from "./select.js";
|
|
5
|
+
import { mountSkill, sandboxSkillsOf, skillSignature } from "./skills.js";
|
|
5
6
|
import { resolveSandboxPath, runSandboxTool, } from "./tools.js";
|
|
6
7
|
import { SandboxFileTooLargeError, } from "./types.js";
|
|
7
8
|
export { sandboxCapabilityOf } from "./capability.js";
|
|
@@ -191,6 +192,9 @@ export class SandboxManager {
|
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
}
|
|
195
|
+
const unmounted = await this.mountSkills(live, live.handle, session, signal);
|
|
196
|
+
if (unmounted)
|
|
197
|
+
return unmounted;
|
|
194
198
|
return await use(live.handle, live);
|
|
195
199
|
}
|
|
196
200
|
finally {
|
|
@@ -236,6 +240,7 @@ export class SandboxManager {
|
|
|
236
240
|
active: 0,
|
|
237
241
|
lastUsedAt: Date.now(),
|
|
238
242
|
tail: Promise.resolve(),
|
|
243
|
+
skills: new Map(),
|
|
239
244
|
};
|
|
240
245
|
this.live.set(key, live);
|
|
241
246
|
}
|
|
@@ -272,6 +277,9 @@ export class SandboxManager {
|
|
|
272
277
|
};
|
|
273
278
|
}
|
|
274
279
|
}
|
|
280
|
+
const unmounted = await this.mountSkills(live, live.handle, session, signal);
|
|
281
|
+
if (unmounted)
|
|
282
|
+
return unmounted;
|
|
275
283
|
const started = Date.now();
|
|
276
284
|
let report = {};
|
|
277
285
|
try {
|
|
@@ -300,6 +308,36 @@ export class SandboxManager {
|
|
|
300
308
|
};
|
|
301
309
|
}
|
|
302
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* Mounts the session's skills the workspace does not hold as they are: on a handle just opened
|
|
313
|
+
* all of them, later only those another session brought or changed. A failed outcome when
|
|
314
|
+
* one cannot be mounted; nothing is marked mounted then, so the next call tries again.
|
|
315
|
+
*/
|
|
316
|
+
async mountSkills(live, handle, session, signal) {
|
|
317
|
+
const read = this.options.definitionFile;
|
|
318
|
+
if (!read)
|
|
319
|
+
return undefined;
|
|
320
|
+
for (const [name, files] of sandboxSkillsOf(session.manifest)) {
|
|
321
|
+
const signature = skillSignature(files);
|
|
322
|
+
if (live.skills.get(name) === signature)
|
|
323
|
+
continue;
|
|
324
|
+
try {
|
|
325
|
+
await mountSkill(handle, name, files, (sha256) => read(sha256, session), signal);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
if (signal.aborted)
|
|
329
|
+
throw error;
|
|
330
|
+
live.skills.delete(name);
|
|
331
|
+
return {
|
|
332
|
+
kind: "failed",
|
|
333
|
+
code: "sandbox.skills_unavailable",
|
|
334
|
+
message: `The files of skill '${name}' could not be put in the sandbox: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
live.skills.set(name, signature);
|
|
338
|
+
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
303
341
|
async open(live, session, spec) {
|
|
304
342
|
const existing = await this.options.records.get(spec.key);
|
|
305
343
|
const payload = {
|
|
@@ -311,6 +349,8 @@ export class SandboxManager {
|
|
|
311
349
|
const creating = await this.record(spec.key, live, session.id, spec.image, "creating", existing);
|
|
312
350
|
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", { state: "creating", ...payload, ...(existing ? { reattach: true } : {}) }, { record: creating, ...claimOf(session) });
|
|
313
351
|
const handle = await live.backend.open(spec);
|
|
352
|
+
// A handle opened afresh holds no skills this manager knows of.
|
|
353
|
+
live.skills.clear();
|
|
314
354
|
const running = await this.record(spec.key, live, session.id, spec.image, "running", existing);
|
|
315
355
|
await this.options.emit(session.id, session.activeTurnId, "sandbox.state", {
|
|
316
356
|
state: "running",
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Gives a session the sandbox chosen when it was opened. An agent session's pinned manifest is
|
|
3
3
|
* its definition plus the `nylorun.sandbox` capability (the six built-in tools, the sandbox
|
|
4
4
|
* instructions and the resolved spec) and the `nylorun.artifacts` capability (`save_artifact`),
|
|
5
|
-
* also added to each agent it uses as a tool, so the tree shares one spec. The
|
|
5
|
+
* also added to each agent it uses as a tool, so the tree shares one spec. The sandbox's
|
|
6
|
+
* instructions name the agent's skills' folders (`/skills/<name>/`). The definition in the
|
|
6
7
|
* registry never changes.
|
|
7
8
|
*/
|
|
8
9
|
import { AgentManifestSchema } from "@nylorun/core/contracts";
|
|
@@ -14,6 +15,10 @@ function isAgentManifest(value) {
|
|
|
14
15
|
value.kind !== "workflow" &&
|
|
15
16
|
Array.isArray(value.capabilities));
|
|
16
17
|
}
|
|
18
|
+
/** The names of the agent's own skills. */
|
|
19
|
+
function skillNamesOf(manifest) {
|
|
20
|
+
return manifest.capabilities.flatMap((capability) => Object.keys(capability.skills ?? {})).sort();
|
|
21
|
+
}
|
|
17
22
|
function addCapability(manifest, spec) {
|
|
18
23
|
const capabilities = manifest.capabilities.map((capability) => capability.tools === undefined
|
|
19
24
|
? capability
|
|
@@ -31,8 +36,13 @@ function addCapability(manifest, spec) {
|
|
|
31
36
|
});
|
|
32
37
|
return {
|
|
33
38
|
...manifest,
|
|
34
|
-
// With a sandbox comes `save_artifact`, so files the agent makes reach the user (F8.1).
|
|
35
|
-
|
|
39
|
+
// With a sandbox comes `save_artifact`, so files the agent makes reach the user (F8.1). The
|
|
40
|
+
// sandbox's instructions name where the agent's skills are (R2 M4).
|
|
41
|
+
capabilities: [
|
|
42
|
+
...capabilities,
|
|
43
|
+
sandboxCapabilityManifest(spec, { skills: skillNamesOf(manifest) }),
|
|
44
|
+
artifactsCapabilityManifest(),
|
|
45
|
+
],
|
|
36
46
|
};
|
|
37
47
|
}
|
|
38
48
|
/** The agent's manifest with the sandbox capability added, or why it cannot take one. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type AgentManifest } from "@nylorun/core/define";
|
|
2
|
+
import type { SandboxHandle } from "./types.js";
|
|
3
|
+
/** A skill's files, by path in its folder, as `sha256:<hex>`. */
|
|
4
|
+
export type SkillFiles = Readonly<Record<string, string>>;
|
|
5
|
+
/** The skills of the agent and the agents it uses as tools; the first of a name wins. */
|
|
6
|
+
export declare function sandboxSkillsOf(manifest: AgentManifest | undefined, into?: Map<string, Readonly<Record<string, string>>>): Map<string, SkillFiles>;
|
|
7
|
+
/** What a mounted skill holds, to tell whether a workspace has these files already. */
|
|
8
|
+
export declare function skillSignature(files: SkillFiles): string;
|
|
9
|
+
/**
|
|
10
|
+
* Writes one skill's files to `/skills/<name>/` of the sandbox, replacing what was there, and
|
|
11
|
+
* makes them read-only. `read` gives a file's bytes by `sha256:<hex>`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function mountSkill(handle: SandboxHandle, name: string, files: SkillFiles, read: (sha256: string) => Promise<Uint8Array>, signal: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sandbox's skills (track R2 M4): each skill of the session's agent, and of the agents it uses
|
|
3
|
+
* as tools (they share its sandbox), gets its files under `/skills/<name>/`, read-only, so the
|
|
4
|
+
* model can read them and run their scripts with bash. The SandboxManager mounts them on a
|
|
5
|
+
* workspace before the first call of a session's run that opens it, and again when a session
|
|
6
|
+
* with other skills uses it; the bytes come from the Tenant's definition files.
|
|
7
|
+
*/
|
|
8
|
+
import { posix } from "node:path";
|
|
9
|
+
import { SKILLS_MOUNT } from "@nylorun/core/define";
|
|
10
|
+
import { canonical } from "../store/canonical.js";
|
|
11
|
+
import { quote } from "./tools.js";
|
|
12
|
+
/** The skills of the agent and the agents it uses as tools; the first of a name wins. */
|
|
13
|
+
export function sandboxSkillsOf(manifest, into = new Map()) {
|
|
14
|
+
for (const capability of manifest?.capabilities ?? []) {
|
|
15
|
+
for (const skill of Object.values(capability.skills ?? {}))
|
|
16
|
+
if (!into.has(skill.name))
|
|
17
|
+
into.set(skill.name, skill.files);
|
|
18
|
+
for (const tool of capability.tools ?? [])
|
|
19
|
+
if (tool.agent !== undefined && !("kind" in tool.agent))
|
|
20
|
+
sandboxSkillsOf(tool.agent, into);
|
|
21
|
+
}
|
|
22
|
+
return into;
|
|
23
|
+
}
|
|
24
|
+
/** What a mounted skill holds, to tell whether a workspace has these files already. */
|
|
25
|
+
export function skillSignature(files) {
|
|
26
|
+
return canonical(files);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Writes one skill's files to `/skills/<name>/` of the sandbox, replacing what was there, and
|
|
30
|
+
* makes them read-only. `read` gives a file's bytes by `sha256:<hex>`.
|
|
31
|
+
*/
|
|
32
|
+
export async function mountSkill(handle, name, files, read, signal) {
|
|
33
|
+
const root = `${SKILLS_MOUNT}/${name}`;
|
|
34
|
+
const exec = async (command) => {
|
|
35
|
+
const result = await handle.exec({ command, cwd: "/", timeoutMs: 60_000 }, signal);
|
|
36
|
+
if (result.exitCode !== 0)
|
|
37
|
+
throw new Error(`${command.slice(0, 120)} failed: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
38
|
+
};
|
|
39
|
+
const directories = new Set([root]);
|
|
40
|
+
for (const path of Object.keys(files))
|
|
41
|
+
directories.add(posix.dirname(posix.join(root, path)));
|
|
42
|
+
await exec(`if [ -e ${quote(root)} ]; then chmod -R u+w ${quote(root)} && rm -rf ${quote(root)}; fi && mkdir -p ${[...directories].map(quote).join(" ")}`);
|
|
43
|
+
for (const [path, sha256] of Object.entries(files))
|
|
44
|
+
await handle.writeFile(posix.join(root, path), await read(sha256));
|
|
45
|
+
await exec(`chmod -R a-w ${quote(root)}`);
|
|
46
|
+
}
|
package/dist/sandbox/types.d.ts
CHANGED
|
@@ -64,8 +64,8 @@ export interface SandboxHandle {
|
|
|
64
64
|
* directory. Stops after `maxEntries + 1` files (`truncated`). For the turn-end export (F8.2).
|
|
65
65
|
*/
|
|
66
66
|
listFiles?(dir: string, maxEntries: number): Promise<SandboxListing | undefined>;
|
|
67
|
-
/** Parent directories must already exist. */
|
|
68
|
-
writeFile(path: string, content: string): Promise<void>;
|
|
67
|
+
/** Text is written as UTF-8, bytes as they are. Parent directories must already exist. */
|
|
68
|
+
writeFile(path: string, content: string | Uint8Array): Promise<void>;
|
|
69
69
|
/** Release compute; files persist. */
|
|
70
70
|
stop(): Promise<void>;
|
|
71
71
|
}
|
|
@@ -17,3 +17,12 @@ export declare function isOwnershipLost(error: unknown): error is OwnershipLostE
|
|
|
17
17
|
* before `fn` runs, so nothing is written.
|
|
18
18
|
*/
|
|
19
19
|
export declare function ownedTx<T, S extends SessionDoc = SessionDoc>(store: SessionStore, sessionId: string, epoch: number, fn: (t: Tx, session: StoredSession<S>) => Promise<T>): Promise<T>;
|
|
20
|
+
/**
|
|
21
|
+
* `putPrincipal` on an id that holds another role (protocol 8): rotating a key never changes
|
|
22
|
+
* what it reaches, so an application key and a management key cannot share a name.
|
|
23
|
+
*/
|
|
24
|
+
export declare class PrincipalRoleConflict extends Error {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly code = "principal.role_conflict";
|
|
27
|
+
constructor(id: string);
|
|
28
|
+
}
|
package/dist/store/ownership.js
CHANGED
|
@@ -31,3 +31,16 @@ export function isOwnershipLost(error) {
|
|
|
31
31
|
export function ownedTx(store, sessionId, epoch, fn) {
|
|
32
32
|
return store.tx(async (t) => fn(t, await t.assertEpoch(sessionId, epoch)));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* `putPrincipal` on an id that holds another role (protocol 8): rotating a key never changes
|
|
36
|
+
* what it reaches, so an application key and a management key cannot share a name.
|
|
37
|
+
*/
|
|
38
|
+
export class PrincipalRoleConflict extends Error {
|
|
39
|
+
id;
|
|
40
|
+
code = "principal.role_conflict";
|
|
41
|
+
constructor(id) {
|
|
42
|
+
super(`Key ${id} holds another role`);
|
|
43
|
+
this.id = id;
|
|
44
|
+
this.name = "PrincipalRoleConflict";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -39,3 +39,5 @@ export declare function createPostgresClient(url: string, options?: PostgresClie
|
|
|
39
39
|
* needs `wal_level = logical` (a restart) and a role allowed to replicate.
|
|
40
40
|
*/
|
|
41
41
|
export declare function assertLogicalReplication(sql: Sql): Promise<void>;
|
|
42
|
+
/** Separate, bounded pool for read projections; inherits the Tenant connection credentials. */
|
|
43
|
+
export declare function createPostgresReadClient(source: PostgresClient): PostgresClient;
|
|
@@ -40,3 +40,32 @@ export async function assertLogicalReplication(sql) {
|
|
|
40
40
|
if (!row.replicates)
|
|
41
41
|
throw new Error("The Runtime's Postgres role cannot replicate: grant it REPLICATION (ALTER ROLE … REPLICATION). See DEPLOYMENT.md.");
|
|
42
42
|
}
|
|
43
|
+
/** Separate, bounded pool for read projections; inherits the Tenant connection credentials. */
|
|
44
|
+
export function createPostgresReadClient(source) {
|
|
45
|
+
const options = {
|
|
46
|
+
host: source.options.host.length === 1
|
|
47
|
+
? source.options.host[0]
|
|
48
|
+
: source.options.host
|
|
49
|
+
.map((host, i) => `${host}:${source.options.port[i] ?? source.options.port[0]}`)
|
|
50
|
+
.join(","),
|
|
51
|
+
port: source.options.port[0],
|
|
52
|
+
database: source.options.database,
|
|
53
|
+
username: source.options.user,
|
|
54
|
+
pass: source.options.pass ?? "",
|
|
55
|
+
ssl: source.options.ssl,
|
|
56
|
+
path: source.options.path,
|
|
57
|
+
max: 4,
|
|
58
|
+
idle_timeout: 30,
|
|
59
|
+
connect_timeout: 2,
|
|
60
|
+
onnotice() { },
|
|
61
|
+
connection: {
|
|
62
|
+
...source.options.connection,
|
|
63
|
+
application_name: "nylorun-reads",
|
|
64
|
+
statement_timeout: 2000,
|
|
65
|
+
idle_in_transaction_session_timeout: 2000,
|
|
66
|
+
default_transaction_read_only: true,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const pool = postgres(options);
|
|
70
|
+
return pool;
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ALTER TABLE "nylorun"."model_usage" ADD COLUMN "tokens_reported" boolean;--> statement-breakpoint
|
|
2
|
+
ALTER TABLE "nylorun"."model_usage" ADD COLUMN "cost_known" boolean;--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "nylorun"."model_usage" ADD COLUMN "txid" "xid8" DEFAULT pg_current_xact_id() NOT NULL;--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "nylorun"."sessions" ADD COLUMN "created_at" timestamp with time zone;--> statement-breakpoint
|
|
5
|
+
ALTER TABLE "nylorun"."sessions" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
|
|
6
|
+
CREATE INDEX "model_usage_session" ON "nylorun"."model_usage" USING btree ("session_id","created_at","id");--> statement-breakpoint
|
|
7
|
+
CREATE INDEX "model_usage_export" ON "nylorun"."model_usage" USING btree ("txid","id");--> statement-breakpoint
|
|
8
|
+
CREATE INDEX "sessions_created" ON "nylorun"."sessions" USING btree ("created_at" DESC NULLS LAST,"id" DESC NULLS FIRST);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
CREATE TABLE "nylorun"."definition_file_uses" (
|
|
2
|
+
"agent_id" text COLLATE "C" NOT NULL,
|
|
3
|
+
"manifest_hash" text COLLATE "C" NOT NULL,
|
|
4
|
+
"sha256" text COLLATE "C" NOT NULL,
|
|
5
|
+
CONSTRAINT "definition_file_uses_pkey" PRIMARY KEY("agent_id","manifest_hash","sha256")
|
|
6
|
+
);
|
|
7
|
+
--> statement-breakpoint
|
|
8
|
+
CREATE TABLE "nylorun"."definition_files" (
|
|
9
|
+
"sha256" text COLLATE "C" PRIMARY KEY NOT NULL,
|
|
10
|
+
"kind" text NOT NULL,
|
|
11
|
+
"size" bigint NOT NULL,
|
|
12
|
+
"content_type" text,
|
|
13
|
+
"created_at" text COLLATE "C" NOT NULL
|
|
14
|
+
);
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
CREATE INDEX "definition_file_uses_sha256" ON "nylorun"."definition_file_uses" USING btree ("sha256");
|
|
@@ -78,6 +78,34 @@
|
|
|
78
78
|
"when": 1791107553412,
|
|
79
79
|
"tag": "0010_oss_auth_removals",
|
|
80
80
|
"breakpoints": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"idx": 11,
|
|
84
|
+
"version": "7",
|
|
85
|
+
"when": 1791196553461,
|
|
86
|
+
"tag": "0011_key_roles",
|
|
87
|
+
"breakpoints": true
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"idx": 12,
|
|
91
|
+
"version": "7",
|
|
92
|
+
"when": 1791209959436,
|
|
93
|
+
"tag": "0012_session_reads",
|
|
94
|
+
"breakpoints": true
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"idx": 13,
|
|
98
|
+
"version": "7",
|
|
99
|
+
"when": 1791214258298,
|
|
100
|
+
"tag": "0013_definition_files",
|
|
101
|
+
"breakpoints": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"idx": 14,
|
|
105
|
+
"version": "7",
|
|
106
|
+
"when": 1791218903443,
|
|
107
|
+
"tag": "0014_action_endpoints_removed",
|
|
108
|
+
"breakpoints": true
|
|
81
109
|
}
|
|
82
110
|
]
|
|
83
111
|
}
|