@happyvertical/smrt-agents 0.42.6 → 0.43.0

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/dist/server.js CHANGED
@@ -1,6 +1,7 @@
1
- import { t as AgentConfig } from "./chunks/config-BRQLhsFp.js";
1
+ import { n as executeAsPrincipal, r as AgentConfig } from "./chunks/execute-as-principal-DltxRqN2.js";
2
2
  import { i as loadManifestsFromPackages, n as extractAgentPackagesFromConfig, r as loadManifestsFromConfig, t as extractAgentManifest } from "./chunks/manifest-utils-CtMyFQDx.js";
3
3
  import { sanitizeConfig } from "@happyvertical/smrt-config";
4
+ import { createHash, randomBytes } from "node:crypto";
4
5
  //#region src/server/api-routes.ts
5
6
  function buildRouteMap(manifests) {
6
7
  const routes = /* @__PURE__ */ new Map();
@@ -74,6 +75,461 @@ function isMissingAgentConfigTableError(error) {
74
75
  return message.includes("Run 'smrt db:migrate'") || /no such table[:\s]+agent_configs/i.test(message) || /relation .*agent_configs.*does not exist/i.test(message) || /table .*agent_configs.*doesn'?t exist/i.test(message);
75
76
  }
76
77
  //#endregion
78
+ //#region src/server/data-surface-actions.ts
79
+ var InMemoryDataSurfaceActionStateStore = class {
80
+ tokens = /* @__PURE__ */ new Map();
81
+ idempotency = /* @__PURE__ */ new Map();
82
+ putToken(token, record) {
83
+ this.tokens.set(token, record);
84
+ }
85
+ getToken(token) {
86
+ return this.tokens.get(token);
87
+ }
88
+ markTokenConsumed(token, idempotencyKey) {
89
+ const record = this.tokens.get(token);
90
+ if (!record) return false;
91
+ if (record.consumedBy && record.consumedBy !== idempotencyKey) return false;
92
+ record.consumedBy = idempotencyKey;
93
+ return true;
94
+ }
95
+ getIdempotency(key) {
96
+ return this.idempotency.get(key);
97
+ }
98
+ reserveIdempotency(key, reservation) {
99
+ const existing = this.idempotency.get(key);
100
+ if (existing) return existing;
101
+ const record = {
102
+ status: "reserved",
103
+ ...reservation
104
+ };
105
+ this.idempotency.set(key, record);
106
+ return record;
107
+ }
108
+ completeIdempotency(key, ownerToken, result2) {
109
+ const existing = this.idempotency.get(key);
110
+ if (existing?.status !== "reserved" || existing.ownerToken !== ownerToken) return false;
111
+ this.idempotency.set(key, {
112
+ status: "completed",
113
+ requestFingerprint: existing.requestFingerprint,
114
+ result: result2
115
+ });
116
+ return true;
117
+ }
118
+ releaseIdempotency(key, ownerToken) {
119
+ const existing = this.idempotency.get(key);
120
+ if (existing?.status !== "reserved" || existing.ownerToken !== ownerToken) return false;
121
+ return this.idempotency.delete(key);
122
+ }
123
+ };
124
+ var DEFAULT_TOKEN_TTL_MS = 300 * 1e3;
125
+ var MAX_IDENTIFIER_LENGTH = 256;
126
+ var MAX_JSON_DEPTH = 16;
127
+ var MAX_JSON_ITEMS = 1e3;
128
+ var FORBIDDEN_JSON_KEYS = /* @__PURE__ */ new Set([
129
+ "__proto__",
130
+ "constructor",
131
+ "prototype"
132
+ ]);
133
+ function isBoundedJsonValue(value, depth = 0, seen = /* @__PURE__ */ new Set()) {
134
+ if (value === null) return true;
135
+ if (["string", "boolean"].includes(typeof value)) return true;
136
+ if (typeof value === "number") return Number.isFinite(value);
137
+ if (typeof value !== "object" || depth >= MAX_JSON_DEPTH || seen.has(value)) return false;
138
+ seen.add(value);
139
+ if (Array.isArray(value)) {
140
+ if (value.length > MAX_JSON_ITEMS) return false;
141
+ return value.every((item) => isBoundedJsonValue(item, depth + 1, seen));
142
+ }
143
+ const prototype = Object.getPrototypeOf(value);
144
+ if (prototype !== Object.prototype && prototype !== null) return false;
145
+ const entries = Object.entries(value);
146
+ if (entries.length > MAX_JSON_ITEMS) return false;
147
+ return entries.every(([key, item]) => !FORBIDDEN_JSON_KEYS.has(key) && isBoundedJsonValue(item, depth + 1, seen));
148
+ }
149
+ function validIdentifier(value) {
150
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_IDENTIFIER_LENGTH;
151
+ }
152
+ function validSelection(selection) {
153
+ if (!selection || typeof selection !== "object") return false;
154
+ const candidate = selection;
155
+ if (candidate.scope === "current-page") return true;
156
+ if (candidate.scope === "all-matching") return validIdentifier(candidate.queryFingerprint);
157
+ if (candidate.scope !== "explicit-ids" || !Array.isArray(candidate.rowIds)) return false;
158
+ if (candidate.rowIds.length > MAX_JSON_ITEMS) return false;
159
+ return candidate.rowIds.every((rowId) => typeof rowId === "string" && rowId.length > 0 || typeof rowId === "number" && Number.isFinite(rowId));
160
+ }
161
+ function stable(value) {
162
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
163
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
164
+ return `{${Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(",")}}`;
165
+ }
166
+ function fingerprint(value) {
167
+ return createHash("sha256").update(stable(value)).digest("hex");
168
+ }
169
+ function identityKey(identity) {
170
+ return stable(canonicalIdentity(identity));
171
+ }
172
+ function canonicalIdentity(identity) {
173
+ return {
174
+ kind: identity.kind,
175
+ surfaceId: identity.surfaceId,
176
+ ...identity.subject ? { subject: {
177
+ type: identity.subject.type,
178
+ id: identity.subject.id
179
+ } } : {}
180
+ };
181
+ }
182
+ function rowIdKey(rowId) {
183
+ return `${typeof rowId}:${String(rowId)}`;
184
+ }
185
+ function compareRowIds(left, right) {
186
+ if (typeof left !== typeof right) return typeof left === "number" ? -1 : 1;
187
+ if (typeof left === "number" && typeof right === "number") return left - right;
188
+ return left < right ? -1 : left > right ? 1 : 0;
189
+ }
190
+ function canonicalRowIds(rowIds) {
191
+ const ids = /* @__PURE__ */ new Map();
192
+ for (const rowId of rowIds) ids.set(rowIdKey(rowId), rowId);
193
+ return [...ids.values()].sort(compareRowIds);
194
+ }
195
+ function canonicalSelection(selection) {
196
+ if (selection.scope !== "explicit-ids") return selection;
197
+ return {
198
+ scope: selection.scope,
199
+ rowIds: canonicalRowIds(selection.rowIds)
200
+ };
201
+ }
202
+ function requestFingerprint(request) {
203
+ return fingerprint({
204
+ identity: canonicalIdentity(request.identity),
205
+ actionId: request.actionId,
206
+ selection: canonicalSelection(request.selection),
207
+ payload: request.payload,
208
+ expectedRevision: request.expectedRevision
209
+ });
210
+ }
211
+ function actionFingerprint(action) {
212
+ return fingerprint({
213
+ descriptor: action.descriptor,
214
+ inputSchema: action.inputSchema,
215
+ confirmation: action.confirmation,
216
+ execution: action.execution,
217
+ tool: action.tool,
218
+ operationId: action.operation.id,
219
+ operationCollection: action.operation.collection,
220
+ operationAction: action.operation.action
221
+ });
222
+ }
223
+ function result(request, ok, reason, details, confirmationToken) {
224
+ return {
225
+ version: 1,
226
+ requestId: request.requestId,
227
+ identity: request.identity,
228
+ actionId: request.actionId,
229
+ phase: request.phase,
230
+ ok,
231
+ ...reason ? { reason } : {},
232
+ ...details ? { details } : {},
233
+ ...confirmationToken ? { confirmationToken } : {}
234
+ };
235
+ }
236
+ function outcomesDetails(outcomes, extra = {}) {
237
+ return {
238
+ accepted: outcomes.filter(({ status }) => status === "accepted").length,
239
+ skipped: outcomes.filter(({ status }) => status === "skipped").length,
240
+ failed: outcomes.filter(({ status }) => status === "failed").length,
241
+ outcomes: outcomes.map(({ rowId, status, reason }) => ({
242
+ rowId,
243
+ status,
244
+ ...reason ? { reason } : {}
245
+ })),
246
+ ...extra
247
+ };
248
+ }
249
+ function validateRequest(request, phase) {
250
+ if (!request || typeof request !== "object") return "invalid_request";
251
+ if (request.version !== 1 || request.phase !== phase) return "invalid_request";
252
+ if (!validIdentifier(request.requestId) || !validIdentifier(request.actionId) || !validIdentifier(request.identity?.surfaceId) || ![
253
+ "table",
254
+ "list",
255
+ "report",
256
+ "custom"
257
+ ].includes(request.identity?.kind) || !validSelection(request.selection) || request.payload !== void 0 && !isBoundedJsonValue(request.payload)) return "invalid_request";
258
+ if (!Number.isSafeInteger(request.expectedRevision) || request.expectedRevision < 0) return "invalid_request";
259
+ if (phase === "apply" && (!validIdentifier(request.idempotencyKey) || request.confirmationToken !== void 0 && !validIdentifier(request.confirmationToken))) return "invalid_request";
260
+ }
261
+ function createDataSurfaceActionAdapter(options) {
262
+ const state = options.state;
263
+ const now = options.now ?? Date.now;
264
+ const createToken = options.createToken ?? (() => randomBytes(32).toString("base64url"));
265
+ const tokenTtlMs = options.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
266
+ const runAsPrincipal = options.runAsPrincipal ?? executeAsPrincipal;
267
+ const idempotencyPollIntervalMs = Math.max(1, options.idempotencyPollIntervalMs ?? 10);
268
+ const idempotencyWaitTimeoutMs = Math.max(0, options.idempotencyWaitTimeoutMs ?? 5e3);
269
+ async function resolveInvocation(request, run) {
270
+ const surface = await options.resolveSurface(run, request.identity);
271
+ if (identityKey(surface.descriptor.identity) !== identityKey(request.identity)) return result(request, false, "not_found");
272
+ const action = surface.actions[request.actionId];
273
+ const declared = surface.descriptor.actions.find(({ id }) => id === request.actionId);
274
+ if (!action || !declared || action.descriptor.id !== declared.id || Boolean(declared.requiresConfirmation) !== (action.confirmation === "required")) return result(request, false, "unsupported");
275
+ if (!action.tool || !validIdentifier(action.operation?.id) || !validIdentifier(action.operation?.action)) return result(request, false, "denied");
276
+ run.assertToolAllowed(action.tool);
277
+ await run.assertOperation(action.operation.collection, action.operation.action);
278
+ const payloadValidation = await action.validatePayload(request.payload);
279
+ if (!payloadValidation.valid) return result(request, false, payloadValidation.reason ?? "invalid_payload");
280
+ if (!action.descriptor.selectionScopes.includes(request.selection.scope)) return result(request, false, "selection_not_supported");
281
+ const base = {
282
+ run,
283
+ request,
284
+ descriptor: surface.descriptor,
285
+ action
286
+ };
287
+ const resolvedSelection = await options.resolveSelection(base, canonicalSelection(request.selection));
288
+ const selection = {
289
+ ...resolvedSelection,
290
+ rowIds: canonicalRowIds(resolvedSelection.rowIds)
291
+ };
292
+ const invocation = {
293
+ ...base,
294
+ selection
295
+ };
296
+ if (!await action.authorize(invocation)) return result(request, false, "denied");
297
+ if (selection.rowIds.length > surface.descriptor.limits.maxSelectionSize) return result(request, false, "limit_exceeded");
298
+ return invocation;
299
+ }
300
+ async function preview(request, context) {
301
+ const invalid = validateRequest(request, "preview");
302
+ if (invalid) return result(request, false, invalid);
303
+ return runAsPrincipal({
304
+ ...context.principal,
305
+ action: "data_surface.action.preview",
306
+ auditMetadata: {
307
+ ...context.principal.auditMetadata,
308
+ surfaceId: request.identity.surfaceId,
309
+ actionId: request.actionId,
310
+ requestId: request.requestId
311
+ }
312
+ }, async (run) => {
313
+ const invocation = await resolveInvocation(request, run);
314
+ if ("ok" in invocation) return invocation;
315
+ if (invocation.selection.revision !== request.expectedRevision) return result(request, false, "stale_revision");
316
+ const outcomes = [];
317
+ for (const rowId of invocation.selection.rowIds) {
318
+ const eligibility = await invocation.action.eligible(invocation, rowId);
319
+ outcomes.push({
320
+ rowId,
321
+ status: eligibility.eligible ? "accepted" : "skipped",
322
+ ...eligibility.reason ? { reason: eligibility.reason } : {}
323
+ });
324
+ }
325
+ const confirmationToken = createToken();
326
+ const selectionFingerprint = fingerprint(canonicalSelection(request.selection));
327
+ const requestFingerprintValue = requestFingerprint(request);
328
+ const expiresAt = now() + tokenTtlMs;
329
+ await state.putToken(confirmationToken, {
330
+ expiresAt,
331
+ actorUserId: context.principal.principal.runAsUserId,
332
+ tenantId: context.principal.principal.tenantId,
333
+ onBehalfOfUserId: context.principal.onBehalfOfUserId ?? null,
334
+ actsAsProfileId: context.principal.principal.actsAsProfileId ?? null,
335
+ identityKey: identityKey(request.identity),
336
+ actionId: request.actionId,
337
+ actionFingerprint: actionFingerprint(invocation.action),
338
+ revision: invocation.selection.revision,
339
+ queryFingerprint: invocation.selection.queryFingerprint,
340
+ selectionFingerprint,
341
+ resolvedRowsFingerprint: fingerprint(canonicalRowIds(invocation.selection.rowIds)),
342
+ requestFingerprint: requestFingerprintValue
343
+ });
344
+ return result(request, true, void 0, outcomesDetails(outcomes, {
345
+ count: invocation.selection.rowIds.length,
346
+ revision: invocation.selection.revision,
347
+ queryFingerprint: invocation.selection.queryFingerprint,
348
+ expiresAt
349
+ }), confirmationToken);
350
+ });
351
+ }
352
+ async function executeForeground(request, invocation) {
353
+ const outcomes = [];
354
+ for (const rowId of invocation.selection.rowIds) try {
355
+ const eligibility = await invocation.action.eligible(invocation, rowId);
356
+ if (!eligibility.eligible) {
357
+ outcomes.push({
358
+ rowId,
359
+ status: "skipped",
360
+ ...eligibility.reason ? { reason: eligibility.reason } : {}
361
+ });
362
+ continue;
363
+ }
364
+ await invocation.action.apply(invocation, rowId);
365
+ outcomes.push({
366
+ rowId,
367
+ status: "accepted"
368
+ });
369
+ } catch {
370
+ outcomes.push({
371
+ rowId,
372
+ status: "failed",
373
+ reason: "execution_failed"
374
+ });
375
+ }
376
+ return result(request, true, void 0, outcomesDetails(outcomes));
377
+ }
378
+ async function executeBackgroundOnce(request, context, token) {
379
+ const ownerToken = randomBytes(16).toString("base64url");
380
+ const executionFingerprint = fingerprint({
381
+ kind: "background-execution",
382
+ request: token?.requestFingerprint ?? requestFingerprint(request),
383
+ action: token?.actionFingerprint ?? request.actionId
384
+ });
385
+ const executionScope = fingerprint({
386
+ kind: "background-execution",
387
+ actorUserId: token?.actorUserId ?? context.principal.principal.runAsUserId,
388
+ tenantId: token?.tenantId ?? context.principal.principal.tenantId,
389
+ onBehalfOfUserId: token?.onBehalfOfUserId ?? context.principal.onBehalfOfUserId ?? null,
390
+ actsAsProfileId: token?.actsAsProfileId ?? context.principal.principal.actsAsProfileId ?? null,
391
+ identity: canonicalIdentity(request.identity),
392
+ actionId: request.actionId,
393
+ idempotencyKey: request.idempotencyKey
394
+ });
395
+ const maxPolls = Math.max(1, Math.ceil(idempotencyWaitTimeoutMs / idempotencyPollIntervalMs));
396
+ for (let poll = 0; poll <= maxPolls; poll += 1) {
397
+ const winner = await state.reserveIdempotency(executionScope, {
398
+ requestFingerprint: executionFingerprint,
399
+ ownerToken,
400
+ reservedAt: now()
401
+ });
402
+ if (winner.requestFingerprint !== executionFingerprint) return result(request, false, "idempotency_conflict");
403
+ if (winner.status === "completed") return winner.result;
404
+ if (winner.ownerToken === ownerToken) {
405
+ let executed;
406
+ try {
407
+ executed = await authorizedApply(request, context, token, false);
408
+ } catch (error) {
409
+ await state.releaseIdempotency(executionScope, ownerToken);
410
+ throw error;
411
+ }
412
+ if (!await state.completeIdempotency(executionScope, ownerToken, executed)) throw new Error("Lost background action idempotency reservation");
413
+ return executed;
414
+ }
415
+ if (poll < maxPolls) {
416
+ await new Promise((resolve) => setTimeout(resolve, idempotencyPollIntervalMs));
417
+ const current = await state.getIdempotency(executionScope);
418
+ if (current?.status === "completed") return current.result;
419
+ }
420
+ }
421
+ return result(request, false, "idempotency_in_progress");
422
+ }
423
+ async function authorizedApply(request, context, token, allowBackground) {
424
+ const idempotencyKey = request.idempotencyKey;
425
+ if (!idempotencyKey) return result(request, false, "invalid_request");
426
+ return runAsPrincipal({
427
+ ...context.principal,
428
+ action: "data_surface.action.apply",
429
+ auditMetadata: {
430
+ ...context.principal.auditMetadata,
431
+ surfaceId: request.identity.surfaceId,
432
+ actionId: request.actionId,
433
+ requestId: request.requestId,
434
+ idempotencyKey: request.idempotencyKey
435
+ }
436
+ }, async (run) => {
437
+ const invocation = await resolveInvocation(request, run);
438
+ if ("ok" in invocation) return invocation;
439
+ if (token) {
440
+ if (invocation.selection.revision !== token.revision || invocation.selection.revision !== request.expectedRevision || invocation.selection.queryFingerprint !== token.queryFingerprint || fingerprint(canonicalSelection(request.selection)) !== token.selectionFingerprint || actionFingerprint(invocation.action) !== token.actionFingerprint || fingerprint(canonicalRowIds(invocation.selection.rowIds)) !== token.resolvedRowsFingerprint) return result(request, false, "stale_preview");
441
+ } else if (invocation.action.confirmation === "required") return result(request, false, "confirmation_required");
442
+ else if (invocation.selection.revision !== request.expectedRevision) return result(request, false, "stale_revision");
443
+ if (invocation.action.execution === "background" && allowBackground) {
444
+ if (!options.backgroundQueue) return result(request, false, "background_unavailable");
445
+ const queued = await options.backgroundQueue.enqueue({
446
+ idempotencyKey,
447
+ identity: request.identity,
448
+ actionId: request.actionId,
449
+ rowIds: invocation.selection.rowIds,
450
+ run: () => executeBackgroundOnce(request, context, token)
451
+ });
452
+ return result(request, true, void 0, {
453
+ accepted: invocation.selection.rowIds.length,
454
+ skipped: 0,
455
+ failed: 0,
456
+ background: true,
457
+ jobId: queued.jobId,
458
+ ...queued.details ?? {}
459
+ });
460
+ }
461
+ return executeForeground(request, invocation);
462
+ });
463
+ }
464
+ async function apply(request, context) {
465
+ const invalid = validateRequest(request, "apply");
466
+ if (invalid) return result(request, false, invalid);
467
+ const confirmationToken = request.confirmationToken;
468
+ const idempotencyKey = request.idempotencyKey;
469
+ if (!idempotencyKey) return result(request, false, "invalid_request");
470
+ const actorUserId = context.principal.principal.runAsUserId;
471
+ const tenantId = context.principal.principal.tenantId;
472
+ const onBehalfOfUserId = context.principal.onBehalfOfUserId ?? null;
473
+ const actsAsProfileId = context.principal.principal.actsAsProfileId ?? null;
474
+ const requestFingerprintValue = requestFingerprint(request);
475
+ const idempotencyScope = fingerprint({
476
+ actorUserId,
477
+ tenantId,
478
+ onBehalfOfUserId,
479
+ actsAsProfileId,
480
+ identity: canonicalIdentity(request.identity),
481
+ actionId: request.actionId,
482
+ idempotencyKey
483
+ });
484
+ const prior = await state.getIdempotency(idempotencyScope);
485
+ if (prior && prior.requestFingerprint !== requestFingerprintValue) return result(request, false, "idempotency_conflict");
486
+ if (prior?.status === "completed") return prior.result;
487
+ let token;
488
+ if (confirmationToken) {
489
+ token = await state.getToken(confirmationToken);
490
+ if (!token || token.expiresAt <= now()) return result(request, false, "invalid_or_expired_confirmation");
491
+ if (token.actorUserId !== actorUserId || token.tenantId !== tenantId || token.onBehalfOfUserId !== onBehalfOfUserId || token.actsAsProfileId !== actsAsProfileId || token.identityKey !== identityKey(request.identity) || token.actionId !== request.actionId || token.requestFingerprint !== requestFingerprintValue) return result(request, false, "confirmation_mismatch");
492
+ if (!await state.markTokenConsumed(confirmationToken, idempotencyKey)) return result(request, false, "confirmation_replayed");
493
+ }
494
+ const ownerToken = randomBytes(16).toString("base64url");
495
+ const maxPolls = Math.max(1, Math.ceil(idempotencyWaitTimeoutMs / idempotencyPollIntervalMs));
496
+ for (let poll = 0; poll <= maxPolls; poll += 1) {
497
+ const winner = await state.reserveIdempotency(idempotencyScope, {
498
+ requestFingerprint: requestFingerprintValue,
499
+ ownerToken,
500
+ reservedAt: now()
501
+ });
502
+ if (winner.requestFingerprint !== requestFingerprintValue) return result(request, false, "idempotency_conflict");
503
+ if (winner.status === "completed") return winner.result;
504
+ if (winner.ownerToken === ownerToken) {
505
+ let applied;
506
+ try {
507
+ applied = await authorizedApply(request, context, token, true);
508
+ } catch (error) {
509
+ await state.releaseIdempotency(idempotencyScope, ownerToken);
510
+ throw error;
511
+ }
512
+ if (!applied.ok && applied.reason === "confirmation_required") {
513
+ await state.releaseIdempotency(idempotencyScope, ownerToken);
514
+ return applied;
515
+ }
516
+ if (!await state.completeIdempotency(idempotencyScope, ownerToken, applied)) throw new Error("Lost data-surface idempotency reservation");
517
+ return applied;
518
+ }
519
+ if (poll < maxPolls) {
520
+ await new Promise((resolve) => setTimeout(resolve, idempotencyPollIntervalMs));
521
+ const current = await state.getIdempotency(idempotencyScope);
522
+ if (current?.status === "completed") return current.result;
523
+ }
524
+ }
525
+ return result(request, false, "idempotency_in_progress");
526
+ }
527
+ return {
528
+ preview,
529
+ apply
530
+ };
531
+ }
532
+ //#endregion
77
533
  //#region src/server/serialization.ts
78
534
  function serializeResolvedAgent(resolved) {
79
535
  const manifest = resolved.manifest;
@@ -93,6 +549,6 @@ function serializeResolvedAgent(resolved) {
93
549
  };
94
550
  }
95
551
  //#endregion
96
- export { buildRouteMap, extractAgentManifest, extractAgentPackagesFromConfig, loadManifestsFromConfig, loadManifestsFromPackages, loadSlotConfigs, resolveAPIRoute, serializeResolvedAgent };
552
+ export { InMemoryDataSurfaceActionStateStore, buildRouteMap, createDataSurfaceActionAdapter, extractAgentManifest, extractAgentPackagesFromConfig, loadManifestsFromConfig, loadManifestsFromPackages, loadSlotConfigs, resolveAPIRoute, serializeResolvedAgent };
97
553
 
98
554
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../src/server/api-routes.ts","../src/server/config-loader.ts","../src/server/serialization.ts"],"sourcesContent":["/**\n * Server-side API route resolution for SMRT agents\n *\n * Reads agent package manifests and builds a route map from resource\n * paths (e.g., 'performers', 'video-contents') to SmrtObject class\n * names and allowed CRUD actions. The catch-all API handler uses this\n * to resolve incoming requests.\n *\n * @module @happyvertical/smrt-agents/server\n */\n\nimport type { PackageManifest } from './manifest-utils.js';\n\n/**\n * Info about a single API route (one SmrtObject with api.include)\n */\nexport interface AgentAPIRouteInfo {\n /** SmrtObject class name (e.g., 'Performer') */\n className: string;\n /** Allowed CRUD actions (e.g., ['list', 'get', 'create', 'update', 'delete']) */\n allowedActions: string[];\n /** Package that owns this resource */\n packageName?: string;\n}\n\n/**\n * Result of resolving a URL path against the route map\n */\nexport interface ResolvedAPIRoute {\n /** The matched route info */\n route: AgentAPIRouteInfo;\n /** Resource ID if path includes one (e.g., 'performers/abc-123') */\n id?: string;\n /** Custom action name if path includes one (e.g., 'performers/abc-123/generate-image') */\n action?: string;\n}\n\n/**\n * Build a route map from loaded package manifests.\n *\n * Iterates all objects in each manifest, and for any object with a\n * `decoratorConfig.api.include` array, registers a route. The route\n * path is derived from `decoratorConfig.api.path` if set, otherwise\n * from the table name with underscores converted to hyphens.\n *\n * @param manifests - Array of parsed package manifest JSON objects\n * @returns Map of resource path -> route info\n *\n * @example\n * ```typescript\n * const manifests = [histrioManifest, praecoManifest];\n * const routes = buildRouteMap(manifests);\n * // routes.get('performers') => { className: 'Performer', allowedActions: ['list', 'get', 'create', 'update', 'delete'] }\n * // routes.get('video-contents') => { className: 'VideoShot', allowedActions: ['list', 'get', 'create', 'update'] }\n * ```\n */\nexport function buildRouteMap(\n manifests: PackageManifest[],\n): Map<string, AgentAPIRouteInfo> {\n const routes = new Map<string, AgentAPIRouteInfo>();\n\n for (const manifest of manifests) {\n const packageName = (manifest as Record<string, unknown>).packageName as\n | string\n | undefined;\n\n for (const obj of Object.values(manifest.objects)) {\n const config = obj.decoratorConfig as Record<string, unknown> | undefined;\n if (!config) continue;\n\n const api = config.api as\n | { include?: string[]; path?: string }\n | undefined;\n if (!api?.include || api.include.length === 0) continue;\n\n // Derive the URL path: explicit api.path, or table name with _ -> -\n const tableName = config.tableName as string | undefined;\n const path =\n api.path || (tableName ? tableName.replace(/_/g, '-') : null);\n if (!path) continue;\n\n routes.set(path, {\n className: obj.className,\n allowedActions: api.include,\n packageName,\n });\n }\n }\n\n return routes;\n}\n\n/**\n * Resolve a URL resource path against a route map.\n *\n * Handles three URL patterns:\n * - `performers` → list/create (no id)\n * - `performers/abc-123` → get/update/delete (with id)\n * - `performers/abc-123/generate-image` → custom action\n *\n * @param urlPath - The resource portion of the URL (after `/api/agents/{agentId}/`)\n * @param routes - Route map from {@link buildRouteMap}\n * @returns Resolved route with optional id/action, or null if no match\n */\nexport function resolveAPIRoute(\n urlPath: string,\n routes: Map<string, AgentAPIRouteInfo>,\n): ResolvedAPIRoute | null {\n // Normalize: strip leading/trailing slashes\n const normalized = urlPath.replace(/^\\/+|\\/+$/g, '');\n if (!normalized) return null;\n\n const segments = normalized.split('/');\n\n // Try 1-segment: \"performers\"\n if (segments.length === 1) {\n const route = routes.get(segments[0]);\n if (route) return { route };\n return null;\n }\n\n // Try 2-segment: \"performers/{id}\"\n if (segments.length === 2) {\n const route = routes.get(segments[0]);\n if (route) return { route, id: segments[1] };\n return null;\n }\n\n // Try 3-segment: \"performers/{id}/{action}\"\n if (segments.length === 3) {\n const route = routes.get(segments[0]);\n if (route) return { route, id: segments[1], action: segments[2] };\n return null;\n }\n\n return null;\n}\n","/**\n * Server-side agent config loading utilities\n *\n * Loads slot configurations from the agent_configs table for a set of agents.\n * Agent-specific table loading (e.g., praeco_sources) stays in the host app.\n *\n * @module @happyvertical/smrt-agents/server\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { AgentConfig } from '../config.js';\n\n/**\n * Load slot configs for multiple agents from the agent_configs table.\n *\n * Returns a nested map: agentId -> slotId -> configData.\n * Agent-specific tables (e.g., praeco_sources, praeco_reports)\n * are NOT loaded here — those stay in the host application.\n *\n * @param agents - Array of agent identifiers (id + agentClass)\n * @param dbOptions - Database options for SmrtCollection.create()\n * @returns Map of agentId -> slotId -> config data\n */\nexport async function loadSlotConfigs(\n agents: Array<{ id: string; agentClass: string }>,\n dbOptions: SmrtClassOptions,\n): Promise<Record<string, Record<string, unknown>>> {\n if (agents.length === 0) {\n return {};\n }\n\n try {\n const configsByAgent = await AgentConfig.forAgents(\n agents.map((agent) => agent.id),\n dbOptions,\n );\n\n const configs: Record<string, Record<string, unknown>> = {};\n for (const [agentId, slotConfigs] of configsByAgent) {\n const agentConfig: Record<string, unknown> = {};\n for (const [slotId, configData] of slotConfigs) {\n agentConfig[slotId] = configData;\n }\n if (Object.keys(agentConfig).length > 0) {\n configs[agentId] = agentConfig;\n }\n }\n\n return configs;\n } catch (error) {\n if (isMissingAgentConfigTableError(error)) {\n return {};\n }\n throw error;\n }\n}\n\nfunction isMissingAgentConfigTableError(error: unknown): boolean {\n const message = String((error as Error)?.message || error || '');\n\n return (\n message.includes(\"Run 'smrt db:migrate'\") ||\n /no such table[:\\s]+agent_configs/i.test(message) ||\n /relation .*agent_configs.*does not exist/i.test(message) ||\n /table .*agent_configs.*doesn'?t exist/i.test(message)\n );\n}\n","/**\n * Serialization utilities for resolved agents\n *\n * Converts ResolvedAgentAvailability (database + manifest data) into\n * a JSON-safe shape suitable for passing to client components.\n *\n * @module @happyvertical/smrt-agents/server\n */\n\nimport { sanitizeConfig } from '@happyvertical/smrt-config';\nimport type { ResolvedAgentAvailability } from '../tenant-agent.js';\nimport type { AgentAdminRoute, AgentUISlots } from '../ui.js';\n\n/**\n * Serialized agent data for passing to client components.\n *\n * Includes manifest-derived fields (icon, permissions, slots)\n * alongside resolution metadata (source, sourceTenantId).\n */\nexport interface SerializedAgent {\n /** Agent instance ID, or a synthetic key if no instance exists */\n id: string;\n /** Human-readable name from manifest */\n name?: string;\n /** Human-readable agent class name (e.g., 'Praeco') */\n agentClass: string;\n /** Canonical agent type (qualified name when available) */\n agentType: string;\n /** STI type discriminator (same as agentType) */\n _meta_type?: string;\n /** UI slot definitions from manifest */\n slots?: AgentUISlots;\n /** Admin route declarations from manifest */\n adminRoutes?: AgentAdminRoute[];\n /** How this agent was resolved for the tenant */\n source?: 'explicit' | 'inherited';\n /** Which tenant the binding came from */\n sourceTenantId?: string;\n /** Merged permissions from manifest + tenant overrides */\n permissions?: Record<string, boolean>;\n /** Agent icon from manifest */\n icon?: string;\n /**\n * Tenant-level config overrides, **secret-sanitized** for client transport.\n *\n * SECURITY (#1553, follow-up to #1552): the raw `TenantAgent.config` is the\n * tenant's own override blob and is `@field({ sensitive: true })` (stripped\n * from the generated CRUD api/mcp surfaces). This hand-written admin\n * serialization runs it through `sanitizeConfig()` from\n * `@happyvertical/smrt-config` before it leaves the server, so secret-shaped\n * keys (apiKey/token/password/…) are dropped and secret-shaped values\n * (`sk-…`, `AKIA…`, `Bearer …`, URL credentials, PEM blocks) are masked —\n * non-secret config still reaches the authorized admin UI for display.\n *\n * This is **display-only**: do not edit-round-trip it back to the server\n * (a masked value would overwrite the real secret). Best practice remains to\n * reference secrets by id via `@happyvertical/smrt-secrets` so only an opaque\n * handle is ever stored in tenant config.\n */\n config?: Record<string, unknown>;\n}\n\n/**\n * Convert a ResolvedAgentAvailability to a serializable shape for the UI.\n *\n * @param resolved - Output from TenantAgentCollection.resolveForTenant()\n * @returns Serialized agent data safe for JSON transport\n */\nexport function serializeResolvedAgent(\n resolved: ResolvedAgentAvailability,\n): SerializedAgent {\n const manifest = resolved.manifest;\n\n return {\n id: resolved.agentId || `${resolved.sourceTenantId}:${resolved.agentType}`,\n name: manifest?.name || resolved.agentClass,\n agentClass: resolved.agentClass,\n agentType: resolved.agentType,\n _meta_type: resolved.agentType,\n slots: manifest?.uiSlots as AgentUISlots | undefined,\n adminRoutes: manifest?.adminRoutes as AgentAdminRoute[] | undefined,\n source: resolved.source,\n sourceTenantId: resolved.sourceTenantId,\n permissions: resolved.permissions,\n icon: manifest?.icon,\n // Secret-sanitize before the blob crosses into the client payload (#1553).\n config: sanitizeConfig(resolved.config) as SerializedAgent['config'],\n };\n}\n"],"mappings":";;;;AAwDO,SAAS,cACd,WACgC;CAChC,MAAM,yBAAS,IAAI,IAA+B;CAElD,KAAA,MAAW,YAAY,WAAW;EAChC,MAAM,cAAe,SAAqC;EAI1D,KAAA,MAAW,OAAO,OAAO,OAAO,SAAS,OAAO,GAAG;GACjD,MAAM,SAAS,IAAI;GACnB,IAAI,CAAC,QAAQ;GAEb,MAAM,MAAM,OAAO;GAGnB,IAAI,CAAC,KAAK,WAAW,IAAI,QAAQ,WAAW,GAAG;GAG/C,MAAM,YAAY,OAAO;GACzB,MAAM,OACJ,IAAI,SAAS,YAAY,UAAU,QAAQ,MAAM,GAAG,IAAI;GAC1D,IAAI,CAAC,MAAM;GAEX,OAAO,IAAI,MAAM;IACf,WAAW,IAAI;IACf,gBAAgB,IAAI;IACpB;GACF,CAAC;EACH;CACF;CAEA,OAAO;AACT;AAcO,SAAS,gBACd,SACA,QACyB;CAEzB,MAAM,aAAa,QAAQ,QAAQ,cAAc,EAAE;CACnD,IAAI,CAAC,YAAY,OAAO;CAExB,MAAM,WAAW,WAAW,MAAM,GAAG;CAGrC,IAAI,SAAS,WAAW,GAAG;EACzB,MAAM,QAAQ,OAAO,IAAI,SAAS,EAAE;EACpC,IAAI,OAAO,OAAO,EAAE,MAAM;EAC1B,OAAO;CACT;CAGA,IAAI,SAAS,WAAW,GAAG;EACzB,MAAM,QAAQ,OAAO,IAAI,SAAS,EAAE;EACpC,IAAI,OAAO,OAAO;GAAE;GAAO,IAAI,SAAS;EAAG;EAC3C,OAAO;CACT;CAGA,IAAI,SAAS,WAAW,GAAG;EACzB,MAAM,QAAQ,OAAO,IAAI,SAAS,EAAE;EACpC,IAAI,OAAO,OAAO;GAAE;GAAO,IAAI,SAAS;GAAI,QAAQ,SAAS;EAAG;EAChE,OAAO;CACT;CAEA,OAAO;AACT;;;ACjHA,eAAsB,gBACpB,QACA,WACkD;CAClD,IAAI,OAAO,WAAW,GACpB,OAAO,CAAC;CAGV,IAAI;EACF,MAAM,iBAAiB,MAAM,YAAY,UACvC,OAAO,KAAK,UAAU,MAAM,EAAE,GAC9B,SACF;EAEA,MAAM,UAAmD,CAAC;EAC1D,KAAA,MAAW,CAAC,SAAS,gBAAgB,gBAAgB;GACnD,MAAM,cAAuC,CAAC;GAC9C,KAAA,MAAW,CAAC,QAAQ,eAAe,aACjC,YAAY,UAAU;GAExB,IAAI,OAAO,KAAK,WAAW,CAAA,CAAE,SAAS,GACpC,QAAQ,WAAW;EAEvB;EAEA,OAAO;CACT,SAAS,OAAO;EACd,IAAI,+BAA+B,KAAK,GACtC,OAAO,CAAC;EAEV,MAAM;CACR;AACF;AAEA,SAAS,+BAA+B,OAAyB;CAC/D,MAAM,UAAU,OAAQ,OAAiB,WAAW,SAAS,EAAE;CAE/D,OACE,QAAQ,SAAS,uBAAuB,KACxC,oCAAoC,KAAK,OAAO,KAChD,4CAA4C,KAAK,OAAO,KACxD,yCAAyC,KAAK,OAAO;AAEzD;;;ACEO,SAAS,uBACd,UACiB;CACjB,MAAM,WAAW,SAAS;CAE1B,OAAO;EACL,IAAI,SAAS,WAAW,GAAG,SAAS,eAAc,GAAI,SAAS;EAC/D,MAAM,UAAU,QAAQ,SAAS;EACjC,YAAY,SAAS;EACrB,WAAW,SAAS;EACpB,YAAY,SAAS;EACrB,OAAO,UAAU;EACjB,aAAa,UAAU;EACvB,QAAQ,SAAS;EACjB,gBAAgB,SAAS;EACzB,aAAa,SAAS;EACtB,MAAM,UAAU;EAEhB,QAAQ,eAAe,SAAS,MAAM;CACxC;AACF"}
1
+ {"version":3,"file":"server.js","names":["result"],"sources":["../src/server/api-routes.ts","../src/server/config-loader.ts","../src/server/data-surface-actions.ts","../src/server/serialization.ts"],"sourcesContent":["/**\n * Server-side API route resolution for SMRT agents\n *\n * Reads agent package manifests and builds a route map from resource\n * paths (e.g., 'performers', 'video-contents') to SmrtObject class\n * names and allowed CRUD actions. The catch-all API handler uses this\n * to resolve incoming requests.\n *\n * @module @happyvertical/smrt-agents/server\n */\n\nimport type { PackageManifest } from './manifest-utils.js';\n\n/**\n * Info about a single API route (one SmrtObject with api.include)\n */\nexport interface AgentAPIRouteInfo {\n /** SmrtObject class name (e.g., 'Performer') */\n className: string;\n /** Allowed CRUD actions (e.g., ['list', 'get', 'create', 'update', 'delete']) */\n allowedActions: string[];\n /** Package that owns this resource */\n packageName?: string;\n}\n\n/**\n * Result of resolving a URL path against the route map\n */\nexport interface ResolvedAPIRoute {\n /** The matched route info */\n route: AgentAPIRouteInfo;\n /** Resource ID if path includes one (e.g., 'performers/abc-123') */\n id?: string;\n /** Custom action name if path includes one (e.g., 'performers/abc-123/generate-image') */\n action?: string;\n}\n\n/**\n * Build a route map from loaded package manifests.\n *\n * Iterates all objects in each manifest, and for any object with a\n * `decoratorConfig.api.include` array, registers a route. The route\n * path is derived from `decoratorConfig.api.path` if set, otherwise\n * from the table name with underscores converted to hyphens.\n *\n * @param manifests - Array of parsed package manifest JSON objects\n * @returns Map of resource path -> route info\n *\n * @example\n * ```typescript\n * const manifests = [histrioManifest, praecoManifest];\n * const routes = buildRouteMap(manifests);\n * // routes.get('performers') => { className: 'Performer', allowedActions: ['list', 'get', 'create', 'update', 'delete'] }\n * // routes.get('video-contents') => { className: 'VideoShot', allowedActions: ['list', 'get', 'create', 'update'] }\n * ```\n */\nexport function buildRouteMap(\n manifests: PackageManifest[],\n): Map<string, AgentAPIRouteInfo> {\n const routes = new Map<string, AgentAPIRouteInfo>();\n\n for (const manifest of manifests) {\n const packageName = (manifest as Record<string, unknown>).packageName as\n | string\n | undefined;\n\n for (const obj of Object.values(manifest.objects)) {\n const config = obj.decoratorConfig as Record<string, unknown> | undefined;\n if (!config) continue;\n\n const api = config.api as\n | { include?: string[]; path?: string }\n | undefined;\n if (!api?.include || api.include.length === 0) continue;\n\n // Derive the URL path: explicit api.path, or table name with _ -> -\n const tableName = config.tableName as string | undefined;\n const path =\n api.path || (tableName ? tableName.replace(/_/g, '-') : null);\n if (!path) continue;\n\n routes.set(path, {\n className: obj.className,\n allowedActions: api.include,\n packageName,\n });\n }\n }\n\n return routes;\n}\n\n/**\n * Resolve a URL resource path against a route map.\n *\n * Handles three URL patterns:\n * - `performers` → list/create (no id)\n * - `performers/abc-123` → get/update/delete (with id)\n * - `performers/abc-123/generate-image` → custom action\n *\n * @param urlPath - The resource portion of the URL (after `/api/agents/{agentId}/`)\n * @param routes - Route map from {@link buildRouteMap}\n * @returns Resolved route with optional id/action, or null if no match\n */\nexport function resolveAPIRoute(\n urlPath: string,\n routes: Map<string, AgentAPIRouteInfo>,\n): ResolvedAPIRoute | null {\n // Normalize: strip leading/trailing slashes\n const normalized = urlPath.replace(/^\\/+|\\/+$/g, '');\n if (!normalized) return null;\n\n const segments = normalized.split('/');\n\n // Try 1-segment: \"performers\"\n if (segments.length === 1) {\n const route = routes.get(segments[0]);\n if (route) return { route };\n return null;\n }\n\n // Try 2-segment: \"performers/{id}\"\n if (segments.length === 2) {\n const route = routes.get(segments[0]);\n if (route) return { route, id: segments[1] };\n return null;\n }\n\n // Try 3-segment: \"performers/{id}/{action}\"\n if (segments.length === 3) {\n const route = routes.get(segments[0]);\n if (route) return { route, id: segments[1], action: segments[2] };\n return null;\n }\n\n return null;\n}\n","/**\n * Server-side agent config loading utilities\n *\n * Loads slot configurations from the agent_configs table for a set of agents.\n * Agent-specific table loading (e.g., praeco_sources) stays in the host app.\n *\n * @module @happyvertical/smrt-agents/server\n */\n\nimport type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { AgentConfig } from '../config.js';\n\n/**\n * Load slot configs for multiple agents from the agent_configs table.\n *\n * Returns a nested map: agentId -> slotId -> configData.\n * Agent-specific tables (e.g., praeco_sources, praeco_reports)\n * are NOT loaded here — those stay in the host application.\n *\n * @param agents - Array of agent identifiers (id + agentClass)\n * @param dbOptions - Database options for SmrtCollection.create()\n * @returns Map of agentId -> slotId -> config data\n */\nexport async function loadSlotConfigs(\n agents: Array<{ id: string; agentClass: string }>,\n dbOptions: SmrtClassOptions,\n): Promise<Record<string, Record<string, unknown>>> {\n if (agents.length === 0) {\n return {};\n }\n\n try {\n const configsByAgent = await AgentConfig.forAgents(\n agents.map((agent) => agent.id),\n dbOptions,\n );\n\n const configs: Record<string, Record<string, unknown>> = {};\n for (const [agentId, slotConfigs] of configsByAgent) {\n const agentConfig: Record<string, unknown> = {};\n for (const [slotId, configData] of slotConfigs) {\n agentConfig[slotId] = configData;\n }\n if (Object.keys(agentConfig).length > 0) {\n configs[agentId] = agentConfig;\n }\n }\n\n return configs;\n } catch (error) {\n if (isMissingAgentConfigTableError(error)) {\n return {};\n }\n throw error;\n }\n}\n\nfunction isMissingAgentConfigTableError(error: unknown): boolean {\n const message = String((error as Error)?.message || error || '');\n\n return (\n message.includes(\"Run 'smrt db:migrate'\") ||\n /no such table[:\\s]+agent_configs/i.test(message) ||\n /relation .*agent_configs.*does not exist/i.test(message) ||\n /table .*agent_configs.*doesn'?t exist/i.test(message)\n );\n}\n","/**\n * Principal-bound preview/apply orchestration for data-surface actions.\n *\n * Browser state is treated only as an input hint. Every preview and apply is\n * executed under the bound principal, resolves the surface and selection\n * afresh, and delegates durable work only after authorization and eligibility\n * checks have passed.\n */\nimport { createHash, randomBytes } from 'node:crypto';\nimport type {\n DataSurfaceActionDescriptor,\n DataSurfaceActionRequest,\n DataSurfaceActionResult,\n DataSurfaceDescriptor,\n DataSurfaceIdentity,\n DataSurfaceJsonObject,\n DataSurfaceJsonValue,\n DataSurfaceRowId,\n DataSurfaceSelectionReference,\n} from '@happyvertical/smrt-ui/data';\nimport {\n type ExecuteAsPrincipalOptions,\n executeAsPrincipal,\n type PrincipalRun,\n} from '../execute-as-principal.js';\n\nexport type DataSurfaceConfirmationPolicy = 'required' | 'none';\nexport type DataSurfaceActionExecution = 'foreground' | 'background';\n\nexport interface DataSurfaceActionEligibility {\n eligible: boolean;\n reason?: string;\n}\n\nexport type DataSurfaceActionPayloadValidation =\n | { valid: true }\n | { valid: false; reason?: string };\n\nexport interface DataSurfaceActionRowOutcome {\n rowId: DataSurfaceRowId;\n status: 'accepted' | 'skipped' | 'failed';\n reason?: string;\n}\n\nexport interface ResolvedDataSurfaceSelection {\n /** Fresh server-side revision of the selected surface/query. */\n revision: number;\n /** Canonical fingerprint of the frozen query represented by the selection. */\n queryFingerprint: string;\n /** Authoritatively resolved row ids. Browser-provided ids are only hints. */\n rowIds: DataSurfaceRowId[];\n}\n\nexport interface DataSurfaceActionInvocation {\n run: PrincipalRun;\n request: DataSurfaceServerActionRequest;\n descriptor: DataSurfaceDescriptor;\n action: DataSurfaceServerActionDefinition;\n selection: ResolvedDataSurfaceSelection;\n}\n\nexport interface DataSurfaceServerActionDefinition {\n descriptor: DataSurfaceActionDescriptor;\n /** Serializable declaration for transport/schema generators; null means no input. */\n inputSchema: DataSurfaceJsonObject | null;\n /** Runtime enforcement for the declared schema; absence is never permissive. */\n validatePayload(\n payload: DataSurfaceJsonValue | undefined,\n ):\n | DataSurfaceActionPayloadValidation\n | Promise<DataSurfaceActionPayloadValidation>;\n /** Explicit for every action, including sensitive/public/destructive ones. */\n confirmation: DataSurfaceConfirmationPolicy;\n execution: DataSurfaceActionExecution;\n /** Fail-closed persona capability checked by PrincipalRun. */\n tool: string;\n /** Explicit RBAC catalog gate, enforced independently of callback convention. */\n operation: {\n id: string;\n collection: Parameters<PrincipalRun['assertOperation']>[0];\n action: string;\n };\n /** Fresh permission/domain authorization check, run for preview and apply. */\n authorize(\n invocation: DataSurfaceActionInvocation,\n ): boolean | Promise<boolean>;\n /** Fresh per-row domain precondition check, repeated at apply time. */\n eligible(\n invocation: DataSurfaceActionInvocation,\n rowId: DataSurfaceRowId,\n ): DataSurfaceActionEligibility | Promise<DataSurfaceActionEligibility>;\n /** Foreground mutation. Background definitions are run by the injected queue. */\n apply(\n invocation: DataSurfaceActionInvocation,\n rowId: DataSurfaceRowId,\n ):\n | undefined\n | DataSurfaceJsonValue\n | Promise<undefined | DataSurfaceJsonValue>;\n}\n\nexport interface ResolvedDataSurfaceActions {\n descriptor: DataSurfaceDescriptor;\n /** Current server-side revision, never trusted from the browser. */\n revision: number;\n actions: Record<string, DataSurfaceServerActionDefinition>;\n}\n\nexport interface DataSurfaceServerActionRequest\n extends DataSurfaceActionRequest {\n /** Required on apply and bound into the preview token. */\n expectedRevision: number;\n /** Required on apply. Identical retries replay the first terminal result. */\n idempotencyKey?: string;\n}\n\nexport interface DataSurfaceActionContext {\n principal: ExecuteAsPrincipalOptions;\n}\n\nexport interface DataSurfaceBackgroundActionJob {\n idempotencyKey: string;\n identity: DataSurfaceIdentity;\n actionId: string;\n rowIds: DataSurfaceRowId[];\n /**\n * The queue must call this task to perform the work. It re-enters the bound\n * principal and repeats descriptor, authorization, selection, and eligibility\n * checks before any mutation.\n */\n run: () => Promise<DataSurfaceActionResult>;\n}\n\nexport interface DataSurfaceBackgroundQueue {\n enqueue(\n job: DataSurfaceBackgroundActionJob,\n ): Promise<{ jobId: string; details?: DataSurfaceJsonObject }>;\n}\n\nexport interface DataSurfacePreviewTokenRecord {\n expiresAt: number;\n actorUserId: string;\n tenantId: string | null;\n onBehalfOfUserId: string | null;\n actsAsProfileId: string | null;\n identityKey: string;\n actionId: string;\n actionFingerprint: string;\n revision: number;\n queryFingerprint: string;\n selectionFingerprint: string;\n resolvedRowsFingerprint: string;\n requestFingerprint: string;\n consumedBy?: string;\n}\n\nexport type DataSurfaceIdempotencyRecord =\n | {\n status: 'reserved';\n requestFingerprint: string;\n ownerToken: string;\n reservedAt: number;\n }\n | {\n status: 'completed';\n requestFingerprint: string;\n result: DataSurfaceActionResult;\n };\n\nexport interface DataSurfaceIdempotencyReservation {\n requestFingerprint: string;\n ownerToken: string;\n reservedAt: number;\n}\n\nexport interface DataSurfaceActionStateStore {\n putToken(\n token: string,\n record: DataSurfacePreviewTokenRecord,\n ): Promise<void> | void;\n getToken(\n token: string,\n ):\n | Promise<DataSurfacePreviewTokenRecord | undefined>\n | DataSurfacePreviewTokenRecord\n | undefined;\n markTokenConsumed(\n token: string,\n idempotencyKey: string,\n ): Promise<boolean> | boolean;\n getIdempotency(\n key: string,\n ):\n | Promise<DataSurfaceIdempotencyRecord | undefined>\n | DataSurfaceIdempotencyRecord\n | undefined;\n /** Atomically create a durable reservation or return the existing record. */\n reserveIdempotency(\n key: string,\n reservation: DataSurfaceIdempotencyReservation,\n ): Promise<DataSurfaceIdempotencyRecord> | DataSurfaceIdempotencyRecord;\n completeIdempotency(\n key: string,\n ownerToken: string,\n result: DataSurfaceActionResult,\n ): Promise<boolean> | boolean;\n releaseIdempotency(\n key: string,\n ownerToken: string,\n ): Promise<boolean> | boolean;\n}\n\n/** Explicit single-process/testing store; production callers inject shared state. */\nexport class InMemoryDataSurfaceActionStateStore\n implements DataSurfaceActionStateStore\n{\n private readonly tokens = new Map<string, DataSurfacePreviewTokenRecord>();\n private readonly idempotency = new Map<\n string,\n DataSurfaceIdempotencyRecord\n >();\n\n putToken(token: string, record: DataSurfacePreviewTokenRecord): void {\n this.tokens.set(token, record);\n }\n\n getToken(token: string): DataSurfacePreviewTokenRecord | undefined {\n return this.tokens.get(token);\n }\n\n markTokenConsumed(token: string, idempotencyKey: string): boolean {\n const record = this.tokens.get(token);\n if (!record) return false;\n if (record.consumedBy && record.consumedBy !== idempotencyKey) return false;\n record.consumedBy = idempotencyKey;\n return true;\n }\n\n getIdempotency(key: string): DataSurfaceIdempotencyRecord | undefined {\n return this.idempotency.get(key);\n }\n\n reserveIdempotency(\n key: string,\n reservation: DataSurfaceIdempotencyReservation,\n ): DataSurfaceIdempotencyRecord {\n const existing = this.idempotency.get(key);\n if (existing) return existing;\n const record: DataSurfaceIdempotencyRecord = {\n status: 'reserved',\n ...reservation,\n };\n this.idempotency.set(key, record);\n return record;\n }\n\n completeIdempotency(\n key: string,\n ownerToken: string,\n result: DataSurfaceActionResult,\n ): boolean {\n const existing = this.idempotency.get(key);\n if (existing?.status !== 'reserved' || existing.ownerToken !== ownerToken)\n return false;\n this.idempotency.set(key, {\n status: 'completed',\n requestFingerprint: existing.requestFingerprint,\n result,\n });\n return true;\n }\n\n releaseIdempotency(key: string, ownerToken: string): boolean {\n const existing = this.idempotency.get(key);\n if (existing?.status !== 'reserved' || existing.ownerToken !== ownerToken)\n return false;\n return this.idempotency.delete(key);\n }\n}\n\nexport interface DataSurfaceActionAdapterOptions {\n resolveSurface(\n run: PrincipalRun,\n identity: DataSurfaceIdentity,\n ): Promise<ResolvedDataSurfaceActions>;\n resolveSelection(\n invocation: Omit<DataSurfaceActionInvocation, 'selection'>,\n selection: DataSurfaceSelectionReference,\n ): Promise<ResolvedDataSurfaceSelection>;\n backgroundQueue?: DataSurfaceBackgroundQueue;\n /** Required durable, shared backend in production; memory storage is opt-in. */\n state: DataSurfaceActionStateStore;\n tokenTtlMs?: number;\n now?: () => number;\n createToken?: () => string;\n runAsPrincipal?: typeof executeAsPrincipal;\n idempotencyPollIntervalMs?: number;\n idempotencyWaitTimeoutMs?: number;\n}\n\nexport interface DataSurfaceActionAdapter {\n preview(\n request: DataSurfaceServerActionRequest,\n context: DataSurfaceActionContext,\n ): Promise<DataSurfaceActionResult>;\n apply(\n request: DataSurfaceServerActionRequest,\n context: DataSurfaceActionContext,\n ): Promise<DataSurfaceActionResult>;\n}\n\nconst DEFAULT_TOKEN_TTL_MS = 5 * 60 * 1_000;\nconst MAX_IDENTIFIER_LENGTH = 256;\nconst MAX_JSON_DEPTH = 16;\nconst MAX_JSON_ITEMS = 1_000;\nconst FORBIDDEN_JSON_KEYS = new Set(['__proto__', 'constructor', 'prototype']);\n\nfunction isBoundedJsonValue(\n value: unknown,\n depth = 0,\n seen = new Set<object>(),\n): value is DataSurfaceJsonValue {\n if (value === null) return true;\n if (['string', 'boolean'].includes(typeof value)) return true;\n if (typeof value === 'number') return Number.isFinite(value);\n if (typeof value !== 'object' || depth >= MAX_JSON_DEPTH || seen.has(value))\n return false;\n seen.add(value);\n if (Array.isArray(value)) {\n if (value.length > MAX_JSON_ITEMS) return false;\n return value.every((item) => isBoundedJsonValue(item, depth + 1, seen));\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) return false;\n const entries = Object.entries(value);\n if (entries.length > MAX_JSON_ITEMS) return false;\n return entries.every(\n ([key, item]) =>\n !FORBIDDEN_JSON_KEYS.has(key) &&\n isBoundedJsonValue(item, depth + 1, seen),\n );\n}\n\nfunction validIdentifier(value: unknown): value is string {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= MAX_IDENTIFIER_LENGTH\n );\n}\n\nfunction validSelection(\n selection: unknown,\n): selection is DataSurfaceSelectionReference {\n if (!selection || typeof selection !== 'object') return false;\n const candidate = selection as Record<string, unknown>;\n if (candidate.scope === 'current-page') return true;\n if (candidate.scope === 'all-matching')\n return validIdentifier(candidate.queryFingerprint);\n if (candidate.scope !== 'explicit-ids' || !Array.isArray(candidate.rowIds))\n return false;\n if (candidate.rowIds.length > MAX_JSON_ITEMS) return false;\n return candidate.rowIds.every(\n (rowId) =>\n (typeof rowId === 'string' && rowId.length > 0) ||\n (typeof rowId === 'number' && Number.isFinite(rowId)),\n );\n}\n\nfunction stable(value: unknown): string {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;\n return `{${Object.entries(value as Record<string, unknown>)\n .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))\n .map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`)\n .join(',')}}`;\n}\n\nfunction fingerprint(value: unknown): string {\n return createHash('sha256').update(stable(value)).digest('hex');\n}\n\nfunction identityKey(identity: DataSurfaceIdentity): string {\n return stable(canonicalIdentity(identity));\n}\n\nfunction canonicalIdentity(identity: DataSurfaceIdentity): DataSurfaceIdentity {\n return {\n kind: identity.kind,\n surfaceId: identity.surfaceId,\n ...(identity.subject\n ? {\n subject: {\n type: identity.subject.type,\n id: identity.subject.id,\n },\n }\n : {}),\n };\n}\n\nfunction rowIdKey(rowId: DataSurfaceRowId): string {\n return `${typeof rowId}:${String(rowId)}`;\n}\n\nfunction compareRowIds(\n left: DataSurfaceRowId,\n right: DataSurfaceRowId,\n): number {\n if (typeof left !== typeof right) return typeof left === 'number' ? -1 : 1;\n if (typeof left === 'number' && typeof right === 'number')\n return left - right;\n return left < right ? -1 : left > right ? 1 : 0;\n}\n\nfunction canonicalRowIds(\n rowIds: readonly DataSurfaceRowId[],\n): DataSurfaceRowId[] {\n const ids = new Map<string, DataSurfaceRowId>();\n for (const rowId of rowIds) ids.set(rowIdKey(rowId), rowId);\n return [...ids.values()].sort(compareRowIds);\n}\n\nfunction canonicalSelection(\n selection: DataSurfaceSelectionReference,\n): DataSurfaceSelectionReference {\n if (selection.scope !== 'explicit-ids') return selection;\n return { scope: selection.scope, rowIds: canonicalRowIds(selection.rowIds) };\n}\n\nfunction requestFingerprint(request: DataSurfaceServerActionRequest): string {\n return fingerprint({\n identity: canonicalIdentity(request.identity),\n actionId: request.actionId,\n selection: canonicalSelection(request.selection),\n payload: request.payload,\n expectedRevision: request.expectedRevision,\n });\n}\n\nfunction actionFingerprint(action: DataSurfaceServerActionDefinition): string {\n return fingerprint({\n descriptor: action.descriptor,\n inputSchema: action.inputSchema,\n confirmation: action.confirmation,\n execution: action.execution,\n tool: action.tool,\n operationId: action.operation.id,\n operationCollection: action.operation.collection,\n operationAction: action.operation.action,\n });\n}\n\nfunction result(\n request: DataSurfaceServerActionRequest,\n ok: boolean,\n reason?: string,\n details?: DataSurfaceJsonObject,\n confirmationToken?: string,\n): DataSurfaceActionResult {\n return {\n version: 1,\n requestId: request.requestId,\n identity: request.identity,\n actionId: request.actionId,\n phase: request.phase,\n ok,\n ...(reason ? { reason } : {}),\n ...(details ? { details } : {}),\n ...(confirmationToken ? { confirmationToken } : {}),\n };\n}\n\nfunction outcomesDetails(\n outcomes: DataSurfaceActionRowOutcome[],\n extra: DataSurfaceJsonObject = {},\n): DataSurfaceJsonObject {\n const accepted = outcomes.filter(\n ({ status }) => status === 'accepted',\n ).length;\n const skipped = outcomes.filter(({ status }) => status === 'skipped').length;\n const failed = outcomes.filter(({ status }) => status === 'failed').length;\n return {\n accepted,\n skipped,\n failed,\n outcomes: outcomes.map(({ rowId, status, reason }) => ({\n rowId,\n status,\n ...(reason ? { reason } : {}),\n })),\n ...extra,\n };\n}\n\nfunction validateRequest(\n request: DataSurfaceServerActionRequest,\n phase: 'preview' | 'apply',\n): string | undefined {\n if (!request || typeof request !== 'object') return 'invalid_request';\n if (request.version !== 1 || request.phase !== phase)\n return 'invalid_request';\n if (\n !validIdentifier(request.requestId) ||\n !validIdentifier(request.actionId) ||\n !validIdentifier(request.identity?.surfaceId) ||\n !['table', 'list', 'report', 'custom'].includes(request.identity?.kind) ||\n !validSelection(request.selection) ||\n (request.payload !== undefined && !isBoundedJsonValue(request.payload))\n )\n return 'invalid_request';\n if (\n !Number.isSafeInteger(request.expectedRevision) ||\n request.expectedRevision < 0\n )\n return 'invalid_request';\n if (\n phase === 'apply' &&\n (!validIdentifier(request.idempotencyKey) ||\n (request.confirmationToken !== undefined &&\n !validIdentifier(request.confirmationToken)))\n )\n return 'invalid_request';\n return undefined;\n}\n\n/** Create a transport-neutral, principal-bound data-surface action adapter. */\nexport function createDataSurfaceActionAdapter(\n options: DataSurfaceActionAdapterOptions,\n): DataSurfaceActionAdapter {\n const state = options.state;\n const now = options.now ?? Date.now;\n const createToken =\n options.createToken ?? (() => randomBytes(32).toString('base64url'));\n const tokenTtlMs = options.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;\n const runAsPrincipal = options.runAsPrincipal ?? executeAsPrincipal;\n const idempotencyPollIntervalMs = Math.max(\n 1,\n options.idempotencyPollIntervalMs ?? 10,\n );\n const idempotencyWaitTimeoutMs = Math.max(\n 0,\n options.idempotencyWaitTimeoutMs ?? 5_000,\n );\n\n async function resolveInvocation(\n request: DataSurfaceServerActionRequest,\n run: PrincipalRun,\n ): Promise<DataSurfaceActionInvocation | DataSurfaceActionResult> {\n const surface = await options.resolveSurface(run, request.identity);\n if (\n identityKey(surface.descriptor.identity) !== identityKey(request.identity)\n ) {\n return result(request, false, 'not_found');\n }\n const action = surface.actions[request.actionId];\n const declared = surface.descriptor.actions.find(\n ({ id }) => id === request.actionId,\n );\n if (\n !action ||\n !declared ||\n action.descriptor.id !== declared.id ||\n Boolean(declared.requiresConfirmation) !==\n (action.confirmation === 'required')\n ) {\n return result(request, false, 'unsupported');\n }\n if (\n !action.tool ||\n !validIdentifier(action.operation?.id) ||\n !validIdentifier(action.operation?.action)\n )\n return result(request, false, 'denied');\n run.assertToolAllowed(action.tool);\n await run.assertOperation(\n action.operation.collection,\n action.operation.action,\n );\n const payloadValidation = await action.validatePayload(request.payload);\n if (!payloadValidation.valid)\n return result(\n request,\n false,\n payloadValidation.reason ?? 'invalid_payload',\n );\n if (!action.descriptor.selectionScopes.includes(request.selection.scope)) {\n return result(request, false, 'selection_not_supported');\n }\n const base = {\n run,\n request,\n descriptor: surface.descriptor,\n action,\n };\n const resolvedSelection = await options.resolveSelection(\n base,\n canonicalSelection(request.selection),\n );\n const selection = {\n ...resolvedSelection,\n rowIds: canonicalRowIds(resolvedSelection.rowIds),\n };\n const invocation = { ...base, selection };\n if (!(await action.authorize(invocation))) {\n return result(request, false, 'denied');\n }\n if (selection.rowIds.length > surface.descriptor.limits.maxSelectionSize) {\n return result(request, false, 'limit_exceeded');\n }\n return invocation;\n }\n\n async function preview(\n request: DataSurfaceServerActionRequest,\n context: DataSurfaceActionContext,\n ): Promise<DataSurfaceActionResult> {\n const invalid = validateRequest(request, 'preview');\n if (invalid) return result(request, false, invalid);\n return runAsPrincipal(\n {\n ...context.principal,\n action: 'data_surface.action.preview',\n auditMetadata: {\n ...context.principal.auditMetadata,\n surfaceId: request.identity.surfaceId,\n actionId: request.actionId,\n requestId: request.requestId,\n },\n },\n async (run) => {\n const invocation = await resolveInvocation(request, run);\n if ('ok' in invocation) return invocation;\n if (invocation.selection.revision !== request.expectedRevision) {\n return result(request, false, 'stale_revision');\n }\n const outcomes: DataSurfaceActionRowOutcome[] = [];\n for (const rowId of invocation.selection.rowIds) {\n const eligibility = await invocation.action.eligible(\n invocation,\n rowId,\n );\n outcomes.push({\n rowId,\n status: eligibility.eligible ? 'accepted' : 'skipped',\n ...(eligibility.reason ? { reason: eligibility.reason } : {}),\n });\n }\n const confirmationToken = createToken();\n const selectionFingerprint = fingerprint(\n canonicalSelection(request.selection),\n );\n const requestFingerprintValue = requestFingerprint(request);\n const expiresAt = now() + tokenTtlMs;\n await state.putToken(confirmationToken, {\n expiresAt,\n actorUserId: context.principal.principal.runAsUserId,\n tenantId: context.principal.principal.tenantId,\n onBehalfOfUserId: context.principal.onBehalfOfUserId ?? null,\n actsAsProfileId: context.principal.principal.actsAsProfileId ?? null,\n identityKey: identityKey(request.identity),\n actionId: request.actionId,\n actionFingerprint: actionFingerprint(invocation.action),\n revision: invocation.selection.revision,\n queryFingerprint: invocation.selection.queryFingerprint,\n selectionFingerprint,\n resolvedRowsFingerprint: fingerprint(\n canonicalRowIds(invocation.selection.rowIds),\n ),\n requestFingerprint: requestFingerprintValue,\n });\n return result(\n request,\n true,\n undefined,\n outcomesDetails(outcomes, {\n count: invocation.selection.rowIds.length,\n revision: invocation.selection.revision,\n queryFingerprint: invocation.selection.queryFingerprint,\n expiresAt,\n }),\n confirmationToken,\n );\n },\n );\n }\n\n async function executeForeground(\n request: DataSurfaceServerActionRequest,\n invocation: DataSurfaceActionInvocation,\n ): Promise<DataSurfaceActionResult> {\n const outcomes: DataSurfaceActionRowOutcome[] = [];\n for (const rowId of invocation.selection.rowIds) {\n try {\n const eligibility = await invocation.action.eligible(invocation, rowId);\n if (!eligibility.eligible) {\n outcomes.push({\n rowId,\n status: 'skipped',\n ...(eligibility.reason ? { reason: eligibility.reason } : {}),\n });\n continue;\n }\n await invocation.action.apply(invocation, rowId);\n outcomes.push({ rowId, status: 'accepted' });\n } catch {\n outcomes.push({\n rowId,\n status: 'failed',\n reason: 'execution_failed',\n });\n }\n }\n return result(request, true, undefined, outcomesDetails(outcomes));\n }\n\n async function executeBackgroundOnce(\n request: DataSurfaceServerActionRequest,\n context: DataSurfaceActionContext,\n token: DataSurfacePreviewTokenRecord | undefined,\n ): Promise<DataSurfaceActionResult> {\n const ownerToken = randomBytes(16).toString('base64url');\n const executionFingerprint = fingerprint({\n kind: 'background-execution',\n request: token?.requestFingerprint ?? requestFingerprint(request),\n action: token?.actionFingerprint ?? request.actionId,\n });\n const executionScope = fingerprint({\n kind: 'background-execution',\n actorUserId:\n token?.actorUserId ?? context.principal.principal.runAsUserId,\n tenantId: token?.tenantId ?? context.principal.principal.tenantId,\n onBehalfOfUserId:\n token?.onBehalfOfUserId ?? context.principal.onBehalfOfUserId ?? null,\n actsAsProfileId:\n token?.actsAsProfileId ??\n context.principal.principal.actsAsProfileId ??\n null,\n identity: canonicalIdentity(request.identity),\n actionId: request.actionId,\n idempotencyKey: request.idempotencyKey,\n });\n const maxPolls = Math.max(\n 1,\n Math.ceil(idempotencyWaitTimeoutMs / idempotencyPollIntervalMs),\n );\n for (let poll = 0; poll <= maxPolls; poll += 1) {\n const winner = await state.reserveIdempotency(executionScope, {\n requestFingerprint: executionFingerprint,\n ownerToken,\n reservedAt: now(),\n });\n if (winner.requestFingerprint !== executionFingerprint)\n return result(request, false, 'idempotency_conflict');\n if (winner.status === 'completed') return winner.result;\n if (winner.ownerToken === ownerToken) {\n let executed: DataSurfaceActionResult;\n try {\n executed = await authorizedApply(request, context, token, false);\n } catch (error) {\n await state.releaseIdempotency(executionScope, ownerToken);\n throw error;\n }\n if (\n !(await state.completeIdempotency(\n executionScope,\n ownerToken,\n executed,\n ))\n ) {\n throw new Error('Lost background action idempotency reservation');\n }\n return executed;\n }\n if (poll < maxPolls) {\n await new Promise<void>((resolve) =>\n setTimeout(resolve, idempotencyPollIntervalMs),\n );\n const current = await state.getIdempotency(executionScope);\n if (current?.status === 'completed') return current.result;\n }\n }\n return result(request, false, 'idempotency_in_progress');\n }\n\n async function authorizedApply(\n request: DataSurfaceServerActionRequest,\n context: DataSurfaceActionContext,\n token: DataSurfacePreviewTokenRecord | undefined,\n allowBackground: boolean,\n ): Promise<DataSurfaceActionResult> {\n const idempotencyKey = request.idempotencyKey;\n if (!idempotencyKey) return result(request, false, 'invalid_request');\n return runAsPrincipal(\n {\n ...context.principal,\n action: 'data_surface.action.apply',\n auditMetadata: {\n ...context.principal.auditMetadata,\n surfaceId: request.identity.surfaceId,\n actionId: request.actionId,\n requestId: request.requestId,\n idempotencyKey: request.idempotencyKey,\n },\n },\n async (run) => {\n const invocation = await resolveInvocation(request, run);\n if ('ok' in invocation) return invocation;\n if (token) {\n if (\n invocation.selection.revision !== token.revision ||\n invocation.selection.revision !== request.expectedRevision ||\n invocation.selection.queryFingerprint !== token.queryFingerprint ||\n fingerprint(canonicalSelection(request.selection)) !==\n token.selectionFingerprint ||\n actionFingerprint(invocation.action) !== token.actionFingerprint ||\n fingerprint(canonicalRowIds(invocation.selection.rowIds)) !==\n token.resolvedRowsFingerprint\n ) {\n return result(request, false, 'stale_preview');\n }\n } else if (invocation.action.confirmation === 'required') {\n return result(request, false, 'confirmation_required');\n } else if (invocation.selection.revision !== request.expectedRevision) {\n return result(request, false, 'stale_revision');\n }\n if (invocation.action.execution === 'background' && allowBackground) {\n if (!options.backgroundQueue) {\n return result(request, false, 'background_unavailable');\n }\n const queued = await options.backgroundQueue.enqueue({\n idempotencyKey,\n identity: request.identity,\n actionId: request.actionId,\n rowIds: invocation.selection.rowIds,\n run: () => executeBackgroundOnce(request, context, token),\n });\n return result(request, true, undefined, {\n accepted: invocation.selection.rowIds.length,\n skipped: 0,\n failed: 0,\n background: true,\n jobId: queued.jobId,\n ...(queued.details ?? {}),\n });\n }\n return executeForeground(request, invocation);\n },\n );\n }\n\n async function apply(\n request: DataSurfaceServerActionRequest,\n context: DataSurfaceActionContext,\n ): Promise<DataSurfaceActionResult> {\n const invalid = validateRequest(request, 'apply');\n if (invalid) return result(request, false, invalid);\n const confirmationToken = request.confirmationToken;\n const idempotencyKey = request.idempotencyKey;\n if (!idempotencyKey) return result(request, false, 'invalid_request');\n const actorUserId = context.principal.principal.runAsUserId;\n const tenantId = context.principal.principal.tenantId;\n const onBehalfOfUserId = context.principal.onBehalfOfUserId ?? null;\n const actsAsProfileId = context.principal.principal.actsAsProfileId ?? null;\n const requestFingerprintValue = requestFingerprint(request);\n const idempotencyScope = fingerprint({\n actorUserId,\n tenantId,\n onBehalfOfUserId,\n actsAsProfileId,\n identity: canonicalIdentity(request.identity),\n actionId: request.actionId,\n idempotencyKey,\n });\n const prior = await state.getIdempotency(idempotencyScope);\n if (prior && prior.requestFingerprint !== requestFingerprintValue)\n return result(request, false, 'idempotency_conflict');\n // A completed durable result is safe to replay from its actor/tenant-bound\n // idempotency scope even when the one-time confirmation has expired.\n if (prior?.status === 'completed') return prior.result;\n\n let token: DataSurfacePreviewTokenRecord | undefined;\n if (confirmationToken) {\n token = await state.getToken(confirmationToken);\n if (!token || token.expiresAt <= now()) {\n return result(request, false, 'invalid_or_expired_confirmation');\n }\n if (\n token.actorUserId !== actorUserId ||\n token.tenantId !== tenantId ||\n token.onBehalfOfUserId !== onBehalfOfUserId ||\n token.actsAsProfileId !== actsAsProfileId ||\n token.identityKey !== identityKey(request.identity) ||\n token.actionId !== request.actionId ||\n token.requestFingerprint !== requestFingerprintValue\n ) {\n return result(request, false, 'confirmation_mismatch');\n }\n if (!(await state.markTokenConsumed(confirmationToken, idempotencyKey))) {\n return result(request, false, 'confirmation_replayed');\n }\n }\n // Ownership is an internal compare-and-set nonce. Keep it independent of\n // the injectable preview-token factory, which tests or callers may make\n // deterministic without weakening concurrent winner selection.\n const ownerToken = randomBytes(16).toString('base64url');\n const maxPolls = Math.max(\n 1,\n Math.ceil(idempotencyWaitTimeoutMs / idempotencyPollIntervalMs),\n );\n for (let poll = 0; poll <= maxPolls; poll += 1) {\n const winner = await state.reserveIdempotency(idempotencyScope, {\n requestFingerprint: requestFingerprintValue,\n ownerToken,\n reservedAt: now(),\n });\n if (winner.requestFingerprint !== requestFingerprintValue)\n return result(request, false, 'idempotency_conflict');\n if (winner.status === 'completed') return winner.result;\n if (winner.ownerToken === ownerToken) {\n let applied: DataSurfaceActionResult;\n try {\n applied = await authorizedApply(request, context, token, true);\n } catch (error) {\n await state.releaseIdempotency(idempotencyScope, ownerToken);\n throw error;\n }\n // A confirmation-required request without a token is a recoverable\n // precondition failure. Do not consume its idempotency key: the caller\n // may preview and retry with the same key.\n if (!applied.ok && applied.reason === 'confirmation_required') {\n await state.releaseIdempotency(idempotencyScope, ownerToken);\n return applied;\n }\n // Once execution returns, never release on a persistence failure: a\n // durable reservation is safer than allowing duplicate side effects.\n if (\n !(await state.completeIdempotency(\n idempotencyScope,\n ownerToken,\n applied,\n ))\n ) {\n throw new Error('Lost data-surface idempotency reservation');\n }\n return applied;\n }\n if (poll < maxPolls) {\n await new Promise<void>((resolve) =>\n setTimeout(resolve, idempotencyPollIntervalMs),\n );\n const current = await state.getIdempotency(idempotencyScope);\n if (current?.status === 'completed') return current.result;\n }\n }\n return result(request, false, 'idempotency_in_progress');\n }\n\n return { preview, apply };\n}\n","/**\n * Serialization utilities for resolved agents\n *\n * Converts ResolvedAgentAvailability (database + manifest data) into\n * a JSON-safe shape suitable for passing to client components.\n *\n * @module @happyvertical/smrt-agents/server\n */\n\nimport { sanitizeConfig } from '@happyvertical/smrt-config';\nimport type { ResolvedAgentAvailability } from '../tenant-agent.js';\nimport type { AgentAdminRoute, AgentUISlots } from '../ui.js';\n\n/**\n * Serialized agent data for passing to client components.\n *\n * Includes manifest-derived fields (icon, permissions, slots)\n * alongside resolution metadata (source, sourceTenantId).\n */\nexport interface SerializedAgent {\n /** Agent instance ID, or a synthetic key if no instance exists */\n id: string;\n /** Human-readable name from manifest */\n name?: string;\n /** Human-readable agent class name (e.g., 'Praeco') */\n agentClass: string;\n /** Canonical agent type (qualified name when available) */\n agentType: string;\n /** STI type discriminator (same as agentType) */\n _meta_type?: string;\n /** UI slot definitions from manifest */\n slots?: AgentUISlots;\n /** Admin route declarations from manifest */\n adminRoutes?: AgentAdminRoute[];\n /** How this agent was resolved for the tenant */\n source?: 'explicit' | 'inherited';\n /** Which tenant the binding came from */\n sourceTenantId?: string;\n /** Merged permissions from manifest + tenant overrides */\n permissions?: Record<string, boolean>;\n /** Agent icon from manifest */\n icon?: string;\n /**\n * Tenant-level config overrides, **secret-sanitized** for client transport.\n *\n * SECURITY (#1553, follow-up to #1552): the raw `TenantAgent.config` is the\n * tenant's own override blob and is `@field({ sensitive: true })` (stripped\n * from the generated CRUD api/mcp surfaces). This hand-written admin\n * serialization runs it through `sanitizeConfig()` from\n * `@happyvertical/smrt-config` before it leaves the server, so secret-shaped\n * keys (apiKey/token/password/…) are dropped and secret-shaped values\n * (`sk-…`, `AKIA…`, `Bearer …`, URL credentials, PEM blocks) are masked —\n * non-secret config still reaches the authorized admin UI for display.\n *\n * This is **display-only**: do not edit-round-trip it back to the server\n * (a masked value would overwrite the real secret). Best practice remains to\n * reference secrets by id via `@happyvertical/smrt-secrets` so only an opaque\n * handle is ever stored in tenant config.\n */\n config?: Record<string, unknown>;\n}\n\n/**\n * Convert a ResolvedAgentAvailability to a serializable shape for the UI.\n *\n * @param resolved - Output from TenantAgentCollection.resolveForTenant()\n * @returns Serialized agent data safe for JSON transport\n */\nexport function serializeResolvedAgent(\n resolved: ResolvedAgentAvailability,\n): SerializedAgent {\n const manifest = resolved.manifest;\n\n return {\n id: resolved.agentId || `${resolved.sourceTenantId}:${resolved.agentType}`,\n name: manifest?.name || resolved.agentClass,\n agentClass: resolved.agentClass,\n agentType: resolved.agentType,\n _meta_type: resolved.agentType,\n slots: manifest?.uiSlots as AgentUISlots | undefined,\n adminRoutes: manifest?.adminRoutes as AgentAdminRoute[] | undefined,\n source: resolved.source,\n sourceTenantId: resolved.sourceTenantId,\n permissions: resolved.permissions,\n icon: manifest?.icon,\n // Secret-sanitize before the blob crosses into the client payload (#1553).\n config: sanitizeConfig(resolved.config) as SerializedAgent['config'],\n };\n}\n"],"mappings":"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"}