@k2b/cloud 0.38.0 → 0.40.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/README.md +8 -0
- package/package.json +12 -5
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-favicon.ts +4 -2
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/capabilities.ts +72 -48
- package/src/_internal/capability-reviews.ts +81 -0
- package/src/_internal/capability-schemas.ts +29 -0
- package/src/_internal/capability-sentences.ts +216 -1
- package/src/_internal/capability-streams.ts +7 -0
- package/src/_internal/define-app.ts +63 -16
- package/src/_internal/font-preloads.ts +2 -1
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/runtime-watcher.ts +6 -0
- package/src/_internal/static-assets.ts +52 -3
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +32 -7
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/EntitySearch.tsx +1 -1
- package/src/ai/action-review.ts +101 -0
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/background-review.ts +72 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +29 -11
- package/src/ai/capability-execution.ts +76 -43
- package/src/ai/capability-result.ts +95 -0
- package/src/ai/chat/action-card.ts +398 -0
- package/src/ai/chat/action-messages.ts +129 -0
- package/src/ai/chat/action-question.browser-harness.tsx +245 -0
- package/src/ai/chat/action-question.fixtures.ts +733 -0
- package/src/ai/chat/action-question.tsx +669 -0
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/blocks.tsx +19 -25
- package/src/ai/chat/message-actions.tsx +6 -0
- package/src/ai/chat/tool-action.tsx +481 -0
- package/src/ai/chat/turn-view.tsx +45 -163
- package/src/ai/chat/view-block.tsx +22 -7
- package/src/ai/chat-task-contracts.ts +31 -3
- package/src/ai/chat-task-reasons-migrate.ts +75 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +438 -78
- package/src/ai/client/controller.ts +59 -0
- package/src/ai/default-tools.ts +9 -1
- package/src/ai/executor.ts +190 -39
- package/src/ai/index.ts +14 -0
- package/src/ai/migrate.ts +39 -4
- package/src/ai/projects-routes.ts +3 -0
- package/src/ai/protocol.ts +14 -2
- package/src/ai/recent-resources.ts +240 -0
- package/src/ai/recent-sent-refs.ts +38 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +172 -2
- package/src/ai/runtime.ts +13 -27
- package/src/ai/skills-routes.ts +3 -0
- package/src/ai/skills.ts +10 -1
- package/src/ai/store.ts +232 -27
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-audit.ts +8 -0
- package/src/ai/tool-wording.ts +67 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +40 -4
- package/src/api/accounts-entities.ts +2 -1
- package/src/api/admin-ai-projects.ts +2 -0
- package/src/api/admin-ai-skills.ts +2 -0
- package/src/api/admin-ai-usage.ts +2 -0
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-geoip.ts +80 -0
- package/src/api/admin-identity.ts +3 -0
- package/src/api/admin-lifecycle.ts +3 -0
- package/src/api/admin-outgoing-mail.ts +6 -2
- package/src/api/app-approval.ts +54 -6
- package/src/api/approvals-loader.ts +23 -1
- package/src/api/approvals.ts +3 -1
- package/src/api/auth.ts +25 -6
- package/src/api/capabilities.ts +44 -11
- package/src/api/capability-streams.ts +12 -0
- package/src/api/help.ts +4 -28
- package/src/api/index.ts +7 -1
- package/src/api/lasting-access-messages.ts +17 -0
- package/src/api/mcp.ts +3 -0
- package/src/api/me-panel-consent.ts +71 -0
- package/src/api/me.ts +80 -33
- package/src/api/search.ts +3 -1
- package/src/api/sync-ops.ts +2 -0
- package/src/api/widgets.ts +3 -0
- package/src/approvals/Approvals.tsx +180 -238
- package/src/approvals/messages.ts +12 -24
- package/src/browser/CloudResourceSearch.browser-harness.tsx +10 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/app-approval.ts +60 -6
- package/src/browser/commands.ts +22 -6
- package/src/browser/composer-catalog.ts +145 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +160 -0
- package/src/browser/composer-open-messages.ts +17 -0
- package/src/browser/composer-open.ts +110 -0
- package/src/browser/discoverable-command.ts +6 -0
- package/src/browser/hints.ts +6 -0
- package/src/browser/live-connection.ts +26 -2
- package/src/browser/navigation-apps.ts +7 -0
- package/src/browser/panel-context-bridge.ts +30 -0
- package/src/browser/panel-context-receiver.ts +53 -0
- package/src/browser/panel-menu.tsx +56 -0
- package/src/browser/panel-messages.ts +35 -0
- package/src/browser/panel-shortcuts.ts +31 -0
- package/src/browser/panel-slot.ts +756 -0
- package/src/browser/panel.ts +65 -0
- package/src/browser/resource-picker.tsx +12 -1
- package/src/browser/search-commands.ts +6 -3
- package/src/capabilities/client.ts +8 -2
- package/src/capabilities/reviews.ts +205 -0
- package/src/capabilities/server.ts +10 -3
- package/src/capabilities/testing.ts +47 -0
- package/src/capabilities/types.ts +2 -2
- package/src/cli/account.ts +13 -3
- package/src/cli/admin/geoip.ts +115 -0
- package/src/cli/admin/index.ts +3 -0
- package/src/contracts/app-approval.ts +159 -12
- package/src/contracts/app.ts +14 -0
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/capabilities.ts +410 -18
- package/src/contracts/geoip.ts +42 -0
- package/src/contracts/index.ts +3 -0
- package/src/contracts/panel-context-plain.ts +99 -0
- package/src/contracts/panel-context.ts +93 -0
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/seen-hints.ts +35 -0
- package/src/contracts/shared.ts +2 -0
- package/src/server/client-ip.ts +5 -0
- package/src/server/help-payload.ts +38 -0
- package/src/server/middleware/auth.ts +11 -1
- package/src/server/middleware/middleware.ts +9 -0
- package/src/server/middleware/no-transform.ts +19 -0
- package/src/server/middleware/rate-limit.ts +4 -6
- package/src/server/request-context/address.ts +45 -0
- package/src/server/request-context/geoip.ts +380 -0
- package/src/server/request-context/index.ts +33 -0
- package/src/server/request-context/user-agent.ts +95 -0
- package/src/server/services/access.ts +5 -1
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/app-approval.ts +533 -90
- package/src/services/approvals/device.ts +9 -5
- package/src/services/approvals/index.ts +6 -5
- package/src/services/approvals/list.ts +26 -5
- package/src/services/auth-flows/notification-sender.ts +3 -0
- package/src/services/geoip/index.ts +164 -0
- package/src/services/geoip/store.ts +30 -0
- package/src/services/geoip/upload.ts +150 -0
- package/src/services/identity/routes.ts +5 -1
- package/src/services/index.ts +32 -3
- package/src/services/known-browsers.ts +312 -0
- package/src/services/login-address-store.ts +51 -0
- package/src/services/login-context.ts +118 -0
- package/src/services/mandates/index.ts +3 -0
- package/src/services/mandates/policy.ts +15 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/notifications/email-frame.ts +1 -1
- package/src/services/oauth-tokens.ts +16 -2
- package/src/services/panel-context-consent.ts +58 -0
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/service-account-credentials.ts +21 -0
- package/src/services/session/index.ts +97 -16
- package/src/services/session/recent.ts +13 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +19 -17
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/services/webauthn.ts +12 -0
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/email-html.ts +2 -0
- package/src/shared/help.ts +3 -0
- package/src/shared/redirect.ts +12 -0
- package/src/shared-text/cursor.ts +22 -0
- package/src/shared-text/documents.ts +142 -0
- package/src/shared-text/events.ts +43 -0
- package/src/shared-text/index.ts +12 -0
- package/src/shared-text/log.ts +48 -0
- package/src/shared-text/text-diff.ts +23 -0
- package/src/shared-text/types.ts +41 -0
- package/src/ssr/AdminLayout.tsx +2 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +95 -10
- package/src/ssr/Layout.tsx +66 -10
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +2 -0
- package/src/ssr/LayoutHelp.tsx +435 -274
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +136 -0
- package/src/ssr/LayoutHelpTrigger.tsx +27 -2
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/MobileNavigation.tsx +2 -2
- package/src/ssr/PanelLayout.tsx +35 -0
- package/src/ssr/PanelRuntime.island.tsx +94 -0
- package/src/ssr/PanelSlot.tsx +158 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +69 -5
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +105 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +28 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +439 -0
- package/src/ssr/dialog-app-colors.browser-harness.tsx +43 -0
- package/src/ssr/help-messages.ts +4 -0
- package/src/ssr/help.ts +20 -0
- package/src/ssr/hints.ts +25 -0
- package/src/ssr/index.ts +5 -0
- package/src/ssr/islands/index.ts +4 -3
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-article.server.ts +50 -0
- package/src/ssr/layout-help-start.ts +33 -7
- package/src/ssr/panel-consent.ts +29 -0
- package/src/ssr/panel-context.ts +10 -0
- package/src/ssr/panel-slot.browser-harness.tsx +159 -0
- package/src/ssr/platform-messages.ts +48 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +5 -0
- package/src/ssr/shell-shortcuts.ts +13 -2
- package/src/styles/effects.css +201 -11
- package/src/styles/file-chooser.css +2 -1
- package/src/styles/global.css +314 -63
- package/src/styles/panel.css +416 -0
- package/src/styles/resource-search.css +6 -2
- package/src/styles/tokens.css +36 -13
- package/src/styles/utilities-buttons.css +3 -2
- package/src/styles/utilities-data.css +2 -1
- package/src/styles/utilities-feedback.css +2 -2
- package/src/styles/utilities-layout.css +3 -13
- package/src/styles/utilities-markdown-editor.css +6 -6
- package/src/styles/utilities-navigation.css +22 -43
package/src/ai/approvals.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { sql } from "bun";
|
|
1
|
+
import { type SQL, sql } from "bun";
|
|
2
|
+
import type { AccessSubject } from "../contracts/shared";
|
|
3
|
+
import type { RequestActor } from "../server/middleware/auth";
|
|
4
|
+
import { type AuditActor, type AuditRecordParams, audit } from "../services/audit";
|
|
5
|
+
import { toPgTextArray } from "../services/postgres";
|
|
2
6
|
import type { AiToolApprovalPolicy, AiTurnRunConfig } from "./types";
|
|
3
7
|
|
|
4
8
|
/** Background authority never inherits a user's interactive Always Allow preference. */
|
|
@@ -31,6 +35,16 @@ export type AiToolApprovalContext = {
|
|
|
31
35
|
actorUserId: string;
|
|
32
36
|
};
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Request identity for approval changes; never persist this context in a turn run config.
|
|
40
|
+
* When actor is absent, audit uses { userId: actorUserId } for the approval's owner.
|
|
41
|
+
* The access subject is derived from actor, or falls back to { type: "user", userId: actorUserId }.
|
|
42
|
+
*/
|
|
43
|
+
export type AiToolApprovalChangeContext = AiToolApprovalContext & {
|
|
44
|
+
actor?: RequestActor;
|
|
45
|
+
requestId?: string | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
34
48
|
/** Where a remembered approval applies: in one chat until it is deleted, or always. */
|
|
35
49
|
export type AiToolApprovalRemember = "chat" | "always";
|
|
36
50
|
|
|
@@ -55,6 +69,59 @@ type AiToolApprovalPreferenceRow = {
|
|
|
55
69
|
expires_at: Date | string | null;
|
|
56
70
|
};
|
|
57
71
|
|
|
72
|
+
type ApprovalAuditRow = Pick<AiToolApprovalPreferenceRow, "id" | "tool_name" | "approval_scope" | "conversation_id">;
|
|
73
|
+
type ApprovalAuditIdentity = {
|
|
74
|
+
actor: AuditActor | null;
|
|
75
|
+
accessSubject: AccessSubject | null;
|
|
76
|
+
serviceAccountId?: string;
|
|
77
|
+
requestId?: string | null;
|
|
78
|
+
reason?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const approvalAuditIdentity = (context: AiToolApprovalChangeContext): ApprovalAuditIdentity => {
|
|
82
|
+
const user = context.actor?.kind === "user" ? context.actor.user : context.actor?.delegatedUser;
|
|
83
|
+
return {
|
|
84
|
+
actor: context.actor
|
|
85
|
+
? user
|
|
86
|
+
? { userId: user.id, uid: user.uid, provider: user.provider, roles: user.roles }
|
|
87
|
+
: null
|
|
88
|
+
: { userId: context.actorUserId },
|
|
89
|
+
// Match aiChatAccessSubject locally: importing assistant-models creates a cycle through firecrawl-tools.
|
|
90
|
+
accessSubject: user
|
|
91
|
+
? {
|
|
92
|
+
type: "user",
|
|
93
|
+
userId: user.id,
|
|
94
|
+
...(context.actor?.kind === "service_account" ? { delegatedByServiceAccountId: context.actor.serviceAccount.id } : {}),
|
|
95
|
+
}
|
|
96
|
+
: context.actor?.kind === "service_account"
|
|
97
|
+
? { type: "service_account", serviceAccountId: context.actor.serviceAccount.id }
|
|
98
|
+
: { type: "user", userId: context.actorUserId },
|
|
99
|
+
...(context.actor?.kind === "service_account" ? { serviceAccountId: context.actor.serviceAccount.id } : {}),
|
|
100
|
+
requestId: context.requestId,
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const approvalAuditEvent = (
|
|
105
|
+
action: "ai.approval.remember" | "ai.approval.revoke",
|
|
106
|
+
row: ApprovalAuditRow,
|
|
107
|
+
identity: ApprovalAuditIdentity,
|
|
108
|
+
): AuditRecordParams => ({
|
|
109
|
+
action,
|
|
110
|
+
outcome: "allowed",
|
|
111
|
+
actor: identity.actor,
|
|
112
|
+
target: { type: "ai_approval", id: row.id, label: row.tool_name },
|
|
113
|
+
requestId: identity.requestId,
|
|
114
|
+
reason: identity.reason,
|
|
115
|
+
metadata: {
|
|
116
|
+
toolName: row.tool_name,
|
|
117
|
+
approvalScope: row.approval_scope,
|
|
118
|
+
remember: row.conversation_id !== null ? "chat" : "always",
|
|
119
|
+
...(row.conversation_id !== null ? { conversationId: row.conversation_id } : {}),
|
|
120
|
+
accessSubject: identity.accessSubject,
|
|
121
|
+
...(identity.serviceAccountId ? { serviceAccountId: identity.serviceAccountId } : {}),
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
58
125
|
export const aiToolApprovalScope = (toolName: string, policy: AiToolApprovalPolicy | undefined): string => {
|
|
59
126
|
if (policy && typeof policy === "object") return policy.scope ?? toolName;
|
|
60
127
|
return toolName;
|
|
@@ -100,20 +167,12 @@ export const hasRememberedAiToolApproval = async (
|
|
|
100
167
|
|
|
101
168
|
/** Remembers an approval everywhere, or in one chat when `conversationId` is set; a chat approval ends with its chat. */
|
|
102
169
|
export const rememberAiToolApproval = async (
|
|
103
|
-
context:
|
|
170
|
+
context: AiToolApprovalChangeContext,
|
|
104
171
|
input: { toolName: string; approvalScope: string; conversationId?: string | null; expiresAt?: Date | null },
|
|
105
172
|
): Promise<void> => {
|
|
106
173
|
const conversationId = input.conversationId ?? null;
|
|
107
|
-
await sql.begin(async () => {
|
|
108
|
-
await
|
|
109
|
-
DELETE FROM ai.tool_approval_preferences
|
|
110
|
-
WHERE actor_user_id = ${context.actorUserId}
|
|
111
|
-
AND tool_name = ${input.toolName}
|
|
112
|
-
AND approval_scope = ${input.approvalScope}
|
|
113
|
-
AND conversation_id IS NOT DISTINCT FROM ${conversationId}::uuid
|
|
114
|
-
`;
|
|
115
|
-
|
|
116
|
-
await sql`
|
|
174
|
+
await sql.begin(async (tx) => {
|
|
175
|
+
const [row] = await tx<{ id: string }[]>`
|
|
117
176
|
INSERT INTO ai.tool_approval_preferences (
|
|
118
177
|
actor_user_id,
|
|
119
178
|
tool_name,
|
|
@@ -130,24 +189,46 @@ export const rememberAiToolApproval = async (
|
|
|
130
189
|
now(),
|
|
131
190
|
${input.expiresAt ?? null}
|
|
132
191
|
)
|
|
192
|
+
ON CONFLICT (actor_user_id, tool_name, approval_scope, conversation_id)
|
|
193
|
+
DO UPDATE SET
|
|
194
|
+
created_at = EXCLUDED.created_at,
|
|
195
|
+
last_used_at = EXCLUDED.last_used_at,
|
|
196
|
+
expires_at = EXCLUDED.expires_at
|
|
197
|
+
RETURNING id
|
|
133
198
|
`;
|
|
199
|
+
await audit.record(
|
|
200
|
+
approvalAuditEvent(
|
|
201
|
+
"ai.approval.remember",
|
|
202
|
+
{
|
|
203
|
+
id: row!.id,
|
|
204
|
+
tool_name: input.toolName,
|
|
205
|
+
approval_scope: input.approvalScope,
|
|
206
|
+
conversation_id: conversationId,
|
|
207
|
+
},
|
|
208
|
+
approvalAuditIdentity(context),
|
|
209
|
+
),
|
|
210
|
+
tx,
|
|
211
|
+
);
|
|
134
212
|
});
|
|
135
213
|
};
|
|
136
214
|
|
|
137
215
|
/** Forgets the approval with exactly this reach: everywhere, or in the one chat `conversationId` names. */
|
|
138
216
|
export const forgetAiToolApproval = async (
|
|
139
|
-
context:
|
|
217
|
+
context: AiToolApprovalChangeContext,
|
|
140
218
|
input: { toolName: string; approvalScope: string; conversationId?: string | null },
|
|
141
219
|
): Promise<boolean> => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
220
|
+
return sql.begin(async (tx) => {
|
|
221
|
+
const rows = await tx<ApprovalAuditRow[]>`
|
|
222
|
+
DELETE FROM ai.tool_approval_preferences
|
|
223
|
+
WHERE actor_user_id = ${context.actorUserId}
|
|
224
|
+
AND tool_name = ${input.toolName}
|
|
225
|
+
AND approval_scope = ${input.approvalScope}
|
|
226
|
+
AND conversation_id IS NOT DISTINCT FROM ${input.conversationId ?? null}::uuid
|
|
227
|
+
RETURNING id, tool_name, approval_scope, conversation_id
|
|
228
|
+
`;
|
|
229
|
+
for (const row of rows) await audit.record(approvalAuditEvent("ai.approval.revoke", row, approvalAuditIdentity(context)), tx);
|
|
230
|
+
return rows.length > 0;
|
|
231
|
+
});
|
|
151
232
|
};
|
|
152
233
|
|
|
153
234
|
const toIsoString = (value: Date | string): string => (value instanceof Date ? value : new Date(value)).toISOString();
|
|
@@ -186,12 +267,42 @@ export const listAiToolApprovalPreferences = async (
|
|
|
186
267
|
return rows.map(approvalPreferenceFromRow);
|
|
187
268
|
};
|
|
188
269
|
|
|
189
|
-
export const revokeAiToolApprovalPreference = async (
|
|
190
|
-
|
|
270
|
+
export const revokeAiToolApprovalPreference = async (
|
|
271
|
+
actorUserId: string,
|
|
272
|
+
preferenceId: string,
|
|
273
|
+
change?: { actor?: RequestActor; requestId?: string | null },
|
|
274
|
+
): Promise<boolean> =>
|
|
275
|
+
sql.begin(async (tx) => {
|
|
276
|
+
const rows = await tx<ApprovalAuditRow[]>`
|
|
277
|
+
DELETE FROM ai.tool_approval_preferences
|
|
278
|
+
WHERE id = ${preferenceId}
|
|
279
|
+
AND actor_user_id = ${actorUserId}
|
|
280
|
+
RETURNING id, tool_name, approval_scope, conversation_id
|
|
281
|
+
`;
|
|
282
|
+
for (const row of rows)
|
|
283
|
+
await audit.record(approvalAuditEvent("ai.approval.revoke", row, approvalAuditIdentity({ ...change, actorUserId })), tx);
|
|
284
|
+
return rows.length > 0;
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/** Core migrations supply their transaction so deletion and system audit events commit together. */
|
|
288
|
+
export const revokeAiToolApprovalPreferencesAsSystem = async (
|
|
289
|
+
db: SQL,
|
|
290
|
+
input: { ids: readonly string[]; reason: string },
|
|
291
|
+
): Promise<number> => {
|
|
292
|
+
if (input.ids.length === 0) return 0;
|
|
293
|
+
const rows = await db<(ApprovalAuditRow & { actor_user_id: string | null })[]>`
|
|
191
294
|
DELETE FROM ai.tool_approval_preferences
|
|
192
|
-
WHERE id = ${
|
|
193
|
-
|
|
194
|
-
RETURNING id
|
|
295
|
+
WHERE id = ANY(${toPgTextArray([...input.ids])}::uuid[])
|
|
296
|
+
RETURNING id, tool_name, approval_scope, conversation_id, actor_user_id
|
|
195
297
|
`;
|
|
196
|
-
|
|
298
|
+
for (const row of rows)
|
|
299
|
+
await audit.record(
|
|
300
|
+
approvalAuditEvent("ai.approval.revoke", row, {
|
|
301
|
+
actor: null,
|
|
302
|
+
accessSubject: row.actor_user_id === null ? null : { type: "user", userId: row.actor_user_id },
|
|
303
|
+
reason: input.reason,
|
|
304
|
+
}),
|
|
305
|
+
db,
|
|
306
|
+
);
|
|
307
|
+
return rows.length;
|
|
197
308
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CapabilityActionPolicy, CapabilityActionReviewData } from "../contracts/capabilities";
|
|
2
|
+
import { aiChatTasks } from "./chat-tasks";
|
|
3
|
+
|
|
4
|
+
export class AiBackgroundApprovalRequiredError extends Error {
|
|
5
|
+
readonly code = "NEEDS_APPROVAL";
|
|
6
|
+
|
|
7
|
+
constructor(readonly action: string) {
|
|
8
|
+
super(
|
|
9
|
+
`NEEDS_APPROVAL: ${action} needs the user's approval every time, and routines never ask. Stop and say in one sentence what was missing.`,
|
|
10
|
+
);
|
|
11
|
+
this.name = "AiBackgroundApprovalRequiredError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type BackgroundActionInput = {
|
|
16
|
+
turnId: string;
|
|
17
|
+
mandate: { id: string; revision: number };
|
|
18
|
+
appId: string;
|
|
19
|
+
capabilityId: string;
|
|
20
|
+
kind: "query" | "action";
|
|
21
|
+
args: unknown;
|
|
22
|
+
policy?: CapabilityActionPolicy;
|
|
23
|
+
approval?: "none" | "rememberable" | "always";
|
|
24
|
+
hasReview: boolean;
|
|
25
|
+
action: string;
|
|
26
|
+
review: () => Promise<CapabilityActionReviewData | null>;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Core routine gate. A review can narrow a mandate, never grant authority. */
|
|
31
|
+
export const authorizeBackgroundActionWith = async (
|
|
32
|
+
input: BackgroundActionInput,
|
|
33
|
+
authorizeCapability: typeof aiChatTasks.authorizeCapability,
|
|
34
|
+
): Promise<CapabilityActionReviewData | null> => {
|
|
35
|
+
// These declarations can never be granted to a routine. Stop with the approval state even if a stale grant exists.
|
|
36
|
+
if (input.kind === "action" && input.policy && (!("silent" in input.policy) || input.policy.routines === false)) {
|
|
37
|
+
await aiChatTasks.markNeedsApproval({ turnId: input.turnId, action: input.action });
|
|
38
|
+
throw new AiBackgroundApprovalRequiredError(input.action);
|
|
39
|
+
}
|
|
40
|
+
await authorizeCapability({
|
|
41
|
+
mandate: input.mandate,
|
|
42
|
+
appId: input.appId,
|
|
43
|
+
capabilityId: input.capabilityId,
|
|
44
|
+
kind: input.kind,
|
|
45
|
+
input: input.args,
|
|
46
|
+
approval: input.approval,
|
|
47
|
+
...(input.policy ? { policy: input.policy } : {}),
|
|
48
|
+
});
|
|
49
|
+
if (input.kind === "query" || (!input.policy && input.approval === "none")) return null;
|
|
50
|
+
input.signal?.throwIfAborted();
|
|
51
|
+
if (!input.policy && input.approval !== "rememberable") {
|
|
52
|
+
await aiChatTasks.markNeedsApproval({ turnId: input.turnId, action: input.action });
|
|
53
|
+
throw new AiBackgroundApprovalRequiredError(input.action);
|
|
54
|
+
}
|
|
55
|
+
let review: CapabilityActionReviewData | null = null;
|
|
56
|
+
if (input.hasReview && !input.signal?.aborted) {
|
|
57
|
+
try {
|
|
58
|
+
review = await input.review();
|
|
59
|
+
} catch {
|
|
60
|
+
// An unavailable review cannot authorize an unattended effect.
|
|
61
|
+
input.signal?.throwIfAborted();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
input.signal?.throwIfAborted();
|
|
65
|
+
if (review && (input.policy ? "sentence" in review && review.ask === undefined : !("sentence" in review) && review.approvalScope))
|
|
66
|
+
return review;
|
|
67
|
+
await aiChatTasks.markNeedsApproval({ turnId: input.turnId, action: input.action });
|
|
68
|
+
throw new AiBackgroundApprovalRequiredError(input.action);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const authorizeBackgroundAction = (input: BackgroundActionInput): Promise<CapabilityActionReviewData | null> =>
|
|
72
|
+
authorizeBackgroundActionWith(input, aiChatTasks.authorizeCapability);
|
package/src/ai/browser.ts
CHANGED
|
@@ -189,6 +189,16 @@ export const launchAssistant = async (input: LaunchAssistantInput): Promise<Assi
|
|
|
189
189
|
};
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
+
export {
|
|
193
|
+
CHAT_TASK_REASON_CODES,
|
|
194
|
+
type ChatTaskReason,
|
|
195
|
+
type ChatTaskReasonCode,
|
|
196
|
+
chatTaskLocale,
|
|
197
|
+
chatTaskNeedsNewTime,
|
|
198
|
+
chatTaskReasonSentence,
|
|
199
|
+
chatTaskReasonState,
|
|
200
|
+
chatTaskStopSentence,
|
|
201
|
+
} from "./chat-task-reasons";
|
|
192
202
|
export {
|
|
193
203
|
CLOUD_AI_STATS_MAX_ITEMS,
|
|
194
204
|
CLOUD_AI_TABLE_MAX_COLUMNS,
|
package/src/ai/capabilities.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { Tool, ToolContext, ToolResolver } from "@k2b/nessi";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { resolveCapabilityActionWording, resolveCapabilityOperationTitle } from "../_internal/capabilities";
|
|
5
|
+
import { capabilityReviewView } from "../_internal/capability-reviews";
|
|
5
6
|
import {
|
|
6
7
|
CAPABILITY_APPROVAL_REASON_FIELD,
|
|
7
8
|
CAPABILITY_APPROVAL_REASON_MAX_CHARS,
|
|
@@ -13,8 +14,9 @@ import {
|
|
|
13
14
|
import { HELP_READ_MAX_CHARS, HELP_SEARCH_MAX_LIMIT, readHelpArticle } from "../_internal/help-catalog";
|
|
14
15
|
import {
|
|
15
16
|
type CapabilityActionManifest,
|
|
16
|
-
type
|
|
17
|
+
type CapabilityActionReviewData,
|
|
17
18
|
type CapabilityQueryManifest,
|
|
19
|
+
capabilityActionPolicy,
|
|
18
20
|
cloudResourceRefAppId,
|
|
19
21
|
LinkedCloudResourceReferenceSchema,
|
|
20
22
|
resolveCapabilityResourceReader,
|
|
@@ -756,9 +758,14 @@ export const createLoadedAiCapabilityTools = (input: {
|
|
|
756
758
|
actor: RequestActor;
|
|
757
759
|
/** Locale of the turn; the approval text a person reads is worded in it. */
|
|
758
760
|
locale?: string;
|
|
759
|
-
review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<
|
|
760
|
-
authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown) => Promise<void>;
|
|
761
|
-
onReview?: (
|
|
761
|
+
review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<CapabilityActionReviewData | null>;
|
|
762
|
+
authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<void>;
|
|
763
|
+
onReview?: (
|
|
764
|
+
callId: string,
|
|
765
|
+
review: CapabilityActionReviewData,
|
|
766
|
+
approval: CapabilityActionManifest["approval"],
|
|
767
|
+
entry: AiCapabilityCatalogEntry,
|
|
768
|
+
) => void | Promise<void>;
|
|
762
769
|
execute: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<unknown>;
|
|
763
770
|
}): AiRuntimeTool[] => {
|
|
764
771
|
const byName = new Map(input.catalog.map((entry) => [entry.name, entry]));
|
|
@@ -769,7 +776,7 @@ export const createLoadedAiCapabilityTools = (input: {
|
|
|
769
776
|
defineAiTool({
|
|
770
777
|
name: entry.providerName,
|
|
771
778
|
canonicalName: entry.name,
|
|
772
|
-
description: `${entry.title}. ${entry.description} Never retry ACTION_OUTCOME_UNKNOWN. Do not retry unchanged after INTERNAL or INVALID_APP_RESPONSE; report the provider error.`,
|
|
779
|
+
description: `${entry.title}. ${entry.description} Never retry ACTION_OUTCOME_UNKNOWN or NEEDS_APPROVAL. Never retry IDEMPOTENCY_UNCERTAIN or IDEMPOTENCY_IN_PROGRESS. After ACTION_NOT_DONE, ask the person before trying again. Do not retry unchanged after INTERNAL or INVALID_APP_RESPONSE; report the provider error.`,
|
|
773
780
|
inputSchema: aiCapabilityInputSchema(
|
|
774
781
|
offersApprovalReason(entry) ? withApprovalReason(entry.operation.inputSchema) : entry.operation.inputSchema,
|
|
775
782
|
),
|
|
@@ -779,16 +786,21 @@ export const createLoadedAiCapabilityTools = (input: {
|
|
|
779
786
|
approval: "never",
|
|
780
787
|
}).server(async (modelArgs, context) => {
|
|
781
788
|
const args = withoutApprovalReason(entry, modelArgs);
|
|
782
|
-
await input.authorizeBackground?.(entry, args);
|
|
789
|
+
await input.authorizeBackground?.(entry, args, context);
|
|
783
790
|
if (!input.authorizeBackground && entry.kind === "action" && (entry.operation as CapabilityActionManifest).approval !== "none") {
|
|
784
791
|
const review = (await input.review?.(entry, args, context)) ?? null;
|
|
785
|
-
if (review && context.callId)
|
|
792
|
+
if (review && context.callId)
|
|
793
|
+
await input.onReview?.(context.callId, review, "approval" in entry.operation ? entry.operation.approval : undefined, entry);
|
|
786
794
|
// Text-only readers such as the CLI read what the card shows: the app's sentence, its review, and the model's reason.
|
|
787
795
|
const locale = input.locale ?? DEFAULT_LOCALE;
|
|
788
796
|
const message = capabilityApprovalText({
|
|
789
797
|
appName: entry.display.appName,
|
|
790
798
|
subject: capabilityActionSubject(aiCapabilityActionWording(entry, locale), args, { locale, timeZone: context.timeZone }),
|
|
791
|
-
review: review
|
|
799
|
+
review: review
|
|
800
|
+
? capabilityReviewView(review, {
|
|
801
|
+
timeZone: context.timeZone ?? "UTC",
|
|
802
|
+
}).message
|
|
803
|
+
: undefined,
|
|
792
804
|
reason: offersApprovalReason(entry) ? capabilityApprovalReason(modelArgs) : null,
|
|
793
805
|
locale,
|
|
794
806
|
});
|
|
@@ -873,9 +885,14 @@ export const createAiToolResolver =
|
|
|
873
885
|
locale?: string;
|
|
874
886
|
maxLoadedTools?: number;
|
|
875
887
|
execute?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<unknown>;
|
|
876
|
-
review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<
|
|
877
|
-
authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown) => Promise<void>;
|
|
878
|
-
onReview?: (
|
|
888
|
+
review?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<CapabilityActionReviewData | null>;
|
|
889
|
+
authorizeBackground?: (entry: AiCapabilityCatalogEntry, args: unknown, context: ToolContext) => Promise<void>;
|
|
890
|
+
onReview?: (
|
|
891
|
+
callId: string,
|
|
892
|
+
review: CapabilityActionReviewData,
|
|
893
|
+
approval: CapabilityActionManifest["approval"],
|
|
894
|
+
entry: AiCapabilityCatalogEntry,
|
|
895
|
+
) => void | Promise<void>;
|
|
879
896
|
onPrepared?: (snapshot: {
|
|
880
897
|
prepared: PreparedAiTools;
|
|
881
898
|
presentations: Map<string, AiToolPresentation>;
|
|
@@ -907,6 +924,7 @@ export const createAiToolResolver =
|
|
|
907
924
|
capabilityId: entry.operation.localId,
|
|
908
925
|
kind: entry.kind,
|
|
909
926
|
approval: entry.kind === "action" && "approval" in entry.operation ? entry.operation.approval : undefined,
|
|
927
|
+
capabilityPolicy: capabilityActionPolicy(entry.app.manifest, entry.operation.localId),
|
|
910
928
|
}));
|
|
911
929
|
const mandateExcluded = new Set(
|
|
912
930
|
fullCapabilityCatalog.filter((entry) => !mandateAllows(entry)).flatMap((entry) => [entry.name, entry.providerName]),
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import type { ToolContext } from "@k2b/nessi";
|
|
3
|
+
import { capabilityActionReceiptIssues, capabilityQueryChecksActionOutcome } from "../_internal/capability-reviews";
|
|
3
4
|
import { type CapabilityDispatchDependencies, dispatchCapability } from "../api/capabilities";
|
|
4
5
|
import { capabilityValueMeta, recordCapabilityExecution } from "../capabilities/executions";
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type CapabilityActionCallReview,
|
|
8
|
+
CapabilityActionCheckSchema,
|
|
9
|
+
type CapabilityActionManifest,
|
|
10
|
+
type CapabilityActionReviewData,
|
|
11
|
+
CapabilityActionReviewDataSchema,
|
|
12
|
+
} from "../contracts/capabilities";
|
|
6
13
|
import type { RequestActor } from "../server";
|
|
7
14
|
import type { AccessSubject } from "../server/services/access";
|
|
8
15
|
import { isAccountCategoryAllowed } from "../services/account-category-policy";
|
|
@@ -10,10 +17,14 @@ import { isAccountExpired } from "../services/account-model";
|
|
|
10
17
|
import { accounts } from "../services/accounts";
|
|
11
18
|
import { withoutAdminRole } from "../services/session/user";
|
|
12
19
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
20
|
+
import type { AiActionCheck } from "./action-review";
|
|
13
21
|
import type { AiCapabilityCatalogEntry } from "./capabilities";
|
|
22
|
+
import { AiCapabilityExecutionError } from "./capability-result";
|
|
14
23
|
import { aiToolAudit } from "./tool-audit";
|
|
15
24
|
import type { AiConversationService } from "./types";
|
|
16
25
|
|
|
26
|
+
export { AiCapabilityExecutionError, aiCapabilityCheckResult } from "./capability-result";
|
|
27
|
+
|
|
17
28
|
type CapabilityActor = Extract<RequestActor, { kind: "user" }>;
|
|
18
29
|
|
|
19
30
|
export const resolveAiCapabilityActor = async (input: {
|
|
@@ -38,17 +49,6 @@ export const resolveAiCapabilityActor = async (input: {
|
|
|
38
49
|
return { actor: { kind: "user", user }, accessSubject: { type: "user", userId: user.id } };
|
|
39
50
|
};
|
|
40
51
|
|
|
41
|
-
export class AiCapabilityExecutionError extends Error {
|
|
42
|
-
constructor(
|
|
43
|
-
readonly code: string,
|
|
44
|
-
readonly status: number,
|
|
45
|
-
message: string,
|
|
46
|
-
) {
|
|
47
|
-
super(`${code}: ${message}`);
|
|
48
|
-
this.name = "AiCapabilityExecutionError";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
52
|
const idempotencyKey = (conversationId: string, callId: string): string =>
|
|
53
53
|
`ai-${createHash("sha256").update(`${conversationId}\0${callId}`).digest("hex")}`;
|
|
54
54
|
|
|
@@ -71,13 +71,13 @@ const validationIssueText = (details: unknown): string => {
|
|
|
71
71
|
return issues.length > 0 ? ` Input issues: ${issues.join("; ")}` : "";
|
|
72
72
|
};
|
|
73
73
|
|
|
74
|
-
const parseCapabilityResponse = async (response: Response): Promise<unknown> => {
|
|
74
|
+
const parseCapabilityResponse = async (response: Response, dispatched: boolean): Promise<unknown> => {
|
|
75
75
|
const body = (await response.json().catch(() => null)) as { code?: unknown; message?: unknown; details?: unknown } | null;
|
|
76
76
|
if (response.ok) return body;
|
|
77
77
|
const code = typeof body?.code === "string" ? body.code : "CAPABILITY_FAILED";
|
|
78
78
|
const message = typeof body?.message === "string" ? body.message : "Capability execution failed";
|
|
79
79
|
const issueText = code === "VALIDATION_FAILED" ? validationIssueText(body?.details) : "";
|
|
80
|
-
throw new AiCapabilityExecutionError(code, response.status, `${message}${issueText}
|
|
80
|
+
throw new AiCapabilityExecutionError(code, response.status, `${message}${issueText}`, dispatched);
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
type AiCapabilityCall = {
|
|
@@ -96,7 +96,10 @@ type AiCapabilityCall = {
|
|
|
96
96
|
};
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
const dispatchAiCapability = async (
|
|
99
|
+
const dispatchAiCapability = async (
|
|
100
|
+
input: Omit<AiCapabilityCall, "context"> & { context: Pick<ToolContext, "callId" | "signal"> },
|
|
101
|
+
review: boolean,
|
|
102
|
+
): Promise<unknown> => {
|
|
100
103
|
if (input.authority.accessSubject.type !== "user" || input.authority.accessSubject.userId !== input.authority.actor.user.id) {
|
|
101
104
|
throw new Error("Cloud capability authority is inconsistent.");
|
|
102
105
|
}
|
|
@@ -128,41 +131,71 @@ const dispatchAiCapability = async (input: AiCapabilityCall, review: boolean): P
|
|
|
128
131
|
headers,
|
|
129
132
|
signal: input.context.signal,
|
|
130
133
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
134
|
+
let dispatched = false;
|
|
135
|
+
const response = await dispatch({
|
|
136
|
+
request,
|
|
137
|
+
kind: input.entry.kind === "query" ? "queries" : "actions",
|
|
138
|
+
origin: "assistant",
|
|
139
|
+
review,
|
|
140
|
+
appId: input.entry.appId,
|
|
141
|
+
capabilityId: input.entry.operation.localId,
|
|
142
|
+
input: input.args,
|
|
143
|
+
authority: {
|
|
144
|
+
actor: input.authority.actor,
|
|
145
|
+
accessSubject: input.authority.accessSubject,
|
|
146
|
+
credentialKind: "session",
|
|
147
|
+
scopes: [],
|
|
148
|
+
},
|
|
149
|
+
mandate: input.mandate
|
|
150
|
+
? {
|
|
151
|
+
mandateId: input.mandate.id,
|
|
152
|
+
mandateRevision: input.mandate.revision,
|
|
153
|
+
ownerAppId: "core",
|
|
154
|
+
...(!review && input.actionApproval ? { actionApproval: input.actionApproval } : {}),
|
|
155
|
+
}
|
|
156
|
+
: undefined,
|
|
157
|
+
dependencies: input.dependencies,
|
|
158
|
+
onUpstreamAttempt: () => {
|
|
159
|
+
dispatched = true;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
return await parseCapabilityResponse(response, dispatched);
|
|
157
163
|
};
|
|
158
164
|
|
|
159
|
-
export const reviewAiCapability = async (input: AiCapabilityCall): Promise<
|
|
165
|
+
export const reviewAiCapability = async (input: AiCapabilityCall): Promise<CapabilityActionReviewData | null> => {
|
|
160
166
|
if (input.entry.kind !== "action" || !(input.entry.operation as CapabilityActionManifest).review) return null;
|
|
161
|
-
return
|
|
167
|
+
return CapabilityActionReviewDataSchema.parse(await dispatchAiCapability(input, true));
|
|
162
168
|
};
|
|
163
169
|
|
|
164
170
|
export const executeAiCapability = (input: AiCapabilityCall): Promise<unknown> => dispatchAiCapability(input, false);
|
|
165
171
|
|
|
172
|
+
/** Run only the app-owned read-only outcome Query, once. Invalid answers stay unknown and remain recoverable. */
|
|
173
|
+
export const checkAiCapability = async (
|
|
174
|
+
input: Omit<AiCapabilityCall, "args" | "actionApproval" | "context"> & {
|
|
175
|
+
context: Pick<ToolContext, "callId" | "signal">;
|
|
176
|
+
check: NonNullable<CapabilityActionCallReview["check"]>;
|
|
177
|
+
/** The app's catalog, in which the check Query is looked up; the caller builds it (no import cycle). */
|
|
178
|
+
catalog: readonly AiCapabilityCatalogEntry[];
|
|
179
|
+
},
|
|
180
|
+
): Promise<AiActionCheck> => {
|
|
181
|
+
try {
|
|
182
|
+
const query = input.catalog.find(
|
|
183
|
+
(entry) => entry.appId === input.entry.appId && entry.kind === "query" && entry.operation.localId === input.check.query,
|
|
184
|
+
);
|
|
185
|
+
if (!query || "idempotency" in query.operation || !capabilityQueryChecksActionOutcome(query.operation))
|
|
186
|
+
throw new Error("Action outcome Query is unavailable.");
|
|
187
|
+
// The Action's audit keeps its own dispatch identity; the Query has its separate platform execution record.
|
|
188
|
+
const result = await dispatchAiCapability({ ...input, turnId: undefined, entry: query, args: input.check.input }, false);
|
|
189
|
+
if (!result || typeof result !== "object" || !("data" in result)) throw new Error("Invalid outcome check envelope.");
|
|
190
|
+
const checked = CapabilityActionCheckSchema.parse(result.data);
|
|
191
|
+
if (checked.receipt && capabilityActionReceiptIssues(checked.receipt, "links" in result ? result.links : undefined).length)
|
|
192
|
+
throw new Error("Invalid outcome check receipt.");
|
|
193
|
+
return { state: checked.outcome, ...(checked.receipt ? { receipt: checked.receipt } : {}), at: new Date().toISOString() };
|
|
194
|
+
} catch {
|
|
195
|
+
return { state: "failed", at: new Date().toISOString() };
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
166
199
|
/**
|
|
167
200
|
* A rejected approval never reaches the dispatcher, so the assistant records
|
|
168
201
|
* the same execution row the dispatcher would have written for the attempt.
|