@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
@@ -23,6 +23,7 @@ export declare const DEVICE_CODE_TTL_MS: number;
23
23
  export declare const DEFAULT_TOKEN_TTL_DAYS = 365;
24
24
  export declare const MIN_TOKEN_TTL_DAYS = 1;
25
25
  export declare const MAX_TOKEN_TTL_DAYS = 365;
26
+ export declare const MAX_SERVICE_TOKEN_TTL_DAYS = 3650;
26
27
  export declare const DEVICE_START_MAX = 20;
27
28
  export declare const DEVICE_START_WINDOW_MS = 60000;
28
29
  export declare function ensureTable(): Promise<void>;
@@ -86,6 +87,12 @@ export declare function listOrgServiceTokens(orgId: string): Promise<MintedToken
86
87
  * tokens. Idempotent; returns true when a row actually transitioned.
87
88
  */
88
89
  export declare function revokeOrgServiceToken(orgId: string, id: string): Promise<boolean>;
90
+ /**
91
+ * Revoke every active token of one service in a single statement. Unlike
92
+ * `listOrgServiceTokens`, a connection error is NOT swallowed: retiring a
93
+ * principal must fail loudly rather than report "0 revoked".
94
+ */
95
+ export declare function revokeServiceTokensByName(orgId: string, serviceName: string): Promise<number>;
89
96
  /**
90
97
  * Revoke a token, but ONLY if it is owned by `ownerEmail` (the caller). The
91
98
  * `owner_email = ?` predicate is the access scope — a caller can never revoke
@@ -26,6 +26,7 @@ export const DEVICE_CODE_TTL_MS = 10 * 60_000;
26
26
  export const DEFAULT_TOKEN_TTL_DAYS = 365;
27
27
  export const MIN_TOKEN_TTL_DAYS = 1;
28
28
  export const MAX_TOKEN_TTL_DAYS = 365;
29
+ export const MAX_SERVICE_TOKEN_TTL_DAYS = 3_650;
29
30
  export const DEVICE_START_MAX = 20;
30
31
  export const DEVICE_START_WINDOW_MS = 60_000;
31
32
  export async function ensureTable() {
@@ -211,20 +212,13 @@ export async function listTokens(ownerEmail) {
211
212
  }
212
213
  }
213
214
  export async function listOrgServiceTokens(orgId) {
214
- try {
215
- await ensureTable();
216
- const client = getDbExec();
217
- const { rows } = await client.execute({
218
- sql: `SELECT id, jti, owner_email, org_id, label, kind, service_name, created_by, created_at, last_used_at, revoked_at FROM mcp_connect_tokens WHERE org_id = ? AND kind = 'service' ORDER BY created_at DESC`,
219
- args: [orgId],
220
- });
221
- return rows.map(mapTokenRow);
222
- }
223
- catch (err) {
224
- if (isConnectionError(err))
225
- return [];
226
- throw err;
227
- }
215
+ await ensureTable();
216
+ const client = getDbExec();
217
+ const { rows } = await client.execute({
218
+ sql: `SELECT id, jti, owner_email, org_id, label, kind, service_name, created_by, created_at, last_used_at, revoked_at FROM mcp_connect_tokens WHERE org_id = ? AND kind = 'service' ORDER BY created_at DESC`,
219
+ args: [orgId],
220
+ });
221
+ return rows.map(mapTokenRow);
228
222
  }
229
223
  /**
230
224
  * Revoke an org service token by id, scoped to `orgId` AND `kind = 'service'`
@@ -242,6 +236,19 @@ export async function revokeOrgServiceToken(orgId, id) {
242
236
  });
243
237
  return result.rowsAffected > 0;
244
238
  }
239
+ /**
240
+ * Revoke every active token of one service in a single statement. Unlike
241
+ * `listOrgServiceTokens`, a connection error is NOT swallowed: retiring a
242
+ * principal must fail loudly rather than report "0 revoked".
243
+ */
244
+ export async function revokeServiceTokensByName(orgId, serviceName) {
245
+ await ensureTable();
246
+ const result = await getDbExec().execute({
247
+ sql: `UPDATE mcp_connect_tokens SET revoked_at = ? WHERE org_id = ? AND kind = 'service' AND service_name = ? AND revoked_at IS NULL`,
248
+ args: [Date.now(), orgId, serviceName],
249
+ });
250
+ return result.rowsAffected;
251
+ }
245
252
  /**
246
253
  * Revoke a token, but ONLY if it is owned by `ownerEmail` (the caller). The
247
254
  * `owner_email = ?` predicate is the access scope — a caller can never revoke
@@ -5,6 +5,7 @@ import { CredentialStoreUnavailableError, resolveSecretPairs, } from "../server/
5
5
  import { getH3App } from "../server/framework-request-handler.js";
6
6
  import { canonicalFrameworkPathname } from "../server/framework-route-prefix.js";
7
7
  import { getAppBasePath, getAppUrl, encodeOAuthState, oauthErrorPage, resolveOAuthRedirectUri, } from "../server/google-oauth.js";
8
+ import { queryEchoSafeRedirect } from "../server/query-echo-safe-redirect.js";
8
9
  import { runWithRequestContext } from "../server/request-context.js";
9
10
  import { isWorkspaceOAuthCallbackRelayEnabled } from "../server/workspace-oauth.js";
10
11
  import { MCP_OAUTH_FLOW_TTL_SECONDS } from "../shared/mcp-oauth-flow-ttl.js";
@@ -491,7 +492,7 @@ async function handleMcpOAuthCallback(event, options) {
491
492
  console.warn("[mcp-client/oauth] saved server did not reconnect:", error);
492
493
  }
493
494
  const returnPath = resolveMcpOAuthReturnPath(connected, flow);
494
- return redirectWithStagedCookies(event, getAppUrl(event, stripMcpOAuthAppBasePath(returnPath, getAppBasePath())));
495
+ return queryEchoSafeRedirect(event, redirectWithStagedCookies(event, getAppUrl(event, stripMcpOAuthAppBasePath(returnPath, getAppBasePath()))));
495
496
  }
496
497
  export function setMcpOAuthFlowCookie(event, flow, secure) {
497
498
  const encrypted = encryptSecretValue(JSON.stringify(flow));
@@ -1,12 +1,12 @@
1
1
  export declare function createNotificationsHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<"" | import("./types.js").Notification[] | {
2
- error?: undefined;
3
2
  count: number;
4
3
  updated?: undefined;
4
+ error?: undefined;
5
5
  ok?: undefined;
6
6
  } | {
7
- error?: undefined;
8
7
  count?: undefined;
9
8
  updated: number;
9
+ error?: undefined;
10
10
  ok?: undefined;
11
11
  } | {
12
12
  count?: undefined;
@@ -14,8 +14,8 @@ export declare function createNotificationsHandler(): import("h3").EventHandlerW
14
14
  error: string;
15
15
  ok?: undefined;
16
16
  } | {
17
- error?: undefined;
18
17
  count?: undefined;
19
18
  updated?: undefined;
20
19
  ok: boolean;
20
+ error?: undefined;
21
21
  }>>;
@@ -27,38 +27,38 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
27
27
  thumbsUpRate: number;
28
28
  avgEvalScore: number;
29
29
  } | {
30
- id?: undefined;
31
30
  error: string;
32
31
  message: string;
33
32
  summary?: undefined;
34
33
  spans?: undefined;
34
+ id?: undefined;
35
35
  ok?: undefined;
36
36
  } | {
37
- id?: undefined;
38
- error?: undefined;
39
37
  message?: undefined;
40
38
  summary: import("./types.js").TraceSummary;
41
39
  spans: import("./types.js").TraceSpan[];
40
+ id?: undefined;
41
+ error?: undefined;
42
42
  ok?: undefined;
43
43
  } | {
44
- error?: undefined;
45
44
  message?: undefined;
46
45
  summary?: undefined;
47
46
  spans?: undefined;
48
47
  id: string;
48
+ error?: undefined;
49
49
  ok?: undefined;
50
50
  } | {
51
- id?: undefined;
52
51
  message?: undefined;
53
52
  summary?: undefined;
54
53
  spans?: undefined;
54
+ id?: undefined;
55
55
  error: any;
56
56
  ok?: undefined;
57
57
  } | {
58
- id?: undefined;
59
- error?: undefined;
60
58
  message?: undefined;
61
59
  summary?: undefined;
62
60
  spans?: undefined;
61
+ id?: undefined;
62
+ error?: undefined;
63
63
  ok: boolean;
64
64
  }>>;
@@ -177,10 +177,10 @@ export declare const getMyOrgHandler: import("h3").EventHandlerWithFetch<import(
177
177
  soloDeploymentAdmin: boolean | undefined;
178
178
  }>>;
179
179
  export declare const createOrgHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
180
- name?: undefined;
181
- success: boolean;
182
180
  role?: undefined;
183
181
  id?: undefined;
182
+ name?: undefined;
183
+ success: boolean;
184
184
  } | {
185
185
  success?: undefined;
186
186
  id: string;
@@ -0,0 +1,64 @@
1
+ import type { DbExec } from "../db/client.js";
2
+ /**
3
+ * Enforcement helpers over the service-principal policy store. MCP admission,
4
+ * MCP tool dispatch, and inbound A2A all ask these instead of reading
5
+ * `evaluateServicePrincipal` themselves, so a refusal means the same thing
6
+ * (status, message, audit row) on every entry point.
7
+ */
8
+ export declare const SERVICE_PRINCIPAL_UNAVAILABLE_MESSAGE = "The service principal's status could not be verified. Retry shortly.";
9
+ export type ServicePrincipalRefusalCode = "service_principal_inactive" | "service_principal_unavailable" | "service_principal_action_denied" | "service_principal_handoff_unsupported";
10
+ /** 403 is a refusal (audited as `denied`); 503 is retryable and never a denial. */
11
+ export declare class ServicePrincipalRefusedError extends Error {
12
+ readonly errorCode: ServicePrincipalRefusalCode;
13
+ readonly statusCode: 403 | 503;
14
+ constructor(code: ServicePrincipalRefusalCode, message: string);
15
+ }
16
+ export declare function servicePrincipalActionDeniedError(actionName: string): ServicePrincipalRefusedError;
17
+ /**
18
+ * Throws `ServicePrincipalRefusedError` when `email` is a service identity that
19
+ * is suspended or retired, or whose policy could not be read. Otherwise returns
20
+ * its action grant: `null` is unrestricted (not a service identity, or a legacy
21
+ * principal with no policy row), a list is the only set of actions it may call.
22
+ * Non-service callers return without touching the database.
23
+ */
24
+ export declare function assertServicePrincipalMayRun(email: string | null | undefined, orgId?: string | null, db?: Pick<DbExec, "execute">): Promise<{
25
+ allowedActions: string[] | null;
26
+ }>;
27
+ /**
28
+ * Serialize a persisted run start with lifecycle changes, then recheck the
29
+ * verified principal state before the run row is inserted.
30
+ */
31
+ export declare function lockAndAssertServicePrincipalMayStartRun(db: DbExec, input: {
32
+ email: string;
33
+ orgId?: string | null;
34
+ }): Promise<ServicePrincipalRefusedError | undefined>;
35
+ export declare function prepareServicePrincipalRunStart(input: {
36
+ email: string;
37
+ orgId?: string | null;
38
+ }): Promise<void>;
39
+ /** Throws a 403 refusal unless the grant covers `actionName`. */
40
+ export declare function assertServicePrincipalMayCall(allowedActions: string[] | null, actionName: string): void;
41
+ /**
42
+ * Writes a `denied` audit row when a refused service-principal call has a
43
+ * verified org scope. Visible to org admins only; the recorder is best-effort.
44
+ */
45
+ export declare function recordServicePrincipalDenial(input: {
46
+ email: string | null | undefined;
47
+ orgId?: string | null;
48
+ actionName: string;
49
+ caller: string;
50
+ error: ServicePrincipalRefusedError;
51
+ }): Promise<void>;
52
+ /**
53
+ * The action-execution check: refuse a service identity's call to `actionName`
54
+ * when it is suspended, unverifiable, or outside its grant, and record one
55
+ * `denied` row for a 403. Evaluated on every call, never cached, so a suspend
56
+ * or re-scope takes effect on the next action of a run already in flight.
57
+ * Non-service callers return without touching the database.
58
+ */
59
+ export declare function enforceServicePrincipalActionGrant(input: {
60
+ email: string | null | undefined;
61
+ orgId?: string | null;
62
+ actionName: string;
63
+ caller: string;
64
+ }): Promise<void>;
@@ -0,0 +1,140 @@
1
+ import { parseServiceIdentityEmail } from "./service-identity.js";
2
+ import { ensureTable, evaluateServicePrincipal, isActionGranted, lockServicePrincipalLifecycle, SERVICE_PRINCIPAL_SUSPENDED_MESSAGE, } from "./service-principal-policy.js";
3
+ /**
4
+ * Enforcement helpers over the service-principal policy store. MCP admission,
5
+ * MCP tool dispatch, and inbound A2A all ask these instead of reading
6
+ * `evaluateServicePrincipal` themselves, so a refusal means the same thing
7
+ * (status, message, audit row) on every entry point.
8
+ */
9
+ export const SERVICE_PRINCIPAL_UNAVAILABLE_MESSAGE = "The service principal's status could not be verified. Retry shortly.";
10
+ /** 403 is a refusal (audited as `denied`); 503 is retryable and never a denial. */
11
+ export class ServicePrincipalRefusedError extends Error {
12
+ errorCode;
13
+ statusCode;
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.name = "ServicePrincipalRefusedError";
17
+ this.errorCode = code;
18
+ this.statusCode = code === "service_principal_unavailable" ? 503 : 403;
19
+ }
20
+ }
21
+ export function servicePrincipalActionDeniedError(actionName) {
22
+ return new ServicePrincipalRefusedError("service_principal_action_denied", `Forbidden: ${actionName} is not permitted for this service principal.`);
23
+ }
24
+ /**
25
+ * Throws `ServicePrincipalRefusedError` when `email` is a service identity that
26
+ * is suspended or retired, or whose policy could not be read. Otherwise returns
27
+ * its action grant: `null` is unrestricted (not a service identity, or a legacy
28
+ * principal with no policy row), a list is the only set of actions it may call.
29
+ * Non-service callers return without touching the database.
30
+ */
31
+ export async function assertServicePrincipalMayRun(email, orgId, db) {
32
+ const state = db
33
+ ? await evaluateServicePrincipal(email, orgId, db)
34
+ : await evaluateServicePrincipal(email, orgId);
35
+ switch (state.status) {
36
+ case "not-service":
37
+ case "ungoverned":
38
+ return { allowedActions: null };
39
+ case "active":
40
+ return { allowedActions: state.policy.allowedActions };
41
+ case "suspended":
42
+ case "retired":
43
+ case "org-mismatch":
44
+ throw new ServicePrincipalRefusedError("service_principal_inactive", SERVICE_PRINCIPAL_SUSPENDED_MESSAGE);
45
+ case "unavailable":
46
+ throw new ServicePrincipalRefusedError("service_principal_unavailable", SERVICE_PRINCIPAL_UNAVAILABLE_MESSAGE);
47
+ }
48
+ }
49
+ /**
50
+ * Serialize a persisted run start with lifecycle changes, then recheck the
51
+ * verified principal state before the run row is inserted.
52
+ */
53
+ export async function lockAndAssertServicePrincipalMayStartRun(db, input) {
54
+ const identity = parseServiceIdentityEmail(input.email);
55
+ if (!identity)
56
+ return;
57
+ if (input.orgId?.trim() === identity.orgId) {
58
+ await lockServicePrincipalLifecycle(db, identity.orgId, identity.serviceName);
59
+ }
60
+ try {
61
+ await assertServicePrincipalMayRun(input.email, input.orgId, db);
62
+ return;
63
+ }
64
+ catch (error) {
65
+ if (error instanceof ServicePrincipalRefusedError)
66
+ return error;
67
+ throw error;
68
+ }
69
+ }
70
+ export async function prepareServicePrincipalRunStart(input) {
71
+ const identity = parseServiceIdentityEmail(input.email);
72
+ if (identity && input.orgId?.trim() === identity.orgId) {
73
+ await ensureTable();
74
+ }
75
+ }
76
+ /** Throws a 403 refusal unless the grant covers `actionName`. */
77
+ export function assertServicePrincipalMayCall(allowedActions, actionName) {
78
+ if (!isActionGranted(allowedActions, actionName)) {
79
+ throw servicePrincipalActionDeniedError(actionName);
80
+ }
81
+ }
82
+ /**
83
+ * Writes a `denied` audit row when a refused service-principal call has a
84
+ * verified org scope. Visible to org admins only; the recorder is best-effort.
85
+ */
86
+ export async function recordServicePrincipalDenial(input) {
87
+ if (input.error.statusCode !== 403)
88
+ return;
89
+ const orgId = input.orgId?.trim();
90
+ if (!orgId)
91
+ return;
92
+ const identity = parseServiceIdentityEmail(input.email);
93
+ if (!identity || identity.orgId !== orgId)
94
+ return;
95
+ const { recordActionAudit } = await import("../audit/record.js");
96
+ await recordActionAudit({
97
+ config: {
98
+ enabled: true,
99
+ target: () => ({
100
+ type: "service-principal",
101
+ id: input.email ?? undefined,
102
+ orgId,
103
+ visibility: "admins",
104
+ }),
105
+ summary: () => input.error.errorCode === "service_principal_inactive"
106
+ ? `Refused ${input.actionName}: service principal is suspended or retired`
107
+ : input.error.errorCode === "service_principal_action_denied"
108
+ ? `Refused ${input.actionName}: not in the service principal's allowed actions`
109
+ : `Refused ${input.actionName}: service principal authorization cannot be preserved across the handoff`,
110
+ },
111
+ args: {},
112
+ ctx: {
113
+ actionName: input.actionName,
114
+ caller: input.caller,
115
+ userEmail: input.email ?? undefined,
116
+ orgId,
117
+ },
118
+ status: "error",
119
+ error: input.error,
120
+ });
121
+ }
122
+ /**
123
+ * The action-execution check: refuse a service identity's call to `actionName`
124
+ * when it is suspended, unverifiable, or outside its grant, and record one
125
+ * `denied` row for a 403. Evaluated on every call, never cached, so a suspend
126
+ * or re-scope takes effect on the next action of a run already in flight.
127
+ * Non-service callers return without touching the database.
128
+ */
129
+ export async function enforceServicePrincipalActionGrant(input) {
130
+ try {
131
+ const { allowedActions } = await assertServicePrincipalMayRun(input.email, input.orgId);
132
+ assertServicePrincipalMayCall(allowedActions, input.actionName);
133
+ }
134
+ catch (error) {
135
+ if (error instanceof ServicePrincipalRefusedError) {
136
+ await recordServicePrincipalDenial({ ...input, error });
137
+ }
138
+ throw error;
139
+ }
140
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Governance record for an org service principal (`svc-<name>@service.<orgId>`).
3
+ *
4
+ * A service token proves WHO is calling; this record says who is ACCOUNTABLE
5
+ * for the principal, whether it may run at all, and which actions it may call.
6
+ * Every entry point that admits a service identity (MCP admission, MCP tool
7
+ * dispatch, agent run start) reads it through `evaluateServicePrincipal` so
8
+ * suspend and the action grant are enforced in one place.
9
+ *
10
+ * Constraints that must survive future edits:
11
+ *
12
+ * 1. No row means a legacy principal minted before governance existed: it
13
+ * stays active and unrestricted ("ungoverned"). Tightening it is an explicit
14
+ * admin act, never a side effect of upgrading.
15
+ * 2. An unreadable row is NOT an absent row. A read failure resolves to
16
+ * "unavailable" and callers refuse the request (503), because answering
17
+ * "ungoverned" would let a suspended principal back in during an outage.
18
+ * 3. A grant is deny-by-default once `allowedActions` is a list: an empty list
19
+ * grants nothing, and `null` is the only unrestricted value.
20
+ */
21
+ import { type DbExec } from "../db/client.js";
22
+ export declare const SERVICE_PRINCIPAL_LIFECYCLES: readonly ["active", "suspended", "retired"];
23
+ export type ServicePrincipalLifecycle = (typeof SERVICE_PRINCIPAL_LIFECYCLES)[number];
24
+ export declare const SERVICE_PRINCIPAL_RISK_TIERS: readonly ["low", "medium", "high"];
25
+ export type ServicePrincipalRiskTier = (typeof SERVICE_PRINCIPAL_RISK_TIERS)[number];
26
+ export declare const MAX_SERVICE_PRINCIPAL_ACTIONS = 200;
27
+ export declare function isServicePrincipalActionPattern(value: string): boolean;
28
+ export interface ServicePrincipalPolicy {
29
+ orgId: string;
30
+ /** Normalized service name, the `<name>` in `svc-<name>@service.<orgId>`. */
31
+ serviceName: string;
32
+ /** Accountable human. Always an email, never the service identity. */
33
+ ownerEmail: string | null;
34
+ /** Operational team or on-call queue that answers for the principal. */
35
+ team: string | null;
36
+ riskTier: ServicePrincipalRiskTier;
37
+ /** What the principal is for, in one line. */
38
+ purpose: string | null;
39
+ lifecycle: ServicePrincipalLifecycle;
40
+ /** `null` is unrestricted. A list is the only set of actions it may call. */
41
+ allowedActions: string[] | null;
42
+ lifecycleReason: string | null;
43
+ lifecycleChangedBy: string | null;
44
+ lifecycleChangedAt: number | null;
45
+ createdAt: number | null;
46
+ updatedAt: number | null;
47
+ }
48
+ export type ServicePrincipalState = {
49
+ status: "not-service";
50
+ } | {
51
+ status: "ungoverned";
52
+ orgId: string;
53
+ serviceName: string;
54
+ } | {
55
+ status: "active";
56
+ policy: ServicePrincipalPolicy;
57
+ } | {
58
+ status: "suspended" | "retired";
59
+ policy: ServicePrincipalPolicy;
60
+ }
61
+ /** No verified org matches the address, so this is not the principal. */
62
+ | {
63
+ status: "org-mismatch";
64
+ } | {
65
+ status: "unavailable";
66
+ };
67
+ export declare function ensureTable(): Promise<void>;
68
+ export declare function servicePrincipalLifecycleLockKey(orgId: string, serviceName: string): string;
69
+ export declare function lockServicePrincipalLifecycle(db: Pick<DbExec, "execute">, orgId: string, serviceName: string): Promise<void>;
70
+ export declare function withServicePrincipalLifecycleLock<T>(orgId: string, serviceName: string, work: (db: DbExec) => Promise<T>): Promise<T>;
71
+ /** Throws when the store cannot be read; `null` means there is no record. */
72
+ export declare function getServicePrincipalPolicy(orgId: string, serviceName: string, db?: Pick<DbExec, "execute">): Promise<ServicePrincipalPolicy | null>;
73
+ export declare function listServicePrincipalPolicies(orgId: string): Promise<ServicePrincipalPolicy[]>;
74
+ export interface ServicePrincipalPolicyInput {
75
+ ownerEmail?: string | null;
76
+ team?: string | null;
77
+ riskTier?: ServicePrincipalRiskTier;
78
+ purpose?: string | null;
79
+ allowedActions?: string[] | null;
80
+ }
81
+ /**
82
+ * Create or update the governance fields. Fields left `undefined` keep their
83
+ * stored value; `null` clears one. Lifecycle changes go through
84
+ * `setServicePrincipalLifecycle` so they always carry an actor and a reason.
85
+ */
86
+ export declare function upsertServicePrincipalPolicy(orgId: string, serviceName: string, input: ServicePrincipalPolicyInput): Promise<ServicePrincipalPolicy>;
87
+ export declare class ServicePrincipalRetiredError extends Error {
88
+ readonly statusCode = 409;
89
+ constructor();
90
+ }
91
+ export declare function setServicePrincipalLifecycle(orgId: string, serviceName: string, lifecycle: ServicePrincipalLifecycle, change: {
92
+ actorEmail: string;
93
+ reason?: string | null;
94
+ }, db?: Pick<DbExec, "execute">): Promise<ServicePrincipalPolicy>;
95
+ /**
96
+ * Resolve whether a caller may run from its identity email and verified org.
97
+ * Non-service identities return `not-service` without touching the database,
98
+ * so the hot path for human callers is unchanged.
99
+ */
100
+ export declare function evaluateServicePrincipal(email: string | null | undefined, orgId?: string | null, db?: Pick<DbExec, "execute">): Promise<ServicePrincipalState>;
101
+ /** Whether the principal may call `actionName`. `null` grants everything. */
102
+ export declare function isActionGranted(allowedActions: string[] | null, actionName: string): boolean;
103
+ /** Message returned to a refused caller; never echoes the stored reason. */
104
+ export declare const SERVICE_PRINCIPAL_SUSPENDED_MESSAGE = "This service principal is suspended or retired. Ask an organization admin to resume it.";