@nylorun/runtime 0.10.0-beta → 0.11.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +8 -3
- package/dist/core/sandbox-routes.js +10 -10
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +48 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +643 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +75 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +790 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +207 -0
- package/dist/store/types.d.ts +440 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +11 -0
- package/dist/tenant/auth.js +34 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +384 -0
- package/dist/tenant/context.d.ts +140 -0
- package/dist/tenant/context.js +40 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +539 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +32 -0
- package/dist/tenant/http.js +51 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +162 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +9 -13
- package/dist/tenant/principals.js +31 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +126 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +142 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +34 -0
- package/dist/tenant/sessions.js +149 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/types.d.ts +95 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +37 -31
- package/dist/vault/service.js +439 -359
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -13
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Action service and the executor registry: discover pending Actions, claim, heartbeat,
|
|
3
|
+
* claim-scoped sandbox tool calls, and executor registration, listing and removal.
|
|
4
|
+
*
|
|
5
|
+
* A claim or heartbeat locks the Action's session and commits in one transaction; the
|
|
6
|
+
* registry is updated only after the executors table commits.
|
|
7
|
+
*
|
|
8
|
+
* Later waves: Wave 2 / Y reads the executor `connected` flag from the work stream instead
|
|
9
|
+
* of `executorStreams`.
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
|
|
13
|
+
import { assertExecutorCredential, hashToken, } from "../core/executors.js";
|
|
14
|
+
import { SandboxRouteError, handleActionSandboxTool, } from "../core/sandbox-routes.js";
|
|
15
|
+
import { lockedSession, sandboxLookup, sessionOf, } from "./context.js";
|
|
16
|
+
import { fail, readBody, requestAborted } from "./http.js";
|
|
17
|
+
import { scoped } from "./auth.js";
|
|
18
|
+
import { endExecutorStreams, executorConnected } from "./live.js";
|
|
19
|
+
/** Session and Action lookups for the sandbox route handlers. */
|
|
20
|
+
export function sandboxRouteDeps(ctx) {
|
|
21
|
+
return {
|
|
22
|
+
sandbox: ctx.sandbox,
|
|
23
|
+
session: (id) => ctx.store.tx(async (t) => {
|
|
24
|
+
const session = await sessionOf(t, id);
|
|
25
|
+
return { session, lookup: await sandboxLookup(t, id) };
|
|
26
|
+
}),
|
|
27
|
+
getAction: (id) => ctx.store.tx((t) => t.get("actions", id)),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** `GET /v1/actions`: the pending Actions of the executor's agent. */
|
|
31
|
+
export async function listPendingActions(ctx, executor) {
|
|
32
|
+
return {
|
|
33
|
+
actions: await ctx.store.tx((t) => t.pendingActions(executor.agentId)),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** `POST /v1/actions/:id/sandbox/:tool`: a claim-scoped sandbox tool call. */
|
|
37
|
+
export async function actionSandboxTool(ctx, scope, actionId, toolName, request) {
|
|
38
|
+
const action = (await ctx.store.tx((t) => t.get("actions", actionId))) ??
|
|
39
|
+
fail(404, "Action not found");
|
|
40
|
+
scoped(scope, action);
|
|
41
|
+
try {
|
|
42
|
+
return await handleActionSandboxTool(sandboxRouteDeps(ctx), actionId, toolName, await readBody(request), requestAborted(request));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof SandboxRouteError)
|
|
46
|
+
fail(error.status, error.message);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** `POST /v1/actions/:id/claim` and `/heartbeat`; anything else under the Action is a 404. */
|
|
51
|
+
export function updateAction(ctx, scope, actionId, method, operation, body) {
|
|
52
|
+
return ctx.store.tx(async (t) => {
|
|
53
|
+
const found = (await t.get("actions", actionId)) ??
|
|
54
|
+
fail(404, "Action not found");
|
|
55
|
+
scoped(scope, found);
|
|
56
|
+
const s = await lockedSession(t, found.sessionId);
|
|
57
|
+
// Read again under the session lock.
|
|
58
|
+
const action = (await t.get("actions", actionId)) ??
|
|
59
|
+
fail(404, "Action not found");
|
|
60
|
+
if (method === "POST" && operation === "claim") {
|
|
61
|
+
ActionClaimRequestSchema.parse(body);
|
|
62
|
+
if (action.status !== "pending" ||
|
|
63
|
+
s.status === "cancelled" ||
|
|
64
|
+
s.activeTurnId !== action.turnId)
|
|
65
|
+
fail(409, "Action unavailable");
|
|
66
|
+
action.status = "claimed";
|
|
67
|
+
action.generation++;
|
|
68
|
+
action.claimId = randomUUID();
|
|
69
|
+
action.leaseExpiresAt = new Date(Date.now() + (ctx.config.leaseMs ?? 30000)).toISOString();
|
|
70
|
+
await t.put("actions", actionId, action);
|
|
71
|
+
await t.event(action.sessionId, action.turnId, "action.claimed", {
|
|
72
|
+
actionId,
|
|
73
|
+
generation: action.generation,
|
|
74
|
+
...(action.agent ? { agent: action.agent } : {}),
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
action,
|
|
78
|
+
claimId: action.claimId,
|
|
79
|
+
generation: action.generation,
|
|
80
|
+
leaseExpiresAt: action.leaseExpiresAt,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (method === "POST" && operation === "heartbeat") {
|
|
84
|
+
const beat = ActionHeartbeatRequestSchema.parse(body);
|
|
85
|
+
if (s.activeTurnId !== action.turnId ||
|
|
86
|
+
action.status !== "claimed" ||
|
|
87
|
+
action.claimId !== beat.claimId ||
|
|
88
|
+
action.generation !== beat.generation ||
|
|
89
|
+
Date.parse(action.leaseExpiresAt) <= Date.now())
|
|
90
|
+
fail(409, "Stale or expired claim");
|
|
91
|
+
action.leaseExpiresAt = new Date(Date.now() + (ctx.config.leaseMs ?? 30000)).toISOString();
|
|
92
|
+
await t.put("actions", actionId, action);
|
|
93
|
+
return { leaseExpiresAt: action.leaseExpiresAt };
|
|
94
|
+
}
|
|
95
|
+
return fail(404, "Route not found");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** `GET /v1/executors`. */
|
|
99
|
+
export function listExecutors(ctx) {
|
|
100
|
+
return {
|
|
101
|
+
executors: ctx.registry.list().map((e) => ({
|
|
102
|
+
agentId: e.agentId,
|
|
103
|
+
implementationVersion: e.implementationVersion,
|
|
104
|
+
...(e.manifestHash === undefined ? {} : { manifestHash: e.manifestHash }),
|
|
105
|
+
connected: executorConnected(ctx.live, e.tokenHash),
|
|
106
|
+
updatedAt: e.updatedAt,
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** `PUT /v1/executors`: persist the batch, then update the registry and end rotated streams. */
|
|
111
|
+
export async function registerExecutors(ctx, principalId, body) {
|
|
112
|
+
const { store, registry } = ctx;
|
|
113
|
+
// Credential rules are shared with startup validation, which throws plainly; on the
|
|
114
|
+
// wire a rejected registration is a bad request, not a server fault.
|
|
115
|
+
const applicationHashes = await store.tx((t) => t.applicationTokenHashes());
|
|
116
|
+
try {
|
|
117
|
+
for (const executor of body.executors)
|
|
118
|
+
assertExecutorCredential(executor, applicationHashes);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
fail(400, error instanceof Error ? error.message : String(error));
|
|
122
|
+
}
|
|
123
|
+
if (new Set(body.executors.map((e) => e.agentId)).size !==
|
|
124
|
+
body.executors.length ||
|
|
125
|
+
new Set(body.executors.map((e) => e.token)).size !== body.executors.length)
|
|
126
|
+
fail(400, "Executor registrations must be unique per agent and token");
|
|
127
|
+
const updatedAt = new Date().toISOString();
|
|
128
|
+
const records = body.executors.map((executor) => ({
|
|
129
|
+
agentId: executor.agentId,
|
|
130
|
+
implementationVersion: executor.implementationVersion,
|
|
131
|
+
...(executor.manifestHash === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: { manifestHash: executor.manifestHash }),
|
|
134
|
+
tokenHash: hashToken(executor.token),
|
|
135
|
+
persisted: true,
|
|
136
|
+
updatedAt,
|
|
137
|
+
principalId,
|
|
138
|
+
}));
|
|
139
|
+
for (const record of records) {
|
|
140
|
+
const collision = registry.find(record.tokenHash);
|
|
141
|
+
if (collision && collision.agentId !== record.agentId)
|
|
142
|
+
fail(409, "Executor tokens must be unique");
|
|
143
|
+
// A token hash may not appear in both principals and executors (D3).
|
|
144
|
+
if (applicationHashes.includes(record.tokenHash))
|
|
145
|
+
fail(400, "Executor tokens require independent credentials and an agent id");
|
|
146
|
+
}
|
|
147
|
+
// Persist the whole batch first; the in-memory registry must never run ahead of the store.
|
|
148
|
+
await store.tx(async (t) => {
|
|
149
|
+
for (const record of records)
|
|
150
|
+
await t.putExecutor({
|
|
151
|
+
agentId: record.agentId,
|
|
152
|
+
tokenHash: record.tokenHash,
|
|
153
|
+
implementationVersion: record.implementationVersion,
|
|
154
|
+
...(record.manifestHash === undefined
|
|
155
|
+
? {}
|
|
156
|
+
: { manifestHash: record.manifestHash }),
|
|
157
|
+
principalId: record.principalId,
|
|
158
|
+
updatedAt,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
const results = records.map((record) => {
|
|
162
|
+
const previous = registry.get(record.agentId);
|
|
163
|
+
const { rotated, previousHash } = registry.upsert(record);
|
|
164
|
+
if (rotated && previousHash)
|
|
165
|
+
endExecutorStreams(ctx.live, previousHash);
|
|
166
|
+
const replacedByDifferent = previous?.principalId !== undefined &&
|
|
167
|
+
previous.principalId !== principalId;
|
|
168
|
+
if (replacedByDifferent)
|
|
169
|
+
ctx.config.logger.warn("executor registration replaced by different application credential", {
|
|
170
|
+
agentId: record.agentId,
|
|
171
|
+
previousPrincipalId: previous.principalId,
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
agentId: record.agentId,
|
|
175
|
+
implementationVersion: record.implementationVersion,
|
|
176
|
+
rotated,
|
|
177
|
+
...(replacedByDifferent
|
|
178
|
+
? { replacedBy: "different-credential" }
|
|
179
|
+
: {}),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
return { executors: results };
|
|
183
|
+
}
|
|
184
|
+
/** `DELETE /v1/executors/:agentId`: forget the executor and end its streams. */
|
|
185
|
+
export async function deleteExecutor(ctx, agentId) {
|
|
186
|
+
const existing = ctx.registry.get(agentId) ?? fail(404, "Executor not found");
|
|
187
|
+
await ctx.store.tx((t) => t.deleteExecutor(agentId));
|
|
188
|
+
ctx.registry.remove(agentId);
|
|
189
|
+
endExecutorStreams(ctx.live, existing.tokenHash);
|
|
190
|
+
return { agentId, deleted: true };
|
|
191
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AdvanceResult } from "../execution/types.js";
|
|
2
|
+
import type { Tx } from "../store/types.js";
|
|
3
|
+
import type { Session, TenantContext } from "./context.js";
|
|
4
|
+
/**
|
|
5
|
+
* Advance session `id` if it has a runnable checkpoint (`WorkerHandlers.advance`). Returns
|
|
6
|
+
* `busy` when another advance holds a live lease, `done` otherwise.
|
|
7
|
+
*/
|
|
8
|
+
export declare function advance(ctx: TenantContext, id: string, signal: AbortSignal): Promise<AdvanceResult>;
|
|
9
|
+
/**
|
|
10
|
+
* Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
|
|
11
|
+
* owner's `invoking` effects become `uncertain` and are never invoked again. When one belongs
|
|
12
|
+
* to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
|
|
13
|
+
* Mutates and writes `s`. Returns the effect ids it marked.
|
|
14
|
+
*/
|
|
15
|
+
export declare function takeOver(t: Tx, s: Session): Promise<string[]>;
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The advance (architecture §10.5): the Tenant's `WorkerHandlers.advance`. It takes ownership
|
|
3
|
+
* of the session, runs one segment of its turn with the engine its definition asks for
|
|
4
|
+
* (`runDurable` for agents, `runFlowDurable` for workflows) and settles the result — status,
|
|
5
|
+
* checkpoint, `turn.*` events, linked-workflow wakes and fail-fast sibling cancels — then
|
|
6
|
+
* releases ownership.
|
|
7
|
+
*
|
|
8
|
+
* Ownership (§10.6):
|
|
9
|
+
* - **Take.** One transaction takes the lease (`Tx.takeOwnership`). A live lease held by
|
|
10
|
+
* another advance returns `busy` with the time left on it; the execution re-wakes later.
|
|
11
|
+
* - **Take over.** A previous owner whose lease expired without release died mid-advance:
|
|
12
|
+
* its `invoking` effects become `uncertain` with `effect.uncertain` events, in the same
|
|
13
|
+
* transaction (§11.4). This replaces the old startup recovery.
|
|
14
|
+
* - **Epoch-checked writes.** Every transaction of the advance and of the engine host
|
|
15
|
+
* (`effects.ts`) runs through `ownedTx`/`ownedSession`, which checks the epoch first. A
|
|
16
|
+
* mismatch throws `ownership.lost` with no write; the advance stops and returns `done`.
|
|
17
|
+
* - **Heartbeat.** While the segment runs, a timer scoped to the advance renews the lease
|
|
18
|
+
* (a column update). A failed renewal aborts the segment.
|
|
19
|
+
* - **Release** when the advance ends, whatever the outcome.
|
|
20
|
+
*
|
|
21
|
+
* Session outcomes never throw out of `advance`: a segment that throws is settled as a
|
|
22
|
+
* failed turn. Only infrastructure errors (the Session Store is unreachable) throw, and the
|
|
23
|
+
* execution retries them.
|
|
24
|
+
*
|
|
25
|
+
* Aborts (the reasons are in `worker.ts`): a `cancel` finds the turn already cancelled and
|
|
26
|
+
* settles nothing; a `deadline` settles the segment (an in-flight effect becomes `uncertain`,
|
|
27
|
+
* otherwise the turn fails with the deadline's message). A `shutdown` or `ownership.lost`
|
|
28
|
+
* settles nothing at all: the segment stops, whatever the engine made of the abort, and the
|
|
29
|
+
* session stays `running` under its checkpoint. After a `shutdown` the advance releases
|
|
30
|
+
* ownership and returns `busy`, so the next advance resumes the segment with the outcomes
|
|
31
|
+
* already recorded; an `ownership.lost` leaves the session to the advance that owns it.
|
|
32
|
+
*/
|
|
33
|
+
import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
|
|
34
|
+
import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
|
|
35
|
+
import { isOwnershipLost, ownedTx } from "../store/ownership.js";
|
|
36
|
+
import { linkedAgentOutput, rebaseSessionState, sessionToolsOf, turnManifestOf, } from "./session.js";
|
|
37
|
+
import { prepareMcp, resolveEffect } from "./effects.js";
|
|
38
|
+
import { command } from "./commands.js";
|
|
39
|
+
import { usesFixtureModel } from "./model-setting.js";
|
|
40
|
+
import { toolFixtureModel } from "../core/provider.js";
|
|
41
|
+
import { AdvanceAbort, AdvanceDeadlineError, abortKind, } from "./worker.js";
|
|
42
|
+
/** The model of Tenants with the fixture-model setting (`model-setting.ts`). Stateless. */
|
|
43
|
+
const fixture = toolFixtureModel();
|
|
44
|
+
/**
|
|
45
|
+
* A segment stopped by a `shutdown` or `ownership.lost` abort before it settled. Never
|
|
46
|
+
* leaves `advance`.
|
|
47
|
+
*/
|
|
48
|
+
class SegmentStopped extends Error {
|
|
49
|
+
kind;
|
|
50
|
+
name = "SegmentStopped";
|
|
51
|
+
constructor(kind) {
|
|
52
|
+
super(`Segment stopped: ${kind}`);
|
|
53
|
+
this.kind = kind;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Throws `SegmentStopped` when the signal aborted for a reason that must not settle. */
|
|
57
|
+
function stopIfLeaving(signal) {
|
|
58
|
+
const kind = abortKind(signal);
|
|
59
|
+
if (kind === "shutdown" || kind === "ownership.lost")
|
|
60
|
+
throw new SegmentStopped(kind);
|
|
61
|
+
}
|
|
62
|
+
const DONE = { status: "done" };
|
|
63
|
+
/** Shortest `busy` retry, so a lease about to lapse is not polled in a tight loop. */
|
|
64
|
+
const MIN_RETRY_MS = 25;
|
|
65
|
+
/**
|
|
66
|
+
* Advance session `id` if it has a runnable checkpoint (`WorkerHandlers.advance`). Returns
|
|
67
|
+
* `busy` when another advance holds a live lease, `done` otherwise.
|
|
68
|
+
*/
|
|
69
|
+
export async function advance(ctx, id, signal) {
|
|
70
|
+
if (ctx.closing || ctx.closed)
|
|
71
|
+
return DONE;
|
|
72
|
+
const taken = await takeOwnership(ctx, id);
|
|
73
|
+
if (taken.status === "busy")
|
|
74
|
+
return {
|
|
75
|
+
status: "busy",
|
|
76
|
+
retryAfterMs: Math.min(Math.max(Date.parse(taken.ownerExpiresAt) - Date.now(), MIN_RETRY_MS), ctx.ownerLeaseMs),
|
|
77
|
+
};
|
|
78
|
+
if (taken.status === "idle")
|
|
79
|
+
return DONE;
|
|
80
|
+
const { lease } = taken;
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const forward = () => controller.abort(signal.reason);
|
|
83
|
+
if (signal.aborted)
|
|
84
|
+
forward();
|
|
85
|
+
else
|
|
86
|
+
signal.addEventListener("abort", forward, { once: true });
|
|
87
|
+
ctx.work.running.set(id, controller);
|
|
88
|
+
const heartbeat = startHeartbeat(ctx, lease, controller);
|
|
89
|
+
let result = DONE;
|
|
90
|
+
try {
|
|
91
|
+
await runSegment(ctx, lease, taken.session, controller.signal);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof SegmentStopped && error.kind === "shutdown") {
|
|
95
|
+
// Left for the next advance, which resumes from the checkpoint once this one releases.
|
|
96
|
+
ctx.config.logger.info("advance stopped for shutdown; session left for the next advance", {
|
|
97
|
+
sessionId: id,
|
|
98
|
+
epoch: lease.epoch,
|
|
99
|
+
});
|
|
100
|
+
result = { status: "busy", retryAfterMs: 0 };
|
|
101
|
+
}
|
|
102
|
+
else if (error instanceof SegmentStopped || isOwnershipLost(error))
|
|
103
|
+
ctx.config.logger.warn("advance lost ownership", {
|
|
104
|
+
sessionId: id,
|
|
105
|
+
epoch: lease.epoch,
|
|
106
|
+
});
|
|
107
|
+
else
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
heartbeat.stop();
|
|
112
|
+
signal.removeEventListener("abort", forward);
|
|
113
|
+
if (ctx.work.running.get(id) === controller)
|
|
114
|
+
ctx.work.running.delete(id);
|
|
115
|
+
// Best effort: a release that fails leaves a lease that simply expires.
|
|
116
|
+
await ctx.store
|
|
117
|
+
.tx((t) => t.releaseOwnership(id, lease.owner, lease.epoch))
|
|
118
|
+
.catch((error) => ctx.config.logger.warn("advance failed to release ownership", {
|
|
119
|
+
sessionId: id,
|
|
120
|
+
message: error instanceof Error ? error.message : String(error),
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Takes ownership of `id` in one transaction, takes over from a dead owner, and marks the
|
|
127
|
+
* session `running` when it has a runnable checkpoint. Releases again when there is
|
|
128
|
+
* nothing to run.
|
|
129
|
+
*/
|
|
130
|
+
async function takeOwnership(ctx, id) {
|
|
131
|
+
const now = new Date();
|
|
132
|
+
return ctx.store.tx(async (t) => {
|
|
133
|
+
const taken = await t.takeOwnership(id, {
|
|
134
|
+
owner: ctx.workerId,
|
|
135
|
+
now,
|
|
136
|
+
leaseMs: ctx.ownerLeaseMs,
|
|
137
|
+
});
|
|
138
|
+
if (taken.status === "missing")
|
|
139
|
+
return { status: "idle" };
|
|
140
|
+
if (taken.status === "busy")
|
|
141
|
+
return { status: "busy", ownerExpiresAt: taken.ownerExpiresAt };
|
|
142
|
+
const lease = {
|
|
143
|
+
sessionId: id,
|
|
144
|
+
owner: ctx.workerId,
|
|
145
|
+
epoch: taken.epoch,
|
|
146
|
+
};
|
|
147
|
+
const s = await t.assertEpoch(id, taken.epoch);
|
|
148
|
+
if (taken.takeover)
|
|
149
|
+
await takeOver(t, s);
|
|
150
|
+
if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
|
|
151
|
+
await t.releaseOwnership(id, lease.owner, lease.epoch);
|
|
152
|
+
return { status: "idle" };
|
|
153
|
+
}
|
|
154
|
+
s.status = "running";
|
|
155
|
+
await t.put("sessions", id, s);
|
|
156
|
+
return { status: "owned", lease, session: s };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 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. When one belongs
|
|
162
|
+
* to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
|
|
163
|
+
* Mutates and writes `s`. Returns the effect ids it marked.
|
|
164
|
+
*/
|
|
165
|
+
export async function takeOver(t, s) {
|
|
166
|
+
const marked = [];
|
|
167
|
+
for (const effect of await t.invokingEffects(s.id)) {
|
|
168
|
+
effect.status = "uncertain";
|
|
169
|
+
effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
|
|
170
|
+
await t.put("effects", effect.request.effectId, effect);
|
|
171
|
+
marked.push(effect.request.effectId);
|
|
172
|
+
if (s.status !== "cancelled" && s.activeTurnId === effect.request.turnId) {
|
|
173
|
+
s.status = "uncertain";
|
|
174
|
+
await t.put("sessions", s.id, s);
|
|
175
|
+
await t.event(s.id, s.activeTurnId, "effect.uncertain", {
|
|
176
|
+
effectId: effect.request.effectId,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return marked;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Renews the lease every third of its length while the advance runs; aborts it when lost.
|
|
184
|
+
* Stops renewing once the advance is aborted (cancel, deadline, Worker stop): an advance that
|
|
185
|
+
* does not wind down within the lease is taken over when it lapses (`worker.ts`).
|
|
186
|
+
*/
|
|
187
|
+
function startHeartbeat(ctx, lease, controller) {
|
|
188
|
+
const every = Math.max(10, Math.floor(ctx.ownerLeaseMs / 3));
|
|
189
|
+
let stopped = false;
|
|
190
|
+
let timer;
|
|
191
|
+
const beat = async () => {
|
|
192
|
+
if (controller.signal.aborted) {
|
|
193
|
+
stopped = true;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const renewed = await ctx.store.tx((t) => t.renewOwnership(lease.sessionId, lease.owner, lease.epoch, new Date(Date.now() + ctx.ownerLeaseMs)));
|
|
198
|
+
if (!renewed && !stopped) {
|
|
199
|
+
stopped = true;
|
|
200
|
+
controller.abort(new AdvanceAbort("ownership.lost", "Ownership lost"));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (!stopped)
|
|
206
|
+
ctx.config.logger.warn("advance heartbeat failed", {
|
|
207
|
+
sessionId: lease.sessionId,
|
|
208
|
+
message: error instanceof Error ? error.message : String(error),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
arm();
|
|
212
|
+
};
|
|
213
|
+
const arm = () => {
|
|
214
|
+
if (stopped)
|
|
215
|
+
return;
|
|
216
|
+
timer = setTimeout(() => void beat(), every);
|
|
217
|
+
timer.unref();
|
|
218
|
+
};
|
|
219
|
+
arm();
|
|
220
|
+
return {
|
|
221
|
+
stop() {
|
|
222
|
+
stopped = true;
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/** Runs one segment and settles it. Throws only `ownership.lost` and infrastructure errors. */
|
|
228
|
+
async function runSegment(ctx, lease, started, signal) {
|
|
229
|
+
const id = lease.sessionId;
|
|
230
|
+
try {
|
|
231
|
+
// prepareMcp mutates the session's mcpSnapshot; read current after it.
|
|
232
|
+
if (!isWorkflowManifest(started.manifest))
|
|
233
|
+
await prepareMcp(ctx, lease, signal);
|
|
234
|
+
const { current, fixtureModel } = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
235
|
+
if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
|
|
236
|
+
rebaseSessionState(current, current.checkpoint.manifestHash);
|
|
237
|
+
const cp = current.checkpoint;
|
|
238
|
+
current.checkpoint = { ...cp, state: current.state };
|
|
239
|
+
await t.put("sessions", id, current);
|
|
240
|
+
}
|
|
241
|
+
return { current, fixtureModel: await usesFixtureModel(t) };
|
|
242
|
+
});
|
|
243
|
+
const segment = fixtureModel ? { model: fixture } : {};
|
|
244
|
+
const host = {
|
|
245
|
+
resolveEffect: (e) => resolveEffect(ctx, e, signal, lease, segment),
|
|
246
|
+
};
|
|
247
|
+
const result = isWorkflowManifest(current.manifest)
|
|
248
|
+
? await runFlowDurable({
|
|
249
|
+
manifest: current.manifest,
|
|
250
|
+
checkpoint: current.checkpoint,
|
|
251
|
+
signal,
|
|
252
|
+
host,
|
|
253
|
+
limits: ctx.flowLimits,
|
|
254
|
+
})
|
|
255
|
+
: await runDurable({
|
|
256
|
+
manifest: turnManifestOf(current),
|
|
257
|
+
checkpoint: current.checkpoint,
|
|
258
|
+
signal,
|
|
259
|
+
sessionTools: sessionToolsOf(current.mcpSnapshot),
|
|
260
|
+
host,
|
|
261
|
+
});
|
|
262
|
+
// The engine turns an abort into a `cancelled` (agents) or `failed` (workflows) result;
|
|
263
|
+
// only a user cancel may settle that, and it already did.
|
|
264
|
+
stopIfLeaving(signal);
|
|
265
|
+
if (signal.reason instanceof AdvanceDeadlineError &&
|
|
266
|
+
(result.status === "cancelled" || result.status === "failed"))
|
|
267
|
+
await settleFailure(ctx, lease, started, signal.reason);
|
|
268
|
+
else
|
|
269
|
+
await settle(ctx, lease, started, result);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (isOwnershipLost(error) || error instanceof SegmentStopped)
|
|
273
|
+
throw error;
|
|
274
|
+
stopIfLeaving(signal);
|
|
275
|
+
// A segment stopped by its deadline fails with the deadline, not the abort it caused.
|
|
276
|
+
const deadline = signal.aborted && signal.reason instanceof AdvanceDeadlineError;
|
|
277
|
+
await settleFailure(ctx, lease, started, deadline ? signal.reason : error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Wake the workflow a linked agent belongs to, in the agent's settle transaction. */
|
|
281
|
+
async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
|
|
282
|
+
if (type === "turn.completed" || type === "turn.failed")
|
|
283
|
+
await wakeLinkedWorkflow({
|
|
284
|
+
t,
|
|
285
|
+
agentSessionId: agent.id,
|
|
286
|
+
turnId,
|
|
287
|
+
output: type === "turn.completed"
|
|
288
|
+
? linkedAgentOutput(agent, payload.output)
|
|
289
|
+
: undefined,
|
|
290
|
+
failed: type === "turn.failed",
|
|
291
|
+
error: type === "turn.failed"
|
|
292
|
+
? String(payload.message ?? "")
|
|
293
|
+
: undefined,
|
|
294
|
+
schedule: ctx.wake,
|
|
295
|
+
});
|
|
296
|
+
else if (type === "turn.cancelled")
|
|
297
|
+
await wakeLinkedWorkflow({
|
|
298
|
+
t,
|
|
299
|
+
agentSessionId: agent.id,
|
|
300
|
+
turnId,
|
|
301
|
+
cancelled: true,
|
|
302
|
+
error: "Agent turn was cancelled",
|
|
303
|
+
schedule: ctx.wake,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/** Record a segment result, then cancel failed siblings and wake a linked workflow. */
|
|
307
|
+
async function settle(ctx, lease, s, result) {
|
|
308
|
+
const id = lease.sessionId;
|
|
309
|
+
const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
310
|
+
const siblings = [];
|
|
311
|
+
if (current.status === "cancelled" ||
|
|
312
|
+
current.activeTurnId !== s.activeTurnId)
|
|
313
|
+
return siblings;
|
|
314
|
+
// A result can arrive during concurrent action persistence. Preserve the runnable marker.
|
|
315
|
+
const resumeRequested = current.status === "runnable";
|
|
316
|
+
if (result.status === "waiting" || result.status === "uncertain") {
|
|
317
|
+
const linkedWaits = isWorkflowManifest(current.manifest)
|
|
318
|
+
? await aggregateWaits({ t, workflowSessionId: id })
|
|
319
|
+
: [];
|
|
320
|
+
if (linkedWaits.length > 0 && !resumeRequested) {
|
|
321
|
+
current.status = "paused";
|
|
322
|
+
current.waits = linkedWaits;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
current.status = resumeRequested
|
|
326
|
+
? "runnable"
|
|
327
|
+
: current.status === "uncertain"
|
|
328
|
+
? "uncertain"
|
|
329
|
+
: result.status;
|
|
330
|
+
current.waits =
|
|
331
|
+
linkedWaits.length > 0 ? linkedWaits : { effectIds: result.effectIds };
|
|
332
|
+
}
|
|
333
|
+
await t.put("sessions", id, current);
|
|
334
|
+
return siblings;
|
|
335
|
+
}
|
|
336
|
+
if (!("result" in result)) {
|
|
337
|
+
await t.put("sessions", id, current);
|
|
338
|
+
return siblings;
|
|
339
|
+
}
|
|
340
|
+
const flow = isWorkflowManifest(current.manifest);
|
|
341
|
+
if (!flow) {
|
|
342
|
+
current.state =
|
|
343
|
+
result.status === "failed"
|
|
344
|
+
? current.turnStartState
|
|
345
|
+
: result.result.state;
|
|
346
|
+
}
|
|
347
|
+
current.checkpoint = result.checkpoint;
|
|
348
|
+
await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, result.checkpoint.segment]), { checkpoint: result.checkpoint, status: result.status });
|
|
349
|
+
current.status = result.status;
|
|
350
|
+
current.waits =
|
|
351
|
+
result.status === "paused" ? result.result.pending : undefined;
|
|
352
|
+
if (result.status === "completed")
|
|
353
|
+
current.lastOutput = result.result.output;
|
|
354
|
+
// Fail-fast: cancel pending Parallel/Map siblings before clearing the turn.
|
|
355
|
+
if (result.status === "failed" &&
|
|
356
|
+
flow &&
|
|
357
|
+
"cancelEffectIds" in result &&
|
|
358
|
+
Array.isArray(result.cancelEffectIds) &&
|
|
359
|
+
result.cancelEffectIds.length > 0 &&
|
|
360
|
+
s.activeTurnId) {
|
|
361
|
+
const cancelResult = await cancelSiblingWork({
|
|
362
|
+
t,
|
|
363
|
+
workflowSessionId: id,
|
|
364
|
+
turnId: s.activeTurnId,
|
|
365
|
+
cancelEffectIds: result.cancelEffectIds,
|
|
366
|
+
});
|
|
367
|
+
siblings.push(...cancelResult.agentSessionIds);
|
|
368
|
+
}
|
|
369
|
+
if (result.status !== "paused") {
|
|
370
|
+
current.activeTurnId = null;
|
|
371
|
+
if (s.activeTurnId)
|
|
372
|
+
current.lastTurnId = s.activeTurnId;
|
|
373
|
+
}
|
|
374
|
+
const type = `turn.${result.status}`;
|
|
375
|
+
const payload = result.status === "completed"
|
|
376
|
+
? { output: result.result.output }
|
|
377
|
+
: result.status === "paused"
|
|
378
|
+
? {
|
|
379
|
+
interactions: (result.result.pending ?? []).map((call) => ({
|
|
380
|
+
invocationId: call.invocationId,
|
|
381
|
+
interaction: call.interaction,
|
|
382
|
+
wait: call.wait,
|
|
383
|
+
status: call.status,
|
|
384
|
+
})),
|
|
385
|
+
}
|
|
386
|
+
: result.status === "failed"
|
|
387
|
+
? {
|
|
388
|
+
error: {
|
|
389
|
+
code: result.result.error?.code,
|
|
390
|
+
message: result.result.error?.message,
|
|
391
|
+
...(result.result.error?.path
|
|
392
|
+
? { path: result.result.error.path }
|
|
393
|
+
: {}),
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
: {};
|
|
397
|
+
await t.event(id, s.activeTurnId, type, payload);
|
|
398
|
+
await t.put("sessions", id, current);
|
|
399
|
+
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, type, payload);
|
|
400
|
+
return siblings;
|
|
401
|
+
});
|
|
402
|
+
for (const agentId of siblingCancelIds) {
|
|
403
|
+
try {
|
|
404
|
+
await command(ctx, agentId, {
|
|
405
|
+
type: "cancel",
|
|
406
|
+
requestId: `flow-sibling-${id}-${agentId}`,
|
|
407
|
+
idempotencyKey: `flow-sibling-cancel:${id}:${agentId}:${s.activeTurnId}`,
|
|
408
|
+
reason: "sibling branch failed",
|
|
409
|
+
}, {
|
|
410
|
+
kind: "application",
|
|
411
|
+
principalId: "flow-host",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
/* agent may already be terminal */
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** A segment threw: fail the turn, restore the turn-start state, wake a linked workflow. */
|
|
420
|
+
async function settleFailure(ctx, lease, s, error) {
|
|
421
|
+
const id = lease.sessionId;
|
|
422
|
+
await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
|
|
423
|
+
if (current.status === "cancelled" ||
|
|
424
|
+
current.activeTurnId !== s.activeTurnId)
|
|
425
|
+
return;
|
|
426
|
+
current.status = "failed";
|
|
427
|
+
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
|
+
current.error = error instanceof Error ? error.message : String(error);
|
|
431
|
+
const payload = { message: current.error };
|
|
432
|
+
await t.event(id, current.activeTurnId, "turn.failed", payload);
|
|
433
|
+
if (current.activeTurnId)
|
|
434
|
+
current.lastTurnId = current.activeTurnId;
|
|
435
|
+
current.activeTurnId = null;
|
|
436
|
+
await t.put("sessions", id, current);
|
|
437
|
+
await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tenant request authentication and authorization: a bearer token resolves to an application
|
|
3
|
+
* principal or a registered executor; anything else is the opaque 404 (D5).
|
|
4
|
+
*/
|
|
5
|
+
import type { IncomingMessage } from "node:http";
|
|
6
|
+
import type { Action } from "@nylorun/core/contracts";
|
|
7
|
+
import type { AuthScope, TenantContext } from "./context.js";
|
|
8
|
+
export declare function authenticate(ctx: TenantContext, request: IncomingMessage): Promise<AuthScope>;
|
|
9
|
+
/** The executor scope must belong to the Action's agent. */
|
|
10
|
+
export declare function scoped(scope: AuthScope, action: Action): void;
|
|
11
|
+
export declare function requireApplication(scope: AuthScope): string;
|