@nylorun/runtime 0.16.0-beta → 0.19.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 +207 -0
- package/README.md +105 -63
- package/dist/adapters/execution/restate.d.ts +1 -3
- package/dist/adapters/execution/restate.js +4 -31
- package/dist/adapters/replication/pgoutput.js +1 -1
- package/dist/adapters/sandbox/local.js +1 -1
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/api/ag-ui/endpoint.js +1 -2
- package/dist/api/ag-ui/routes.d.ts +1 -1
- package/dist/api/ag-ui/routes.js +11 -13
- package/dist/api/ag-ui/translate.d.ts +1 -3
- package/dist/api/ag-ui/translate.js +2 -17
- package/dist/api/components.d.ts +4783 -2588
- package/dist/api/components.js +19 -21
- package/dist/api/gate/routes.d.ts +7 -8
- package/dist/api/gate/routes.js +58 -52
- package/dist/api/http/app.d.ts +6 -0
- package/dist/api/http/app.js +20 -14
- package/dist/api/http/define.d.ts +22 -13
- package/dist/api/http/define.js +43 -39
- package/dist/api/http/routes/access.d.ts +3 -19
- package/dist/api/http/routes/access.js +25 -184
- package/dist/api/http/routes/artifacts.js +3 -7
- package/dist/api/http/routes/files.d.ts +8 -0
- package/dist/api/http/routes/files.js +77 -0
- package/dist/api/http/routes/keys.d.ts +10 -0
- package/dist/api/http/routes/keys.js +64 -0
- package/dist/api/http/routes/me.d.ts +13 -0
- package/dist/api/http/routes/me.js +48 -0
- package/dist/api/http/routes/reads.d.ts +66 -0
- package/dist/api/http/routes/reads.js +127 -0
- package/dist/api/http/routes/sandboxes.d.ts +2 -2
- package/dist/api/http/routes/sandboxes.js +8 -7
- package/dist/api/http/routes/sessions.js +31 -20
- package/dist/api/http/routes/tenant.js +15 -18
- package/dist/api/http/routes/vaults.d.ts +13 -3
- package/dist/api/http/routes/vaults.js +136 -64
- package/dist/api/openapi.d.ts +10 -11
- package/dist/api/openapi.js +292 -68
- package/dist/artifacts/links.js +6 -6
- package/dist/artifacts/service.js +1 -1
- package/dist/core/flow-host.d.ts +44 -23
- package/dist/core/flow-host.js +88 -106
- package/dist/core/provider.d.ts +4 -1
- package/dist/core/provider.js +6 -1
- package/dist/core/sandbox-routes.d.ts +0 -9
- package/dist/core/sandbox-routes.js +2 -21
- package/dist/execution/memory.d.ts +4 -5
- package/dist/execution/memory.js +11 -25
- package/dist/execution/types.d.ts +3 -33
- package/dist/execution/types.js +2 -12
- package/dist/gates/http-tool.d.ts +86 -0
- package/dist/gates/http-tool.js +197 -0
- package/dist/gates/mcp-handler.d.ts +3 -0
- package/dist/gates/mcp-handler.js +6 -4
- package/dist/gates/meter.d.ts +2 -2
- package/dist/gates/meter.js +6 -0
- package/dist/gates/tenant-vaults.d.ts +19 -14
- package/dist/gates/tenant-vaults.js +17 -12
- package/dist/gates/tool-calls.d.ts +7 -15
- package/dist/gates/tool-calls.js +4 -4
- package/dist/gates/tool-client.d.ts +1 -1
- package/dist/gates/tool-client.js +23 -47
- package/dist/gates/tool-contract.d.ts +27 -39
- package/dist/gates/tool-contract.js +30 -21
- package/dist/gates/tool-gate.d.ts +25 -24
- package/dist/gates/tool-gate.js +11 -7
- package/dist/harness/calls.d.ts +10 -4
- package/dist/harness/calls.js +15 -6
- package/dist/harness/executors.d.ts +1 -1
- package/dist/harness/executors.js +17 -12
- package/dist/harness/main.d.ts +2 -2
- package/dist/harness/main.js +5 -13
- package/dist/harness/service.d.ts +4 -5
- package/dist/harness/service.js +9 -2
- package/dist/harness-api/in-process.js +2 -2
- package/dist/harness-api/record.d.ts +6 -3
- package/dist/harness-api/record.js +65 -72
- package/dist/harness-api/server.d.ts +1 -9
- package/dist/harness-api/server.js +19 -17
- package/dist/harness-api/start.js +1 -5
- package/dist/harness-api/workspace.js +2 -3
- package/dist/harness-api/ws-server.js +1 -1
- package/dist/host/app.d.ts +2 -10
- package/dist/host/app.js +24 -44
- package/dist/host/config-for.d.ts +5 -5
- package/dist/host/config-for.js +2 -3
- package/dist/host/config.d.ts +0 -11
- package/dist/host/create-host.d.ts +3 -40
- package/dist/host/create-host.js +18 -91
- package/dist/host/environment.d.ts +0 -3
- package/dist/host/environment.js +0 -9
- package/dist/host/gates.d.ts +4 -1
- package/dist/host/gates.js +9 -3
- package/dist/host/http.d.ts +1 -2
- package/dist/host/http.js +1 -7
- package/dist/host/index.d.ts +2 -2
- package/dist/host/index.js +2 -2
- package/dist/host/main.js +55 -40
- package/dist/host/operate.d.ts +29 -0
- package/dist/host/operate.js +223 -0
- package/dist/host/stack-config.d.ts +23 -20
- package/dist/host/stack-config.js +28 -44
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/infra/workers.d.ts +1 -1
- package/dist/keys/client.js +9 -1
- package/dist/keys/keys.d.ts +12 -2
- package/dist/keys/keys.js +10 -2
- package/dist/management-openapi.json +6491 -0
- package/dist/mcp/connect.d.ts +8 -4
- package/dist/mcp/connect.js +8 -27
- package/dist/mcp/pool.d.ts +6 -9
- package/dist/mcp/pool.js +8 -18
- package/dist/mcp/snapshot.d.ts +8 -2
- package/dist/mcp/snapshot.js +29 -2
- package/dist/media.d.ts +1 -1
- package/dist/media.js +1 -1
- package/dist/model/pi-model.js +3 -0
- package/dist/openapi.json +6228 -15615
- package/dist/reads/access.d.ts +4 -0
- package/dist/reads/access.js +12 -0
- package/dist/reads/cursor.d.ts +11 -0
- package/dist/reads/cursor.js +80 -0
- package/dist/reads/types.d.ts +25 -0
- package/dist/reads/types.js +1 -0
- package/dist/sandbox/manager.d.ts +16 -0
- package/dist/sandbox/manager.js +40 -0
- package/dist/sandbox/session-sandbox.js +13 -3
- package/dist/sandbox/skills.d.ts +13 -0
- package/dist/sandbox/skills.js +46 -0
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/ownership.d.ts +9 -0
- package/dist/store/ownership.js +13 -0
- package/dist/store/postgres/connect.d.ts +2 -0
- package/dist/store/postgres/connect.js +29 -0
- package/dist/store/postgres/drizzle/0008_installation_vaults.sql +2 -0
- package/dist/store/postgres/drizzle/0009_oauth_pending.sql +19 -0
- package/dist/store/postgres/drizzle/0010_oss_auth_removals.sql +3 -0
- package/dist/store/postgres/drizzle/0011_key_roles.sql +3 -0
- package/dist/store/postgres/drizzle/0012_session_reads.sql +8 -0
- package/dist/store/postgres/drizzle/0013_definition_files.sql +16 -0
- package/dist/store/postgres/drizzle/0014_action_endpoints_removed.sql +2 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +49 -0
- package/dist/store/postgres/reads.d.ts +3 -0
- package/dist/store/postgres/reads.js +272 -0
- package/dist/store/postgres/schema.d.ts +423 -594
- package/dist/store/postgres/schema.js +84 -101
- package/dist/store/postgres/store.js +96 -248
- package/dist/store/postgres/tenant.d.ts +34 -2
- package/dist/store/postgres/tenant.js +16 -32
- package/dist/store/types.d.ts +44 -119
- package/dist/store/types.js +0 -1
- package/dist/streams/control.d.ts +0 -7
- package/dist/streams/control.js +0 -13
- package/dist/streams/types.d.ts +2 -20
- package/dist/streams/types.js +1 -1
- package/dist/tenant/advance.d.ts +3 -3
- package/dist/tenant/advance.js +10 -7
- package/dist/tenant/auth.d.ts +24 -15
- package/dist/tenant/auth.js +44 -55
- package/dist/tenant/cause.d.ts +3 -3
- package/dist/tenant/cause.js +1 -1
- package/dist/tenant/commands.d.ts +2 -18
- package/dist/tenant/commands.js +38 -179
- package/dist/tenant/context.d.ts +33 -45
- package/dist/tenant/definition-files.d.ts +24 -0
- package/dist/tenant/definition-files.js +103 -0
- package/dist/tenant/effects.d.ts +20 -18
- package/dist/tenant/effects.js +59 -133
- package/dist/tenant/ephemeral.d.ts +21 -8
- package/dist/tenant/ephemeral.js +11 -12
- package/dist/tenant/host-token.js +5 -5
- package/dist/tenant/identity-file.d.ts +75 -0
- package/dist/tenant/identity-file.js +202 -0
- package/dist/tenant/issuers.d.ts +48 -0
- package/dist/tenant/issuers.js +311 -0
- package/dist/tenant/jwt.js +3 -3
- package/dist/tenant/module.d.ts +1 -4
- package/dist/tenant/module.js +3 -12
- package/dist/tenant/operator-keys.d.ts +44 -0
- package/dist/tenant/operator-keys.js +87 -0
- package/dist/tenant/outbound.d.ts +40 -1
- package/dist/tenant/outbound.js +167 -18
- package/dist/tenant/paths.d.ts +1 -1
- package/dist/tenant/paths.js +1 -3
- package/dist/tenant/principals.d.ts +13 -13
- package/dist/tenant/principals.js +35 -36
- package/dist/tenant/run-token.js +6 -6
- package/dist/tenant/runtime.d.ts +5 -3
- package/dist/tenant/runtime.js +29 -15
- package/dist/tenant/sandboxes.d.ts +9 -6
- package/dist/tenant/sandboxes.js +16 -5
- package/dist/tenant/scheduler.d.ts +1 -5
- package/dist/tenant/scheduler.js +2 -5
- package/dist/tenant/session-streams.d.ts +8 -11
- package/dist/tenant/session-streams.js +48 -32
- package/dist/tenant/session.d.ts +8 -41
- package/dist/tenant/session.js +0 -37
- package/dist/tenant/sessions.d.ts +2 -2
- package/dist/tenant/sessions.js +17 -9
- package/dist/tenant/signing-keys.d.ts +3 -2
- package/dist/tenant/signing-keys.js +5 -5
- package/dist/tenant/skill-tool.d.ts +20 -0
- package/dist/tenant/skill-tool.js +72 -0
- package/dist/tenant/slim.d.ts +1 -1
- package/dist/tenant/slim.js +1 -1
- package/dist/tenant/status.js +7 -12
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +7 -3
- package/dist/tenant/streams.d.ts +3 -11
- package/dist/tenant/streams.js +2 -29
- package/dist/tenant/sweep.js +0 -5
- package/dist/tenant/transcript.d.ts +2 -2
- package/dist/tenant/transcript.js +2 -2
- package/dist/tenant/types.d.ts +33 -19
- package/dist/tenant/worker.d.ts +1 -3
- package/dist/tenant/worker.js +3 -8
- package/dist/vault/crypto.d.ts +7 -0
- package/dist/vault/crypto.js +16 -0
- package/dist/vault/oauth.d.ts +55 -0
- package/dist/vault/oauth.js +186 -0
- package/dist/vault/service.d.ts +54 -7
- package/dist/vault/service.js +261 -17
- package/dist/vault/sources.d.ts +60 -0
- package/dist/vault/sources.js +196 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -4
- package/dist/admin-openapi.json +0 -604
- package/dist/api/http/routes/actions.d.ts +0 -8
- package/dist/api/http/routes/actions.js +0 -76
- package/dist/api/http/routes/endpoints.d.ts +0 -7
- package/dist/api/http/routes/endpoints.js +0 -100
- package/dist/host/admin-api.d.ts +0 -23
- package/dist/host/admin-api.js +0 -74
- package/dist/host/cors.d.ts +0 -27
- package/dist/host/cors.js +0 -64
- package/dist/mcp/stdio.d.ts +0 -17
- package/dist/mcp/stdio.js +0 -82
- package/dist/tenant/access-policy.d.ts +0 -30
- package/dist/tenant/access-policy.js +0 -52
- package/dist/tenant/actions.d.ts +0 -14
- package/dist/tenant/actions.js +0 -29
- package/dist/tenant/browser.d.ts +0 -15
- package/dist/tenant/browser.js +0 -39
- package/dist/tenant/delivery-token.d.ts +0 -41
- package/dist/tenant/delivery-token.js +0 -104
- package/dist/tenant/delivery.d.ts +0 -62
- package/dist/tenant/delivery.js +0 -481
- package/dist/tenant/endpoints.d.ts +0 -67
- package/dist/tenant/endpoints.js +0 -90
- package/dist/tenant/subject-limits.d.ts +0 -16
- package/dist/tenant/subject-limits.js +0 -45
- package/dist/tenant/tokens.d.ts +0 -14
- package/dist/tenant/tokens.js +0 -187
package/dist/tenant/auth.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { SCOPES_HEADER, SUBJECT_HEADER, } from "@nylorun/core/compatibility";
|
|
2
|
-
import {
|
|
2
|
+
import { parseSubjectHeaders } from "@nylorun/core/contracts";
|
|
3
3
|
import { hashToken } from "../core/bearer.js";
|
|
4
4
|
import { fail, failOpaque } from "./http.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { tokenType } from "./jwt.js";
|
|
8
|
-
import { readPolicy } from "./access-policy.js";
|
|
5
|
+
import { verifyIssuerToken } from "./issuers.js";
|
|
6
|
+
import { looksLikeToken } from "./jwt.js";
|
|
9
7
|
const SUBJECT_INVALID = { code: "subject_invalid" };
|
|
10
8
|
/** One header's value, or `undefined`; a header sent twice is refused. */
|
|
11
9
|
function singleHeader(request, name) {
|
|
@@ -18,42 +16,31 @@ function singleHeader(request, name) {
|
|
|
18
16
|
return fail(400, `${name} must be sent once`, SUBJECT_INVALID);
|
|
19
17
|
return values[0];
|
|
20
18
|
}
|
|
21
|
-
|
|
19
|
+
/** Every route that does not say otherwise is a Runtime API route. */
|
|
20
|
+
export const RUNTIME_KEYS = { application: true, management: false };
|
|
21
|
+
export async function authenticate(ctx, request, keys = RUNTIME_KEYS) {
|
|
22
22
|
const header = request.headers.authorization;
|
|
23
23
|
const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
|
|
24
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
|
-
}
|
|
35
25
|
ctx.config.logger.warn("credential rejected", {
|
|
36
26
|
reason: "missing_bearer",
|
|
37
27
|
});
|
|
38
28
|
return failOpaque();
|
|
39
29
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const scope = await
|
|
30
|
+
// A trusted issuer's token (Host feature `trusted-issuers`), from a server or a browser: its
|
|
31
|
+
// unverified `iss` names an issuer of the identity file, and only that issuer verifies it.
|
|
32
|
+
const issuer = ctx.config.issuers?.claimed(token);
|
|
33
|
+
if (issuer) {
|
|
34
|
+
const scope = await verifyIssuerToken(ctx, issuer, token);
|
|
45
35
|
if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
|
|
46
36
|
singleHeader(request, SCOPES_HEADER) !== undefined)
|
|
47
|
-
fail(403, "
|
|
37
|
+
fail(403, "An issuer token cannot act for another subject");
|
|
48
38
|
return scope;
|
|
49
39
|
}
|
|
40
|
+
// Any other JWT: no issuer of this Host signed it (subject tokens are gone, protocol 7).
|
|
50
41
|
if (looksLikeToken(token)) {
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
42
|
+
ctx.config.logger.warn("credential rejected", { reason: "token_unknown_issuer" });
|
|
43
|
+
return failOpaque();
|
|
57
44
|
}
|
|
58
45
|
// Application keys never come from a browser or a shipped app: refused before they are even
|
|
59
46
|
// looked up.
|
|
@@ -61,10 +48,6 @@ export async function authenticate(ctx, request, client) {
|
|
|
61
48
|
fail(403, "Application keys are not accepted from browsers", {
|
|
62
49
|
code: "origin_rejected",
|
|
63
50
|
});
|
|
64
|
-
if (client)
|
|
65
|
-
fail(400, "Nylorun-Key is for browser and mobile clients, not with a Tenant key", {
|
|
66
|
-
code: "invalid_request",
|
|
67
|
-
});
|
|
68
51
|
const tokenHash = hashToken(token);
|
|
69
52
|
const principal = await ctx.store.tx((t) => t.principalByTokenHash(tokenHash));
|
|
70
53
|
if (!principal) {
|
|
@@ -76,6 +59,22 @@ export async function authenticate(ctx, request, client) {
|
|
|
76
59
|
// Read only after authentication, so an unknown caller sees the opaque 404 either way.
|
|
77
60
|
const subject = singleHeader(request, SUBJECT_HEADER);
|
|
78
61
|
const scopes = singleHeader(request, SCOPES_HEADER);
|
|
62
|
+
// A management key acts as itself on the Management API; Studio's key does too on a route
|
|
63
|
+
// that takes no application key. Neither acts for a subject there.
|
|
64
|
+
const asManagement = principal.role === "management" || (principal.role === "studio" && !keys.application);
|
|
65
|
+
if (asManagement) {
|
|
66
|
+
if (!keys.management)
|
|
67
|
+
fail(403, "A management key reaches only the Management API (/v1/tenant/*)", {
|
|
68
|
+
code: "key_role_mismatch",
|
|
69
|
+
});
|
|
70
|
+
if (subject !== undefined || scopes !== undefined)
|
|
71
|
+
fail(403, "A management key acts as itself, never for a subject", SUBJECT_INVALID);
|
|
72
|
+
return { kind: "management", principalId: principal.id };
|
|
73
|
+
}
|
|
74
|
+
if (!keys.application)
|
|
75
|
+
fail(403, "This is the Management API: it takes a management key, not an application key", {
|
|
76
|
+
code: "key_role_mismatch",
|
|
77
|
+
});
|
|
79
78
|
if (subject === undefined) {
|
|
80
79
|
if (scopes !== undefined)
|
|
81
80
|
fail(400, `${SCOPES_HEADER} requires ${SUBJECT_HEADER}`, SUBJECT_INVALID);
|
|
@@ -92,13 +91,11 @@ export async function authenticate(ctx, request, client) {
|
|
|
92
91
|
};
|
|
93
92
|
}
|
|
94
93
|
/**
|
|
95
|
-
* A subject
|
|
96
|
-
*
|
|
94
|
+
* A subject or a token caller reaches a route only with one of the scopes it declares:
|
|
95
|
+
* `403 scope_required`, decided from the route alone before anything is read.
|
|
97
96
|
*/
|
|
98
97
|
export function requireScopes(scope, access) {
|
|
99
|
-
if (scope.kind !== "subject" &&
|
|
100
|
-
scope.kind !== "token" &&
|
|
101
|
-
scope.kind !== "publishable")
|
|
98
|
+
if (scope.kind !== "subject" && scope.kind !== "token")
|
|
102
99
|
return;
|
|
103
100
|
if (access === "any")
|
|
104
101
|
return;
|
|
@@ -107,7 +104,8 @@ export function requireScopes(scope, access) {
|
|
|
107
104
|
code: "scope_required",
|
|
108
105
|
details: { scopes: [] },
|
|
109
106
|
});
|
|
110
|
-
|
|
107
|
+
const held = scope.scopes;
|
|
108
|
+
if (!access.some((name) => held.has(name)))
|
|
111
109
|
fail(403, `Scope ${access.join(" or ")} required`, {
|
|
112
110
|
code: "scope_required",
|
|
113
111
|
details: { scopes: access },
|
|
@@ -117,6 +115,9 @@ export function accessOf(scope) {
|
|
|
117
115
|
switch (scope.kind) {
|
|
118
116
|
case "application":
|
|
119
117
|
return undefined;
|
|
118
|
+
// A management key reaches the Management API only: no session is its.
|
|
119
|
+
case "management":
|
|
120
|
+
return fail(404, "Not found");
|
|
120
121
|
case "subject":
|
|
121
122
|
return { owner: scope.subject };
|
|
122
123
|
case "token":
|
|
@@ -124,21 +125,16 @@ export function accessOf(scope) {
|
|
|
124
125
|
owner: scope.subject,
|
|
125
126
|
...(scope.agents === "*" ? {} : { agents: scope.agents }),
|
|
126
127
|
};
|
|
127
|
-
// A
|
|
128
|
-
case "publishable":
|
|
129
|
-
// Nor does a request with no credential, on a route that serves public data.
|
|
128
|
+
// A request with no credential, on a route that serves public data, owns nothing.
|
|
130
129
|
case "anonymous":
|
|
131
130
|
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
131
|
default: {
|
|
136
132
|
const unknown = scope;
|
|
137
133
|
return fail(404, `Unknown credential ${String(unknown.kind)}`);
|
|
138
134
|
}
|
|
139
135
|
}
|
|
140
136
|
}
|
|
141
|
-
/** The subject whose sessions
|
|
137
|
+
/** The subject whose sessions the request is limited to, if it acts for one. */
|
|
142
138
|
export function ownerOf(scope) {
|
|
143
139
|
return accessOf(scope)?.owner;
|
|
144
140
|
}
|
|
@@ -148,16 +144,9 @@ export function requirePrincipal(scope) {
|
|
|
148
144
|
return scope.principalId;
|
|
149
145
|
return fail(403, "Application credential required");
|
|
150
146
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
*/
|
|
155
|
-
export function scoped(scope, action) {
|
|
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");
|
|
147
|
+
/** True when a token caller may reach `agentId` (its issuer's allowlist). Other callers always may. */
|
|
148
|
+
export function mayUseAgent(scope, agentId) {
|
|
149
|
+
return scope.kind !== "token" || scope.agents === "*" || scope.agents.has(agentId);
|
|
161
150
|
}
|
|
162
151
|
export function requireApplication(scope) {
|
|
163
152
|
if (scope.kind === "application")
|
package/dist/tenant/cause.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Why the Host's Tenant could not be opened (tenancy.md §5). A cause is about the Tenant's
|
|
3
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
|
-
*
|
|
4
|
+
* database has an old layout): the Host fails readiness, names the cause in the logs (and
|
|
5
|
+
* `nylorun-operate status` reads it from the database), and answers Tenant requests with the opaque 404 until it is
|
|
6
6
|
* repaired and the Host restarted. A failure outside the Tenant (Postgres unreachable) is not
|
|
7
7
|
* a cause: it is `TenantUnavailableError`, and the next request tries again.
|
|
8
8
|
*/
|
|
@@ -12,7 +12,7 @@ export type { TenantCause };
|
|
|
12
12
|
export declare class TenantOpenError extends Error {
|
|
13
13
|
readonly code: TenantCause["code"];
|
|
14
14
|
readonly repair: string;
|
|
15
|
-
/** The Tenant, when it was read before opening failed
|
|
15
|
+
/** The Tenant, when it was read before opening failed. */
|
|
16
16
|
envelope?: TenantEnvelope;
|
|
17
17
|
constructor(fields: TenantCause);
|
|
18
18
|
toCause(): TenantCause;
|
package/dist/tenant/cause.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export class TenantOpenError extends Error {
|
|
3
3
|
code;
|
|
4
4
|
repair;
|
|
5
|
-
/** The Tenant, when it was read before opening failed
|
|
5
|
+
/** The Tenant, when it was read before opening failed. */
|
|
6
6
|
envelope;
|
|
7
7
|
constructor(fields) {
|
|
8
8
|
super(fields.message);
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type
|
|
3
|
-
import { type AuthScope, type Session, type TenantContext } from "./context.js";
|
|
4
|
-
/** `outcome`, or a failed one when a tool's output does not match its stored output schema. */
|
|
5
|
-
export declare function acceptedOutcome(action: Action, outcome: ActionOutcome): ActionOutcome;
|
|
1
|
+
import type { SessionCommand } from "@nylorun/core/contracts";
|
|
2
|
+
import { type AuthScope, type TenantContext } from "./context.js";
|
|
6
3
|
export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope): Promise<unknown>;
|
|
7
|
-
/**
|
|
8
|
-
* Completes a delivered Action with `outcome`, in the caller's transaction, which
|
|
9
|
-
* holds the lock of the Action's session `s`: the Action and its effect, the
|
|
10
|
-
* `action.completed` event (and `loop.verified` or `loop.decided`), the wake that resumes the
|
|
11
|
-
* turn, and queued workflow effects. The Action deliverer and the background-result callback
|
|
12
|
-
* both record outcomes here. It sets `s.status`; the caller writes `s`.
|
|
13
|
-
*/
|
|
14
|
-
export declare function recordActionOutcome(t: Tx, ctx: TenantContext, s: Session, action: Action, received: ActionOutcome, options?: {
|
|
15
|
-
requestId?: string;
|
|
16
|
-
}): Promise<{
|
|
17
|
-
event: LiveEvent;
|
|
18
|
-
receipt: Record<string, unknown>;
|
|
19
|
-
}>;
|
package/dist/tenant/commands.js
CHANGED
|
@@ -7,81 +7,26 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Wakes (`ctx.wake`, architecture §12.3) carry the command type as the reason and a dedupe
|
|
9
9
|
* key naming the cause: `<type>:<turnId>:<segment>` for `message`, `approve` and `respond`
|
|
10
|
-
* (every accepted one writes a new checkpoint segment)
|
|
11
|
-
*
|
|
12
|
-
* `action_result:<turnId>:<actionId>:<generation>`.
|
|
10
|
+
* (every accepted one writes a new checkpoint segment), except that a workflow's `approve` and
|
|
11
|
+
* `respond` name `<type>:<turnId>:<interactionId>`: a flow resumes in the same segment.
|
|
13
12
|
*
|
|
14
13
|
* Cancel commits `cancelled` first; the engine host sees it before its next effect and before
|
|
15
14
|
* settlement on any Worker. It then aborts an advance running on this process
|
|
16
15
|
* (`ctx.abortLocal`); reaching an advance on another process is the control stream's job.
|
|
17
16
|
*/
|
|
18
17
|
import { randomUUID } from "node:crypto";
|
|
19
|
-
import { createDurableCheckpoint, createFlowCheckpoint, } from "@nylorun/harness/run";
|
|
20
|
-
import {
|
|
21
|
-
import { commandKey, fenceWorkflowActions, foreignInteractionConflict, isWorkflowManifest, planCancelCascade, wakeForQueuedEffects, wakeLinkedWorkflow, } from "../core/flow-host.js";
|
|
18
|
+
import { createDurableCheckpoint, createFlowCheckpoint, resumeFlowCheckpoint, } from "@nylorun/harness/run";
|
|
19
|
+
import { commandKey, cancelQueuedEffects, flowInteractionOf, foreignInteractionConflict, isWorkflowManifest, planCancelCascade, wakeLinkedWorkflow, } from "../core/flow-host.js";
|
|
22
20
|
import { resolveMessageManifest } from "../core/turn-manifest.js";
|
|
23
21
|
import { canonical } from "../store/canonical.js";
|
|
24
22
|
import { lockedSession, } from "./context.js";
|
|
25
23
|
import { fail } from "./http.js";
|
|
26
24
|
import { accessOf } from "./auth.js";
|
|
27
|
-
import { chargeTurn } from "./subject-limits.js";
|
|
28
25
|
import { checkSandboxTurn } from "./sandboxes.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { toolIds } from "./transcript.js";
|
|
26
|
+
import { rebaseSessionState, turnManifestOf, variantStore } from "./session.js";
|
|
27
|
+
import { signalSessionCancel } from "./streams.js";
|
|
32
28
|
import { slimModelEffects } from "./slim.js";
|
|
33
29
|
import { resolveMessageParts } from "../artifacts/parts.js";
|
|
34
|
-
/** `outcome`, or a failed one when a tool's output does not match its stored output schema. */
|
|
35
|
-
export function acceptedOutcome(action, outcome) {
|
|
36
|
-
if (action.kind !== "tool" || !action.outputSchema)
|
|
37
|
-
return outcome;
|
|
38
|
-
const value = outcome.value;
|
|
39
|
-
// Only a result carries output: failures, denials, interactions and deferrals pass as sent.
|
|
40
|
-
if (!isResultToolValue(value))
|
|
41
|
-
return outcome;
|
|
42
|
-
// Endpoints wrap successful tool output as `{ kind: "completed", output }`.
|
|
43
|
-
// Validate the tool payload, not the outcome envelope.
|
|
44
|
-
const candidate = completedToolOutput(value);
|
|
45
|
-
let matches = false;
|
|
46
|
-
try {
|
|
47
|
-
matches = schemaFromJSON(action.outputSchema).validate(candidate).ok;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
matches = false;
|
|
51
|
-
}
|
|
52
|
-
if (matches)
|
|
53
|
-
return outcome;
|
|
54
|
-
return {
|
|
55
|
-
...outcome,
|
|
56
|
-
value: {
|
|
57
|
-
kind: "failed",
|
|
58
|
-
code: "tool.invalid-output",
|
|
59
|
-
message: "Tool result does not match the output schema stored on the action",
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
function completedToolOutput(value) {
|
|
64
|
-
if (value &&
|
|
65
|
-
typeof value === "object" &&
|
|
66
|
-
!Array.isArray(value) &&
|
|
67
|
-
value.kind === "completed" &&
|
|
68
|
-
"output" in value) {
|
|
69
|
-
return value.output;
|
|
70
|
-
}
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
const NON_RESULT_KINDS = new Set([
|
|
74
|
-
"failed",
|
|
75
|
-
"denied",
|
|
76
|
-
"interaction-required",
|
|
77
|
-
"deferred",
|
|
78
|
-
]);
|
|
79
|
-
function isResultToolValue(value) {
|
|
80
|
-
return !(!!value &&
|
|
81
|
-
typeof value === "object" &&
|
|
82
|
-
!Array.isArray(value) &&
|
|
83
|
-
NON_RESULT_KINDS.has(value.kind));
|
|
84
|
-
}
|
|
85
30
|
/** A command's identity for idempotency: everything but the request id. */
|
|
86
31
|
const semantic = (value) => {
|
|
87
32
|
const { requestId: _, ...body } = value;
|
|
@@ -97,7 +42,7 @@ export async function command(ctx, id, input, scope) {
|
|
|
97
42
|
if (scope.kind === "token" &&
|
|
98
43
|
command.type === "message" &&
|
|
99
44
|
command.manifest !== undefined)
|
|
100
|
-
fail(403, "A
|
|
45
|
+
fail(403, "A token caller cannot send message.manifest", {
|
|
101
46
|
code: "scope_required",
|
|
102
47
|
});
|
|
103
48
|
const key = commandKey(id, command.idempotencyKey);
|
|
@@ -121,28 +66,12 @@ export async function command(ctx, id, input, scope) {
|
|
|
121
66
|
turnId: cancelledTurnId,
|
|
122
67
|
});
|
|
123
68
|
s.status = "cancelled";
|
|
124
|
-
if (workflowCancel)
|
|
125
|
-
await
|
|
69
|
+
if (workflowCancel)
|
|
70
|
+
await cancelQueuedEffects({
|
|
126
71
|
t,
|
|
127
72
|
workflowSessionId: id,
|
|
128
73
|
turnId: cancelledTurnId,
|
|
129
74
|
});
|
|
130
|
-
}
|
|
131
|
-
else if (cancelledTurnId !== null) {
|
|
132
|
-
for (const a of await t.actionsForSession(id, {
|
|
133
|
-
turnId: cancelledTurnId,
|
|
134
|
-
statuses: ["pending", "delivering"],
|
|
135
|
-
})) {
|
|
136
|
-
// Delivered work may already have an external effect. Preserve it for reconciliation.
|
|
137
|
-
a.status = a.status === "pending" ? "cancelled" : "uncertain";
|
|
138
|
-
await t.put("actions", a.actionId, a);
|
|
139
|
-
const effect = await t.get("effects", a.actionId);
|
|
140
|
-
if (effect) {
|
|
141
|
-
effect.status = a.status;
|
|
142
|
-
await t.put("effects", a.actionId, effect);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
75
|
if (cancelledTurnId !== null)
|
|
147
76
|
for (const effect of await t.effectsForTurn(id, cancelledTurnId, ["invoking"])) {
|
|
148
77
|
effect.status = "uncertain";
|
|
@@ -180,12 +109,9 @@ export async function command(ctx, id, input, scope) {
|
|
|
180
109
|
if (command.type === "message") {
|
|
181
110
|
if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
|
|
182
111
|
fail(409, "Session has active or unresolved work");
|
|
183
|
-
// Every turn start on a sandbox: it exists, the token's
|
|
184
|
-
// session's turn holds it.
|
|
112
|
+
// Every turn start on a sandbox: it exists, the token's grants reach it, and no other
|
|
113
|
+
// session's turn holds it.
|
|
185
114
|
await checkSandboxTurn(t, s, scope, ctx);
|
|
186
|
-
// After the replay check above, so a retried message is never charged twice.
|
|
187
|
-
if (scope.kind === "token" && scope.limits)
|
|
188
|
-
await chargeTurn(t, scope.subject, scope.limits);
|
|
189
115
|
s.turnStartState = s.state;
|
|
190
116
|
// A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
|
|
191
117
|
if (s.history?.snapshot !== undefined)
|
|
@@ -239,6 +165,28 @@ export async function command(ctx, id, input, scope) {
|
|
|
239
165
|
}
|
|
240
166
|
}
|
|
241
167
|
}
|
|
168
|
+
else if (isWorkflowManifest(s.manifest)) {
|
|
169
|
+
// Approvals owned by linked agent sessions must be answered there (WF-R52).
|
|
170
|
+
const conflict = await foreignInteractionConflict({
|
|
171
|
+
t,
|
|
172
|
+
workflowSessionId: id,
|
|
173
|
+
interactionId: command.interactionId,
|
|
174
|
+
});
|
|
175
|
+
if (conflict)
|
|
176
|
+
fail(409, conflict.message);
|
|
177
|
+
if (s.status !== "paused" || !s.checkpoint || !s.activeTurnId)
|
|
178
|
+
fail(409, "Session is not awaiting a response");
|
|
179
|
+
// The flow's own interactions: a tool node that asked (`ctx.approve`, `ctx.ask`).
|
|
180
|
+
const asked = flowInteractionOf(s.waits, id, command.interactionId);
|
|
181
|
+
if (!asked)
|
|
182
|
+
fail(409, "Unknown interaction");
|
|
183
|
+
else if ((command.type === "approve") !== (asked.kind === "approval"))
|
|
184
|
+
fail(409, "Interaction response kind does not match the saved request");
|
|
185
|
+
// Same segment: the flow replays its journal and runs the tool again with the answer.
|
|
186
|
+
s.checkpoint = resumeFlowCheckpoint(s.checkpoint, command.interactionId, command.type === "approve"
|
|
187
|
+
? { kind: "approval", approved: command.approved }
|
|
188
|
+
: { kind: "response", value: command.value });
|
|
189
|
+
}
|
|
242
190
|
else {
|
|
243
191
|
if (s.status !== "paused" || !s.state || !s.activeTurnId)
|
|
244
192
|
fail(409, "Session is not awaiting a response");
|
|
@@ -259,19 +207,6 @@ export async function command(ctx, id, input, scope) {
|
|
|
259
207
|
interactionId: command.interactionId,
|
|
260
208
|
value: command.value,
|
|
261
209
|
};
|
|
262
|
-
if (isWorkflowManifest(s.manifest)) {
|
|
263
|
-
// Approvals owned by linked agent sessions must be answered there (WF-R52).
|
|
264
|
-
const conflict = await foreignInteractionConflict({
|
|
265
|
-
t,
|
|
266
|
-
workflowSessionId: id,
|
|
267
|
-
interactionId: command.interactionId,
|
|
268
|
-
});
|
|
269
|
-
if (conflict)
|
|
270
|
-
fail(409, conflict.message);
|
|
271
|
-
// Workflow-owned interactions (tool-node / verify) resume on this session once
|
|
272
|
-
// the flow engine supports pause segments; tracer root Loop has none yet.
|
|
273
|
-
fail(409, "Workflow sessions do not accept approve/respond on the root without a pending interaction");
|
|
274
|
-
}
|
|
275
210
|
s.checkpoint = createDurableCheckpoint({
|
|
276
211
|
manifest: turnManifestOf(s),
|
|
277
212
|
sessionId: id,
|
|
@@ -284,9 +219,13 @@ export async function command(ctx, id, input, scope) {
|
|
|
284
219
|
}
|
|
285
220
|
s.status = "runnable";
|
|
286
221
|
s.waits = undefined;
|
|
222
|
+
// A flow answers each interaction in the same segment: the interaction names the cause.
|
|
223
|
+
const cause = command.type !== "message" && isWorkflowManifest(s.manifest)
|
|
224
|
+
? command.interactionId
|
|
225
|
+
: s.checkpoint.segment;
|
|
287
226
|
const commandWake = {
|
|
288
227
|
reason: command.type,
|
|
289
|
-
dedupeKey: `${command.type}:${s.activeTurnId}:${
|
|
228
|
+
dedupeKey: `${command.type}:${s.activeTurnId}:${cause}`,
|
|
290
229
|
};
|
|
291
230
|
t.afterCommit(() => ctx.wake(id, commandWake));
|
|
292
231
|
event = await t.event(id, s.activeTurnId, `command.${command.type}`, command);
|
|
@@ -330,83 +269,3 @@ export async function command(ctx, id, input, scope) {
|
|
|
330
269
|
}
|
|
331
270
|
return response;
|
|
332
271
|
}
|
|
333
|
-
/**
|
|
334
|
-
* Completes a delivered Action with `outcome`, in the caller's transaction, which
|
|
335
|
-
* holds the lock of the Action's session `s`: the Action and its effect, the
|
|
336
|
-
* `action.completed` event (and `loop.verified` or `loop.decided`), the wake that resumes the
|
|
337
|
-
* turn, and queued workflow effects. The Action deliverer and the background-result callback
|
|
338
|
-
* both record outcomes here. It sets `s.status`; the caller writes `s`.
|
|
339
|
-
*/
|
|
340
|
-
export async function recordActionOutcome(t, ctx, s, action, received, options = {}) {
|
|
341
|
-
const outcome = acceptedOutcome(action, received);
|
|
342
|
-
const prior = await t.get("effects", action.actionId);
|
|
343
|
-
action.status = "completed";
|
|
344
|
-
await t.put("actions", action.actionId, action);
|
|
345
|
-
prior.status = "completed";
|
|
346
|
-
prior.outcome = outcome;
|
|
347
|
-
s.status = "runnable";
|
|
348
|
-
const resultWake = {
|
|
349
|
-
reason: "action_result",
|
|
350
|
-
dedupeKey: `action_result:${action.turnId}:${action.actionId}:${action.generation}`,
|
|
351
|
-
};
|
|
352
|
-
t.afterCommit(() => ctx.wake(s.id, resultWake));
|
|
353
|
-
// A run held while the Action was pending goes on with it in the same lease (F6.2): here, or
|
|
354
|
-
// on the process whose harness holds it. The wake finds the session settled, or resumes it
|
|
355
|
-
// by replay when the run had already ended.
|
|
356
|
-
t.afterCommit(() => {
|
|
357
|
-
if (ctx.harness.holds(s.id))
|
|
358
|
-
ctx.harness.resolved(s.id, action.actionId, outcome);
|
|
359
|
-
else
|
|
360
|
-
signalActionOutcome(ctx, s.id, action.actionId);
|
|
361
|
-
});
|
|
362
|
-
const event = await t.event(s.id, s.activeTurnId, "action.completed", {
|
|
363
|
-
actionId: action.actionId,
|
|
364
|
-
...actionTarget(action),
|
|
365
|
-
kind: action.kind,
|
|
366
|
-
...(action.kind === "tool" ? toolIds(action.context) : {}),
|
|
367
|
-
result: outcome.value,
|
|
368
|
-
});
|
|
369
|
-
if (action.kind === "verify") {
|
|
370
|
-
const verdict = outcome.value;
|
|
371
|
-
if (verdict?.kind !== "failed") {
|
|
372
|
-
await t.event(s.id, s.activeTurnId, "loop.verified", {
|
|
373
|
-
path: String(action.context?.loopPath ?? action.path ?? ""),
|
|
374
|
-
n: Number(action.context?.n ?? 1),
|
|
375
|
-
pass: Boolean(verdict?.pass),
|
|
376
|
-
...(verdict?.feedback !== undefined
|
|
377
|
-
? { feedback: verdict.feedback }
|
|
378
|
-
: {}),
|
|
379
|
-
...(verdict?.data !== undefined ? { data: verdict.data } : {}),
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else if (action.kind === "fn" && action.context?.role === "decide") {
|
|
384
|
-
const decision = outcome.value;
|
|
385
|
-
await t.event(s.id, s.activeTurnId, "loop.decided", {
|
|
386
|
-
path: String(action.context?.loopPath ?? action.path ?? ""),
|
|
387
|
-
n: Number(action.context?.n ?? 1),
|
|
388
|
-
next: "output" in decision && !("input" in decision)
|
|
389
|
-
? "output"
|
|
390
|
-
: "input",
|
|
391
|
-
patched: Boolean(decision.agent),
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
const receipt = {
|
|
395
|
-
status: "accepted",
|
|
396
|
-
turnId: s.activeTurnId,
|
|
397
|
-
cursor: event.cursor,
|
|
398
|
-
...(options.requestId === undefined ? {} : { requestId: options.requestId }),
|
|
399
|
-
};
|
|
400
|
-
prior.receipt = receipt;
|
|
401
|
-
await t.put("effects", action.actionId, prior);
|
|
402
|
-
if (isWorkflowManifest(s.manifest) && s.activeTurnId) {
|
|
403
|
-
await wakeForQueuedEffects({
|
|
404
|
-
t,
|
|
405
|
-
workflowSessionId: s.id,
|
|
406
|
-
turnId: s.activeTurnId,
|
|
407
|
-
limits: ctx.flowLimits,
|
|
408
|
-
schedule: ctx.wake,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
return { event, receipt };
|
|
412
|
-
}
|