@agent-native/core 0.203.2-nightly-20261007213109 → 0.204.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.
Files changed (121) hide show
  1. package/dist/a2a/handlers.js +57 -1
  2. package/dist/a2a/server.js +37 -1
  3. package/dist/action.d.ts +1 -0
  4. package/dist/action.js +26 -3
  5. package/dist/agent/contain-principal.d.ts +9 -0
  6. package/dist/agent/contain-principal.js +53 -0
  7. package/dist/agent/production-agent.js +32 -2
  8. package/dist/agent/run-manager.js +14 -2
  9. package/dist/agent/run-store.js +66 -12
  10. package/dist/audit/actions/export-audit-events.d.ts +1 -1
  11. package/dist/audit/actions/export-audit-events.js +2 -2
  12. package/dist/audit/actions/export-audit-ocsf.d.ts +11 -0
  13. package/dist/audit/actions/export-audit-ocsf.js +183 -0
  14. package/dist/audit/actions/list-audit-events.d.ts +1 -1
  15. package/dist/audit/actions/list-audit-events.js +3 -3
  16. package/dist/audit/config.d.ts +1 -1
  17. package/dist/audit/config.js +5 -1
  18. package/dist/audit/index.d.ts +1 -0
  19. package/dist/audit/index.js +1 -0
  20. package/dist/audit/ocsf.d.ts +70 -0
  21. package/dist/audit/ocsf.js +228 -0
  22. package/dist/audit/record.js +1 -1
  23. package/dist/audit/store.js +9 -2
  24. package/dist/audit/types.d.ts +12 -1
  25. package/dist/cli/agent.js +2 -11
  26. package/dist/cli/connect.js +7 -2
  27. package/dist/client/use-db-sync.d.ts +13 -0
  28. package/dist/client/use-db-sync.js +22 -1
  29. package/dist/coding-tools/run-code.js +22 -1
  30. package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
  31. package/dist/collab/client.js +36 -3
  32. package/dist/collab/routes.d.ts +9 -4
  33. package/dist/collab/routes.js +15 -1
  34. package/dist/collab/struct-routes.d.ts +1 -1
  35. package/dist/deploy/build.js +15 -8
  36. package/dist/deploy/workspace-deploy.js +88 -13
  37. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  38. package/dist/file-upload/builder.js +18 -4
  39. package/dist/framework-tools.js +3 -0
  40. package/dist/identity/rekey.js +6 -0
  41. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  42. package/dist/localization/core-messages/supplemental/ar-SA.js +15 -0
  43. package/dist/localization/core-messages/supplemental/de-DE.js +15 -0
  44. package/dist/localization/core-messages/supplemental/en-US.js +15 -0
  45. package/dist/localization/core-messages/supplemental/es-ES.js +15 -0
  46. package/dist/localization/core-messages/supplemental/fr-FR.js +15 -0
  47. package/dist/localization/core-messages/supplemental/hi-IN.js +15 -0
  48. package/dist/localization/core-messages/supplemental/ja-JP.js +15 -0
  49. package/dist/localization/core-messages/supplemental/ko-KR.js +15 -0
  50. package/dist/localization/core-messages/supplemental/pt-BR.js +15 -0
  51. package/dist/localization/core-messages/supplemental/zh-CN.js +15 -0
  52. package/dist/localization/core-messages/supplemental/zh-TW.js +15 -0
  53. package/dist/localization/mcp-settings-messages.d.ts +15 -0
  54. package/dist/mcp/actions/create-org-service-token.d.ts +8 -0
  55. package/dist/mcp/actions/create-org-service-token.js +93 -4
  56. package/dist/mcp/actions/list-org-service-tokens.d.ts +2 -0
  57. package/dist/mcp/actions/list-org-service-tokens.js +36 -8
  58. package/dist/mcp/actions/service-principal-input.d.ts +48 -0
  59. package/dist/mcp/actions/service-principal-input.js +102 -0
  60. package/dist/mcp/actions/set-service-principal-lifecycle.d.ts +15 -0
  61. package/dist/mcp/actions/set-service-principal-lifecycle.js +150 -0
  62. package/dist/mcp/actions/set-service-principal-policy.d.ts +14 -0
  63. package/dist/mcp/actions/set-service-principal-policy.js +105 -0
  64. package/dist/mcp/build-server.js +117 -13
  65. package/dist/mcp/connect-route.js +116 -4
  66. package/dist/mcp/connect-store.d.ts +7 -0
  67. package/dist/mcp/connect-store.js +21 -14
  68. package/dist/mcp-client/oauth-routes.js +2 -1
  69. package/dist/notifications/routes.d.ts +3 -3
  70. package/dist/observability/routes.d.ts +7 -7
  71. package/dist/org/index.d.ts +2 -2
  72. package/dist/org/service-principal-guard.d.ts +64 -0
  73. package/dist/org/service-principal-guard.js +140 -0
  74. package/dist/org/service-principal-policy.d.ts +104 -0
  75. package/dist/org/service-principal-policy.js +272 -0
  76. package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
  77. package/dist/provider-api/actions/provider-api.d.ts +13 -13
  78. package/dist/scripts/dev/index.js +2 -8
  79. package/dist/scripts/parse-args.d.ts +7 -0
  80. package/dist/scripts/parse-args.js +76 -0
  81. package/dist/scripts/utils.d.ts +1 -1
  82. package/dist/scripts/utils.js +1 -1
  83. package/dist/secrets/routes.d.ts +11 -11
  84. package/dist/server/action-discovery.js +16 -42
  85. package/dist/server/agent-chat/script-entries.js +3 -8
  86. package/dist/server/agent-chat-plugin.js +8 -42
  87. package/dist/server/agent-engine-default-model-route.d.ts +2 -2
  88. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  89. package/dist/server/auth.js +5 -2
  90. package/dist/server/core-routes-plugin.js +25 -6
  91. package/dist/server/google-oauth.js +8 -4
  92. package/dist/server/identity-sso.js +2 -1
  93. package/dist/server/index.d.ts +1 -0
  94. package/dist/server/index.js +1 -0
  95. package/dist/server/poll.d.ts +9 -0
  96. package/dist/server/poll.js +86 -7
  97. package/dist/server/query-echo-safe-redirect.d.ts +10 -0
  98. package/dist/server/query-echo-safe-redirect.js +102 -0
  99. package/dist/server/release-schema.js +4 -0
  100. package/dist/server/workspace-provider-oauth.js +2 -1
  101. package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
  102. package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
  103. package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
  104. package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
  105. package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
  106. package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
  107. package/dist/triggers/actions/manage-automation.d.ts +7 -7
  108. package/dist/vite/client.js +1 -0
  109. package/docs/content/audit-log.mdx +15 -0
  110. package/docs/content/external-agents-catalog.mdx +19 -0
  111. package/docs/content/http-api.mdx +21 -2
  112. package/docs/content/pr-visual-recap.mdx +3 -1
  113. package/docs/content/template-clips-embed.mdx +1 -1
  114. package/docs/content/turn-into-app.mdx +118 -0
  115. package/package.json +4 -4
  116. package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
  117. package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
  118. package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
  119. package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
  120. package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
  121. package/src/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
