@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.
- package/dist/a2a/handlers.js +57 -1
- package/dist/a2a/server.js +37 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +26 -3
- package/dist/agent/contain-principal.d.ts +9 -0
- package/dist/agent/contain-principal.js +53 -0
- package/dist/agent/production-agent.js +32 -2
- package/dist/agent/run-manager.js +14 -2
- package/dist/agent/run-store.js +66 -12
- package/dist/audit/actions/export-audit-events.d.ts +1 -1
- package/dist/audit/actions/export-audit-events.js +2 -2
- package/dist/audit/actions/export-audit-ocsf.d.ts +11 -0
- package/dist/audit/actions/export-audit-ocsf.js +183 -0
- package/dist/audit/actions/list-audit-events.d.ts +1 -1
- package/dist/audit/actions/list-audit-events.js +3 -3
- package/dist/audit/config.d.ts +1 -1
- package/dist/audit/config.js +5 -1
- package/dist/audit/index.d.ts +1 -0
- package/dist/audit/index.js +1 -0
- package/dist/audit/ocsf.d.ts +70 -0
- package/dist/audit/ocsf.js +228 -0
- package/dist/audit/record.js +1 -1
- package/dist/audit/store.js +9 -2
- package/dist/audit/types.d.ts +12 -1
- package/dist/cli/agent.js +2 -11
- package/dist/cli/connect.js +7 -2
- package/dist/client/use-db-sync.d.ts +13 -0
- package/dist/client/use-db-sync.js +22 -1
- package/dist/coding-tools/run-code.js +22 -1
- package/dist/coding-tools/sandbox/local-child-process-adapter.js +20 -0
- package/dist/collab/client.js +36 -3
- package/dist/collab/routes.d.ts +9 -4
- package/dist/collab/routes.js +15 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.js +15 -8
- package/dist/deploy/workspace-deploy.js +88 -13
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/file-upload/builder.js +18 -4
- package/dist/framework-tools.js +3 -0
- package/dist/identity/rekey.js +6 -0
- package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
- package/dist/localization/core-messages/supplemental/ar-SA.js +15 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +15 -0
- package/dist/localization/core-messages/supplemental/en-US.js +15 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +15 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +15 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +15 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +15 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +15 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +15 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +15 -0
- package/dist/localization/mcp-settings-messages.d.ts +15 -0
- package/dist/mcp/actions/create-org-service-token.d.ts +8 -0
- package/dist/mcp/actions/create-org-service-token.js +93 -4
- package/dist/mcp/actions/list-org-service-tokens.d.ts +2 -0
- package/dist/mcp/actions/list-org-service-tokens.js +36 -8
- package/dist/mcp/actions/service-principal-input.d.ts +48 -0
- package/dist/mcp/actions/service-principal-input.js +102 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.d.ts +15 -0
- package/dist/mcp/actions/set-service-principal-lifecycle.js +150 -0
- package/dist/mcp/actions/set-service-principal-policy.d.ts +14 -0
- package/dist/mcp/actions/set-service-principal-policy.js +105 -0
- package/dist/mcp/build-server.js +117 -13
- package/dist/mcp/connect-route.js +116 -4
- package/dist/mcp/connect-store.d.ts +7 -0
- package/dist/mcp/connect-store.js +21 -14
- package/dist/mcp-client/oauth-routes.js +2 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +7 -7
- package/dist/org/index.d.ts +2 -2
- package/dist/org/service-principal-guard.d.ts +64 -0
- package/dist/org/service-principal-guard.js +140 -0
- package/dist/org/service-principal-policy.d.ts +104 -0
- package/dist/org/service-principal-policy.js +272 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +5 -5
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/scripts/dev/index.js +2 -8
- package/dist/scripts/parse-args.d.ts +7 -0
- package/dist/scripts/parse-args.js +76 -0
- package/dist/scripts/utils.d.ts +1 -1
- package/dist/scripts/utils.js +1 -1
- package/dist/secrets/routes.d.ts +11 -11
- package/dist/server/action-discovery.js +16 -42
- package/dist/server/agent-chat/script-entries.js +3 -8
- package/dist/server/agent-chat-plugin.js +8 -42
- package/dist/server/agent-engine-default-model-route.d.ts +2 -2
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/auth.js +5 -2
- package/dist/server/core-routes-plugin.js +25 -6
- package/dist/server/google-oauth.js +8 -4
- package/dist/server/identity-sso.js +2 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/poll.d.ts +9 -0
- package/dist/server/poll.js +86 -7
- package/dist/server/query-echo-safe-redirect.d.ts +10 -0
- package/dist/server/query-echo-safe-redirect.js +102 -0
- package/dist/server/release-schema.js +4 -0
- package/dist/server/workspace-provider-oauth.js +2 -1
- package/dist/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
- package/dist/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/dist/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/templates/workspace-core/.agents/skills/actions/references/action-fields.md +1 -0
- package/dist/triggers/actions/manage-automation.d.ts +7 -7
- package/dist/vite/client.js +1 -0
- package/docs/content/audit-log.mdx +15 -0
- package/docs/content/external-agents-catalog.mdx +19 -0
- package/docs/content/http-api.mdx +21 -2
- package/docs/content/pr-visual-recap.mdx +3 -1
- package/docs/content/template-clips-embed.mdx +1 -1
- package/docs/content/turn-into-app.mdx +118 -0
- package/package.json +4 -4
- package/src/templates/chat/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/default/.agents/skills/actions/references/action-fields.md +1 -0
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +24 -25
- package/src/templates/factory/.agents/skills/review-latest-feedback/references/slack-reaction-replies.md +33 -0
- package/src/templates/headless/.agents/skills/actions/references/action-fields.md +1 -0
- 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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
}>>;
|
package/dist/org/index.d.ts
CHANGED
|
@@ -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.";
|