@nylorun/runtime 0.11.0-beta → 0.13.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 +71 -0
- package/dist/core/flow-host.js +3 -0
- package/dist/core/sandbox-routes.d.ts +5 -1
- package/dist/core/sandbox-routes.js +4 -2
- package/dist/host/create-host.js +3 -0
- package/dist/store/memory.js +3 -1
- package/dist/store/postgres/migrations/002_session_owner.d.ts +6 -0
- package/dist/store/postgres/migrations/002_session_owner.js +13 -0
- package/dist/store/postgres/migrations/index.js +2 -1
- package/dist/store/postgres/store.js +9 -2
- package/dist/store/postgres/tenants.js +9 -2
- package/dist/store/types.d.ts +2 -0
- package/dist/tenant/auth.d.ts +23 -2
- package/dist/tenant/auth.js +140 -5
- package/dist/tenant/commands.js +30 -23
- package/dist/tenant/context.d.ts +11 -4
- package/dist/tenant/context.js +15 -7
- package/dist/tenant/effects.js +87 -13
- package/dist/tenant/http.d.ts +8 -2
- package/dist/tenant/http.js +5 -3
- package/dist/tenant/module.js +3 -0
- package/dist/tenant/principals.d.ts +4 -1
- package/dist/tenant/principals.js +22 -3
- package/dist/tenant/routes-tenant.js +13 -2
- package/dist/tenant/routes.js +16 -7
- package/dist/tenant/sessions.d.ts +9 -3
- package/dist/tenant/sessions.js +20 -6
- package/dist/tenant/transcript.d.ts +104 -0
- package/dist/tenant/transcript.js +58 -0
- package/dist/tenant/types.d.ts +6 -0
- package/dist/vault/service.d.ts +9 -2
- package/dist/vault/service.js +18 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -4
package/dist/tenant/effects.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { embeddedAgent, flowDelegateManifest, hashManifest, isWorkflowManifestV2, } from "@nylorun/core/define";
|
|
2
|
+
import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowEffect, isFlowToolEffect, isWorkflowManifest, linkedMessageKey, linkedTurnEnd, } from "../core/flow-host.js";
|
|
2
3
|
import { mayDispatchMore } from "../core/limits.js";
|
|
3
4
|
import { canonical } from "../store/canonical.js";
|
|
4
5
|
import { isOwnershipLost } from "../store/ownership.js";
|
|
@@ -10,8 +11,9 @@ import { sandboxCapabilityOf } from "../sandbox/manager.js";
|
|
|
10
11
|
import { owningSandboxSessionId, sandboxSpecOf } from "../sandbox/share.js";
|
|
11
12
|
import { loadSession, ownedSession, sandboxLookup, sessionOf, } from "./context.js";
|
|
12
13
|
import { fail } from "./http.js";
|
|
13
|
-
import { actionTarget, linkedAgentOutput, manifestFor, mcpToolOf, pinnedTool, } from "./session.js";
|
|
14
|
+
import { actionTarget, linkedAgentOutput, manifestFor, mcpToolOf, pinnedTool, turnManifestOf, } from "./session.js";
|
|
14
15
|
import { command } from "./commands.js";
|
|
16
|
+
import { assistantMessage, toolCompleted, toolIds } from "./transcript.js";
|
|
15
17
|
import { abortKind } from "./worker.js";
|
|
16
18
|
export function invokeModel(ctx, request, signal, model) {
|
|
17
19
|
if (model)
|
|
@@ -51,6 +53,20 @@ async function linkedOutcome(t, effect, agent) {
|
|
|
51
53
|
},
|
|
52
54
|
};
|
|
53
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* What an effect's journal row must match on replay. A delegation's context carries only the
|
|
58
|
+
* parent's tool call id, for its events; rows journaled before it existed still match.
|
|
59
|
+
*/
|
|
60
|
+
function requestIdentity(request) {
|
|
61
|
+
if (request.kind !== "delegation")
|
|
62
|
+
return request;
|
|
63
|
+
const { context: _, ...identity } = request;
|
|
64
|
+
return identity;
|
|
65
|
+
}
|
|
66
|
+
function delegationCallId(request) {
|
|
67
|
+
const callId = request.context?.callId;
|
|
68
|
+
return typeof callId === "string" ? { callId } : {};
|
|
69
|
+
}
|
|
54
70
|
export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
55
71
|
const { store } = ctx;
|
|
56
72
|
const journaled = await store.tx(async (t) => {
|
|
@@ -65,7 +81,7 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
65
81
|
});
|
|
66
82
|
const existing = await t.get("effects", request.effectId);
|
|
67
83
|
if (existing) {
|
|
68
|
-
if (canonical(existing.request) !== canonical(request))
|
|
84
|
+
if (canonical(requestIdentity(existing.request)) !== canonical(requestIdentity(request)))
|
|
69
85
|
throw new Error("Effect identity request drift");
|
|
70
86
|
if (existing.status === "completed")
|
|
71
87
|
return resolved({ status: "completed", outcome: existing.outcome });
|
|
@@ -104,7 +120,11 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
104
120
|
outcome,
|
|
105
121
|
});
|
|
106
122
|
const settled = request.effectId.endsWith(":settled");
|
|
107
|
-
await t.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", {
|
|
123
|
+
await t.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", {
|
|
124
|
+
agent: request.agent,
|
|
125
|
+
...delegationCallId(request),
|
|
126
|
+
...request.input,
|
|
127
|
+
});
|
|
108
128
|
return resolved({ status: "completed", outcome });
|
|
109
129
|
}
|
|
110
130
|
const agentManifest = manifestFor(s.manifest, request.agent);
|
|
@@ -166,6 +186,7 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
166
186
|
actionId: action.actionId,
|
|
167
187
|
kind: action.kind,
|
|
168
188
|
...actionTarget(action),
|
|
189
|
+
...(action.kind === "tool" ? toolIds(request.context) : {}),
|
|
169
190
|
input: action.input,
|
|
170
191
|
});
|
|
171
192
|
t.signalWork();
|
|
@@ -198,6 +219,11 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
|
|
|
198
219
|
effect.status = "completed";
|
|
199
220
|
effect.outcome = { value };
|
|
200
221
|
await t.put("effects", request.effectId, effect);
|
|
222
|
+
const transcript = invoke === "model"
|
|
223
|
+
? assistantMessage(request, value)
|
|
224
|
+
: toolCompleted(request, value);
|
|
225
|
+
if (transcript)
|
|
226
|
+
await t.event(s.id, request.turnId, invoke === "model" ? "message.assistant" : "tool.completed", transcript);
|
|
201
227
|
return { status: "completed", outcome: effect.outcome };
|
|
202
228
|
});
|
|
203
229
|
}
|
|
@@ -361,8 +387,7 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
|
|
|
361
387
|
await ownedSession(t, lease, workflow.id);
|
|
362
388
|
if (exists)
|
|
363
389
|
return;
|
|
364
|
-
const definition =
|
|
365
|
-
fail(404, "Definition not found");
|
|
390
|
+
const definition = await leafDefinition(t, workflow, body, request);
|
|
366
391
|
const sandboxOwnerId = sandboxSpecOf(workflow.manifest) || workflow.sandboxOwnerId
|
|
367
392
|
? owningSandboxSessionId(workflow, await sandboxLookup(t, workflow.sandboxOwnerId))
|
|
368
393
|
: undefined;
|
|
@@ -415,13 +440,15 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
|
|
|
415
440
|
return store.tx(async (t) => {
|
|
416
441
|
const s = await ownedSession(t, lease, request.sessionId);
|
|
417
442
|
const agent = await sessionOf(t, agentSessionId);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
443
|
+
// Only a Loop's own agent turns are iterations; a step elsewhere in the flow is not.
|
|
444
|
+
if (typeof request.context.loopPath === "string")
|
|
445
|
+
await t.event(s.id, s.activeTurnId, "loop.iteration", {
|
|
446
|
+
path: request.context.loopPath,
|
|
447
|
+
n,
|
|
448
|
+
sessionId: agentSessionId,
|
|
449
|
+
turnId: accepted.turnId ?? undefined,
|
|
450
|
+
manifestHash: agent.checkpoint?.manifestHash,
|
|
451
|
+
});
|
|
425
452
|
await t.event(s.id, s.activeTurnId, "node.agent", {
|
|
426
453
|
path,
|
|
427
454
|
iterations,
|
|
@@ -537,3 +564,50 @@ export async function authorize(ctx, sessionId, request) {
|
|
|
537
564
|
}
|
|
538
565
|
return result;
|
|
539
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* The definition a flow leaf's session runs. A v2 workflow embeds its leaves, so they
|
|
569
|
+
* come from the workflow's own manifest and can't drift from it; v1 leaves come from the
|
|
570
|
+
* registry by id.
|
|
571
|
+
*/
|
|
572
|
+
async function leafDefinition(t, workflow, body, request) {
|
|
573
|
+
// A flow agent used as a tool: its manifest is inlined in the parent's pinned tool.
|
|
574
|
+
if (request.context.role === "delegate" && !isWorkflowManifest(workflow.manifest)) {
|
|
575
|
+
const flow = flowDelegateManifest(turnManifestOf(workflow), body.agentId);
|
|
576
|
+
if (!flow)
|
|
577
|
+
return fail(404, `Agent '${body.agentId}' is not a flow agent used as a tool`);
|
|
578
|
+
return {
|
|
579
|
+
manifest: flow,
|
|
580
|
+
manifestHash: hashManifest(flow),
|
|
581
|
+
implementationVersion: workflow.implementationVersion,
|
|
582
|
+
pluginRoots: leafPluginRoots(workflow.pluginRoots, body.agentId),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (isWorkflowManifestV2(workflow.manifest)) {
|
|
586
|
+
const leaf = embeddedAgent(workflow.manifest, body.flow ?? [], body.agentId);
|
|
587
|
+
if (!leaf || isWorkflowManifestV2(leaf))
|
|
588
|
+
fail(404, `Agent '${body.agentId}' is not embedded in workflow '${workflow.manifest.id}'`);
|
|
589
|
+
const manifest = leaf;
|
|
590
|
+
return {
|
|
591
|
+
manifest,
|
|
592
|
+
manifestHash: hashManifest(manifest),
|
|
593
|
+
implementationVersion: workflow.implementationVersion,
|
|
594
|
+
pluginRoots: leafPluginRoots(workflow.pluginRoots, body.agentId),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const definition = await t.get("definitions", body.agentId);
|
|
598
|
+
if (!definition)
|
|
599
|
+
fail(404, "Definition not found");
|
|
600
|
+
return definition;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* A v2 workflow keys its leaves' plugin roots `<agentId>/<capability>`, and an agent keys
|
|
604
|
+
* a flow agent it uses as a tool the same way (`<flowId>/<leafId>/<capability>`).
|
|
605
|
+
*/
|
|
606
|
+
function leafPluginRoots(roots, agentId) {
|
|
607
|
+
const own = {};
|
|
608
|
+
const prefix = `${agentId}/`;
|
|
609
|
+
for (const [key, root] of Object.entries(roots ?? {}))
|
|
610
|
+
if (key.startsWith(prefix))
|
|
611
|
+
own[key.slice(prefix.length)] = root;
|
|
612
|
+
return own;
|
|
613
|
+
}
|
package/dist/tenant/http.d.ts
CHANGED
|
@@ -11,11 +11,17 @@ export declare const OPAQUE_NOT_FOUND: {
|
|
|
11
11
|
code: "not_found";
|
|
12
12
|
message: string;
|
|
13
13
|
};
|
|
14
|
+
/** A rejection's stable code and details, for the ones a client acts on (default `request_rejected`). */
|
|
15
|
+
export interface Rejection {
|
|
16
|
+
readonly code?: string;
|
|
17
|
+
readonly details?: unknown;
|
|
18
|
+
}
|
|
14
19
|
export declare class HttpError extends Error {
|
|
15
20
|
readonly status: number;
|
|
16
|
-
|
|
21
|
+
readonly rejection: Rejection;
|
|
22
|
+
constructor(status: number, message: string, rejection?: Rejection);
|
|
17
23
|
}
|
|
18
|
-
export declare const fail: (status: number, message: string) => never;
|
|
24
|
+
export declare const fail: (status: number, message: string, rejection?: Rejection) => never;
|
|
19
25
|
export declare class OpaqueAuthError extends Error {
|
|
20
26
|
readonly status = 404;
|
|
21
27
|
readonly body: {
|
package/dist/tenant/http.js
CHANGED
|
@@ -6,13 +6,15 @@ export const OPAQUE_NOT_FOUND = {
|
|
|
6
6
|
};
|
|
7
7
|
export class HttpError extends Error {
|
|
8
8
|
status;
|
|
9
|
-
|
|
9
|
+
rejection;
|
|
10
|
+
constructor(status, message, rejection = {}) {
|
|
10
11
|
super(message);
|
|
11
12
|
this.status = status;
|
|
13
|
+
this.rejection = rejection;
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
|
-
export const fail = (status, message) => {
|
|
15
|
-
throw new HttpError(status, message);
|
|
16
|
+
export const fail = (status, message, rejection) => {
|
|
17
|
+
throw new HttpError(status, message, rejection);
|
|
16
18
|
};
|
|
17
19
|
export class OpaqueAuthError extends Error {
|
|
18
20
|
status = 404;
|
package/dist/tenant/module.js
CHANGED
|
@@ -152,6 +152,9 @@ export function createTenantModule(options) {
|
|
|
152
152
|
...(input.studioCredentialHash
|
|
153
153
|
? { studioCredentialHash: input.studioCredentialHash }
|
|
154
154
|
: {}),
|
|
155
|
+
...(input.derivedPrincipals?.length
|
|
156
|
+
? { derivedPrincipals: input.derivedPrincipals }
|
|
157
|
+
: {}),
|
|
155
158
|
};
|
|
156
159
|
// The store records its own schema version.
|
|
157
160
|
const envelope = envelopeNow({
|
|
@@ -9,5 +9,8 @@ export declare const STUDIO_PRINCIPAL_ID = "studio";
|
|
|
9
9
|
* Exported for WS-B's `TenantStore.create` (A3).
|
|
10
10
|
*/
|
|
11
11
|
export declare function bootstrapPrincipal(t: Tx, bootstrap: BootstrapPrincipal, now?: Date): Promise<void>;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Whether the stored bootstrap, Studio and derived principals equal `bootstrap`. A retried
|
|
14
|
+
* create names the same derived principals, so each must be stored with its hash.
|
|
15
|
+
*/
|
|
13
16
|
export declare function bootstrapPrincipalMatches(t: Tx, bootstrap: BootstrapPrincipal): Promise<boolean>;
|
|
@@ -22,8 +22,19 @@ export async function bootstrapPrincipal(t, bootstrap, now = new Date()) {
|
|
|
22
22
|
idempotencyKey: null,
|
|
23
23
|
createdAt,
|
|
24
24
|
});
|
|
25
|
+
for (const principal of bootstrap.derivedPrincipals ?? [])
|
|
26
|
+
await t.insertPrincipal({
|
|
27
|
+
id: principal.id,
|
|
28
|
+
role: "application",
|
|
29
|
+
tokenHash: principal.credentialHash,
|
|
30
|
+
idempotencyKey: null,
|
|
31
|
+
createdAt,
|
|
32
|
+
});
|
|
25
33
|
}
|
|
26
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Whether the stored bootstrap, Studio and derived principals equal `bootstrap`. A retried
|
|
36
|
+
* create names the same derived principals, so each must be stored with its hash.
|
|
37
|
+
*/
|
|
27
38
|
export async function bootstrapPrincipalMatches(t, bootstrap) {
|
|
28
39
|
const row = await t.principalById(bootstrap.principalId);
|
|
29
40
|
if (!row ||
|
|
@@ -32,6 +43,14 @@ export async function bootstrapPrincipalMatches(t, bootstrap) {
|
|
|
32
43
|
row.idempotencyKey !== bootstrap.idempotencyKey)
|
|
33
44
|
return false;
|
|
34
45
|
const studio = await t.principalById(STUDIO_PRINCIPAL_ID);
|
|
35
|
-
|
|
36
|
-
bootstrap.studioCredentialHash)
|
|
46
|
+
if ((studio?.role === "application" ? studio.tokenHash : undefined) !==
|
|
47
|
+
bootstrap.studioCredentialHash)
|
|
48
|
+
return false;
|
|
49
|
+
for (const principal of bootstrap.derivedPrincipals ?? []) {
|
|
50
|
+
const stored = await t.principalById(principal.id);
|
|
51
|
+
if (stored?.role !== "application" ||
|
|
52
|
+
stored.tokenHash !== principal.credentialHash)
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
37
56
|
}
|
|
@@ -2,6 +2,7 @@ import { CreateCredentialRequestSchema, CreateVaultRequestSchema, PutHostModelRe
|
|
|
2
2
|
import { hostModelCatalog } from "../model/catalog.js";
|
|
3
3
|
import { resetTenant } from "./reset.js";
|
|
4
4
|
import { buildTenantStatus, seedTenantConfig } from "./status.js";
|
|
5
|
+
import { ownerOf } from "./auth.js";
|
|
5
6
|
import { fail, readBody } from "./http.js";
|
|
6
7
|
import { clearExecutorStreams, clearObservers } from "./live.js";
|
|
7
8
|
import { requestStreamCollection, sessionStreamsAbandoned, streamsStatus, } from "./streams.js";
|
|
@@ -9,7 +10,8 @@ import { clearWork, drain } from "./scheduler.js";
|
|
|
9
10
|
import { usesFixtureModel } from "./model-setting.js";
|
|
10
11
|
export async function dispatchTenant(ctx, scope, method, path, request) {
|
|
11
12
|
const { vault } = ctx;
|
|
12
|
-
|
|
13
|
+
// A subject reaches only the routes `authorize` allowed its scopes.
|
|
14
|
+
if (scope.kind === "executor") {
|
|
13
15
|
await vault.reject(path.join("/"));
|
|
14
16
|
fail(403, "Application credential required");
|
|
15
17
|
}
|
|
@@ -83,22 +85,31 @@ export async function dispatchTenant(ctx, scope, method, path, request) {
|
|
|
83
85
|
}
|
|
84
86
|
export async function dispatchVault(ctx, scope, method, path, url, request) {
|
|
85
87
|
const { vault } = ctx;
|
|
86
|
-
if (scope.kind
|
|
88
|
+
if (scope.kind === "executor") {
|
|
87
89
|
await vault.reject(path.join("/"));
|
|
88
90
|
fail(403, "Application credential required");
|
|
89
91
|
}
|
|
92
|
+
// Acting for a subject: only the subject's own vaults, and never on another's behalf.
|
|
93
|
+
const owner = ownerOf(scope);
|
|
90
94
|
if (path.length === 2 && method === "POST") {
|
|
91
95
|
const body = CreateVaultRequestSchema.parse(await readBody(request));
|
|
96
|
+
if (owner !== undefined && body.ownerUserId !== owner)
|
|
97
|
+
fail(403, "ownerUserId must be the subject");
|
|
92
98
|
return vault.createVault(body);
|
|
93
99
|
}
|
|
94
100
|
if (path.length === 2 && method === "GET") {
|
|
95
101
|
const ownerUserId = url.searchParams.get("ownerUserId") ??
|
|
102
|
+
owner ??
|
|
96
103
|
fail(400, "ownerUserId is required");
|
|
104
|
+
if (owner !== undefined && ownerUserId !== owner)
|
|
105
|
+
fail(403, "ownerUserId must be the subject");
|
|
97
106
|
return { vaults: await vault.listVaults(ownerUserId) };
|
|
98
107
|
}
|
|
99
108
|
const vaultId = path[2];
|
|
100
109
|
if (!vaultId)
|
|
101
110
|
fail(404, "Vault not found");
|
|
111
|
+
if (owner !== undefined)
|
|
112
|
+
await vault.assertOwner(vaultId, owner);
|
|
102
113
|
if (path.length === 3 && method === "GET")
|
|
103
114
|
return vault.getVault(vaultId);
|
|
104
115
|
if (path.length === 3 && method === "DELETE")
|
package/dist/tenant/routes.js
CHANGED
|
@@ -3,7 +3,7 @@ import { VaultError } from "../vault/error.js";
|
|
|
3
3
|
import { SandboxRouteError, handleSessionSandboxTool, } from "../core/sandbox-routes.js";
|
|
4
4
|
import { loadSession, sessionOf } from "./context.js";
|
|
5
5
|
import { HttpError, OpaqueAuthError, fail, readBody, requestAborted, } from "./http.js";
|
|
6
|
-
import { authenticate, requireApplication } from "./auth.js";
|
|
6
|
+
import { authenticate, authorize, ownerOf, requireApplication, requirePrincipal, } from "./auth.js";
|
|
7
7
|
import { command } from "./commands.js";
|
|
8
8
|
import { actionSandboxTool, deleteExecutor, listExecutors, listPendingActions, registerExecutors, sandboxRouteDeps, updateAction, } from "./actions.js";
|
|
9
9
|
import { listDefinitions, listSessions, putDefinition, putSession, sessionView, } from "./sessions.js";
|
|
@@ -28,6 +28,9 @@ export async function handle(ctx, request, response, _url) {
|
|
|
28
28
|
const scope = await authenticate(ctx, request);
|
|
29
29
|
if (path[0] !== "v1")
|
|
30
30
|
fail(404, "Route not found");
|
|
31
|
+
authorize(scope, method, path);
|
|
32
|
+
// Set when the request acts for a subject: only the subject's own sessions are reachable.
|
|
33
|
+
const owner = ownerOf(scope);
|
|
31
34
|
if (path[1] === "executors" && path[2] === "connect" && method === "GET") {
|
|
32
35
|
if (scope.kind !== "executor")
|
|
33
36
|
return fail(403, "Executor credential required");
|
|
@@ -59,7 +62,7 @@ export async function handle(ctx, request, response, _url) {
|
|
|
59
62
|
return json(await dispatchVault(ctx, scope, method, path, url, request));
|
|
60
63
|
if (path[1] === "tenant")
|
|
61
64
|
return json(await dispatchTenant(ctx, scope, method, path, request));
|
|
62
|
-
const principalId =
|
|
65
|
+
const principalId = requirePrincipal(scope);
|
|
63
66
|
if (path[1] === "executors" && path.length === 2 && method === "GET")
|
|
64
67
|
return json(listExecutors(ctx));
|
|
65
68
|
// The length guard matters: the connect branch above only matches GET, so without it a
|
|
@@ -74,7 +77,7 @@ export async function handle(ctx, request, response, _url) {
|
|
|
74
77
|
if (path[1] === "agents" && path.length === 2 && method === "GET")
|
|
75
78
|
return json(await listDefinitions(ctx));
|
|
76
79
|
if (path[1] === "sessions" && path.length === 2 && method === "GET")
|
|
77
|
-
return json(await listSessions(ctx, url.searchParams.get("agentId")));
|
|
80
|
+
return json(await listSessions(ctx, url.searchParams.get("agentId"), owner));
|
|
78
81
|
if (path[1] === "agents" &&
|
|
79
82
|
path[2] &&
|
|
80
83
|
path.length === 3 &&
|
|
@@ -84,13 +87,13 @@ export async function handle(ctx, request, response, _url) {
|
|
|
84
87
|
const id = path[2];
|
|
85
88
|
if (method === "PUT" && path.length === 3) {
|
|
86
89
|
const body = PutSessionRequestSchema.parse(await readBody(request));
|
|
87
|
-
const session = await putSession(ctx, id, body);
|
|
90
|
+
const session = await putSession(ctx, id, body, owner);
|
|
88
91
|
return json(await ctx.store.tx((t) => sessionView(t, session)));
|
|
89
92
|
}
|
|
90
93
|
if (method === "GET" && path.length === 3)
|
|
91
|
-
return json(await ctx.store.tx(async (t) => sessionView(t, await sessionOf(t, id))));
|
|
94
|
+
return json(await ctx.store.tx(async (t) => sessionView(t, await sessionOf(t, id, owner))));
|
|
92
95
|
// Every other session route needs the session to exist.
|
|
93
|
-
await loadSession(ctx, id);
|
|
96
|
+
await loadSession(ctx, id, owner);
|
|
94
97
|
const cursor = requestCursor(request, url);
|
|
95
98
|
if (method === "GET" && path[3] === "items")
|
|
96
99
|
return json(await readHistory(ctx, id, cursor, url.searchParams.get("agent") ?? undefined));
|
|
@@ -133,10 +136,16 @@ export async function handle(ctx, request, response, _url) {
|
|
|
133
136
|
error?.message === "Invalid cursor"
|
|
134
137
|
? 400
|
|
135
138
|
: 500;
|
|
139
|
+
const rejection = error instanceof HttpError ? error.rejection : {};
|
|
136
140
|
json({
|
|
137
141
|
status: "rejected",
|
|
138
|
-
code: status === 500
|
|
142
|
+
code: status === 500
|
|
143
|
+
? "internal_error"
|
|
144
|
+
: rejection.code ?? "request_rejected",
|
|
139
145
|
message: status === 500 ? "Runtime request failed" : error.message,
|
|
146
|
+
...(rejection.details === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { details: rejection.details }),
|
|
140
149
|
}, status);
|
|
141
150
|
}
|
|
142
151
|
}
|
|
@@ -19,7 +19,8 @@ export declare function putDefinition(ctx: TenantContext, agentId: string, body:
|
|
|
19
19
|
manifestHash: string;
|
|
20
20
|
implementationVersion: string;
|
|
21
21
|
}>;
|
|
22
|
-
|
|
22
|
+
/** `owner`, when the request acts for a subject, limits the list to that subject's sessions. */
|
|
23
|
+
export declare function listSessions(ctx: TenantContext, agentId: string | null, owner?: string): Promise<{
|
|
23
24
|
sessions: {
|
|
24
25
|
id: string;
|
|
25
26
|
agentId: string;
|
|
@@ -28,7 +29,12 @@ export declare function listSessions(ctx: TenantContext, agentId: string | null)
|
|
|
28
29
|
activeTurnId: string | null;
|
|
29
30
|
}[];
|
|
30
31
|
}>;
|
|
31
|
-
/**
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Create a session from its definition, or re-attach vaults to an identical one. `owner`, when
|
|
34
|
+
* the request acts for a subject, must be the session's owner: another owner's session id is
|
|
35
|
+
* the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
|
|
36
|
+
* sandboxes.
|
|
37
|
+
*/
|
|
38
|
+
export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest, owner?: string): Promise<Session>;
|
|
33
39
|
/** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
|
|
34
40
|
export declare function sessionView(t: Tx, s: Session): Promise<unknown>;
|
package/dist/tenant/sessions.js
CHANGED
|
@@ -42,8 +42,12 @@ export async function putDefinition(ctx, agentId, body) {
|
|
|
42
42
|
implementationVersion: body.implementationVersion,
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
/** `owner`, when the request acts for a subject, limits the list to that subject's sessions. */
|
|
46
|
+
export async function listSessions(ctx, agentId, owner) {
|
|
47
|
+
const sessions = await ctx.store.tx((t) => t.listSessions({
|
|
48
|
+
...(agentId === null ? {} : { agentId }),
|
|
49
|
+
...(owner === undefined ? {} : { ownerUserId: owner }),
|
|
50
|
+
}));
|
|
47
51
|
return {
|
|
48
52
|
sessions: sessions.map((s) => ({
|
|
49
53
|
id: s.id,
|
|
@@ -54,20 +58,30 @@ export async function listSessions(ctx, agentId) {
|
|
|
54
58
|
})),
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Create a session from its definition, or re-attach vaults to an identical one. `owner`, when
|
|
63
|
+
* the request acts for a subject, must be the session's owner: another owner's session id is
|
|
64
|
+
* the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
|
|
65
|
+
* sandboxes.
|
|
66
|
+
*/
|
|
67
|
+
export function putSession(ctx, id, body, owner) {
|
|
59
68
|
const vaultIds = body.vaultIds ?? [];
|
|
60
69
|
const credentialSelections = body.credentialSelections ?? [];
|
|
70
|
+
const opaque = owner !== undefined;
|
|
71
|
+
if (opaque && body.ownerUserId !== owner)
|
|
72
|
+
fail(403, "ownerUserId must be the subject");
|
|
61
73
|
return ctx.store.tx(async (t) => {
|
|
62
74
|
const prior = await t.lockSession(id);
|
|
75
|
+
if (opaque && prior && prior.ownerUserId !== owner)
|
|
76
|
+
fail(404, "Session not found");
|
|
63
77
|
const definition = prior === undefined || body.sandbox
|
|
64
78
|
? (await t.get("definitions", body.agentId)) ??
|
|
65
79
|
fail(404, "Definition not found")
|
|
66
80
|
: undefined;
|
|
67
81
|
const sandboxOwnerId = body.sandbox
|
|
68
|
-
? validateSandboxAttach(body, (definition ?? prior).manifest, await sandboxLookup(t, body.sandbox.session))
|
|
82
|
+
? validateSandboxAttach(body, (definition ?? prior).manifest, await sandboxLookup(t, body.sandbox.session), { opaque })
|
|
69
83
|
: undefined;
|
|
70
|
-
await ctx.vault.assertAttachment(t, body.ownerUserId, vaultIds, credentialSelections);
|
|
84
|
+
await ctx.vault.assertAttachment(t, body.ownerUserId, vaultIds, credentialSelections, { opaque });
|
|
71
85
|
if (prior) {
|
|
72
86
|
if (sessionIdentity(prior.creation) !== sessionIdentity(body))
|
|
73
87
|
fail(409, "Session already exists with different creation parameters");
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript events (Host feature `transcript-events`): what a chat UI shows, written in the
|
|
3
|
+
* transaction that completes the effect, so a replayed effect never writes them again.
|
|
4
|
+
* Payload schemas: `@nylorun/core/contracts` (`parseTranscriptEvent`).
|
|
5
|
+
*/
|
|
6
|
+
import type { HostEffect } from "@nylorun/harness/run";
|
|
7
|
+
/** The model's tool call id and the harness invocation, for tool effects and tool actions. */
|
|
8
|
+
export declare function toolIds(context: unknown): {
|
|
9
|
+
callId: string;
|
|
10
|
+
invocationId: string;
|
|
11
|
+
} | Record<string, never>;
|
|
12
|
+
/**
|
|
13
|
+
* `message.assistant` for a completed model effect: the step's text and tool calls. The
|
|
14
|
+
* provider returns a `ModelCandidate` or a bare string; anything else writes no event.
|
|
15
|
+
*/
|
|
16
|
+
export declare function assistantMessage(request: HostEffect, value: unknown): {
|
|
17
|
+
agent: import("@nylorun/core").AgentRef;
|
|
18
|
+
invocationId: string;
|
|
19
|
+
text: string;
|
|
20
|
+
toolCalls: {
|
|
21
|
+
callId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
input: unknown;
|
|
24
|
+
}[];
|
|
25
|
+
} | {
|
|
26
|
+
agent?: undefined;
|
|
27
|
+
invocationId: string;
|
|
28
|
+
text: string;
|
|
29
|
+
toolCalls: {
|
|
30
|
+
callId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
input: unknown;
|
|
33
|
+
}[];
|
|
34
|
+
} | undefined;
|
|
35
|
+
/** `tool.completed` for an MCP or sandbox tool: its output, or the tool error it reported. */
|
|
36
|
+
export declare function toolCompleted(request: HostEffect, value: unknown): {
|
|
37
|
+
agent: import("@nylorun/core").AgentRef;
|
|
38
|
+
error: {
|
|
39
|
+
code: string;
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
output?: undefined;
|
|
43
|
+
capabilityId: string;
|
|
44
|
+
toolName: string;
|
|
45
|
+
} | {
|
|
46
|
+
agent?: undefined;
|
|
47
|
+
error: {
|
|
48
|
+
code: string;
|
|
49
|
+
message: string;
|
|
50
|
+
};
|
|
51
|
+
output?: undefined;
|
|
52
|
+
capabilityId: string;
|
|
53
|
+
toolName: string;
|
|
54
|
+
} | {
|
|
55
|
+
agent: import("@nylorun/core").AgentRef;
|
|
56
|
+
error?: undefined;
|
|
57
|
+
output: unknown;
|
|
58
|
+
capabilityId: string;
|
|
59
|
+
toolName: string;
|
|
60
|
+
} | {
|
|
61
|
+
agent?: undefined;
|
|
62
|
+
error?: undefined;
|
|
63
|
+
output: unknown;
|
|
64
|
+
capabilityId: string;
|
|
65
|
+
toolName: string;
|
|
66
|
+
} | {
|
|
67
|
+
callId: string;
|
|
68
|
+
invocationId: string;
|
|
69
|
+
agent: import("@nylorun/core").AgentRef;
|
|
70
|
+
error: {
|
|
71
|
+
code: string;
|
|
72
|
+
message: string;
|
|
73
|
+
};
|
|
74
|
+
output?: undefined;
|
|
75
|
+
capabilityId: string;
|
|
76
|
+
toolName: string;
|
|
77
|
+
} | {
|
|
78
|
+
callId: string;
|
|
79
|
+
invocationId: string;
|
|
80
|
+
agent?: undefined;
|
|
81
|
+
error: {
|
|
82
|
+
code: string;
|
|
83
|
+
message: string;
|
|
84
|
+
};
|
|
85
|
+
output?: undefined;
|
|
86
|
+
capabilityId: string;
|
|
87
|
+
toolName: string;
|
|
88
|
+
} | {
|
|
89
|
+
callId: string;
|
|
90
|
+
invocationId: string;
|
|
91
|
+
agent: import("@nylorun/core").AgentRef;
|
|
92
|
+
error?: undefined;
|
|
93
|
+
output: unknown;
|
|
94
|
+
capabilityId: string;
|
|
95
|
+
toolName: string;
|
|
96
|
+
} | {
|
|
97
|
+
callId: string;
|
|
98
|
+
invocationId: string;
|
|
99
|
+
agent?: undefined;
|
|
100
|
+
error?: undefined;
|
|
101
|
+
output: unknown;
|
|
102
|
+
capabilityId: string;
|
|
103
|
+
toolName: string;
|
|
104
|
+
} | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The model's tool call id and the harness invocation, for tool effects and tool actions. */
|
|
2
|
+
export function toolIds(context) {
|
|
3
|
+
const c = context;
|
|
4
|
+
return typeof c?.callId === "string" && typeof c.invocationId === "string"
|
|
5
|
+
? { callId: c.callId, invocationId: c.invocationId }
|
|
6
|
+
: {};
|
|
7
|
+
}
|
|
8
|
+
const agentOf = (request) => request.agent ? { agent: request.agent } : {};
|
|
9
|
+
/**
|
|
10
|
+
* `message.assistant` for a completed model effect: the step's text and tool calls. The
|
|
11
|
+
* provider returns a `ModelCandidate` or a bare string; anything else writes no event.
|
|
12
|
+
*/
|
|
13
|
+
export function assistantMessage(request, value) {
|
|
14
|
+
const invocationId = request.context
|
|
15
|
+
?.invocationId;
|
|
16
|
+
if (typeof invocationId !== "string")
|
|
17
|
+
return undefined;
|
|
18
|
+
const blocks = typeof value === "string"
|
|
19
|
+
? [{ type: "text", text: value }]
|
|
20
|
+
: Array.isArray(value?.output)
|
|
21
|
+
? value.output
|
|
22
|
+
: [];
|
|
23
|
+
let text = "";
|
|
24
|
+
const toolCalls = [];
|
|
25
|
+
for (const block of blocks) {
|
|
26
|
+
if (block?.type === "text" && typeof block.text === "string")
|
|
27
|
+
text += block.text;
|
|
28
|
+
else if (block?.type === "tool-call" &&
|
|
29
|
+
typeof block.id === "string" &&
|
|
30
|
+
typeof block.name === "string")
|
|
31
|
+
toolCalls.push({ callId: block.id, name: block.name, input: block.args });
|
|
32
|
+
}
|
|
33
|
+
if (!text && toolCalls.length === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
return { invocationId, text, toolCalls, ...agentOf(request) };
|
|
36
|
+
}
|
|
37
|
+
/** `tool.completed` for an MCP or sandbox tool: its output, or the tool error it reported. */
|
|
38
|
+
export function toolCompleted(request, value) {
|
|
39
|
+
const ids = toolIds(request.context);
|
|
40
|
+
if (!("callId" in ids))
|
|
41
|
+
return undefined;
|
|
42
|
+
const outcome = value;
|
|
43
|
+
const result = outcome?.kind === "failed"
|
|
44
|
+
? {
|
|
45
|
+
error: {
|
|
46
|
+
code: String(outcome.code ?? "tool.failed"),
|
|
47
|
+
message: String(outcome.message ?? ""),
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
: { output: outcome?.kind === "completed" ? outcome.output : value };
|
|
51
|
+
return {
|
|
52
|
+
...ids,
|
|
53
|
+
capabilityId: request.capabilityId,
|
|
54
|
+
toolName: request.toolName,
|
|
55
|
+
...result,
|
|
56
|
+
...agentOf(request),
|
|
57
|
+
};
|
|
58
|
+
}
|
package/dist/tenant/types.d.ts
CHANGED
|
@@ -109,6 +109,12 @@ export interface BootstrapPrincipal {
|
|
|
109
109
|
idempotencyKey: string;
|
|
110
110
|
/** When set, also registers application principal `studio` with this hash. */
|
|
111
111
|
studioCredentialHash?: string;
|
|
112
|
+
/** Application principals whose keys the admin key derives (feature `derived-principals`). */
|
|
113
|
+
derivedPrincipals?: readonly DerivedPrincipal[];
|
|
114
|
+
}
|
|
115
|
+
export interface DerivedPrincipal {
|
|
116
|
+
id: string;
|
|
117
|
+
credentialHash: string;
|
|
112
118
|
}
|
|
113
119
|
/**
|
|
114
120
|
* The deep module (§8). HTTP, CLI and tests use only this.
|