@@ -2,6 +2,8 @@ import crypto from "node:crypto";
2
2
  import { setResponseHeader, setResponseStatus } from "h3";
3
3
  import { AGENT_BACKGROUND_PROCESSOR_A2A, AGENT_BACKGROUND_PROCESSOR_FIELD, dispatchPathTargetsNetlifyBackgroundFunction, isAgentChatDurableBackgroundEnabled, resolveAgentChatProcessRunDispatchPath, } from "../agent/durable-background.js";
4
4
  import { trackingIdentityProperties } from "../observability/tracking-identity.js";
5
+ import { parseServiceIdentityEmail } from "../org/service-identity.js";
6
+ import { assertServicePrincipalMayRun, recordServicePrincipalDenial, ServicePrincipalRefusedError, } from "../org/service-principal-guard.js";
5
7
  import { findWorkspaceDispatchAgent } from "../server/agent-discovery.js";
6
8
  import { withConfiguredAppBasePath } from "../server/app-base-path.js";
7
9
  import { getOrigin, isConfiguredAppOrigin } from "../server/google-oauth.js";
@@ -12,7 +14,7 @@ import { track } from "../tracking/registry.js";
12
14
  import { hasConfiguredA2ASecret, isA2AProductionRuntime, } from "./auth-policy.js";
13
15
  import { callAction } from "./client.js";
14
16
  import { sanitizeA2ACorrelationMetadata } from "./correlation.js";
15
- import { createTask, createOrReuseTask, getTask, getTaskOwnership, updateTask, claimA2ATaskForProcessing, getA2ATaskDispatchState, failStuckA2ATask, failStuckQueuedA2ATask, settleProcessingA2ATask, touchQueuedA2ATaskDispatch, touchProcessingA2ATask, pauseProcessingA2ATask, MAX_A2A_IDEMPOTENCY_KEY_CHARS, A2A_PERSONAL_OWNER_SCOPE, A2A_ORG_ID_OWNER_SCOPE_PREFIX, } from "./task-store.js";
17
+ import { createTask, createOrReuseTask, getTask, getTaskOwnership, updateTask, claimA2ATaskForProcessing, getA2ATaskDispatchState, failStuckA2ATask, failStuckQueuedA2ATask, settleProcessingA2ATask, resetStuckA2ATaskForRetry, touchQueuedA2ATaskDispatch, touchProcessingA2ATask, pauseProcessingA2ATask, MAX_A2A_IDEMPOTENCY_KEY_CHARS, A2A_PERSONAL_OWNER_SCOPE, A2A_ORG_ID_OWNER_SCOPE_PREFIX, } from "./task-store.js";
16
18
  const A2A_PROCESS_TASK_PATH = "/_agent-native/a2a/_process-task";
