@nylorun/runtime 0.13.0-beta → 0.15.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 +434 -0
- package/README.md +137 -41
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +40 -10
- package/dist/adapters/replication/pgoutput.d.ts +21 -0
- package/dist/adapters/replication/pgoutput.js +170 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +570 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +8065 -0
- package/dist/api/components.js +82 -0
- package/dist/api/gate/keys.d.ts +14 -0
- package/dist/api/gate/keys.js +59 -0
- package/dist/api/gate/routes.d.ts +34 -0
- package/dist/api/gate/routes.js +235 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +122 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +266 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +11 -0
- package/dist/api/http/routes/tenant.js +244 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +137 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/gates/contract.d.ts +51 -0
- package/dist/gates/contract.js +37 -0
- package/dist/gates/handler.d.ts +21 -0
- package/dist/gates/handler.js +55 -0
- package/dist/gates/http-client.d.ts +12 -0
- package/dist/gates/http-client.js +160 -0
- package/dist/gates/in-process.d.ts +32 -0
- package/dist/gates/in-process.js +42 -0
- package/dist/gates/inflight.d.ts +39 -0
- package/dist/gates/inflight.js +70 -0
- package/dist/gates/mcp-handler.d.ts +35 -0
- package/dist/gates/mcp-handler.js +142 -0
- package/dist/gates/meter.d.ts +24 -0
- package/dist/gates/meter.js +165 -0
- package/dist/gates/model-gate.d.ts +45 -0
- package/dist/gates/model-gate.js +1 -0
- package/dist/gates/tenant-vaults.d.ts +64 -0
- package/dist/gates/tenant-vaults.js +94 -0
- package/dist/gates/tool-calls.d.ts +51 -0
- package/dist/gates/tool-calls.js +64 -0
- package/dist/gates/tool-client.d.ts +12 -0
- package/dist/gates/tool-client.js +201 -0
- package/dist/gates/tool-contract.d.ts +116 -0
- package/dist/gates/tool-contract.js +64 -0
- package/dist/gates/tool-gate.d.ts +45 -0
- package/dist/gates/tool-gate.js +10 -0
- package/dist/host/admin-api.d.ts +23 -0
- package/dist/host/admin-api.js +74 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +181 -0
- package/dist/host/config-for.d.ts +3 -1
- package/dist/host/config-for.js +3 -2
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +36 -8
- package/dist/host/create-host.js +161 -330
- package/dist/host/execution.d.ts +20 -18
- package/dist/host/execution.js +2 -2
- package/dist/host/gates.d.ts +58 -0
- package/dist/host/gates.js +127 -0
- package/dist/host/http.d.ts +20 -4
- package/dist/host/http.js +81 -23
- package/dist/host/index.d.ts +1 -1
- package/dist/host/main.js +189 -58
- package/dist/host/stack-config.d.ts +91 -19
- package/dist/host/stack-config.js +246 -17
- package/dist/infra/database.d.ts +2 -2
- package/dist/infra/execution.d.ts +5 -5
- package/dist/infra/execution.js +8 -6
- package/dist/infra/index.d.ts +1 -1
- package/dist/infra/workers.d.ts +17 -17
- package/dist/infra/workers.js +9 -9
- package/dist/keys/client.d.ts +9 -0
- package/dist/keys/client.js +70 -0
- package/dist/keys/contract.d.ts +24 -0
- package/dist/keys/contract.js +12 -0
- package/dist/keys/keys.d.ts +41 -0
- package/dist/keys/keys.js +50 -0
- package/dist/mcp/connect.d.ts +43 -3
- package/dist/mcp/connect.js +12 -5
- package/dist/mcp/pool.d.ts +39 -3
- package/dist/mcp/pool.js +74 -11
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25889 -0
- package/dist/record/append.d.ts +51 -0
- package/dist/record/append.js +20 -0
- package/dist/record/envelope.d.ts +19 -0
- package/dist/record/envelope.js +49 -0
- package/dist/record/index.d.ts +11 -0
- package/dist/record/index.js +11 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/postgres/connect.d.ts +25 -9
- package/dist/store/postgres/connect.js +27 -10
- package/dist/store/postgres/db.d.ts +16 -0
- package/dist/store/postgres/db.js +28 -0
- package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
- package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
- package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
- package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
- package/dist/store/postgres/migrate.d.ts +68 -0
- package/dist/store/postgres/migrate.js +183 -0
- package/dist/store/postgres/record-writer.d.ts +4 -0
- package/dist/store/postgres/record-writer.js +34 -0
- package/dist/store/postgres/record.d.ts +5 -0
- package/dist/store/postgres/record.js +60 -0
- package/dist/store/postgres/schema.d.ts +3096 -0
- package/dist/store/postgres/schema.js +363 -0
- package/dist/store/postgres/store.d.ts +2 -4
- package/dist/store/postgres/store.js +590 -434
- package/dist/store/postgres/tenant.d.ts +56 -0
- package/dist/store/postgres/tenant.js +154 -0
- package/dist/store/types.d.ts +183 -132
- package/dist/store/types.js +0 -1
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +33 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/types.d.ts +78 -0
- package/dist/streams/relay/types.js +8 -0
- package/dist/streams/types.d.ts +71 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.d.ts +7 -2
- package/dist/tenant/advance.js +125 -14
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/cause.d.ts +25 -0
- package/dist/tenant/cause.js +45 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +111 -115
- package/dist/tenant/context.d.ts +101 -21
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +104 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +481 -0
- package/dist/tenant/effects.d.ts +20 -1
- package/dist/tenant/effects.js +159 -42
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +22 -7
- package/dist/tenant/ephemeral.js +83 -125
- package/dist/tenant/history.d.ts +64 -0
- package/dist/tenant/history.js +120 -0
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/model-setting.d.ts +2 -2
- package/dist/tenant/module.d.ts +17 -28
- package/dist/tenant/module.js +133 -279
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/paths.d.ts +11 -16
- package/dist/tenant/paths.js +24 -68
- package/dist/tenant/pool.d.ts +1 -3
- package/dist/tenant/pool.js +1 -19
- package/dist/tenant/principals.d.ts +23 -11
- package/dist/tenant/principals.js +61 -49
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +28 -20
- package/dist/tenant/runtime.js +95 -64
- package/dist/tenant/scheduler.d.ts +10 -3
- package/dist/tenant/scheduler.js +9 -5
- package/dist/tenant/session-streams.d.ts +127 -0
- package/dist/tenant/session-streams.js +404 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +3 -3
- package/dist/tenant/status.js +20 -13
- package/dist/tenant/store-pg.d.ts +9 -6
- package/dist/tenant/store-pg.js +60 -125
- package/dist/tenant/streams.d.ts +62 -85
- package/dist/tenant/streams.js +143 -226
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +180 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +69 -98
- package/dist/tenant/types.js +4 -15
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +13 -7
- package/dist/vault/host-model.d.ts +31 -0
- package/dist/vault/host-model.js +57 -0
- package/dist/vault/service.d.ts +44 -17
- package/dist/vault/service.js +126 -121
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +25 -9
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/store/memory.d.ts +0 -57
- package/dist/store/memory.js +0 -645
- package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
- package/dist/store/postgres/migrations/001_initial.js +0 -196
- package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
- package/dist/store/postgres/migrations/002_session_owner.js +0 -13
- package/dist/store/postgres/migrations/index.d.ts +0 -49
- package/dist/store/postgres/migrations/index.js +0 -76
- package/dist/store/postgres/names.d.ts +0 -11
- package/dist/store/postgres/names.js +0 -42
- package/dist/store/postgres/tenants.d.ts +0 -90
- package/dist/store/postgres/tenants.js +0 -214
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/envelope.d.ts +0 -7
- package/dist/tenant/envelope.js +0 -22
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/quarantine-error.d.ts +0 -6
- package/dist/tenant/quarantine-error.js +0 -9
- package/dist/tenant/quarantine.d.ts +0 -27
- package/dist/tenant/quarantine.js +0 -72
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
- package/dist/tenant/store-memory.d.ts +0 -16
- package/dist/tenant/store-memory.js +0 -111
- /package/dist/{store → record}/cursor.d.ts +0 -0
- /package/dist/{store → record}/cursor.js +0 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { decodeCursor, encodeCursor } from "../record/index.js";
|
|
2
|
+
import { basinOf } from "../streams/basin.js";
|
|
3
|
+
import { sessionStream, } from "../streams/types.js";
|
|
4
|
+
import { fail } from "./http.js";
|
|
5
|
+
export function createSessionStreams() {
|
|
6
|
+
return { wiring: undefined, generation: 0, sessions: new Map() };
|
|
7
|
+
}
|
|
8
|
+
const RETRY_MIN_MS = 100;
|
|
9
|
+
const RETRY_MAX_MS = 2000;
|
|
10
|
+
function frame(event) {
|
|
11
|
+
return `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
12
|
+
}
|
|
13
|
+
export function send(response, data) {
|
|
14
|
+
if (!response.write(data))
|
|
15
|
+
response.destroy();
|
|
16
|
+
}
|
|
17
|
+
/** The frame that tells a client why the Runtime ended its stream (no `id:`: not an event). */
|
|
18
|
+
function closedFrame(reason) {
|
|
19
|
+
return `event: nylorun.closed\ndata: ${JSON.stringify({ reason })}\n\n`;
|
|
20
|
+
}
|
|
21
|
+
/** An observer sink writing SSE frames to `response`. */
|
|
22
|
+
function sseSink(response) {
|
|
23
|
+
return {
|
|
24
|
+
write: (event) => send(response, frame(event)),
|
|
25
|
+
end: (reason) => {
|
|
26
|
+
if (reason && !response.writableEnded)
|
|
27
|
+
send(response, closedFrame(reason));
|
|
28
|
+
response.end();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Start an SSE response with keepalives; `onClose` runs once the client goes away. */
|
|
33
|
+
export function openSse(request, response, onClose) {
|
|
34
|
+
response.writeHead(200, {
|
|
35
|
+
"content-type": "text/event-stream",
|
|
36
|
+
"cache-control": "no-cache",
|
|
37
|
+
connection: "keep-alive",
|
|
38
|
+
"x-accel-buffering": "no",
|
|
39
|
+
});
|
|
40
|
+
response.flushHeaders();
|
|
41
|
+
const timer = setInterval(() => send(response, ": keepalive\n\n"), 15000);
|
|
42
|
+
timer.unref();
|
|
43
|
+
request.on("close", () => {
|
|
44
|
+
clearInterval(timer);
|
|
45
|
+
onClose();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** The resume cursor: `?cursor=` wins over `Last-Event-ID`. */
|
|
49
|
+
export function requestCursor(request, url) {
|
|
50
|
+
return (url.searchParams.get("cursor") ??
|
|
51
|
+
(typeof request.headers["last-event-id"] === "string"
|
|
52
|
+
? request.headers["last-event-id"]
|
|
53
|
+
: undefined));
|
|
54
|
+
}
|
|
55
|
+
/** The first sequence to read: after the cursor's, or the session's first event. */
|
|
56
|
+
function startSeq(sessionId, cursor) {
|
|
57
|
+
return cursor ? decodeCursor(sessionId, cursor) + 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
/** `agent` keeps only events of one agent used as a tool, by delegationId or path. */
|
|
60
|
+
function belongsTo(event, agent) {
|
|
61
|
+
const ref = event.payload?.agent;
|
|
62
|
+
return ref?.delegationId === agent || ref?.path === agent;
|
|
63
|
+
}
|
|
64
|
+
/** The basin the Tenant's session streams are in now. */
|
|
65
|
+
export function currentBasin(ctx) {
|
|
66
|
+
return basinOf(ctx.config.tenantId, ctx.sessionStreams.generation);
|
|
67
|
+
}
|
|
68
|
+
/** The session's stream now, or undefined when the session is gone. */
|
|
69
|
+
async function currentStream(ctx, sessionId) {
|
|
70
|
+
const session = await ctx.store.tx((t) => t.get("sessions", sessionId));
|
|
71
|
+
return session && { basin: currentBasin(ctx), stream: sessionStream(sessionId) };
|
|
72
|
+
}
|
|
73
|
+
const sameStream = (feed, ref) => ref !== undefined && ref.basin === feed.basin && ref.stream === feed.stream;
|
|
74
|
+
function streamsOf(ctx) {
|
|
75
|
+
return (ctx.sessionStreams.wiring?.streams ?? fail(503, "Session streams are unavailable"));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `GET /v1/sessions/:id/items`: the session stream after the cursor, up to its tail when the
|
|
79
|
+
* read starts. The cursor returned is the last record read (the request's cursor when none
|
|
80
|
+
* was), whatever the `agent` filter kept.
|
|
81
|
+
*/
|
|
82
|
+
export async function readHistory(ctx, sessionId, cursor, agent) {
|
|
83
|
+
const from = startSeq(sessionId, cursor);
|
|
84
|
+
const streams = streamsOf(ctx);
|
|
85
|
+
const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
|
|
86
|
+
const items = [];
|
|
87
|
+
let last;
|
|
88
|
+
try {
|
|
89
|
+
for await (const record of streams.read(ref.basin, ref.stream, from, { follow: false })) {
|
|
90
|
+
last = record.seq;
|
|
91
|
+
if (served(record.body) && (agent === undefined || belongsTo(record.body, agent)))
|
|
92
|
+
items.push(record.body);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
ctx.config.logger.warn("session history read failed", {
|
|
97
|
+
sessionId,
|
|
98
|
+
message: error instanceof Error ? error.message : String(error),
|
|
99
|
+
});
|
|
100
|
+
fail(503, "Session history is unavailable");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
items,
|
|
104
|
+
cursor: last !== undefined ? encodeCursor(sessionId, last) : cursor ?? null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
|
|
109
|
+
* follows it until the client leaves, the session is reset or deleted, or the Tenant closes.
|
|
110
|
+
*/
|
|
111
|
+
export async function streamSessionEvents(ctx, request, response, sessionId, cursor, holder) {
|
|
112
|
+
const observer = {
|
|
113
|
+
sink: sseSink(response),
|
|
114
|
+
next: startSeq(sessionId, cursor),
|
|
115
|
+
...(holder ? { holder } : {}),
|
|
116
|
+
};
|
|
117
|
+
const joined = await join(ctx, sessionId, observer);
|
|
118
|
+
openSse(request, response, () => leave(ctx.sessionStreams, joined, observer));
|
|
119
|
+
armDeadline(ctx.sessionStreams, joined, observer);
|
|
120
|
+
}
|
|
121
|
+
/** Thrown by `observeSession` when the Runtime ended the stream for its token. */
|
|
122
|
+
export class StreamClosed extends Error {
|
|
123
|
+
reason;
|
|
124
|
+
cursor;
|
|
125
|
+
constructor(reason,
|
|
126
|
+
/** The cursor of the last event delivered, if any. */
|
|
127
|
+
cursor) {
|
|
128
|
+
super(`The stream ended: ${reason}`);
|
|
129
|
+
this.reason = reason;
|
|
130
|
+
this.cursor = cursor;
|
|
131
|
+
this.name = "StreamClosed";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A session's events from `cursor`, in process: the same shared read and deadlines as an SSE
|
|
136
|
+
* client (`streamSessionEvents`). Ends when `signal` aborts or the feed ends; throws
|
|
137
|
+
* `StreamClosed` when the Runtime ends it for its token (expiry or revocation).
|
|
138
|
+
*/
|
|
139
|
+
export async function* observeSession(ctx, sessionId, cursor, options) {
|
|
140
|
+
const queue = [];
|
|
141
|
+
let ended;
|
|
142
|
+
let wake;
|
|
143
|
+
const notify = () => {
|
|
144
|
+
const resolve = wake;
|
|
145
|
+
wake = undefined;
|
|
146
|
+
resolve?.();
|
|
147
|
+
};
|
|
148
|
+
const observer = {
|
|
149
|
+
sink: {
|
|
150
|
+
write: (event) => {
|
|
151
|
+
queue.push(event);
|
|
152
|
+
notify();
|
|
153
|
+
},
|
|
154
|
+
end: (reason) => {
|
|
155
|
+
ended = reason ? { reason } : {};
|
|
156
|
+
notify();
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
next: startSeq(sessionId, cursor),
|
|
160
|
+
...(options.holder ? { holder: options.holder } : {}),
|
|
161
|
+
};
|
|
162
|
+
const joined = await join(ctx, sessionId, observer);
|
|
163
|
+
armDeadline(ctx.sessionStreams, joined, observer);
|
|
164
|
+
const stop = () => {
|
|
165
|
+
leave(ctx.sessionStreams, joined, observer);
|
|
166
|
+
notify();
|
|
167
|
+
};
|
|
168
|
+
options.signal.addEventListener("abort", stop, { once: true });
|
|
169
|
+
let last = cursor;
|
|
170
|
+
try {
|
|
171
|
+
for (;;) {
|
|
172
|
+
while (queue.length > 0) {
|
|
173
|
+
const event = queue.shift();
|
|
174
|
+
last = event.cursor;
|
|
175
|
+
yield event;
|
|
176
|
+
}
|
|
177
|
+
if (options.signal.aborted)
|
|
178
|
+
return;
|
|
179
|
+
if (ended) {
|
|
180
|
+
if (ended.reason)
|
|
181
|
+
throw new StreamClosed(ended.reason, last);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await new Promise((resolve) => (wake = resolve));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
options.signal.removeEventListener("abort", stop);
|
|
189
|
+
leave(ctx.sessionStreams, joined, observer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Ends the observer when its token expires. */
|
|
193
|
+
function armDeadline(hub, feed, observer) {
|
|
194
|
+
if (!observer.holder)
|
|
195
|
+
return;
|
|
196
|
+
observer.deadline = setTimeout(() => endObserver(hub, feed, observer, "token_expired"), Math.max(0, observer.holder.expiresAt - Date.now()));
|
|
197
|
+
observer.deadline.unref();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Adds `observer` to the session's shared read, starting or restarting it from the
|
|
201
|
+
* observer's position when needed. 404 when the session has no stream.
|
|
202
|
+
*/
|
|
203
|
+
async function join(ctx, sessionId, observer) {
|
|
204
|
+
const from = observer.next;
|
|
205
|
+
streamsOf(ctx);
|
|
206
|
+
const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
|
|
207
|
+
const hub = ctx.sessionStreams;
|
|
208
|
+
let feed = hub.sessions.get(sessionId);
|
|
209
|
+
if (feed && !sameStream(feed, ref)) {
|
|
210
|
+
// The Tenant moved to a new basin since that feed started: it follows an abandoned stream.
|
|
211
|
+
endFeed(hub, feed);
|
|
212
|
+
feed = undefined;
|
|
213
|
+
}
|
|
214
|
+
if (!feed) {
|
|
215
|
+
feed = {
|
|
216
|
+
sessionId,
|
|
217
|
+
basin: ref.basin,
|
|
218
|
+
stream: ref.stream,
|
|
219
|
+
next: from,
|
|
220
|
+
observers: new Set(),
|
|
221
|
+
read: new AbortController(),
|
|
222
|
+
};
|
|
223
|
+
hub.sessions.set(sessionId, feed);
|
|
224
|
+
runFeed(ctx, feed);
|
|
225
|
+
}
|
|
226
|
+
else if (from < feed.next) {
|
|
227
|
+
// Behind the shared read: restart it here; observers ahead skip what they have.
|
|
228
|
+
feed.read.abort();
|
|
229
|
+
feed.read = new AbortController();
|
|
230
|
+
feed.next = from;
|
|
231
|
+
runFeed(ctx, feed);
|
|
232
|
+
}
|
|
233
|
+
feed.observers.add(observer);
|
|
234
|
+
return feed;
|
|
235
|
+
}
|
|
236
|
+
/** Ends one observer early (its token expired or was revoked). */
|
|
237
|
+
function endObserver(hub, feed, observer, reason) {
|
|
238
|
+
if (!feed.observers.has(observer))
|
|
239
|
+
return;
|
|
240
|
+
leave(hub, feed, observer);
|
|
241
|
+
observer.sink.end(reason);
|
|
242
|
+
}
|
|
243
|
+
function leave(hub, feed, observer) {
|
|
244
|
+
if (observer.deadline)
|
|
245
|
+
clearTimeout(observer.deadline);
|
|
246
|
+
if (!feed.observers.delete(observer))
|
|
247
|
+
return;
|
|
248
|
+
if (feed.observers.size > 0)
|
|
249
|
+
return;
|
|
250
|
+
feed.read.abort();
|
|
251
|
+
if (hub.sessions.get(feed.sessionId) === feed)
|
|
252
|
+
hub.sessions.delete(feed.sessionId);
|
|
253
|
+
}
|
|
254
|
+
function deliver(observer, record) {
|
|
255
|
+
if (record.seq < observer.next)
|
|
256
|
+
return;
|
|
257
|
+
observer.next = record.seq + 1;
|
|
258
|
+
if (served(record.body))
|
|
259
|
+
observer.sink.write(record.body);
|
|
260
|
+
}
|
|
261
|
+
/** Internal events (`transcript.updated`) are recorded and streamed but never served. */
|
|
262
|
+
export function served(event) {
|
|
263
|
+
return event.visibility !== "internal";
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Follows the feed's stream from `feed.next` for the current `feed.read`, retrying failed
|
|
267
|
+
* reads from where it stopped while the session still has that stream. A read that ends by
|
|
268
|
+
* itself (the stream's Tenant is gone or the streams closed), or a session that is gone or
|
|
269
|
+
* whose Tenant moved to a new basin, ends the observers.
|
|
270
|
+
*/
|
|
271
|
+
function runFeed(ctx, feed) {
|
|
272
|
+
const read = feed.read;
|
|
273
|
+
const signal = read.signal;
|
|
274
|
+
void (async () => {
|
|
275
|
+
let delay = RETRY_MIN_MS;
|
|
276
|
+
while (!signal.aborted) {
|
|
277
|
+
const streams = ctx.sessionStreams.wiring?.streams;
|
|
278
|
+
if (!streams)
|
|
279
|
+
break;
|
|
280
|
+
try {
|
|
281
|
+
for await (const record of streams.read(feed.basin, feed.stream, feed.next, { signal })) {
|
|
282
|
+
// A restart replaced this read: the new one owns `feed.next`.
|
|
283
|
+
if (signal.aborted || feed.read !== read)
|
|
284
|
+
return;
|
|
285
|
+
if (record.seq < feed.next)
|
|
286
|
+
continue;
|
|
287
|
+
feed.next = record.seq + 1;
|
|
288
|
+
delay = RETRY_MIN_MS;
|
|
289
|
+
for (const observer of feed.observers)
|
|
290
|
+
deliver(observer, record);
|
|
291
|
+
}
|
|
292
|
+
if (signal.aborted)
|
|
293
|
+
return;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
if (signal.aborted)
|
|
298
|
+
return;
|
|
299
|
+
ctx.config.logger.warn("session stream read failed; retrying", {
|
|
300
|
+
sessionId: feed.sessionId,
|
|
301
|
+
message: error instanceof Error ? error.message : String(error),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (!(await stillCurrent(ctx, feed)))
|
|
305
|
+
break;
|
|
306
|
+
await sleep(delay, signal);
|
|
307
|
+
delay = Math.min(delay * 2, RETRY_MAX_MS);
|
|
308
|
+
}
|
|
309
|
+
if (signal.aborted || feed.read !== read)
|
|
310
|
+
return;
|
|
311
|
+
endFeed(ctx.sessionStreams, feed);
|
|
312
|
+
})();
|
|
313
|
+
}
|
|
314
|
+
/** False when the feed's session is gone or the Tenant moved to a new basin. True when unsure. */
|
|
315
|
+
async function stillCurrent(ctx, feed) {
|
|
316
|
+
try {
|
|
317
|
+
return sameStream(feed, await currentStream(ctx, feed.sessionId));
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Ends every feed in this process whose session is gone or whose Tenant moved to a new basin
|
|
325
|
+
* generation, and moves this process to the Tenant's current generation. Run on a
|
|
326
|
+
* `sessions.reset` signal and periodically, so feeds on nodes other than the one that reset
|
|
327
|
+
* the Tenant end too.
|
|
328
|
+
*/
|
|
329
|
+
export async function checkSessionStreams(ctx) {
|
|
330
|
+
const feeds = [...ctx.sessionStreams.sessions.values()];
|
|
331
|
+
// Also a backstop for lost `subject.revoked` signals: the holders' current epochs.
|
|
332
|
+
const subjects = new Set();
|
|
333
|
+
for (const feed of feeds)
|
|
334
|
+
for (const observer of feed.observers)
|
|
335
|
+
if (observer.holder)
|
|
336
|
+
subjects.add(observer.holder.subject);
|
|
337
|
+
const { exists, epochs, generation } = await ctx.store.tx(async (t) => {
|
|
338
|
+
const exists = new Set();
|
|
339
|
+
for (const feed of feeds)
|
|
340
|
+
if (await t.get("sessions", feed.sessionId))
|
|
341
|
+
exists.add(feed.sessionId);
|
|
342
|
+
return {
|
|
343
|
+
exists,
|
|
344
|
+
generation: (await t.basinGenerations()).current,
|
|
345
|
+
epochs: subjects.size === 0
|
|
346
|
+
? new Map()
|
|
347
|
+
: await t.subjectEpochs([...subjects]),
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
if (generation !== ctx.sessionStreams.generation)
|
|
351
|
+
ctx.sessionStreams.wiring?.moveTo(generation);
|
|
352
|
+
for (const feed of feeds)
|
|
353
|
+
if (ctx.sessionStreams.sessions.get(feed.sessionId) === feed &&
|
|
354
|
+
!sameStream(feed, exists.has(feed.sessionId)
|
|
355
|
+
? { basin: currentBasin(ctx), stream: sessionStream(feed.sessionId) }
|
|
356
|
+
: undefined))
|
|
357
|
+
endFeed(ctx.sessionStreams, feed);
|
|
358
|
+
for (const [subject, epoch] of epochs)
|
|
359
|
+
endSubjectStreams(ctx.sessionStreams, subject, epoch);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
|
|
363
|
+
* this process. Other processes do the same on the `subject.revoked` signal.
|
|
364
|
+
*/
|
|
365
|
+
export function endSubjectStreams(hub, subject, epoch) {
|
|
366
|
+
for (const feed of [...hub.sessions.values()])
|
|
367
|
+
for (const observer of [...feed.observers])
|
|
368
|
+
if (observer.holder?.subject === subject && observer.holder.epoch < epoch)
|
|
369
|
+
endObserver(hub, feed, observer, "revoked");
|
|
370
|
+
}
|
|
371
|
+
function endFeed(hub, feed) {
|
|
372
|
+
feed.read.abort();
|
|
373
|
+
if (hub.sessions.get(feed.sessionId) === feed)
|
|
374
|
+
hub.sessions.delete(feed.sessionId);
|
|
375
|
+
for (const observer of feed.observers) {
|
|
376
|
+
if (observer.deadline)
|
|
377
|
+
clearTimeout(observer.deadline);
|
|
378
|
+
observer.sink.end();
|
|
379
|
+
}
|
|
380
|
+
feed.observers.clear();
|
|
381
|
+
}
|
|
382
|
+
/** Reset or close: end every session observer and stop every shared read. */
|
|
383
|
+
export function clearObservers(hub) {
|
|
384
|
+
for (const feed of [...hub.sessions.values()])
|
|
385
|
+
endFeed(hub, feed);
|
|
386
|
+
hub.sessions.clear();
|
|
387
|
+
}
|
|
388
|
+
/** Close: end every session observer. */
|
|
389
|
+
export function endAllStreams(hub) {
|
|
390
|
+
clearObservers(hub);
|
|
391
|
+
}
|
|
392
|
+
export function sleep(ms, signal) {
|
|
393
|
+
return new Promise((resolve) => {
|
|
394
|
+
if (signal.aborted)
|
|
395
|
+
return resolve();
|
|
396
|
+
const done = () => {
|
|
397
|
+
clearTimeout(timer);
|
|
398
|
+
signal.removeEventListener("abort", done);
|
|
399
|
+
resolve();
|
|
400
|
+
};
|
|
401
|
+
const timer = setTimeout(done, ms);
|
|
402
|
+
signal.addEventListener("abort", done, { once: true });
|
|
403
|
+
});
|
|
404
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { type PutAgentRequest, type PutSessionRequest } from "@nylorun/core/contracts";
|
|
7
7
|
import type { Tx } from "../store/types.js";
|
|
8
|
-
import { type Session, type TenantContext } from "./context.js";
|
|
8
|
+
import { type Session, type SessionAccess, type TenantContext } from "./context.js";
|
|
9
9
|
export declare function listDefinitions(ctx: TenantContext): Promise<{
|
|
10
10
|
agents: {
|
|
11
11
|
agentId: any;
|
|
@@ -14,13 +14,27 @@ export declare function listDefinitions(ctx: TenantContext): Promise<{
|
|
|
14
14
|
implementationVersion: string;
|
|
15
15
|
}[];
|
|
16
16
|
}>;
|
|
17
|
+
/**
|
|
18
|
+
* The agents a subject token or publishable key may see: id, name and description only, never
|
|
19
|
+
* instructions, tools or MCP servers, and only the agents allowed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listAgentsPublic(ctx: TenantContext, agents: ReadonlySet<string> | "*"): Promise<{
|
|
22
|
+
agents: {
|
|
23
|
+
agentId: string;
|
|
24
|
+
name?: any;
|
|
25
|
+
description?: any;
|
|
26
|
+
}[];
|
|
27
|
+
}>;
|
|
17
28
|
export declare function putDefinition(ctx: TenantContext, agentId: string, body: PutAgentRequest): Promise<{
|
|
18
29
|
agentId: string;
|
|
19
30
|
manifestHash: string;
|
|
20
31
|
implementationVersion: string;
|
|
21
32
|
}>;
|
|
22
|
-
/**
|
|
23
|
-
|
|
33
|
+
/**
|
|
34
|
+
* `access`, when the request acts for a person, limits the list to that person's sessions of
|
|
35
|
+
* the agents they may use.
|
|
36
|
+
*/
|
|
37
|
+
export declare function listSessions(ctx: TenantContext, agentId: string | null, access?: SessionAccess): Promise<{
|
|
24
38
|
sessions: {
|
|
25
39
|
id: string;
|
|
26
40
|
agentId: string;
|
|
@@ -34,7 +48,17 @@ export declare function listSessions(ctx: TenantContext, agentId: string | null,
|
|
|
34
48
|
* the request acts for a subject, must be the session's owner: another owner's session id is
|
|
35
49
|
* the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
|
|
36
50
|
* sandboxes.
|
|
51
|
+
*
|
|
52
|
+
* The session's sandbox is decided here and fixed for its life (§5 of Sandboxes v3): shared
|
|
53
|
+
* with `{ session }`, none with `false`, inline, or the Tenant default when omitted. A definition
|
|
54
|
+
* that declares its own sandbox (`.sandbox()`) keeps it and takes no sandbox request.
|
|
37
55
|
*/
|
|
38
|
-
export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest,
|
|
56
|
+
export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest, access?: SessionAccess, options?: {
|
|
57
|
+
/**
|
|
58
|
+
* Create the session if it is missing and otherwise leave it as it is (an AG-UI thread's
|
|
59
|
+
* session): an existing one must have the same owner and agent, or it is a 404.
|
|
60
|
+
*/
|
|
61
|
+
createOnly?: boolean;
|
|
62
|
+
}): Promise<Session>;
|
|
39
63
|
/** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
|
|
40
64
|
export declare function sessionView(t: Tx, s: Session): Promise<unknown>;
|
package/dist/tenant/sessions.js
CHANGED
|
@@ -7,9 +7,12 @@ import { DefinitionDocumentSchema, } from "@nylorun/core/contracts";
|
|
|
7
7
|
import { hashManifest } from "@nylorun/core/compatibility";
|
|
8
8
|
import { aggregateWaits, isWorkflowManifest } from "../core/flow-host.js";
|
|
9
9
|
import { canonical } from "../store/canonical.js";
|
|
10
|
-
import { newStreamIncarnation } from "../streams/types.js";
|
|
11
10
|
import { validateSandboxAttach } from "../core/sandbox-routes.js";
|
|
12
|
-
import {
|
|
11
|
+
import { resolveSandbox } from "../sandbox/resolve.js";
|
|
12
|
+
import { withSandboxCapability } from "../sandbox/session-sandbox.js";
|
|
13
|
+
import { declaredSandboxes, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
|
|
14
|
+
import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
|
|
15
|
+
import { sandboxLookup, } from "./context.js";
|
|
13
16
|
import { fail } from "./http.js";
|
|
14
17
|
/** A session's creation identity: everything but the request id and vault attachments. */
|
|
15
18
|
const sessionIdentity = (value) => {
|
|
@@ -27,10 +30,31 @@ export async function listDefinitions(ctx) {
|
|
|
27
30
|
})),
|
|
28
31
|
};
|
|
29
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The agents a subject token or publishable key may see: id, name and description only, never
|
|
35
|
+
* instructions, tools or MCP servers, and only the agents allowed.
|
|
36
|
+
*/
|
|
37
|
+
export async function listAgentsPublic(ctx, agents) {
|
|
38
|
+
const definitions = await ctx.store.tx((t) => t.listDefinitions());
|
|
39
|
+
return {
|
|
40
|
+
agents: definitions
|
|
41
|
+
.filter((d) => agents === "*" || agents.has(d.manifest.id))
|
|
42
|
+
.map((d) => ({
|
|
43
|
+
agentId: d.manifest.id,
|
|
44
|
+
...(typeof d.manifest.name === "string" ? { name: d.manifest.name } : {}),
|
|
45
|
+
...(typeof d.manifest.description === "string"
|
|
46
|
+
? { description: d.manifest.description }
|
|
47
|
+
: {}),
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
30
51
|
export async function putDefinition(ctx, agentId, body) {
|
|
31
52
|
if (body.manifest.id !== agentId)
|
|
32
53
|
fail(400, "Agent id mismatch");
|
|
33
54
|
DefinitionDocumentSchema.parse(body.manifest);
|
|
55
|
+
const declared = declaredSandboxes(body.manifest);
|
|
56
|
+
if (declared.length > 0)
|
|
57
|
+
fail(400, `${declared.join(", ")} declares a sandbox. Agents no longer declare one: remove .sandbox() and open the session with it, createSession({ sandbox: { image, network: { allow }, resources } }), or set the Tenant's default sandbox. See MIGRATION.md.`);
|
|
34
58
|
const definition = {
|
|
35
59
|
...body,
|
|
36
60
|
manifestHash: hashManifest(body.manifest),
|
|
@@ -42,14 +66,20 @@ export async function putDefinition(ctx, agentId, body) {
|
|
|
42
66
|
implementationVersion: body.implementationVersion,
|
|
43
67
|
};
|
|
44
68
|
}
|
|
45
|
-
/**
|
|
46
|
-
|
|
69
|
+
/**
|
|
70
|
+
* `access`, when the request acts for a person, limits the list to that person's sessions of
|
|
71
|
+
* the agents they may use.
|
|
72
|
+
*/
|
|
73
|
+
export async function listSessions(ctx, agentId, access) {
|
|
47
74
|
const sessions = await ctx.store.tx((t) => t.listSessions({
|
|
48
75
|
...(agentId === null ? {} : { agentId }),
|
|
49
|
-
...(
|
|
76
|
+
...(access === undefined ? {} : { ownerUserId: access.owner }),
|
|
50
77
|
}));
|
|
78
|
+
const allowed = access?.agents;
|
|
51
79
|
return {
|
|
52
|
-
sessions: sessions
|
|
80
|
+
sessions: sessions
|
|
81
|
+
.filter((s) => allowed === undefined || allowed.has(s.agentId))
|
|
82
|
+
.map((s) => ({
|
|
53
83
|
id: s.id,
|
|
54
84
|
agentId: s.agentId,
|
|
55
85
|
ownerUserId: s.ownerUserId,
|
|
@@ -63,24 +93,33 @@ export async function listSessions(ctx, agentId, owner) {
|
|
|
63
93
|
* the request acts for a subject, must be the session's owner: another owner's session id is
|
|
64
94
|
* the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
|
|
65
95
|
* sandboxes.
|
|
96
|
+
*
|
|
97
|
+
* The session's sandbox is decided here and fixed for its life (§5 of Sandboxes v3): shared
|
|
98
|
+
* with `{ session }`, none with `false`, inline, or the Tenant default when omitted. A definition
|
|
99
|
+
* that declares its own sandbox (`.sandbox()`) keeps it and takes no sandbox request.
|
|
66
100
|
*/
|
|
67
|
-
export function putSession(ctx, id, body,
|
|
101
|
+
export async function putSession(ctx, id, body, access, options = {}) {
|
|
68
102
|
const vaultIds = body.vaultIds ?? [];
|
|
69
103
|
const credentialSelections = body.credentialSelections ?? [];
|
|
70
|
-
const opaque =
|
|
71
|
-
if (opaque && body.ownerUserId !== owner)
|
|
104
|
+
const opaque = access !== undefined;
|
|
105
|
+
if (opaque && body.ownerUserId !== access.owner)
|
|
72
106
|
fail(403, "ownerUserId must be the subject");
|
|
107
|
+
// An agent the subject may not use is the 404 of a missing definition.
|
|
108
|
+
if (access?.agents !== undefined && !access.agents.has(body.agentId))
|
|
109
|
+
fail(404, "Definition not found");
|
|
73
110
|
return ctx.store.tx(async (t) => {
|
|
74
111
|
const prior = await t.lockSession(id);
|
|
75
|
-
if (opaque &&
|
|
112
|
+
if (opaque &&
|
|
113
|
+
prior &&
|
|
114
|
+
(prior.ownerUserId !== access.owner ||
|
|
115
|
+
(access.agents !== undefined && !access.agents.has(prior.agentId))))
|
|
76
116
|
fail(404, "Session not found");
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: undefined;
|
|
117
|
+
if (prior && options.createOnly) {
|
|
118
|
+
if (prior.ownerUserId !== body.ownerUserId ||
|
|
119
|
+
prior.agentId !== body.agentId)
|
|
120
|
+
fail(404, "Session not found");
|
|
121
|
+
return prior;
|
|
122
|
+
}
|
|
84
123
|
await ctx.vault.assertAttachment(t, body.ownerUserId, vaultIds, credentialSelections, { opaque });
|
|
85
124
|
if (prior) {
|
|
86
125
|
if (sessionIdentity(prior.creation) !== sessionIdentity(body))
|
|
@@ -88,18 +127,19 @@ export function putSession(ctx, id, body, owner) {
|
|
|
88
127
|
prior.vaultIds = vaultIds;
|
|
89
128
|
prior.credentialSelections = credentialSelections;
|
|
90
129
|
prior.creation = body;
|
|
91
|
-
if (sandboxOwnerId !== undefined)
|
|
92
|
-
prior.sandboxOwnerId = sandboxOwnerId;
|
|
93
130
|
await t.put("sessions", id, prior);
|
|
94
131
|
await ctx.vault.recordAttachment(t, id, vaultIds);
|
|
95
132
|
return prior;
|
|
96
133
|
}
|
|
134
|
+
const definition = (await t.get("definitions", body.agentId)) ??
|
|
135
|
+
fail(404, "Definition not found");
|
|
136
|
+
const sandbox = await sessionSandbox(t, body, definition, { opaque });
|
|
97
137
|
const created = {
|
|
98
138
|
id,
|
|
99
139
|
agentId: body.agentId,
|
|
100
140
|
ownerUserId: body.ownerUserId,
|
|
101
|
-
manifest: definition.manifest,
|
|
102
|
-
manifestHash: definition.manifestHash,
|
|
141
|
+
manifest: sandbox.manifest ?? definition.manifest,
|
|
142
|
+
manifestHash: sandbox.manifestHash ?? definition.manifestHash,
|
|
103
143
|
implementationVersion: definition.implementationVersion,
|
|
104
144
|
info: body.info,
|
|
105
145
|
status: "idle",
|
|
@@ -108,14 +148,66 @@ export function putSession(ctx, id, body, owner) {
|
|
|
108
148
|
vaultIds,
|
|
109
149
|
credentialSelections,
|
|
110
150
|
pluginRoots: definition.pluginRoots ?? {},
|
|
111
|
-
...(sandboxOwnerId !== undefined
|
|
112
|
-
|
|
151
|
+
...(sandbox.sandboxOwnerId !== undefined
|
|
152
|
+
? { sandboxOwnerId: sandbox.sandboxOwnerId }
|
|
153
|
+
: {}),
|
|
154
|
+
...(sandbox.spec !== undefined
|
|
155
|
+
? { sandbox: sandbox.spec, sandboxSource: sandbox.source }
|
|
156
|
+
: {}),
|
|
113
157
|
};
|
|
114
158
|
await t.put("sessions", id, created);
|
|
115
159
|
await ctx.vault.recordAttachment(t, id, vaultIds);
|
|
116
160
|
return created;
|
|
117
161
|
});
|
|
118
162
|
}
|
|
163
|
+
function isShare(value) {
|
|
164
|
+
return typeof value === "object" && value !== null && "session" in value;
|
|
165
|
+
}
|
|
166
|
+
/** Decide a new session's sandbox and, for an agent, the manifest that carries its tools. */
|
|
167
|
+
async function sessionSandbox(t, body, definition, options) {
|
|
168
|
+
const request = body.sandbox;
|
|
169
|
+
const declared = sandboxSpecOf(definition.manifest) !== undefined;
|
|
170
|
+
if (isShare(request)) {
|
|
171
|
+
const lookup = await sandboxLookup(t, request.session);
|
|
172
|
+
const sandboxOwnerId = validateSandboxAttach(body, definition.manifest, lookup, {
|
|
173
|
+
opaque: options.opaque,
|
|
174
|
+
inherit: !declared,
|
|
175
|
+
});
|
|
176
|
+
if (declared || sandboxOwnerId === undefined)
|
|
177
|
+
return { sandboxOwnerId };
|
|
178
|
+
const spec = sessionSandboxSpec(lookup(request.session));
|
|
179
|
+
return { ...(await pin(definition, spec, "shared")), sandboxOwnerId };
|
|
180
|
+
}
|
|
181
|
+
if (declared) {
|
|
182
|
+
if (request !== undefined)
|
|
183
|
+
fail(400, `'${body.agentId}' declares its own sandbox with .sandbox(). Remove it from the agent to choose the sandbox when opening the session, or omit sandbox here.`);
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
const resolved = resolveSandbox({
|
|
187
|
+
request,
|
|
188
|
+
config: effectiveSandboxConfig(await readSandboxConfig(t)),
|
|
189
|
+
actingForSubject: options.opaque,
|
|
190
|
+
});
|
|
191
|
+
if (resolved.kind === "none")
|
|
192
|
+
return {};
|
|
193
|
+
if (resolved.kind === "error")
|
|
194
|
+
return fail(resolved.status, resolved.errors.join(" "));
|
|
195
|
+
return pin(definition, resolved.spec, resolved.source);
|
|
196
|
+
}
|
|
197
|
+
async function pin(definition, spec, source) {
|
|
198
|
+
// A workflow's manifest stays as registered: Action endpoints match workflow actions by its hash.
|
|
199
|
+
if (isWorkflowManifest(definition.manifest))
|
|
200
|
+
return { spec, source };
|
|
201
|
+
const sandboxed = withSandboxCapability(definition.manifest, spec);
|
|
202
|
+
if (!sandboxed.ok)
|
|
203
|
+
return fail(400, sandboxed.message);
|
|
204
|
+
return {
|
|
205
|
+
spec,
|
|
206
|
+
source,
|
|
207
|
+
manifest: sandboxed.manifest,
|
|
208
|
+
manifestHash: sandboxed.manifestHash,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
119
211
|
/** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
|
|
120
212
|
export async function sessionView(t, s) {
|
|
121
213
|
let waits = Array.isArray(s.waits)
|
|
@@ -132,7 +224,7 @@ export async function sessionView(t, s) {
|
|
|
132
224
|
waits = aggregated;
|
|
133
225
|
}
|
|
134
226
|
const actions = await t.actionsForSession(s.id, {
|
|
135
|
-
statuses: ["pending", "
|
|
227
|
+
statuses: ["pending", "delivering", "uncertain"],
|
|
136
228
|
});
|
|
137
229
|
const uncertain = await t.effectsForSession(s.id, {
|
|
138
230
|
statuses: ["uncertain"],
|
|
@@ -148,6 +240,8 @@ export async function sessionView(t, s) {
|
|
|
148
240
|
vaultIds: s.vaultIds ?? [],
|
|
149
241
|
credentialSelections: s.credentialSelections ?? [],
|
|
150
242
|
sandboxOwnerId: s.sandboxOwnerId ?? null,
|
|
243
|
+
sandbox: s.sandbox ?? null,
|
|
244
|
+
...(s.sandboxSource ? { sandboxSource: s.sandboxSource } : {}),
|
|
151
245
|
mcpSnapshot: s.mcpSnapshot ?? null,
|
|
152
246
|
mcpDiagnostics: s.mcpDiagnostics ?? [],
|
|
153
247
|
waits,
|