@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.
@@ -1,4 +1,5 @@
1
- import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowEffect, isFlowToolEffect, linkedMessageKey, linkedTurnEnd, } from "../core/flow-host.js";
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", { agent: request.agent, ...request.input });
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 = (await t.get("definitions", body.agentId)) ??
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
- await t.event(s.id, s.activeTurnId, "loop.iteration", {
419
- path: String(request.context.loopPath ?? path.split("/")[0]),
420
- n,
421
- sessionId: agentSessionId,
422
- turnId: accepted.turnId ?? undefined,
423
- manifestHash: agent.checkpoint?.manifestHash,
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
+ }
@@ -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
- constructor(status: number, message: string);
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: {
@@ -6,13 +6,15 @@ export const OPAQUE_NOT_FOUND = {
6
6
  };
7
7
  export class HttpError extends Error {
8
8
  status;
9
- constructor(status, message) {
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;
@@ -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
- /** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
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
- /** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
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
- return ((studio?.role === "application" ? studio.tokenHash : undefined) ===
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
- if (scope.kind !== "application") {
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 !== "application") {
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")
@@ -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 = requireApplication(scope);
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 ? "internal_error" : "request_rejected",
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
- export declare function listSessions(ctx: TenantContext, agentId: string | null): Promise<{
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
- /** Create a session from its definition, or re-attach vaults to an identical one. */
32
- export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest): Promise<Session>;
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>;
@@ -42,8 +42,12 @@ export async function putDefinition(ctx, agentId, body) {
42
42
  implementationVersion: body.implementationVersion,
43
43
  };
44
44
  }
45
- export async function listSessions(ctx, agentId) {
46
- const sessions = await ctx.store.tx((t) => t.listSessions(agentId === null ? {} : { agentId }));
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
- /** Create a session from its definition, or re-attach vaults to an identical one. */
58
- export function putSession(ctx, id, body) {
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
+ }
@@ -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.