@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
package/dist/tenant/advance.js
CHANGED
|
@@ -34,7 +34,9 @@ import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
|
|
|
34
34
|
import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
|
|
35
35
|
import { isOwnershipLost, ownedTx } from "../store/ownership.js";
|
|
36
36
|
import { linkedAgentOutput, rebaseSessionState, sessionToolsOf, turnManifestOf, } from "./session.js";
|
|
37
|
-
import { prepareMcp, resolveEffect } from "./effects.js";
|
|
37
|
+
import { isRemoteMcpEffect, prepareMcp, recoversMcpCalls, recoversModelCalls, resolveEffect, } from "./effects.js";
|
|
38
|
+
import { slimModelEffects } from "./slim.js";
|
|
39
|
+
import { checkParity, foldTranscript, leanState, transcriptOf, transcriptUpdates, withTranscript, } from "./history.js";
|
|
38
40
|
import { command } from "./commands.js";
|
|
39
41
|
import { usesFixtureModel } from "./model-setting.js";
|
|
40
42
|
import { toolFixtureModel } from "../core/provider.js";
|
|
@@ -85,6 +87,7 @@ export async function advance(ctx, id, signal) {
|
|
|
85
87
|
else
|
|
86
88
|
signal.addEventListener("abort", forward, { once: true });
|
|
87
89
|
ctx.work.running.set(id, controller);
|
|
90
|
+
ctx.work.runningTurns.set(id, taken.session.activeTurnId ?? null);
|
|
88
91
|
const heartbeat = startHeartbeat(ctx, lease, controller);
|
|
89
92
|
let result = DONE;
|
|
90
93
|
try {
|
|
@@ -110,8 +113,10 @@ export async function advance(ctx, id, signal) {
|
|
|
110
113
|
finally {
|
|
111
114
|
heartbeat.stop();
|
|
112
115
|
signal.removeEventListener("abort", forward);
|
|
113
|
-
if (ctx.work.running.get(id) === controller)
|
|
116
|
+
if (ctx.work.running.get(id) === controller) {
|
|
114
117
|
ctx.work.running.delete(id);
|
|
118
|
+
ctx.work.runningTurns.delete(id);
|
|
119
|
+
}
|
|
115
120
|
// Best effort: a release that fails leaves a lease that simply expires.
|
|
116
121
|
await ctx.store
|
|
117
122
|
.tx((t) => t.releaseOwnership(id, lease.owner, lease.epoch))
|
|
@@ -146,7 +151,10 @@ async function takeOwnership(ctx, id) {
|
|
|
146
151
|
};
|
|
147
152
|
const s = await t.assertEpoch(id, taken.epoch);
|
|
148
153
|
if (taken.takeover)
|
|
149
|
-
await takeOver(t, s
|
|
154
|
+
await takeOver(t, s, {
|
|
155
|
+
recoversModelCalls: recoversModelCalls(ctx),
|
|
156
|
+
recoversMcpCalls: recoversMcpCalls(ctx),
|
|
157
|
+
});
|
|
150
158
|
if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
|
|
151
159
|
await t.releaseOwnership(id, lease.owner, lease.epoch);
|
|
152
160
|
return { status: "idle" };
|
|
@@ -158,13 +166,20 @@ async function takeOwnership(ctx, id) {
|
|
|
158
166
|
}
|
|
159
167
|
/**
|
|
160
168
|
* Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
|
|
161
|
-
* owner's `invoking` effects become `uncertain` and are never invoked again
|
|
169
|
+
* owner's `invoking` effects become `uncertain` and are never invoked again, except model
|
|
170
|
+
* calls when the gate recovers them (P1.2) and remote MCP calls when the Tool Gate does (F4.1
|
|
171
|
+
* G3): those stay `invoking`, and the replay re-sends them. When one belongs
|
|
162
172
|
* to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
|
|
163
173
|
* Mutates and writes `s`. Returns the effect ids it marked.
|
|
164
174
|
*/
|
|
165
|
-
export async function takeOver(t, s) {
|
|
175
|
+
export async function takeOver(t, s, options = {}) {
|
|
166
176
|
const marked = [];
|
|
167
177
|
for (const effect of await t.invokingEffects(s.id)) {
|
|
178
|
+
// Still running at the gate, or finished there: the replay re-sends it (P1.2).
|
|
179
|
+
if (options.recoversModelCalls && effect.request.kind === "model")
|
|
180
|
+
continue;
|
|
181
|
+
if (options.recoversMcpCalls && isRemoteMcpEffect(s, effect.request))
|
|
182
|
+
continue;
|
|
168
183
|
effect.status = "uncertain";
|
|
169
184
|
effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
|
|
170
185
|
await t.put("effects", effect.request.effectId, effect);
|
|
@@ -224,6 +239,9 @@ function startHeartbeat(ctx, lease, controller) {
|
|
|
224
239
|
},
|
|
225
240
|
};
|
|
226
241
|
}
|
|
242
|
+
/** Segment rollover defaults (Model Calls §10), well inside the advance deadline. */
|
|
243
|
+
const ROLLOVER_STEPS = 50;
|
|
244
|
+
const ROLLOVER_MS = 20 * 60_000;
|
|
227
245
|
/** Runs one segment and settles it. Throws only `ownership.lost` and infrastructure errors. */
|
|
228
246
|
async function runSegment(ctx, lease, started, signal) {
|
|
229
247
|
const id = lease.sessionId;
|
|
@@ -234,12 +252,24 @@ async function runSegment(ctx, lease, started, signal) {
|
|
|
234
252
|
const { current, fixtureModel } = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
235
253
|
if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
|
|
236
254
|
rebaseSessionState(current, current.checkpoint.manifestHash);
|
|
255
|
+
await adoptStoredTranscript(t, current);
|
|
237
256
|
const cp = current.checkpoint;
|
|
238
257
|
current.checkpoint = { ...cp, state: current.state };
|
|
239
258
|
await t.put("sessions", id, current);
|
|
240
259
|
}
|
|
241
260
|
return { current, fixtureModel: await usesFixtureModel(t) };
|
|
242
261
|
});
|
|
262
|
+
// One history (P0.3): the engine resumes from the transcript folded from the record.
|
|
263
|
+
let startTranscript = [];
|
|
264
|
+
let engineCheckpoint = current.checkpoint;
|
|
265
|
+
if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
|
|
266
|
+
const cp = current.checkpoint;
|
|
267
|
+
if (cp.state) {
|
|
268
|
+
startTranscript = await foldSession(ctx, current);
|
|
269
|
+
checkParity(id, startTranscript, transcriptOf(cp.state));
|
|
270
|
+
engineCheckpoint = { ...cp, state: withTranscript(cp.state, startTranscript) };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
243
273
|
const segment = fixtureModel ? { model: fixture } : {};
|
|
244
274
|
const host = {
|
|
245
275
|
resolveEffect: (e) => resolveEffect(ctx, e, signal, lease, segment),
|
|
@@ -254,10 +284,14 @@ async function runSegment(ctx, lease, started, signal) {
|
|
|
254
284
|
})
|
|
255
285
|
: await runDurable({
|
|
256
286
|
manifest: turnManifestOf(current),
|
|
257
|
-
checkpoint:
|
|
287
|
+
checkpoint: engineCheckpoint,
|
|
258
288
|
signal,
|
|
259
289
|
sessionTools: sessionToolsOf(current.mcpSnapshot),
|
|
260
290
|
host,
|
|
291
|
+
yieldAfter: {
|
|
292
|
+
steps: ctx.config.rollover?.steps ?? ROLLOVER_STEPS,
|
|
293
|
+
ms: ctx.config.rollover?.ms ?? ROLLOVER_MS,
|
|
294
|
+
},
|
|
261
295
|
});
|
|
262
296
|
// The engine turns an abort into a `cancelled` (agents) or `failed` (workflows) result;
|
|
263
297
|
// only a user cancel may settle that, and it already did.
|
|
@@ -265,8 +299,15 @@ async function runSegment(ctx, lease, started, signal) {
|
|
|
265
299
|
if (signal.reason instanceof AdvanceDeadlineError &&
|
|
266
300
|
(result.status === "cancelled" || result.status === "failed"))
|
|
267
301
|
await settleFailure(ctx, lease, started, signal.reason);
|
|
268
|
-
else
|
|
269
|
-
|
|
302
|
+
else {
|
|
303
|
+
const state = !isWorkflowManifest(current.manifest) &&
|
|
304
|
+
(result.status === "yielded" || result.status === "completed" || result.status === "paused") &&
|
|
305
|
+
"result" in result
|
|
306
|
+
? result.result?.state
|
|
307
|
+
: undefined;
|
|
308
|
+
const updates = state ? transcriptUpdates(startTranscript, transcriptOf(state)) : [];
|
|
309
|
+
await settle(ctx, lease, started, result, updates);
|
|
310
|
+
}
|
|
270
311
|
}
|
|
271
312
|
catch (error) {
|
|
272
313
|
if (isOwnershipLost(error) || error instanceof SegmentStopped)
|
|
@@ -304,7 +345,7 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
|
|
|
304
345
|
});
|
|
305
346
|
}
|
|
306
347
|
/** Record a segment result, then cancel failed siblings and wake a linked workflow. */
|
|
307
|
-
async function settle(ctx, lease, s, result) {
|
|
348
|
+
async function settle(ctx, lease, s, result, updates = []) {
|
|
308
349
|
const id = lease.sessionId;
|
|
309
350
|
const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
310
351
|
const siblings = [];
|
|
@@ -337,15 +378,38 @@ async function settle(ctx, lease, s, result) {
|
|
|
337
378
|
await t.put("sessions", id, current);
|
|
338
379
|
return siblings;
|
|
339
380
|
}
|
|
381
|
+
if (result.status === "yielded") {
|
|
382
|
+
// The turn goes on in a new segment: same turn, next checkpoint, woken right away.
|
|
383
|
+
const finished = result.checkpoint;
|
|
384
|
+
await recordTranscript(t, id, s.activeTurnId, current, updates);
|
|
385
|
+
current.state = leanState(result.result.state);
|
|
386
|
+
current.checkpoint = {
|
|
387
|
+
...finished,
|
|
388
|
+
segment: finished.segment + 1,
|
|
389
|
+
input: { kind: "continue" },
|
|
390
|
+
state: current.state,
|
|
391
|
+
};
|
|
392
|
+
current.status = "runnable";
|
|
393
|
+
current.waits = undefined;
|
|
394
|
+
await slimModelEffects(t, id, s.activeTurnId);
|
|
395
|
+
await t.put("sessions", id, current);
|
|
396
|
+
const segment = finished.segment + 1;
|
|
397
|
+
t.afterCommit(() => ctx.wake(id, {
|
|
398
|
+
reason: "rollover",
|
|
399
|
+
dedupeKey: `rollover:${s.activeTurnId}:${segment}`,
|
|
400
|
+
}));
|
|
401
|
+
return siblings;
|
|
402
|
+
}
|
|
340
403
|
const flow = isWorkflowManifest(current.manifest);
|
|
341
404
|
if (!flow) {
|
|
405
|
+
if (result.status !== "failed")
|
|
406
|
+
await recordTranscript(t, id, s.activeTurnId, current, updates);
|
|
342
407
|
current.state =
|
|
343
408
|
result.status === "failed"
|
|
344
409
|
? current.turnStartState
|
|
345
|
-
: result.result.state;
|
|
410
|
+
: leanState(result.result.state);
|
|
346
411
|
}
|
|
347
|
-
current.checkpoint = result.checkpoint;
|
|
348
|
-
await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, result.checkpoint.segment]), { checkpoint: result.checkpoint, status: result.status });
|
|
412
|
+
current.checkpoint = leanCheckpoint(result.checkpoint);
|
|
349
413
|
current.status = result.status;
|
|
350
414
|
current.waits =
|
|
351
415
|
result.status === "paused" ? result.result.pending : undefined;
|
|
@@ -370,6 +434,7 @@ async function settle(ctx, lease, s, result) {
|
|
|
370
434
|
current.activeTurnId = null;
|
|
371
435
|
if (s.activeTurnId)
|
|
372
436
|
current.lastTurnId = s.activeTurnId;
|
|
437
|
+
await slimModelEffects(t, id, s.activeTurnId);
|
|
373
438
|
}
|
|
374
439
|
const type = `turn.${result.status}`;
|
|
375
440
|
const payload = result.status === "completed"
|
|
@@ -425,11 +490,10 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
425
490
|
return;
|
|
426
491
|
current.status = "failed";
|
|
427
492
|
current.state = current.turnStartState;
|
|
428
|
-
if (current.checkpoint)
|
|
429
|
-
await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, current.checkpoint.segment]), { checkpoint: current.checkpoint, status: "failed" });
|
|
430
493
|
current.error = error instanceof Error ? error.message : String(error);
|
|
431
494
|
const payload = { message: current.error };
|
|
432
495
|
await t.event(id, current.activeTurnId, "turn.failed", payload);
|
|
496
|
+
await slimModelEffects(t, id, current.activeTurnId);
|
|
433
497
|
if (current.activeTurnId)
|
|
434
498
|
current.lastTurnId = current.activeTurnId;
|
|
435
499
|
current.activeTurnId = null;
|
|
@@ -437,3 +501,50 @@ async function settleFailure(ctx, lease, s, error) {
|
|
|
437
501
|
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
|
|
438
502
|
});
|
|
439
503
|
}
|
|
504
|
+
/** `checkpoint` with its engine state stored lean (`history.ts`). */
|
|
505
|
+
function leanCheckpoint(checkpoint) {
|
|
506
|
+
const state = checkpoint?.state;
|
|
507
|
+
if (!checkpoint || !state)
|
|
508
|
+
return checkpoint;
|
|
509
|
+
return { ...checkpoint, state: leanState(state) };
|
|
510
|
+
}
|
|
511
|
+
/** Appends a segment's transcript edits; a snapshot moves `history.snapshot`. */
|
|
512
|
+
async function recordTranscript(t, id, turnId, s, updates) {
|
|
513
|
+
for (const update of updates) {
|
|
514
|
+
const event = await t.event(id, turnId, "transcript.updated", update);
|
|
515
|
+
if (update.keep === 0)
|
|
516
|
+
s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* A session written before transcripts were recorded keeps its transcript on its row: record
|
|
521
|
+
* it once (the turn's starting transcript, then the turn so far), and store the row lean.
|
|
522
|
+
*/
|
|
523
|
+
async function adoptStoredTranscript(t, s) {
|
|
524
|
+
if (s.history)
|
|
525
|
+
return;
|
|
526
|
+
const now = transcriptOf(s.state);
|
|
527
|
+
const start = s.activeTurnId ? transcriptOf(s.turnStartState) : now;
|
|
528
|
+
if (now.length === 0 && start.length === 0)
|
|
529
|
+
return;
|
|
530
|
+
let first;
|
|
531
|
+
const write = async (turnId, updates) => {
|
|
532
|
+
for (const update of updates) {
|
|
533
|
+
const event = await t.event(s.id, turnId, "transcript.updated", update);
|
|
534
|
+
first ??= event.seq;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
await write(null, transcriptUpdates([], start));
|
|
538
|
+
if (s.activeTurnId)
|
|
539
|
+
await write(s.activeTurnId, transcriptUpdates(start, now));
|
|
540
|
+
s.history = { from: first ?? 0, snapshot: first ?? 0 };
|
|
541
|
+
s.state = leanState(s.state);
|
|
542
|
+
s.turnStartState = leanState(s.turnStartState);
|
|
543
|
+
}
|
|
544
|
+
/** The session's transcript, folded from its record from `history.from`. */
|
|
545
|
+
async function foldSession(ctx, s) {
|
|
546
|
+
const rows = await ctx.store
|
|
547
|
+
.record()
|
|
548
|
+
.readRange(ctx.store.tenantId, s.id, s.history?.from ?? 0, Number.MAX_SAFE_INTEGER);
|
|
549
|
+
return foldTranscript(rows.map((row) => row.body));
|
|
550
|
+
}
|
package/dist/tenant/auth.d.ts
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tenant request authentication and authorization: a bearer token resolves to an application
|
|
3
|
-
* principal or a
|
|
4
|
-
* principal may act for a subject (`Nylorun-Subject`, `Nylorun-Scopes`);
|
|
5
|
-
*
|
|
3
|
+
* principal, a subject token or a delivery token; anything else is the opaque 404 (D5).
|
|
4
|
+
* An application principal may act for a subject (`Nylorun-Subject`, `Nylorun-Scopes`); a
|
|
5
|
+
* subject token names its subject itself. `authorize` limits both to the routes their scopes
|
|
6
|
+
* allow, decided from the route alone.
|
|
6
7
|
*/
|
|
7
8
|
import type { IncomingMessage } from "node:http";
|
|
8
9
|
import { type Action, type SubjectScope } from "@nylorun/core/contracts";
|
|
9
|
-
import type { AuthScope, TenantContext } from "./context.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type RouteAccess = readonly SubjectScope[] | "never";
|
|
10
|
+
import type { AuthScope, SessionAccess, TenantContext } from "./context.js";
|
|
11
|
+
import type { BrowserClient } from "./browser.js";
|
|
12
|
+
export declare function authenticate(ctx: TenantContext, request: IncomingMessage, client?: BrowserClient): Promise<AuthScope>;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* What a subject needs for a route: any one of the scopes, nothing grants it (`never`), or
|
|
15
|
+
* any caller may (`any`, public data such as the JWKS). Each route declares it
|
|
16
|
+
* (`api/http/define.ts`).
|
|
17
17
|
*/
|
|
18
|
-
export
|
|
18
|
+
export type SubjectAccess = readonly SubjectScope[] | "never" | "any";
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* know is a 404 for a subject: new routes stay closed until they are added here. Application
|
|
23
|
-
* and executor scopes pass through to the checks each route makes today.
|
|
20
|
+
* A subject, subject token or publishable key reaches a route only with one of the scopes it
|
|
21
|
+
* declares: `403 scope_required`, decided from the route alone before anything is read.
|
|
24
22
|
*/
|
|
25
|
-
export declare function
|
|
23
|
+
export declare function requireScopes(scope: AuthScope, access: SubjectAccess): void;
|
|
24
|
+
export declare function accessOf(scope: AuthScope): SessionAccess | undefined;
|
|
26
25
|
/** The subject whose sessions and vaults the request is limited to, if it acts for one. */
|
|
27
26
|
export declare function ownerOf(scope: AuthScope): string | undefined;
|
|
28
27
|
/** The application principal behind the request, acting for a subject or not. */
|
|
29
28
|
export declare function requirePrincipal(scope: AuthScope): string;
|
|
30
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The caller may act on `action`: only the delivery token minted for it. Whether its generation
|
|
31
|
+
* is still the Action's is checked by each callback.
|
|
32
|
+
*/
|
|
31
33
|
export declare function scoped(scope: AuthScope, action: Action): void;
|
|
32
34
|
export declare function requireApplication(scope: AuthScope): string;
|
package/dist/tenant/auth.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { SCOPES_HEADER, SUBJECT_HEADER, } from "@nylorun/core/compatibility";
|
|
2
|
-
import { parseSubjectHeaders, } from "@nylorun/core/contracts";
|
|
3
|
-
import { hashToken } from "../core/
|
|
2
|
+
import { DELIVERY_TOKEN_TYPE, parseSubjectHeaders, } from "@nylorun/core/contracts";
|
|
3
|
+
import { hashToken } from "../core/bearer.js";
|
|
4
4
|
import { fail, failOpaque } from "./http.js";
|
|
5
|
+
import { looksLikeToken, verifySubjectToken } from "./tokens.js";
|
|
6
|
+
import { verifyDeliveryToken } from "./delivery-token.js";
|
|
7
|
+
import { tokenType } from "./jwt.js";
|
|
8
|
+
import { readPolicy } from "./access-policy.js";
|
|
5
9
|
const SUBJECT_INVALID = { code: "subject_invalid" };
|
|
6
10
|
/** One header's value, or `undefined`; a header sent twice is refused. */
|
|
7
11
|
function singleHeader(request, name) {
|
|
@@ -14,19 +18,56 @@ function singleHeader(request, name) {
|
|
|
14
18
|
return fail(400, `${name} must be sent once`, SUBJECT_INVALID);
|
|
15
19
|
return values[0];
|
|
16
20
|
}
|
|
17
|
-
export async function authenticate(ctx, request) {
|
|
21
|
+
export async function authenticate(ctx, request, client) {
|
|
18
22
|
const header = request.headers.authorization;
|
|
19
23
|
const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
|
|
20
24
|
if (!token || !header?.startsWith("Bearer ")) {
|
|
25
|
+
// A publishable key alone: what the policy grants `anon` (nothing by default).
|
|
26
|
+
if (client && header === undefined) {
|
|
27
|
+
const { anon } = await ctx.store.tx((t) => readPolicy(t));
|
|
28
|
+
return {
|
|
29
|
+
kind: "publishable",
|
|
30
|
+
keyId: client.keyId,
|
|
31
|
+
scopes: new Set(anon.scopes),
|
|
32
|
+
agents: anon.agents === "*" ? "*" : new Set(anon.agents),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
21
35
|
ctx.config.logger.warn("credential rejected", {
|
|
22
36
|
reason: "missing_bearer",
|
|
23
37
|
});
|
|
24
38
|
return failOpaque();
|
|
25
39
|
}
|
|
40
|
+
if (looksLikeToken(token) && tokenType(token) === DELIVERY_TOKEN_TYPE) {
|
|
41
|
+
// Delivery tokens come back from the application's server, never from a browser.
|
|
42
|
+
if (request.headers.origin !== undefined)
|
|
43
|
+
fail(403, "Delivery tokens are not accepted from browsers", { code: "origin_rejected" });
|
|
44
|
+
const scope = await verifyDeliveryToken(ctx, token);
|
|
45
|
+
if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
|
|
46
|
+
singleHeader(request, SCOPES_HEADER) !== undefined)
|
|
47
|
+
fail(403, "A delivery token cannot act for a subject");
|
|
48
|
+
return scope;
|
|
49
|
+
}
|
|
50
|
+
if (looksLikeToken(token)) {
|
|
51
|
+
const scope = await verifySubjectToken(ctx, token);
|
|
52
|
+
// Read only after verification, so an unknown token still sees the opaque 404.
|
|
53
|
+
if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
|
|
54
|
+
singleHeader(request, SCOPES_HEADER) !== undefined)
|
|
55
|
+
fail(403, "A subject token cannot act for another subject");
|
|
56
|
+
return scope;
|
|
57
|
+
}
|
|
58
|
+
// Application keys never come from a browser or a shipped app: refused before they are even
|
|
59
|
+
// looked up.
|
|
60
|
+
if (request.headers.origin !== undefined)
|
|
61
|
+
fail(403, "Application keys are not accepted from browsers", {
|
|
62
|
+
code: "origin_rejected",
|
|
63
|
+
});
|
|
64
|
+
if (client)
|
|
65
|
+
fail(400, "Nylorun-Key is for browser and mobile clients, not with a Tenant key", {
|
|
66
|
+
code: "invalid_request",
|
|
67
|
+
});
|
|
26
68
|
const tokenHash = hashToken(token);
|
|
27
69
|
const principal = await ctx.store.tx((t) => t.principalByTokenHash(tokenHash));
|
|
28
|
-
|
|
29
|
-
if (!principal && !executor) {
|
|
70
|
+
if (!principal) {
|
|
30
71
|
ctx.config.logger.warn("credential rejected", {
|
|
31
72
|
reason: "unknown_token",
|
|
32
73
|
});
|
|
@@ -35,11 +76,6 @@ export async function authenticate(ctx, request) {
|
|
|
35
76
|
// Read only after authentication, so an unknown caller sees the opaque 404 either way.
|
|
36
77
|
const subject = singleHeader(request, SUBJECT_HEADER);
|
|
37
78
|
const scopes = singleHeader(request, SCOPES_HEADER);
|
|
38
|
-
if (executor) {
|
|
39
|
-
if (subject !== undefined || scopes !== undefined)
|
|
40
|
-
fail(403, "An executor credential cannot act for a subject");
|
|
41
|
-
return { kind: "executor", executor };
|
|
42
|
-
}
|
|
43
79
|
if (subject === undefined) {
|
|
44
80
|
if (scopes !== undefined)
|
|
45
81
|
fail(400, `${SCOPES_HEADER} requires ${SUBJECT_HEADER}`, SUBJECT_INVALID);
|
|
@@ -55,87 +91,17 @@ export async function authenticate(ctx, request) {
|
|
|
55
91
|
scopes: parsed.scopes,
|
|
56
92
|
};
|
|
57
93
|
}
|
|
58
|
-
const SESSIONS = ["sessions:own"];
|
|
59
|
-
const VAULTS = ["vaults:own"];
|
|
60
|
-
const SETTINGS = ["tenant:settings"];
|
|
61
94
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* executors, actions and the sandbox tool routes.
|
|
95
|
+
* A subject, subject token or publishable key reaches a route only with one of the scopes it
|
|
96
|
+
* declares: `403 scope_required`, decided from the route alone before anything is read.
|
|
65
97
|
*/
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
case "executors":
|
|
73
|
-
case "actions":
|
|
74
|
-
return "never";
|
|
75
|
-
case "agents":
|
|
76
|
-
if (n === 2 && method === "GET")
|
|
77
|
-
return ["agents:read", "agents:write"];
|
|
78
|
-
if (n === 3 && method === "PUT")
|
|
79
|
-
return ["agents:write"];
|
|
80
|
-
return undefined;
|
|
81
|
-
case "sessions":
|
|
82
|
-
if (n === 2 && method === "GET")
|
|
83
|
-
return SESSIONS;
|
|
84
|
-
if (n === 3 && (method === "GET" || method === "PUT"))
|
|
85
|
-
return SESSIONS;
|
|
86
|
-
if (n === 4 && method === "GET" && (sub === "items" || sub === "events"))
|
|
87
|
-
return SESSIONS;
|
|
88
|
-
if (n === 4 && method === "POST" && sub === "commands")
|
|
89
|
-
return SESSIONS;
|
|
90
|
-
if (n === 5 && method === "POST" && sub === "sandbox")
|
|
91
|
-
return "never";
|
|
92
|
-
return undefined;
|
|
93
|
-
case "vaults":
|
|
94
|
-
if (n === 2)
|
|
95
|
-
return method === "GET" || method === "POST" ? VAULTS : undefined;
|
|
96
|
-
if (n === 3)
|
|
97
|
-
return method === "GET" || method === "DELETE" ? VAULTS : undefined;
|
|
98
|
-
if (sub !== "credentials")
|
|
99
|
-
return undefined;
|
|
100
|
-
if (n === 4)
|
|
101
|
-
return method === "GET" || method === "POST" ? VAULTS : undefined;
|
|
102
|
-
if (n === 5)
|
|
103
|
-
return method === "GET" || method === "POST" || method === "DELETE"
|
|
104
|
-
? VAULTS
|
|
105
|
-
: undefined;
|
|
106
|
-
return undefined;
|
|
107
|
-
case "tenant":
|
|
108
|
-
if (n === 2 && method === "GET")
|
|
109
|
-
return SETTINGS;
|
|
110
|
-
if (n === 3 && id === "reset" && method === "POST")
|
|
111
|
-
return "never";
|
|
112
|
-
if (n === 4 && id === "config" && sub === "seed" && method === "PUT")
|
|
113
|
-
return "never";
|
|
114
|
-
if (n === 3 && method === "GET" && (id === "models" || id === "providers"))
|
|
115
|
-
return ["tenant:settings", "agents:write"];
|
|
116
|
-
if (n === 3 && method === "GET" && id === "sandbox")
|
|
117
|
-
return SETTINGS;
|
|
118
|
-
if (n === 3 && id === "model" && (method === "GET" || method === "PUT"))
|
|
119
|
-
return SETTINGS;
|
|
120
|
-
if (n === 4 && id === "model" && sub === "selection" && method === "PUT")
|
|
121
|
-
return SETTINGS;
|
|
122
|
-
return undefined;
|
|
123
|
-
default:
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Limits a subject to the routes its scopes allow. Decided from the route alone, before any
|
|
129
|
-
* lookup, so a refusal reveals nothing about which resources exist. A route the table does not
|
|
130
|
-
* know is a 404 for a subject: new routes stay closed until they are added here. Application
|
|
131
|
-
* and executor scopes pass through to the checks each route makes today.
|
|
132
|
-
*/
|
|
133
|
-
export function authorize(scope, method, path) {
|
|
134
|
-
if (scope.kind !== "subject")
|
|
98
|
+
export function requireScopes(scope, access) {
|
|
99
|
+
if (scope.kind !== "subject" &&
|
|
100
|
+
scope.kind !== "token" &&
|
|
101
|
+
scope.kind !== "publishable")
|
|
102
|
+
return;
|
|
103
|
+
if (access === "any")
|
|
135
104
|
return;
|
|
136
|
-
const access = routeAccess(method, path);
|
|
137
|
-
if (access === undefined)
|
|
138
|
-
fail(404, "Route not found");
|
|
139
105
|
if (access === "never")
|
|
140
106
|
fail(403, "This route is not available when acting for a subject", {
|
|
141
107
|
code: "scope_required",
|
|
@@ -147,20 +113,51 @@ export function authorize(scope, method, path) {
|
|
|
147
113
|
details: { scopes: access },
|
|
148
114
|
});
|
|
149
115
|
}
|
|
116
|
+
export function accessOf(scope) {
|
|
117
|
+
switch (scope.kind) {
|
|
118
|
+
case "application":
|
|
119
|
+
return undefined;
|
|
120
|
+
case "subject":
|
|
121
|
+
return { owner: scope.subject };
|
|
122
|
+
case "token":
|
|
123
|
+
return {
|
|
124
|
+
owner: scope.subject,
|
|
125
|
+
...(scope.agents === "*" ? {} : { agents: scope.agents }),
|
|
126
|
+
};
|
|
127
|
+
// A publishable key alone owns nothing: no session or vault is ever reachable.
|
|
128
|
+
case "publishable":
|
|
129
|
+
// Nor does a request with no credential, on a route that serves public data.
|
|
130
|
+
case "anonymous":
|
|
131
|
+
return fail(404, "Not found");
|
|
132
|
+
// A delivery token reaches its Action's callbacks, never a session or vault.
|
|
133
|
+
case "delivery":
|
|
134
|
+
return fail(404, "Not found");
|
|
135
|
+
default: {
|
|
136
|
+
const unknown = scope;
|
|
137
|
+
return fail(404, `Unknown credential ${String(unknown.kind)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
150
141
|
/** The subject whose sessions and vaults the request is limited to, if it acts for one. */
|
|
151
142
|
export function ownerOf(scope) {
|
|
152
|
-
return scope
|
|
143
|
+
return accessOf(scope)?.owner;
|
|
153
144
|
}
|
|
154
145
|
/** The application principal behind the request, acting for a subject or not. */
|
|
155
146
|
export function requirePrincipal(scope) {
|
|
156
|
-
if (scope.kind
|
|
147
|
+
if (scope.kind === "application" || scope.kind === "subject")
|
|
157
148
|
return scope.principalId;
|
|
158
149
|
return fail(403, "Application credential required");
|
|
159
150
|
}
|
|
160
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* The caller may act on `action`: only the delivery token minted for it. Whether its generation
|
|
153
|
+
* is still the Action's is checked by each callback.
|
|
154
|
+
*/
|
|
161
155
|
export function scoped(scope, action) {
|
|
162
|
-
if (scope.kind
|
|
163
|
-
|
|
156
|
+
if (scope.kind === "delivery" &&
|
|
157
|
+
scope.actionId === action.actionId &&
|
|
158
|
+
scope.agentId === action.agentId)
|
|
159
|
+
return;
|
|
160
|
+
fail(403, "This delivery token is for another Action");
|
|
164
161
|
}
|
|
165
162
|
export function requireApplication(scope) {
|
|
166
163
|
if (scope.kind === "application")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which client app a request comes from (Host feature `browser-access`), decided before the
|
|
3
|
+
* bearer is looked at. `Nylorun-Key` names a publishable key of this Tenant; an `Origin` must
|
|
4
|
+
* be on that key's allowlist, and only then does the response carry CORS headers. The
|
|
5
|
+
* allowlist protects browsers from other sites' pages; it is not authentication, since a
|
|
6
|
+
* non-browser can send any `Origin`: tokens authorize.
|
|
7
|
+
*/
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
+
import type { TenantContext } from "./context.js";
|
|
10
|
+
/** The client app of a request: its publishable key, and the origin when from a browser. */
|
|
11
|
+
export interface BrowserClient {
|
|
12
|
+
readonly keyId: string;
|
|
13
|
+
readonly origin?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function identifyClient(ctx: TenantContext, request: IncomingMessage, response: ServerResponse): Promise<BrowserClient | undefined>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { PUBLISHABLE_KEY_HEADER } from "@nylorun/core/compatibility";
|
|
2
|
+
import { originAllowed } from "@nylorun/core/contracts";
|
|
3
|
+
import { browserOrigin, setCorsHeaders } from "../host/cors.js";
|
|
4
|
+
import { fail, failOpaque } from "./http.js";
|
|
5
|
+
function single(request, name) {
|
|
6
|
+
const raw = request.headers[name.toLowerCase()];
|
|
7
|
+
return Array.isArray(raw) ? raw[0] : raw;
|
|
8
|
+
}
|
|
9
|
+
export async function identifyClient(ctx, request, response) {
|
|
10
|
+
const key = single(request, PUBLISHABLE_KEY_HEADER);
|
|
11
|
+
const hasOrigin = request.headers.origin !== undefined;
|
|
12
|
+
if (key === undefined) {
|
|
13
|
+
if (hasOrigin)
|
|
14
|
+
fail(403, `Browser requests need ${PUBLISHABLE_KEY_HEADER}`, {
|
|
15
|
+
code: "origin_rejected",
|
|
16
|
+
});
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const row = await ctx.store.tx((t) => t.publishableKeyByKey(key));
|
|
20
|
+
if (!row || row.revokedAt !== null) {
|
|
21
|
+
ctx.config.logger.warn("credential rejected", {
|
|
22
|
+
reason: "publishable_key_rejected",
|
|
23
|
+
});
|
|
24
|
+
return failOpaque();
|
|
25
|
+
}
|
|
26
|
+
if (!hasOrigin)
|
|
27
|
+
return { keyId: row.id };
|
|
28
|
+
const origin = browserOrigin(request);
|
|
29
|
+
const origins = JSON.parse(row.originsJson);
|
|
30
|
+
if (origin === undefined || !originAllowed(origins, origin)) {
|
|
31
|
+
ctx.config.logger.warn("credential rejected", {
|
|
32
|
+
reason: "origin_not_allowed",
|
|
33
|
+
keyId: row.id,
|
|
34
|
+
});
|
|
35
|
+
return failOpaque();
|
|
36
|
+
}
|
|
37
|
+
setCorsHeaders(response, origin);
|
|
38
|
+
return { keyId: row.id, origin };
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why the Host's Tenant could not be opened (tenancy.md §5). A cause is about the Tenant's
|
|
3
|
+
* own data or files (its database is newer than this Runtime, its vault key is missing, the
|
|
4
|
+
* database has an old layout): the Host fails readiness, names the cause in
|
|
5
|
+
* `/v1/admin/status` and the logs, and answers Tenant requests with the opaque 404 until it is
|
|
6
|
+
* repaired and the Host restarted. A failure outside the Tenant (Postgres unreachable) is not
|
|
7
|
+
* a cause: it is `TenantUnavailableError`, and the next request tries again.
|
|
8
|
+
*/
|
|
9
|
+
import type { TenantCause, TenantEnvelope } from "@nylorun/core/contracts";
|
|
10
|
+
export type { TenantCause };
|
|
11
|
+
/** Thrown by migrations, the Tenant bootstrap and the Tenant Runtime when the Tenant cannot open. */
|
|
12
|
+
export declare class TenantOpenError extends Error {
|
|
13
|
+
readonly code: TenantCause["code"];
|
|
14
|
+
readonly repair: string;
|
|
15
|
+
/** The Tenant, when it was read before opening failed (`/v1/admin/status` names it). */
|
|
16
|
+
envelope?: TenantEnvelope;
|
|
17
|
+
constructor(fields: TenantCause);
|
|
18
|
+
toCause(): TenantCause;
|
|
19
|
+
}
|
|
20
|
+
/** A `TenantOpenError` with the standard repair for `code`. */
|
|
21
|
+
export declare function openError(code: TenantCause["code"], message: string): TenantOpenError;
|
|
22
|
+
/** The cause an error carries, when it is a `TenantOpenError`. */
|
|
23
|
+
export declare function causeOf(error: unknown): TenantCause | undefined;
|
|
24
|
+
/** What an operator does about each cause. */
|
|
25
|
+
export declare function repairFor(code: TenantCause["code"]): string;
|