@nylorun/runtime 0.13.0-beta → 0.14.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 +330 -0
- package/README.md +69 -15
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +38 -8
- package/dist/adapters/replication/pgoutput.d.ts +16 -0
- package/dist/adapters/replication/pgoutput.js +169 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +1071 -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 +7825 -0
- package/dist/api/components.js +83 -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 +130 -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 +259 -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 +10 -0
- package/dist/api/http/routes/tenant.js +172 -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 +138 -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/host/admin-api.d.ts +22 -0
- package/dist/host/admin-api.js +178 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +185 -0
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- 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 +31 -3
- package/dist/host/create-host.js +168 -327
- package/dist/host/http.d.ts +14 -3
- package/dist/host/http.js +64 -23
- package/dist/host/main.js +55 -4
- package/dist/host/stack-config.d.ts +20 -0
- package/dist/host/stack-config.js +55 -0
- package/dist/mcp/pool.d.ts +19 -1
- package/dist/mcp/pool.js +60 -10
- 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 +25696 -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/event.d.ts +19 -0
- package/dist/store/event.js +49 -0
- package/dist/store/memory.d.ts +20 -8
- package/dist/store/memory.js +265 -100
- package/dist/store/postgres/connect.d.ts +13 -0
- package/dist/store/postgres/connect.js +5 -0
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
- package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
- package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
- package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
- package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
- package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
- package/dist/store/postgres/migrations/007_event_time.js +14 -0
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
- package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
- package/dist/store/postgres/migrations/index.d.ts +5 -1
- package/dist/store/postgres/migrations/index.js +22 -2
- package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
- package/dist/store/postgres/migrations/shared/index.js +92 -0
- package/dist/store/postgres/record.d.ts +11 -0
- package/dist/store/postgres/record.js +64 -0
- package/dist/store/postgres/store.js +337 -95
- package/dist/store/postgres/tenants.js +14 -0
- package/dist/store/types.d.ts +176 -81
- 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 +29 -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/memory.d.ts +69 -0
- package/dist/streams/relay/memory.js +176 -0
- package/dist/streams/relay/types.d.ts +77 -0
- package/dist/streams/relay/types.js +7 -0
- package/dist/streams/types.d.ts +66 -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.js +32 -0
- 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/commands.d.ts +18 -2
- package/dist/tenant/commands.js +107 -113
- package/dist/tenant/context.d.ts +75 -18
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +105 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +476 -0
- package/dist/tenant/effects.d.ts +5 -0
- package/dist/tenant/effects.js +78 -37
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +6 -0
- package/dist/tenant/ephemeral.js +60 -36
- 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/module.d.ts +3 -0
- package/dist/tenant/module.js +6 -12
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/pool.d.ts +0 -2
- package/dist/tenant/pool.js +0 -18
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +10 -20
- package/dist/tenant/runtime.js +59 -42
- package/dist/tenant/scheduler.d.ts +7 -2
- package/dist/tenant/scheduler.js +7 -3
- package/dist/tenant/session-streams.d.ts +125 -0
- package/dist/tenant/session-streams.js +399 -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 +0 -3
- package/dist/tenant/status.js +19 -12
- package/dist/tenant/streams.d.ts +60 -67
- package/dist/tenant/streams.js +140 -208
- 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 +178 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +29 -7
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +9 -3
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +57 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -8
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- 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/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,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant signing keys for subject tokens. Each key is an ES256 pair: the public JWK is stored
|
|
3
|
+
* as is, the PKCS#8 private key is sealed with the vault KEK (AAD bound to the Tenant and key
|
|
4
|
+
* id), like a credential.
|
|
5
|
+
*
|
|
6
|
+
* States: `standby` (published, not used yet), `current` (signs), `previous` (verifies only),
|
|
7
|
+
* `revoked`. The first use creates `current` and `standby`. Rotating moves standby → current →
|
|
8
|
+
* previous → revoked and makes a new standby; it is refused while the key it would revoke may
|
|
9
|
+
* still verify live tokens, so a rotation never signs anyone out (`force` skips that guard).
|
|
10
|
+
*
|
|
11
|
+
* Key states are read from the database on every use, so a revocation applies at once on
|
|
12
|
+
* every node. Only imported key objects are cached: public keys by id (immutable) and the
|
|
13
|
+
* current private key.
|
|
14
|
+
*/
|
|
15
|
+
import { importJWK } from "jose";
|
|
16
|
+
import type { PublicJwk, SigningKeyView } from "@nylorun/core/contracts";
|
|
17
|
+
import type { SigningKeyRow, Tx } from "../store/types.js";
|
|
18
|
+
type KeyLike = Awaited<ReturnType<typeof importJWK>>;
|
|
19
|
+
export interface SigningKeysOptions {
|
|
20
|
+
tenantId: string;
|
|
21
|
+
/** The vault KEK, created on first use. Resolve it before opening a transaction. */
|
|
22
|
+
kek(): Buffer;
|
|
23
|
+
}
|
|
24
|
+
export interface CurrentKey {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly key: KeyLike;
|
|
27
|
+
}
|
|
28
|
+
export declare class SigningKeys {
|
|
29
|
+
private readonly options;
|
|
30
|
+
private readonly publicKeys;
|
|
31
|
+
private current;
|
|
32
|
+
constructor(options: SigningKeysOptions);
|
|
33
|
+
kek(): Buffer;
|
|
34
|
+
private aad;
|
|
35
|
+
/** A new sealed key row in `state`. In-memory crypto only; safe inside a transaction. */
|
|
36
|
+
private create;
|
|
37
|
+
/** The current key row, creating `current` and `standby` if the Tenant has none. */
|
|
38
|
+
ensure(t: Tx, kek: Buffer): Promise<SigningKeyRow>;
|
|
39
|
+
/** The private key of `row` (the current key), imported once per key id. */
|
|
40
|
+
privateKey(row: SigningKeyRow, kek: Buffer): Promise<CurrentKey>;
|
|
41
|
+
/** The public key of `row`, imported once per key id. */
|
|
42
|
+
publicKey(row: SigningKeyRow): Promise<KeyLike>;
|
|
43
|
+
/**
|
|
44
|
+
* Rotates: previous → revoked, current → previous, standby → current, new standby. Refused
|
|
45
|
+
* with 409 while the previous key may still verify live tokens (retired less than the
|
|
46
|
+
* longest token lifetime, `maxTtlSeconds` or a delivery token's, plus a minute ago), unless
|
|
47
|
+
* `force`.
|
|
48
|
+
*/
|
|
49
|
+
rotate(t: Tx, kek: Buffer, maxTtlSeconds: number, force: boolean): Promise<SigningKeyRow[]>;
|
|
50
|
+
/** Revokes a `previous` or `standby` key. The current key must be rotated first. */
|
|
51
|
+
revoke(t: Tx, id: string): Promise<SigningKeyRow>;
|
|
52
|
+
}
|
|
53
|
+
export declare function publicJwk(row: SigningKeyRow): PublicJwk;
|
|
54
|
+
export declare function signingKeyView(row: SigningKeyRow): SigningKeyView;
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant signing keys for subject tokens. Each key is an ES256 pair: the public JWK is stored
|
|
3
|
+
* as is, the PKCS#8 private key is sealed with the vault KEK (AAD bound to the Tenant and key
|
|
4
|
+
* id), like a credential.
|
|
5
|
+
*
|
|
6
|
+
* States: `standby` (published, not used yet), `current` (signs), `previous` (verifies only),
|
|
7
|
+
* `revoked`. The first use creates `current` and `standby`. Rotating moves standby → current →
|
|
8
|
+
* previous → revoked and makes a new standby; it is refused while the key it would revoke may
|
|
9
|
+
* still verify live tokens, so a rotation never signs anyone out (`force` skips that guard).
|
|
10
|
+
*
|
|
11
|
+
* Key states are read from the database on every use, so a revocation applies at once on
|
|
12
|
+
* every node. Only imported key objects are cached: public keys by id (immutable) and the
|
|
13
|
+
* current private key.
|
|
14
|
+
*/
|
|
15
|
+
import { exportJWK, exportPKCS8, generateKeyPair, importJWK, importPKCS8, } from "jose";
|
|
16
|
+
import { newSigningKeyId, SIGNING_KEY_ID_PATTERN, } from "@nylorun/core/compatibility";
|
|
17
|
+
import { DELIVERY_TOKEN_MAX_TTL_SECONDS } from "@nylorun/core/contracts";
|
|
18
|
+
import { decryptSecret, encryptSecret } from "../vault/crypto.js";
|
|
19
|
+
import { fail } from "./http.js";
|
|
20
|
+
const ALG = "ES256";
|
|
21
|
+
/** Grace added to the longest token lifetime before a previous key may be revoked. */
|
|
22
|
+
const ROTATION_GRACE_MS = 60_000;
|
|
23
|
+
export class SigningKeys {
|
|
24
|
+
options;
|
|
25
|
+
publicKeys = new Map();
|
|
26
|
+
current;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
}
|
|
30
|
+
kek() {
|
|
31
|
+
return this.options.kek();
|
|
32
|
+
}
|
|
33
|
+
aad(id) {
|
|
34
|
+
return Buffer.from(`nylorun.signing-key:${this.options.tenantId}:${id}`);
|
|
35
|
+
}
|
|
36
|
+
/** A new sealed key row in `state`. In-memory crypto only; safe inside a transaction. */
|
|
37
|
+
async create(kek, state, now) {
|
|
38
|
+
const id = newSigningKeyId();
|
|
39
|
+
const pair = await generateKeyPair(ALG, { extractable: true });
|
|
40
|
+
const jwk = await exportJWK(pair.publicKey);
|
|
41
|
+
const pkcs8 = await exportPKCS8(pair.privateKey);
|
|
42
|
+
const sealed = encryptSecret(kek, this.aad(id), Buffer.from(pkcs8, "utf8"));
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
state,
|
|
46
|
+
alg: ALG,
|
|
47
|
+
publicJwk: JSON.stringify({
|
|
48
|
+
kty: jwk.kty,
|
|
49
|
+
crv: jwk.crv,
|
|
50
|
+
x: jwk.x,
|
|
51
|
+
y: jwk.y,
|
|
52
|
+
}),
|
|
53
|
+
createdAt: now,
|
|
54
|
+
activatedAt: state === "current" ? now : null,
|
|
55
|
+
retiredAt: null,
|
|
56
|
+
revokedAt: null,
|
|
57
|
+
kekId: sealed.kekId,
|
|
58
|
+
nonce: sealed.nonce,
|
|
59
|
+
ciphertext: sealed.ciphertext,
|
|
60
|
+
wrappedDek: sealed.wrappedDek,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The current key row, creating `current` and `standby` if the Tenant has none. */
|
|
64
|
+
async ensure(t, kek) {
|
|
65
|
+
let live = await t.signingKeys(["standby", "current"]);
|
|
66
|
+
if (live.length < 2) {
|
|
67
|
+
// Concurrent first uses would both create keys: lock, then read what the winner made.
|
|
68
|
+
await t.lockSigningKeys();
|
|
69
|
+
live = await t.signingKeys(["standby", "current"]);
|
|
70
|
+
}
|
|
71
|
+
const now = new Date().toISOString();
|
|
72
|
+
let current = live.find((row) => row.state === "current");
|
|
73
|
+
if (!current) {
|
|
74
|
+
const standby = live.find((row) => row.state === "standby");
|
|
75
|
+
if (standby) {
|
|
76
|
+
await t.setSigningKeyState(standby.id, "standby", "current", now);
|
|
77
|
+
current = { ...standby, state: "current", activatedAt: now };
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
current = await this.create(kek, "current", now);
|
|
81
|
+
await t.insertSigningKey(current);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!live.some((row) => row.state === "standby" && row.id !== current.id))
|
|
85
|
+
await t.insertSigningKey(await this.create(kek, "standby", now));
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
/** The private key of `row` (the current key), imported once per key id. */
|
|
89
|
+
async privateKey(row, kek) {
|
|
90
|
+
if (this.current?.id === row.id)
|
|
91
|
+
return this.current;
|
|
92
|
+
const pkcs8 = decryptSecret(kek, this.aad(row.id), Buffer.from(row.nonce), Buffer.from(row.ciphertext), Buffer.from(row.wrappedDek), row.kekId).toString("utf8");
|
|
93
|
+
const key = await importPKCS8(pkcs8, ALG);
|
|
94
|
+
this.current = { id: row.id, key };
|
|
95
|
+
return this.current;
|
|
96
|
+
}
|
|
97
|
+
/** The public key of `row`, imported once per key id. */
|
|
98
|
+
async publicKey(row) {
|
|
99
|
+
const cached = this.publicKeys.get(row.id);
|
|
100
|
+
if (cached)
|
|
101
|
+
return cached;
|
|
102
|
+
const key = await importJWK({ ...JSON.parse(row.publicJwk), alg: ALG }, ALG);
|
|
103
|
+
this.publicKeys.set(row.id, key);
|
|
104
|
+
return key;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Rotates: previous → revoked, current → previous, standby → current, new standby. Refused
|
|
108
|
+
* with 409 while the previous key may still verify live tokens (retired less than the
|
|
109
|
+
* longest token lifetime, `maxTtlSeconds` or a delivery token's, plus a minute ago), unless
|
|
110
|
+
* `force`.
|
|
111
|
+
*/
|
|
112
|
+
async rotate(t, kek, maxTtlSeconds, force) {
|
|
113
|
+
await t.lockSigningKeys();
|
|
114
|
+
await this.ensure(t, kek);
|
|
115
|
+
const now = new Date();
|
|
116
|
+
const at = now.toISOString();
|
|
117
|
+
const [previous] = await t.signingKeys(["previous"]);
|
|
118
|
+
if (previous && !force) {
|
|
119
|
+
const retired = Date.parse(previous.retiredAt ?? previous.createdAt);
|
|
120
|
+
// Delivery tokens may outlive the policy's subject tokens.
|
|
121
|
+
const longest = Math.max(maxTtlSeconds, DELIVERY_TOKEN_MAX_TTL_SECONDS);
|
|
122
|
+
const until = retired + longest * 1000 + ROTATION_GRACE_MS;
|
|
123
|
+
if (until > now.getTime())
|
|
124
|
+
fail(409, "The previous key may still verify live tokens", {
|
|
125
|
+
code: "request_rejected",
|
|
126
|
+
details: { retryAfterSeconds: Math.ceil((until - now.getTime()) / 1000) },
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (previous)
|
|
130
|
+
await t.setSigningKeyState(previous.id, "previous", "revoked", at);
|
|
131
|
+
const [current] = await t.signingKeys(["current"]);
|
|
132
|
+
const [standby] = await t.signingKeys(["standby"]);
|
|
133
|
+
if (current)
|
|
134
|
+
await t.setSigningKeyState(current.id, "current", "previous", at);
|
|
135
|
+
if (standby)
|
|
136
|
+
await t.setSigningKeyState(standby.id, "standby", "current", at);
|
|
137
|
+
else
|
|
138
|
+
await t.insertSigningKey(await this.create(kek, "current", at));
|
|
139
|
+
await t.insertSigningKey(await this.create(kek, "standby", at));
|
|
140
|
+
return t.signingKeys();
|
|
141
|
+
}
|
|
142
|
+
/** Revokes a `previous` or `standby` key. The current key must be rotated first. */
|
|
143
|
+
async revoke(t, id) {
|
|
144
|
+
const row = SIGNING_KEY_ID_PATTERN.test(id)
|
|
145
|
+
? await t.signingKey(id)
|
|
146
|
+
: undefined;
|
|
147
|
+
if (!row)
|
|
148
|
+
return fail(404, "Signing key not found");
|
|
149
|
+
if (row.state === "revoked")
|
|
150
|
+
return row;
|
|
151
|
+
if (row.state === "current")
|
|
152
|
+
return fail(409, "Rotate before revoking the current key");
|
|
153
|
+
const at = new Date().toISOString();
|
|
154
|
+
await t.setSigningKeyState(row.id, row.state, "revoked", at);
|
|
155
|
+
return { ...row, state: "revoked", revokedAt: at };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export function publicJwk(row) {
|
|
159
|
+
return { ...JSON.parse(row.publicJwk), kid: row.id, alg: ALG, use: "sig" };
|
|
160
|
+
}
|
|
161
|
+
export function signingKeyView(row) {
|
|
162
|
+
return {
|
|
163
|
+
id: row.id,
|
|
164
|
+
state: row.state,
|
|
165
|
+
alg: row.alg,
|
|
166
|
+
publicKey: publicJwk(row),
|
|
167
|
+
createdAt: row.createdAt,
|
|
168
|
+
activatedAt: row.activatedAt,
|
|
169
|
+
retiredAt: row.retiredAt,
|
|
170
|
+
revokedAt: row.revokedAt,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Tx } from "../store/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Slim the completed model effects of a turn that ended (Model Calls §9): drop the prompt
|
|
4
|
+
* and the answer, which the transcript already holds, and keep the identity and status.
|
|
5
|
+
* Replay only ever reads the effects of a running segment. Tool, flow and uncertain rows
|
|
6
|
+
* are never touched: completed actions return their receipts, and status lists uncertain work.
|
|
7
|
+
*/
|
|
8
|
+
export declare function slimModelEffects(t: Tx, sessionId: string, turnId: string | null): Promise<void>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slim the completed model effects of a turn that ended (Model Calls §9): drop the prompt
|
|
3
|
+
* and the answer, which the transcript already holds, and keep the identity and status.
|
|
4
|
+
* Replay only ever reads the effects of a running segment. Tool, flow and uncertain rows
|
|
5
|
+
* are never touched: completed actions return their receipts, and status lists uncertain work.
|
|
6
|
+
*/
|
|
7
|
+
export async function slimModelEffects(t, sessionId, turnId) {
|
|
8
|
+
if (!turnId)
|
|
9
|
+
return;
|
|
10
|
+
for (const effect of await t.effectsForTurn(sessionId, turnId, ["completed"])) {
|
|
11
|
+
if (effect.request?.kind !== "model" || effect.slimmed)
|
|
12
|
+
continue;
|
|
13
|
+
const { input: _input, ...request } = effect.request;
|
|
14
|
+
await t.put("effects", request.effectId, {
|
|
15
|
+
...effect,
|
|
16
|
+
request,
|
|
17
|
+
outcome: {},
|
|
18
|
+
slimmed: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/tenant/status.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { SeedTenantConfigRequest, SeedTenantConfigResponse, TenantStatus } from "@nylorun/core/contracts";
|
|
2
2
|
import type { SessionStore } from "../store/types.js";
|
|
3
|
-
import type { ExecutorRegistry } from "../core/executors.js";
|
|
4
3
|
import type { VaultService } from "../vault/service.js";
|
|
5
4
|
import type { SandboxManager } from "../sandbox/manager.js";
|
|
6
5
|
import type { TenantConfig } from "./types.js";
|
|
@@ -11,12 +10,10 @@ export interface TenantStatusContext {
|
|
|
11
10
|
envelope: TenantEnvelope;
|
|
12
11
|
config: TenantConfig;
|
|
13
12
|
store: SessionStore;
|
|
14
|
-
registry: ExecutorRegistry;
|
|
15
13
|
vault: VaultService;
|
|
16
14
|
sandbox: SandboxManager;
|
|
17
15
|
closing: boolean;
|
|
18
16
|
modelConfigured: boolean;
|
|
19
|
-
executorStreams: Map<string, Set<unknown>>;
|
|
20
17
|
/**
|
|
21
18
|
* This Tenant's execution invocations that need an operator (`TenantExecution`). Absent
|
|
22
19
|
* when the execution cannot report them.
|
package/dist/tenant/status.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FIXTURE_MODEL_SETTING, seedFixtureModel } from "./model-setting.js";
|
|
2
|
+
import { SANDBOX_CONFIG_SETTING, writeSandboxConfig } from "../sandbox/tenant-config.js";
|
|
2
3
|
/** How long status waits for the execution to list stuck invocations. */
|
|
3
4
|
const STUCK_TIMEOUT_MS = 5000;
|
|
4
5
|
/** `TenantStatus.execution`: paused and backing-off invocations, or why they are unknown. */
|
|
@@ -54,17 +55,13 @@ export async function buildTenantStatus(ctx) {
|
|
|
54
55
|
const definitionIds = new Set(definitions
|
|
55
56
|
.map((d) => d.manifest?.id)
|
|
56
57
|
.filter((id) => typeof id === "string"));
|
|
57
|
-
const
|
|
58
|
-
const agentIds = new Set([...definitionIds, ...
|
|
59
|
-
const agents = [...agentIds].sort().map((agentId) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
connected: !!record &&
|
|
65
|
-
(ctx.executorStreams.get(record.tokenHash)?.size ?? 0) > 0,
|
|
66
|
-
};
|
|
67
|
-
});
|
|
58
|
+
const endpointIds = new Set((await ctx.store.tx((t) => t.listEndpoints())).map((e) => e.agentId));
|
|
59
|
+
const agentIds = new Set([...definitionIds, ...endpointIds]);
|
|
60
|
+
const agents = [...agentIds].sort().map((agentId) => ({
|
|
61
|
+
agentId,
|
|
62
|
+
registered: definitionIds.has(agentId),
|
|
63
|
+
endpoint: endpointIds.has(agentId),
|
|
64
|
+
}));
|
|
68
65
|
return {
|
|
69
66
|
tenant: ctx.envelope,
|
|
70
67
|
path: ctx.config.paths.root,
|
|
@@ -72,7 +69,7 @@ export async function buildTenantStatus(ctx) {
|
|
|
72
69
|
store: health.schemaVersion > 0,
|
|
73
70
|
scheduler: !ctx.closing,
|
|
74
71
|
model: ctx.modelConfigured || modelView.configured,
|
|
75
|
-
|
|
72
|
+
endpoints: true,
|
|
76
73
|
schema: health.ok,
|
|
77
74
|
},
|
|
78
75
|
model: modelView,
|
|
@@ -108,6 +105,16 @@ export async function seedTenantConfig(ctx, body) {
|
|
|
108
105
|
});
|
|
109
106
|
(inserted ? applied : kept).push("sandbox.backend");
|
|
110
107
|
}
|
|
108
|
+
const sandboxConfig = body.sandbox?.config;
|
|
109
|
+
if (sandboxConfig) {
|
|
110
|
+
const inserted = await ctx.store.tx(async (t) => {
|
|
111
|
+
if ((await t.getSetting(SANDBOX_CONFIG_SETTING)) !== undefined)
|
|
112
|
+
return false;
|
|
113
|
+
await writeSandboxConfig(t, sandboxConfig);
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
(inserted ? applied : kept).push(SANDBOX_CONFIG_SETTING);
|
|
117
|
+
}
|
|
111
118
|
if (body.fixtureModel) {
|
|
112
119
|
const inserted = await ctx.store.tx((t) => seedFixtureModel(t));
|
|
113
120
|
(inserted ? applied : kept).push(FIXTURE_MODEL_SETTING);
|