17
19
  const PORTABLE_FALLBACK_HANDOFF_TIMEOUT_MS = 1_000;
18
20
  const A2A_QUEUED_DISPATCH_STUCK_AFTER_MS = 10_000;
@@ -246,6 +248,38 @@ export async function processA2ATaskFromQueue(taskId, config, event) {
246
248
  const verifiedOrgId = typeof processorMeta.verifiedOrgId === "string"
247
249
  ? processorMeta.verifiedOrgId.trim()
248
250
  : undefined;
251
+ let servicePrincipalAllowedActions;
252
+ try {
253
+ const admission = await assertServicePrincipalMayRun(verifiedEmail, verifiedOrgId);
254
+ if (parseServiceIdentityEmail(verifiedEmail)) {
255
+ servicePrincipalAllowedActions = admission.allowedActions;
256
+ }
257
+ }
258
+ catch (error) {
259
+ if (!(error instanceof ServicePrincipalRefusedError))
260
+ throw error;
261
+ if (error.statusCode === 503) {
262
+ await resetStuckA2ATaskForRetry(taskId, Date.now());
263
+ throw error;
264
+ }
265
+ if (error.statusCode !== 403)
266
+ throw error;
267
+ await recordServicePrincipalDenial({
268
+ email: verifiedEmail,
269
+ orgId: verifiedOrgId,
270
+ actionName: "a2a:process-task",
271
+ caller: "a2a",
272
+ error,
273
+ });
274
+ await settleProcessingA2ATask(taskId, {
275
+ state: "failed",
276
+ message: {
277
+ role: "agent",
278
+ parts: [{ type: "text", text: error.message }],
279
+ },
280
+ });
281
+ return;
282
+ }
249
283
  const requestOrigin = requestOriginFromMetadata(processorMeta) ?? requestOriginFromEvent(event);
250
284
  const contextId = processorMeta.contextId ?? undefined;
251
285
  const callerMetadata = processorMeta.callerMetadata ?? undefined;
@@ -261,6 +295,10 @@ export async function processA2ATaskFromQueue(taskId, config, event) {
261
295
  event.context.__a2aOrgDomain = orgDomainHint;
262
296
  if (verifiedOrgId)
263
297
  event.context.__a2aVerifiedOrgId = verifiedOrgId;
298
+ if (servicePrincipalAllowedActions !== undefined) {
299
+ event.context.__a2aServicePrincipalAllowedActions =
300
+ servicePrincipalAllowedActions;
301
+ }
264
302
  if (verifiedEmail && !resolvedOrgId)
265
303
  markExplicitPersonalOrgScope(event);
266
304
  }
@@ -294,6 +332,24 @@ export async function processA2ATaskFromQueue(taskId, config, event) {
294
332
  }
295
333
  }
296
334
  const defaultHandler = async (message, context) => {
335
+ const eventContext = context.event?.context;
336
+ const verifiedEmail = typeof eventContext?.__a2aVerifiedEmail === "string"
337
+ ? eventContext.__a2aVerifiedEmail
338
+ : undefined;
339
+ const serviceIdentity = parseServiceIdentityEmail(verifiedEmail);
340
+ if (serviceIdentity) {
341
+ const error = new ServicePrincipalRefusedError("service_principal_handoff_unsupported", "The default A2A chat handoff cannot preserve service-principal authorization. Use a service-aware A2A handler.");
342
+ await recordServicePrincipalDenial({
343
+ email: verifiedEmail,
344
+ orgId: typeof eventContext?.__a2aVerifiedOrgId === "string"
345
+ ? eventContext.__a2aVerifiedOrgId
346
+ : undefined,
347
+ actionName: "a2a:agent-chat-handoff",
348
+ caller: "a2a",
349
+ error,
350
+ });
351
+ throw error;
352
+ }
297
353
  const text = message.parts
298
354
  .filter((p) => p.type === "text")
299
355
  .map((p) => p.text)
@@ -3,6 +3,8 @@ import * as jose from "jose";
3
3
  import "../authorization/check-action.js";
4
4
  import { redactArgsToJson } from "../audit/redact.js";
5
5
  import { extractBearerToken, verifyInternalToken, } from "../integrations/internal-token.js";
6
+ import { parseServiceIdentityEmail } from "../org/service-identity.js";
7
+ import { assertServicePrincipalMayRun, recordServicePrincipalDenial, ServicePrincipalRefusedError, } from "../org/service-principal-guard.js";
6
8
  import { readDeployCredentialEnv } from "../server/credential-provider.js";
7
9
  import { getH3App } from "../server/framework-request-handler.js";
8
10
  import { publicFrameworkPath } from "../server/framework-route-prefix.js";
@@ -462,7 +464,7 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
462
464
  }
463
465
  catch (err) {
464
466
  console.error("[a2a] process-task failed:", err);
465
- setResponseStatus(event, 500);
467
+ setResponseStatus(event, err instanceof ServicePrincipalRefusedError ? err.statusCode : 500);
466
468
  return { error: err?.message ?? "process-task failed" };
467
469
  }
468
470
  }));
@@ -480,6 +482,7 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
480
482
  let verifiedOrgDomain = null;
481
483
  let verifiedOrgId;
482
484
  let verifiedIdentityAssurance;
485
+ let servicePrincipalAllowedActions;
483
486
  let verifiedAudienceBound = false;
484
487
  let legacyApiKeyAuthenticated = false;
485
488
  let bearerTokenVerified = false;
@@ -512,6 +515,35 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
512
515
  },
513
516
  };
514
517
  }
518
+ try {
519
+ const admission = await assertServicePrincipalMayRun(tokenPayload.email, verifiedOrgId);
520
+ if (parseServiceIdentityEmail(tokenPayload.email)) {
521
+ servicePrincipalAllowedActions = admission.allowedActions;
522
+ }
523
+ }
524
+ catch (error) {
525
+ if (!(error instanceof ServicePrincipalRefusedError))
526
+ throw error;
527
+ if (error.statusCode === 403) {
528
+ await recordServicePrincipalDenial({
529
+ email: tokenPayload.email,
530
+ orgId: verifiedOrgId,
531
+ actionName: "a2a:admission",
532
+ caller: "a2a",
533
+ error,
534
+ });
535
+ }
536
+ setResponseStatus(event, error.statusCode);
537
+ return {
538
+ jsonrpc: "2.0",
539
+ id: null,
540
+ error: {
541
+ code: error.statusCode === 503 ? -32003 : -32001,
542
+ message: error.message,
543
+ data: { errorCode: error.errorCode },
544
+ },
545
+ };
546
+ }
515
547
  verifiedCallerEmail = tokenPayload.email;
516
548
  verifiedOrgDomain = tokenPayload.orgDomain;
517
549
  verifiedIdentityAssurance = tokenPayload.email
@@ -601,6 +633,10 @@ export function mountA2A(nitroApp, config, routePrefix = "/_agent-native") {
601
633
  if (verifiedOrgId) {
602
634
  event.context.__a2aVerifiedOrgId = verifiedOrgId;
603
635
  }
636
+ if (servicePrincipalAllowedActions !== undefined) {
637
+ event.context.__a2aServicePrincipalAllowedActions =
638
+ servicePrincipalAllowedActions;
639
+ }
604
640
  const body = await readBody(event);
605
641
  return handleJsonRpcH3(body, event, config);
606
642
  }));
package/dist/action.d.ts CHANGED
@@ -230,6 +230,7 @@ export interface ActionMcpToolAnnotations {
230
230
  readOnlyHint: boolean;
231
231
  destructiveHint: boolean;
232
232
  openWorldHint: boolean;
233
+ idempotentHint?: boolean;
233
234
  }
234
235
  interface DefineActionWithSchema<TSchema extends StandardSchemaV1, TReturn = any, TOutputSchema extends StandardSchemaV1 | undefined = undefined> {
235
236
  title?: string;
package/dist/action.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { normalizeActionChatUIConfig, } from "./action-ui.js";
2
2
  import { normalizeAuditConfig, resolveAuditAttach } from "./audit/config.js";
3
3
  import { assertRegisteredActionAccess, } from "./authorization/action-access-runtime.js";
4
+ import { parseServiceIdentityEmail } from "./org/service-identity.js";
4
5
  import { wrapRunWithActionTracking } from "./tracking/action-lifecycle.js";
5
6
  export class ActionContractError extends Error {
6
7
  actionContractError = true;
@@ -136,7 +137,7 @@ export function defineAction(options) {
136
137
  const finalRun = resolveAuditAttach(auditConfig, readOnly)
137
138
  ? wrapRunWithAudit(run, auditConfig)
138
139
  : run;
139
- const trackedRun = wrapRunWithActionTracking(finalRun, readOnly);
140
+ const trackedRun = wrapRunWithServicePrincipalGrant(wrapRunWithActionTracking(finalRun, readOnly));
140
141
  const toolCallable = typeof options.toolCallable === "boolean"
141
142
  ? options.toolCallable
142
143
  : undefined;
@@ -149,10 +150,12 @@ export function defineAction(options) {
149
150
  !Array.isArray(options.mcpAnnotations) &&
150
151
  typeof options.mcpAnnotations.readOnlyHint === "boolean" &&
151
152
  typeof options.mcpAnnotations.destructiveHint === "boolean" &&
152
- typeof options.mcpAnnotations.openWorldHint === "boolean"
153
+ typeof options.mcpAnnotations.openWorldHint === "boolean" &&
154
+ (options.mcpAnnotations.idempotentHint === undefined ||
155
+ typeof options.mcpAnnotations.idempotentHint === "boolean")
153
156
  ? options.mcpAnnotations
154
157
  : (() => {
155
- throw new TypeError("mcpAnnotations must define boolean readOnlyHint, destructiveHint, and openWorldHint values.");
158
+ throw new TypeError("mcpAnnotations must define boolean readOnlyHint, destructiveHint, and openWorldHint values; idempotentHint is an optional boolean.");
156
159
  })();
157
160
  const deferLoading = typeof options.deferLoading === "boolean"
158
161
  ? options.deferLoading
@@ -302,6 +305,26 @@ function wrapRunWithAccess(run, access, authorize) {
302
305
  return run(args, ctx);
303
306
  };
304
307
  }
308
+ /**
309
+ * Outermost wrapper, so a refused call is audited once as a denial and never
310
+ * as a failed run of the action. Every route to running an action as a service
311
+ * identity (MCP, HTTP, delegated agent runs, sandbox bridges) passes here.
312
+ */
313
+ function wrapRunWithServicePrincipalGrant(run) {
314
+ return function grantCheckedRun(args, ctx) {
315
+ if (!parseServiceIdentityEmail(ctx?.userEmail))
316
+ return run(args, ctx);
317
+ return import("./org/service-principal-guard.js")
318
+ .then(({ enforceServicePrincipalActionGrant }) => enforceServicePrincipalActionGrant({
319
+ email: ctx.userEmail,
320
+ orgId: ctx.orgId,
321
+ // A missing name only passes an unrestricted grant.
322
+ actionName: ctx.actionName ?? "",
323
+ caller: ctx.caller,
324
+ }))
325
+ .then(() => run(args, ctx));
326
+ };
327
+ }
305
328
  function wrapRunWithAudit(run, auditConfig) {
306
329
  return async function auditedRun(args, ctx) {
307
330
  let result;
@@ -0,0 +1,9 @@
1
+ /** Terminal reason recorded on the aborted runs; the client shows it as a stopped run. */
2
+ export declare const PRINCIPAL_SUSPENDED_ABORT_REASON = "principal-suspended";
3
+ export interface ContainmentResult {
4
+ abortedRuns: number;
5
+ /** One entry per run that could not be confirmed stopped. Empty means contained. */
6
+ containmentErrors: string[];
7
+ }
8
+ export declare function prepareServicePrincipalContainment(): Promise<void>;
9
+ export declare function containServicePrincipal(orgId: string, serviceName: string, reason?: string): Promise<ContainmentResult>;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Stop the in-flight agent runs a service principal owns.
3
+ *
4
+ * Suspending a principal blocks NEW work at admission, but a run that started
5
+ * before the suspend keeps spending its tool budget until it ends. Containment
6
+ * aborts those runs so "suspended" means stopped, not "stops eventually".
7
+ *
8
+ * A run's owner is its chat thread's `owner_email`. `abortRunDurably` logs and
9
+ * swallows a failed SQL write, so the outcome is re-read from `agent_runs`:
10
+ * only a confirmed `aborted` status counts as aborted.
11
+ */
12
+ import { ensureChatThreadTables } from "../chat-threads/store.js";
13
+ import { getDbExec } from "../db/client.js";
14
+ import { serviceIdentityEmail } from "../mcp/connect-store.js";
15
+ import { abortRunDurably } from "./run-manager.js";
16
+ import { ensureRunTables, getRunStatus } from "./run-store.js";
17
+ /** Terminal reason recorded on the aborted runs; the client shows it as a stopped run. */
18
+ export const PRINCIPAL_SUSPENDED_ABORT_REASON = "principal-suspended";
19
+ export async function prepareServicePrincipalContainment() {
20
+ await ensureRunTables();
21
+ await ensureChatThreadTables();
22
+ }
23
+ export async function containServicePrincipal(orgId, serviceName, reason = PRINCIPAL_SUSPENDED_ABORT_REASON) {
24
+ const email = serviceIdentityEmail(serviceName, orgId).toLowerCase();
25
+ await prepareServicePrincipalContainment();
26
+ const { rows } = await getDbExec().execute({
27
+ sql: `SELECT r.id FROM agent_runs r
28
+ JOIN chat_threads t ON t.id = r.thread_id
29
+ WHERE r.status = 'running' AND LOWER(t.owner_email) = ?`,
30
+ args: [email],
31
+ });
32
+ const result = { abortedRuns: 0, containmentErrors: [] };
33
+ for (const row of rows) {
34
+ const runId = String(row.id);
35
+ try {
36
+ await abortRunDurably(runId, reason);
37
+ const status = await getRunStatus(runId);
38
+ if (status === "aborted") {
39
+ result.abortedRuns += 1;
40
+ }
41
+ else if (status === "running") {
42
+ result.containmentErrors.push(`${runId}: still running after abort`);
43
+ }
44
+ else {
45
+ result.containmentErrors.push(`${runId}: ${status ?? "unknown"} after abort; not confirmed aborted`);
46
+ }
47
+ }
48
+ catch (error) {
49
+ result.containmentErrors.push(`${runId}: ${error instanceof Error ? error.message : String(error)}`);
50
+ }
51
+ }
52
+ return result;
53
+ }
@@ -24,6 +24,8 @@ import { isMcpToolAllowedForRequest } from "../mcp-client/visibility.js";
24
24
  import { isObjectOnly } from "../mcp/tool-input-schema.js";
25
25
  import { describeSettingsViewForAgent, SETTINGS_VIEW_STATE_KEY, } from "../navigation/settings-redirects.js";
26
26
  import { shouldInferSentimentForTurn } from "../observability/sentiment.js";
27
+ import { parseServiceIdentityEmail } from "../org/service-identity.js";
28
+ import { ServicePrincipalRefusedError } from "../org/service-principal-guard.js";
27
29
  import { completeRun as completeProgressRun, startRun as startProgressRun, updateRunProgress, } from "../progress/registry.js";
28
30
  import { readOptionalKeyCache, writeOptionalKeyCache, } from "../secrets/optional-key-cache.js";
29
31
  import { COMPACT_PROMPT_RESOURCES_TOTAL_MAX_CHARS, preloadJevContextForPrompt, } from "../server/agent-chat/prompt-resources.js";
@@ -4696,6 +4698,28 @@ export async function runAgentLoop(opts) {
4696
4698
  isError: true,
4697
4699
  };
4698
4700
  }
4701
+ // Before approval, so a human is never asked to approve a call the
4702
+ // principal's grant already forbids. Covers framework tools that bypass
4703
+ // `defineAction`, which enforces the same grant for the actions.
4704
+ const userEmail = opts.ownerEmail ?? getRequestUserEmail();
4705
+ if (parseServiceIdentityEmail(userEmail)) {
4706
+ const { enforceServicePrincipalActionGrant, ServicePrincipalRefusedError, } = await import("../org/service-principal-guard.js");
4707
+ try {
4708
+ await enforceServicePrincipalActionGrant({
4709
+ email: userEmail,
4710
+ orgId: opts.orgId ?? getRequestOrgId() ?? null,
4711
+ actionName: toolCall.name,
4712
+ caller: opts.actionCaller ?? "tool",
4713
+ });
4714
+ }
4715
+ catch (error) {
4716
+ if (!(error instanceof ServicePrincipalRefusedError) ||
4717
+ error.statusCode !== 403) {
4718
+ throw error;
4719
+ }
4720
+ return declineToolCall(error.message);
4721
+ }
4722
+ }
4699
4723
  const approvalKey = toolCallCacheKey(toolCall.name, toolCall.input);
4700
4724
  const approvalBinding = {
4701
4725
  toolName: toolCall.name,
@@ -6401,7 +6425,8 @@ export async function chainServerDrivenContinuation(opts) {
6401
6425
  }
6402
6426
  catch (insertErr) {
6403
6427
  if (insertErr instanceof AgentTurnInitiatorMismatchError ||
6404
- insertErr instanceof AgentTurnInitiatorUnavailableError) {
6428
+ insertErr instanceof AgentTurnInitiatorUnavailableError ||
6429
+ insertErr instanceof ServicePrincipalRefusedError) {
6405
6430
  throw insertErr;
6406
6431
  }
6407
6432
  await d
@@ -7785,6 +7810,8 @@ export function createProductionAgentHandler(options) {
7785
7810
  backgroundRowInserted = true;
7786
7811
  }
7787
7812
  catch (err) {
7813
+ if (err instanceof ServicePrincipalRefusedError)
7814
+ throw err;
7788
7815
  console.error("[agent-chat] background insertRun failed; falling back to inline:", err instanceof Error ? err.message : err);
7789
7816
  }
7790
7817
  }
@@ -8042,7 +8069,10 @@ export function createProductionAgentHandler(options) {
8042
8069
  await insertRun(runId, effectiveThreadId, effectiveTurnId, {
8043
8070
  dispatchMode: "background",
8044
8071
  ...(turnInitiator ? { turnInitiator } : {}),
8045
- }).catch(() => { });
8072
+ }).catch((error) => {
8073
+ if (error instanceof ServicePrincipalRefusedError)
8074
+ throw error;
8075
+ });
8046
8076
  }
8047
8077
  const won = await claimBackgroundRun(runId);
8048
8078
  if (!won) {
@@ -1,6 +1,8 @@
1
1
  import { getAppConfig } from "../app-config/index.js";
2
2
  import { BACKGROUND_AUTOMATION_SOFT_TIMEOUT_HEADROOM_MS, BACKGROUND_SOFT_TIMEOUT_CEILING_MS, RUN_NO_PROGRESS_HARD_TIMEOUT_MS, } from "../app-config/run-lifecycle-invariants.js";
3
3
  import { recordAgentRun } from "../observability/metrics.js";
4
+ import { parseServiceIdentityEmail } from "../org/service-identity.js";
5
+ import { ServicePrincipalRefusedError } from "../org/service-principal-guard.js";
4
6
  import { captureError } from "../server/capture-error.js";
5
7
  import { isLlmCredentialError, LLM_MISSING_CREDENTIALS_ERROR_CODE, LLM_MISSING_CREDENTIALS_MESSAGE, } from "./engine/credential-errors.js";
6
8
  import { classifyTerminalErrorCode, describeErrorWithCauses, isProviderConnectionError, } from "./engine/error-detail.js";
@@ -556,12 +558,16 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
556
558
  : {}),
557
559
  }
558
560
  : undefined;
559
- const insertRunPromise = (options?.runRowAlreadyInserted
561
+ const servicePrincipalRunStart = Boolean(options?.turnInitiator &&
562
+ parseServiceIdentityEmail(options.turnInitiator.email));
563
+ const insertRunPromise = (options?.runRowAlreadyInserted && !servicePrincipalRunStart
560
564
  ? Promise.resolve()
561
565
  : insertOptions
562
566
  ? insertRun(runId, threadId, options?.turnId, insertOptions)
563
567
  : insertRun(runId, threadId, options?.turnId)).catch((error) => {
564
568
  captureRunPersistenceError(error, "insert-run");
569
+ if (error instanceof ServicePrincipalRefusedError)
570
+ throw error;
565
571
  });
566
572
  let persistenceChain = Promise.resolve();
567
573
  const PROGRESS_BUMP_INTERVAL_MS = 1_000;
@@ -1068,7 +1074,13 @@ export function startRun(runId, threadId, runFn, onComplete, options) {
1068
1074
  }
1069
1075
  void emitRunEvent(runEvent);
1070
1076
  };
1071
- const runPromise = runFn(send, runControl.chunkSignal, runControl)
1077
+ const runPromise = (servicePrincipalRunStart
1078
+ ? insertRunPromise.then(() => {
1079
+ if (!abort.signal.aborted) {
1080
+ return runFn(send, runControl.chunkSignal, runControl);
1081
+ }
1082
+ })
1083
+ : runFn(send, runControl.chunkSignal, runControl))
1072
1084
  .then(() => {
1073
1085
  settleBoundary(false);
1074
1086
  if (abort.signal.aborted) {
@@ -3,6 +3,7 @@ import { isArtifactReceipt, } from "../artifacts/detect.js";
3
3
  import { getDbExec } from "../db/client.js";
4
4
  import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
5
5
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
6
+ import { lockAndAssertServicePrincipalMayStartRun, prepareServicePrincipalRunStart, recordServicePrincipalDenial, } from "../org/service-principal-guard.js";
6
7
  import { captureError } from "../server/capture-error.js";
7
8
  import { isRequestedStopAbortReason } from "./abort-reasons.js";
8
9
  import { admitAutoContinue, } from "./auto-continue.js";
@@ -435,11 +436,34 @@ export async function insertRun(id, threadId, turnId, options) {
435
436
  (await nextContinuationOrder(client, threadId, logicalTurnId)));
436
437
  return;
437
438
  }
438
- await client.transaction(async (tx) => {
439
+ if (options?.turnInitiator) {
440
+ await prepareServicePrincipalRunStart(options.turnInitiator);
441
+ }
442
+ const refusal = await client.transaction(async (tx) => {
443
+ if (options?.turnInitiator) {
444
+ const denied = await lockAndAssertServicePrincipalMayStartRun(tx, {
445
+ email: options.turnInitiator.email,
446
+ orgId: options.turnInitiator.orgId,
447
+ });
448
+ if (denied)
449
+ return denied;
450
+ }
439
451
  await lockContinuationOrder(tx, threadId, logicalTurnId);
440
452
  await insert(tx, explicitContinuationOrder ??
441
453
  (await nextContinuationOrder(tx, threadId, logicalTurnId)));
454
+ return undefined;
442
455
  });
456
+ if (refusal) {
457
+ if (options?.turnInitiator) {
458
+ await recordServicePrincipalDenial({
459
+ ...options.turnInitiator,
460
+ actionName: "agent-run:start",
461
+ caller: "agent-run",
462
+ error: refusal,
463
+ });
464
+ }
465
+ throw refusal;
466
+ }
443
467
  }
444
468
  function normalizeContinuationOrder(value) {
445
469
  if (value === undefined)
@@ -790,7 +814,18 @@ export async function tryClaimRunSlot(threadId, runId, maxStaleMs, options) {
790
814
  if (!client.transaction) {
791
815
  throw new Error("Atomic run-slot claims require transaction support");
792
816
  }
793
- return client.transaction(async (tx) => {
817
+ if (options?.turnInitiator) {
818
+ await prepareServicePrincipalRunStart(options.turnInitiator);
819
+ }
820
+ const transactionResult = await client.transaction(async (tx) => {
821
+ if (options?.turnInitiator) {
822
+ const denied = await lockAndAssertServicePrincipalMayStartRun(tx, {
823
+ email: options.turnInitiator.email,
824
+ orgId: options.turnInitiator.orgId,
825
+ });
826
+ if (denied)
827
+ return { refused: denied };
828
+ }
794
829
  await tx.execute({
795
830
  sql: "SELECT pg_advisory_xact_lock(hashtextextended(?, 0::bigint))",
796
831
  args: [`agent-native:run-slot:${threadId}`],
@@ -801,14 +836,17 @@ export async function tryClaimRunSlot(threadId, runId, maxStaleMs, options) {
801
836
  args: [threadId, turnId],
802
837
  });
803
838
  if (abortMarker.rows.length > 0) {
804
- return { claimed: false, activeRunId: null, turnAborted: true };
839
+ return {
840
+ result: { claimed: false, activeRunId: null, turnAborted: true },
841
+ };
805
842
  }
806
843
  if (options?.turnInitiator) {
807
844
  await bindTurnInitiator(tx, threadId, turnId, runId, options.turnInitiator);
808
845
  }
809
846
  const activeRunId = await selectSlotHoldingRunId(tx, threadId, now, maxStaleMs);
810
- if (activeRunId)
811
- return { claimed: false, activeRunId };
847
+ if (activeRunId) {
848
+ return { result: { claimed: false, activeRunId } };
849
+ }
812
850
  if (replayCompletedTurn) {
813
851
  const latest = await tx.execute({
814
852
  sql: `SELECT id,
@@ -837,9 +875,11 @@ export async function tryClaimRunSlot(threadId, runId, maxStaleMs, options) {
837
875
  const latestRun = latest.rows[0];
838
876
  if (latestRun?.id && latestRun.has_terminal_event === true) {
839
877
  return {
840
- claimed: false,
841
- activeRunId: null,
842
- completedRunId: latestRun.id,
878
+ result: {
879
+ claimed: false,
880
+ activeRunId: null,
881
+ completedRunId: latestRun.id,
882
+ },
843
883
  };
844
884
  }
845
885
  }
@@ -875,9 +915,11 @@ export async function tryClaimRunSlot(threadId, runId, maxStaleMs, options) {
875
915
  });
876
916
  if (!admission.admit) {
877
917
  return {
878
- claimed: false,
879
- activeRunId: null,
880
- autoContinueRefused: admission.code,
918
+ result: {
919
+ claimed: false,
920
+ activeRunId: null,
921
+ autoContinueRefused: admission.code,
922
+ },
881
923
  };
882
924
  }
883
925
  }
@@ -901,8 +943,20 @@ export async function tryClaimRunSlot(threadId, runId, maxStaleMs, options) {
901
943
  if ((inserted.rowsAffected ?? 0) !== 1) {
902
944
  throw new Error(`Failed to insert claimed run ${runId}`);
903
945
  }
904
- return { claimed: true, activeRunId: null };
946
+ return { result: { claimed: true, activeRunId: null } };
905
947
  });
948
+ if ("refused" in transactionResult && transactionResult.refused) {
949
+ if (options?.turnInitiator) {
950
+ await recordServicePrincipalDenial({
951
+ ...options.turnInitiator,
952
+ actionName: "agent-run:start",
953
+ caller: "agent-run",
954
+ error: transactionResult.refused,
955
+ });
956
+ }
957
+ throw transactionResult.refused;
958
+ }
959
+ return transactionResult.result;
906
960
  }
907
961
  export async function setRunError(runId, errorCode, errorDetail) {
908
962
  if (!errorCode && !errorDetail)
@@ -2,7 +2,7 @@ declare const _default: import("../../action.js").ActionDefinition<{
2
2
  scope?: "accessible" | "organization";
3
3
  targetType?: string;
4
4
  targetId?: string;
5
- actorKind?: "agent" | "human" | "system";
5
+ actorKind?: "agent" | "human" | "service" | "system";
6
6
  actorEmail?: string;
7
7
  status?: "denied" | "error" | "success";
8
8
  threadId?: string;
@@ -56,9 +56,9 @@ export default defineAction({
56
56
  .optional()
57
57
  .describe("Filter to one resource id (pair with targetType)."),
58
58
  actorKind: z
59
- .enum(["agent", "human", "system"])
59
+ .enum(["agent", "human", "service", "system"])
60
60
  .optional()
61
- .describe("Filter to changes made by the agent, a human, or the system."),
61
+ .describe("Filter to changes made by an agent, human, service principal, or the system."),
62
62
  actorEmail: z.string().optional().describe("Filter to one actor's email."),
63
63
  status: z
64
64
  .enum(["success", "error", "denied"])
@@ -0,0 +1,11 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<{
2
+ since?: string | number;
3
+ until?: string | number;
4
+ cursor?: string;
5
+ limit?: number;
6
+ }, {
7
+ events: import("../ocsf.js").OcsfApiActivity[];
8
+ hasMore: boolean;
9
+ nextCursor: string;
10
+ }>;
11
+ export default _default;