@k2b/cloud 0.40.0 → 0.42.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/package.json +9 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-sentences.ts +2 -1
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +158 -92
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +76 -4
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +214 -23
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +64 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/http.ts +1 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +28 -0
- package/src/ai/open-questions.ts +168 -0
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +119 -35
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +184 -31
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +25 -4
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +73 -41
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/me.ts +79 -3
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +30 -66
- package/src/approvals/approval-card.ts +68 -0
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app-approval.ts +3 -3
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +149 -55
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +3 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +5 -3
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/accounts/users.ts +12 -1
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/device.ts +49 -33
- package/src/services/approvals/index.ts +15 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +72 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +11 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/session/index.ts +203 -29
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +185 -542
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +7 -5
- package/src/ssr/index.ts +2 -1
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
package/src/ai/tool-wording.ts
CHANGED
|
@@ -4,13 +4,12 @@ import {
|
|
|
4
4
|
capabilityActionSubject,
|
|
5
5
|
capabilityReviewSentence,
|
|
6
6
|
capabilitySentenceText,
|
|
7
|
-
resolveCapabilityActionSentences,
|
|
8
7
|
} from "../_internal/capability-sentences";
|
|
9
8
|
import { CapabilityActionReceiptSchema } from "../contracts/capabilities";
|
|
10
9
|
import { type AiStoredActionReview, isLegacyActionReview, storedActionReview } from "./action-review";
|
|
11
|
-
import {
|
|
10
|
+
import { isRecord } from "./chat/message-utils";
|
|
12
11
|
import type { AiTurnBlock } from "./protocol";
|
|
13
|
-
import {
|
|
12
|
+
import { cloudToolReview } from "./tool-sentences";
|
|
14
13
|
|
|
15
14
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
16
15
|
|
|
@@ -20,7 +19,7 @@ const reviewOf = (block: ToolBlock): AiStoredActionReview | null =>
|
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* How the chat words one call that acts on something: an app Action through its saved presentation, a
|
|
23
|
-
* Cloud tool through
|
|
22
|
+
* Cloud tool through its per-call review. `null` for calls that only read.
|
|
24
23
|
*/
|
|
25
24
|
export const toolWording = (block: ToolBlock, locale: string): CapabilityActionWording | null => {
|
|
26
25
|
const presentation = block.presentation;
|
|
@@ -32,8 +31,10 @@ export const toolWording = (block: ToolBlock, locale: string): CapabilityActionW
|
|
|
32
31
|
...(presentation.fields ? { fields: presentation.fields } : {}),
|
|
33
32
|
};
|
|
34
33
|
}
|
|
35
|
-
const
|
|
36
|
-
return
|
|
34
|
+
const review = cloudToolReview(block.name, block.args, locale);
|
|
35
|
+
return review
|
|
36
|
+
? { title: capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone: "UTC" }) }
|
|
37
|
+
: null;
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -45,6 +46,8 @@ export const toolSubject = (block: ToolBlock, locale: string, timeZone: string):
|
|
|
45
46
|
const review = reviewOf(block);
|
|
46
47
|
if (review && !isLegacyActionReview(review))
|
|
47
48
|
return capabilitySentenceText(review.sentence.action, review.slots, { locale: review.locale, timeZone });
|
|
49
|
+
const cloudReview = cloudToolReview(block.name, block.args, locale);
|
|
50
|
+
if (cloudReview) return capabilitySentenceText(cloudReview.sentence.action, cloudReview.slots, { locale: cloudReview.locale, timeZone });
|
|
48
51
|
const wording = toolWording(block, locale);
|
|
49
52
|
return wording ? capabilityActionSubject(wording, block.args, { locale, timeZone }) : null;
|
|
50
53
|
};
|
|
@@ -59,6 +62,9 @@ export const toolOutcome = (block: ToolBlock, key: "done" | "rejected" | "notRun
|
|
|
59
62
|
return capabilityReviewSentence(review, key, { timeZone });
|
|
60
63
|
}
|
|
61
64
|
if (review && key === "done" && review.legacy.summary) return review.legacy.summary;
|
|
65
|
+
const cloudReview = cloudToolReview(block.name, block.args, locale);
|
|
66
|
+
// Only a form the tool names; otherwise the card keeps Cloud's own outcome words for the call.
|
|
67
|
+
if (cloudReview) return key === "done" || cloudReview.sentence[key] ? capabilityReviewSentence(cloudReview, key, { timeZone }) : null;
|
|
62
68
|
const wording = toolWording(block, locale);
|
|
63
69
|
if (!wording) return null;
|
|
64
70
|
// App Actions return their data inside the result envelope; Cloud tools return the data itself.
|
package/src/ai/tools.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Tool, ToolContext } from "@k2b/nessi";
|
|
2
2
|
import { defineTool as defineNessiTool } from "@k2b/nessi";
|
|
3
3
|
import type { z } from "zod";
|
|
4
|
+
import type { CapabilityActionCallReview } from "../contracts/capabilities";
|
|
4
5
|
import type { RequestActor } from "../server";
|
|
5
6
|
import { aiToolNeedsApproval } from "./approvals";
|
|
6
7
|
import type {
|
|
@@ -64,7 +65,7 @@ export const defineAiTool = <TInput extends z.ZodType, TOutput extends z.ZodType
|
|
|
64
65
|
/** Localized short status; never include payloads or secrets. */
|
|
65
66
|
reportProgress?: (message: string) => Promise<void>;
|
|
66
67
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
67
|
-
requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
|
|
68
|
+
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
68
69
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
69
70
|
requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
|
|
70
71
|
/** Shows requests a website approval let through as they go out, so the chat keeps them whatever the outcome. */
|
|
@@ -122,6 +123,8 @@ export type AiToolPreparationContext = {
|
|
|
122
123
|
linkApproval?: (approvalCallId: string, approvalId: string) => void;
|
|
123
124
|
/** Hears what a nested approval asks for before the approval is requested. */
|
|
124
125
|
describeApproval?: (approvalCallId: string, target: AiApprovalTarget) => void;
|
|
126
|
+
/** Registers the deterministic review before requesting or consuming a nested approval. */
|
|
127
|
+
describeReview?: (approvalCallId: string, review: CapabilityActionCallReview) => void | Promise<void>;
|
|
125
128
|
reportWebsiteReceipts?: (callId: string, receipts: AiWebsiteReceipt[]) => Promise<void>;
|
|
126
129
|
};
|
|
127
130
|
|
|
@@ -168,10 +171,11 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
|
|
|
168
171
|
}
|
|
169
172
|
// Nessi numbers the approvals of one execution in request order; count them to name each one's call ID.
|
|
170
173
|
let approvals = 0;
|
|
171
|
-
const requestApproval = async (message: string, target?: AiApprovalTarget): Promise<boolean> => {
|
|
174
|
+
const requestApproval = async (message: string, target?: AiApprovalTarget, review?: CapabilityActionCallReview): Promise<boolean> => {
|
|
172
175
|
while (true) {
|
|
173
176
|
const approvalCallId = `${callId}-approval-${approvals++}`;
|
|
174
177
|
if (target) input.describeApproval?.(approvalCallId, target);
|
|
178
|
+
if (review) await input.describeReview?.(approvalCallId, review);
|
|
175
179
|
if (!(await ctx.requestApproval(message))) return false;
|
|
176
180
|
const used = await use(approvalCallId);
|
|
177
181
|
if (used === "approved") return true;
|
|
@@ -182,7 +186,8 @@ export const prepareAiTools = (input: AiToolPreparationContext & { tools?: AiRun
|
|
|
182
186
|
return {
|
|
183
187
|
...ctx,
|
|
184
188
|
requestApproval,
|
|
185
|
-
requestApprovalFor: (message: string, target: AiApprovalTarget) =>
|
|
189
|
+
requestApprovalFor: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) =>
|
|
190
|
+
requestApproval(message, target, review),
|
|
186
191
|
requestRelayedApproval: async (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => {
|
|
187
192
|
const approvalCallId = `${callId}-approval-${approvals++}`;
|
|
188
193
|
if (options.approvalId) input.linkApproval?.(approvalCallId, options.approvalId);
|
package/src/ai/types.ts
CHANGED
|
@@ -13,7 +13,12 @@ import type {
|
|
|
13
13
|
import type { Usage } from "@k2b/nessi/ai";
|
|
14
14
|
import type { z } from "zod";
|
|
15
15
|
import type { CapabilitySentenceField } from "../_internal/capability-sentences";
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
CapabilityActionCallReview,
|
|
18
|
+
CapabilityActionReviewPresentation,
|
|
19
|
+
CapabilityActionSentences,
|
|
20
|
+
CloudResourceRef,
|
|
21
|
+
} from "../contracts/capabilities";
|
|
17
22
|
import type { RequestActor } from "../server";
|
|
18
23
|
import type { AiActionCheck, AiStoredActionReview } from "./action-review";
|
|
19
24
|
import type { AiLocalClient, AiLocalContext, AiLocalToolName } from "./local-tool-contracts";
|
|
@@ -451,7 +456,14 @@ export type AiPendingTurnAction =
|
|
|
451
456
|
name: string;
|
|
452
457
|
args: unknown;
|
|
453
458
|
message?: string;
|
|
454
|
-
review
|
|
459
|
+
/** Stored review, including the declared ask reason for new-shape reviews. */
|
|
460
|
+
review?: CapabilityActionReviewPresentation;
|
|
461
|
+
/** Owning application, with its registry name in the turn's request locale. */
|
|
462
|
+
app?: { id: string; name: string };
|
|
463
|
+
/** The chat's current title, when known. */
|
|
464
|
+
chatTitle?: string | null;
|
|
465
|
+
/** Expiry of the platform approval request, as an ISO timestamp. */
|
|
466
|
+
expiresAt?: string;
|
|
455
467
|
allowAlways: boolean;
|
|
456
468
|
/** Whether the approval can be remembered for this chat. */
|
|
457
469
|
allowChat: boolean;
|
|
@@ -487,7 +499,7 @@ export type AiPendingTurnActionRecord = {
|
|
|
487
499
|
name: string;
|
|
488
500
|
args: unknown;
|
|
489
501
|
message?: string;
|
|
490
|
-
review?:
|
|
502
|
+
review?: CapabilityActionReviewPresentation;
|
|
491
503
|
approvalScope: string;
|
|
492
504
|
allowAlways: boolean;
|
|
493
505
|
/** Whether the approval can be remembered for this chat; defaults to `allowAlways`. */
|
|
@@ -724,7 +736,14 @@ export type AiTurnSweepResult = {
|
|
|
724
736
|
};
|
|
725
737
|
|
|
726
738
|
export type AiConversationService = {
|
|
739
|
+
getDraftSubmission(input: {
|
|
740
|
+
conversationId: string;
|
|
741
|
+
ownerUserId: string;
|
|
742
|
+
revision: number;
|
|
743
|
+
}): Promise<{ messageId: string; turnId: string | null } | null>;
|
|
727
744
|
createConversation(input: {
|
|
745
|
+
/** Stable identity for one logical creation; replay never creates a replacement. */
|
|
746
|
+
requestId?: string;
|
|
728
747
|
ownerUserId: string;
|
|
729
748
|
title?: string;
|
|
730
749
|
description?: string;
|
|
@@ -1032,6 +1051,8 @@ export type AiConversationService = {
|
|
|
1032
1051
|
getPendingTurnAction(input: { conversationId: string; turnId: string; callId: string }): Promise<AiPendingTurnActionRecord | null>;
|
|
1033
1052
|
findPendingTurnActionByApproval(input: { approvalId: string; ownerUserId: string }): Promise<AiPendingTurnActionRecord | null>;
|
|
1034
1053
|
listPendingActionRecords(input: { conversationId: string; turnId: string }): Promise<AiPendingTurnActionRecord[]>;
|
|
1054
|
+
/** The reviews Cloud stored with a turn's tool calls, keyed by call ID; the newest message wins. */
|
|
1055
|
+
listTurnToolReviews(input: { conversationId: string; turnId: string }): Promise<Map<string, AiStoredActionReview>>;
|
|
1035
1056
|
listResolvedPendingActions(input: { conversationId: string; turnId: string }): Promise<AiPendingTurnActionRecord[]>;
|
|
1036
1057
|
resolvePendingTurnAction(input: {
|
|
1037
1058
|
conversationId: string;
|
|
@@ -1151,7 +1172,7 @@ export type AiToolRuntime<TInput extends z.ZodType = z.ZodType, TOutput extends
|
|
|
1151
1172
|
/** Localized short status; never include payloads or secrets. */
|
|
1152
1173
|
reportProgress?: (message: string) => Promise<void>;
|
|
1153
1174
|
/** Asks like `requestApproval`, and lets the person remember the approval of `target`. */
|
|
1154
|
-
requestApprovalFor?: (message: string, target: AiApprovalTarget) => Promise<boolean>;
|
|
1175
|
+
requestApprovalFor?: (message: string, target: AiApprovalTarget, review?: CapabilityActionCallReview) => Promise<boolean>;
|
|
1155
1176
|
/** Relays a decision for an effect another platform component consumes and executes. */
|
|
1156
1177
|
requestRelayedApproval?: (message: string, options: { approvalId?: string; target?: AiApprovalTarget }) => Promise<boolean>;
|
|
1157
1178
|
/** Shows requests a website approval let through as they go out, so the chat keeps them whatever the outcome. */
|
package/src/ai/usage.ts
CHANGED
|
@@ -317,8 +317,8 @@ export const aiUsage = {
|
|
|
317
317
|
: field === "appId"
|
|
318
318
|
? sql`e.app_id`
|
|
319
319
|
: sql`e.task`;
|
|
320
|
-
const label = field === "userId" ? sql`COALESCE(NULLIF(u.display_name,''),u.uid,sa.name
|
|
321
|
-
return sql<{ id: string; label: string }[]>`SELECT DISTINCT ${key} AS id,${label} AS label FROM (${events(since, until)}) e
|
|
320
|
+
const label = field === "userId" ? sql`COALESCE(NULLIF(u.display_name,''),u.uid,sa.name)` : key;
|
|
321
|
+
return sql<{ id: string; label: string | null }[]>`SELECT DISTINCT ${key} AS id,${label} AS label FROM (${events(since, until)}) e
|
|
322
322
|
LEFT JOIN auth.users u ON u.id=e.user_id LEFT JOIN auth.service_accounts sa ON sa.id=e.user_id WHERE ${key} IS NOT NULL
|
|
323
323
|
AND (position(lower(${search}) in lower(${label}))>0 OR ${key}=${search} OR (${field === "userId"} AND position(lower(${search}) in lower(COALESCE(u.uid,'')))>0))
|
|
324
324
|
ORDER BY label,id LIMIT ${query.perPage}`;
|
|
@@ -39,6 +39,7 @@ import { GotenbergRenderError, testGotenberg } from "../services/pdf";
|
|
|
39
39
|
import * as settings from "../services/settings";
|
|
40
40
|
import { SETTINGS_MAP, validateSettingValue } from "../services/settings/defaults";
|
|
41
41
|
|
|
42
|
+
import { assertGeoBudgetPair, GeoBudgetSettingError } from "../services/settings/geo-budget";
|
|
42
43
|
import { invalidateSettingsCacheForAdmin } from "../services/settings/store";
|
|
43
44
|
|
|
44
45
|
const BulkSaveSchema = z.union([
|
|
@@ -67,6 +68,9 @@ const settingsNotSavedMessage = (locale: string): string =>
|
|
|
67
68
|
locale.startsWith("de") ? "Die Einstellungen wurden nicht gespeichert." : "The settings were not saved.";
|
|
68
69
|
|
|
69
70
|
const isKnownSetting = (key: string): boolean => SETTINGS_MAP.has(key);
|
|
71
|
+
// The Geoapify key is written only where Cloud verifies it with the provider first, so a refused key never replaces a working one.
|
|
72
|
+
const GEO_KEY = "geo.api_key";
|
|
73
|
+
const GEO_KEY_DEDICATED = "Set or remove the Geoapify key with /api/admin/core/geo/key, which verifies it first.";
|
|
70
74
|
|
|
71
75
|
const AI_PROFILES_KEY = "ai.model_profiles_json";
|
|
72
76
|
const AI_ENABLED_KEY = "ai.enabled";
|
|
@@ -333,7 +337,10 @@ const app = new Hono<AuthContext>()
|
|
|
333
337
|
return c.json({ ok: true, bytes: result.bytes, contentType: result.contentType });
|
|
334
338
|
} catch (error) {
|
|
335
339
|
if (error instanceof GotenbergRenderError) {
|
|
336
|
-
return c.json(
|
|
340
|
+
return c.json(
|
|
341
|
+
{ message: error.message, code: error.code, status: error.status },
|
|
342
|
+
error.code === "busy" ? 503 : error.code === "not_configured" ? 400 : 502,
|
|
343
|
+
);
|
|
337
344
|
}
|
|
338
345
|
return c.json({ message: "Failed to test Gotenberg PDF rendering" }, 500);
|
|
339
346
|
}
|
|
@@ -384,6 +391,7 @@ const app = new Hono<AuthContext>()
|
|
|
384
391
|
ownership[key] = `Setting "${key}" cannot be updated and reset in the same save`;
|
|
385
392
|
}
|
|
386
393
|
}
|
|
394
|
+
if (keys.includes(GEO_KEY)) ownership[GEO_KEY] = GEO_KEY_DEDICATED;
|
|
387
395
|
if (Object.keys(ownership).length > 0) {
|
|
388
396
|
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
389
397
|
}
|
|
@@ -469,11 +477,12 @@ const app = new Hono<AuthContext>()
|
|
|
469
477
|
// Every settings and AI credential write runs on the transaction's own
|
|
470
478
|
// connection, so a failure rolls the whole save back.
|
|
471
479
|
await sql.begin(async (tx) => {
|
|
480
|
+
await assertGeoBudgetPair(tx, validatedValues, resets);
|
|
472
481
|
for (const [key, value] of Object.entries(validatedValues)) {
|
|
473
482
|
try {
|
|
474
483
|
await settings.set(key, value, tx, { replaceUnreadable: replace.has(key) });
|
|
475
484
|
} catch (error) {
|
|
476
|
-
fieldErrors[key] = `Failed to
|
|
485
|
+
fieldErrors[key] = `Failed to save ${key}`;
|
|
477
486
|
throw error;
|
|
478
487
|
}
|
|
479
488
|
}
|
|
@@ -514,6 +523,7 @@ const app = new Hono<AuthContext>()
|
|
|
514
523
|
}
|
|
515
524
|
});
|
|
516
525
|
} catch (error) {
|
|
526
|
+
if (error instanceof GeoBudgetSettingError) return error.getResponse();
|
|
517
527
|
if (error instanceof settings.SettingUnreadableError) {
|
|
518
528
|
return c.json(
|
|
519
529
|
{
|
|
@@ -553,6 +563,7 @@ const app = new Hono<AuthContext>()
|
|
|
553
563
|
if (!isKnownSetting(key)) {
|
|
554
564
|
return c.json({ message: `Unknown setting "${key}"` }, 400);
|
|
555
565
|
}
|
|
566
|
+
if (key === GEO_KEY) return c.json({ message: GEO_KEY_DEDICATED, errors: { [GEO_KEY]: GEO_KEY_DEDICATED } }, 400);
|
|
556
567
|
|
|
557
568
|
let aiPlan: AiSettingsMutationPlan;
|
|
558
569
|
try {
|
|
@@ -567,6 +578,7 @@ const app = new Hono<AuthContext>()
|
|
|
567
578
|
|
|
568
579
|
try {
|
|
569
580
|
await sql.begin(async (tx) => {
|
|
581
|
+
await assertGeoBudgetPair(tx, {}, [key]);
|
|
570
582
|
await settings.remove(key, tx);
|
|
571
583
|
if (key.startsWith("user.category."))
|
|
572
584
|
await audit.record(
|
|
@@ -588,6 +600,7 @@ const app = new Hono<AuthContext>()
|
|
|
588
600
|
}
|
|
589
601
|
});
|
|
590
602
|
} catch (error) {
|
|
603
|
+
if (error instanceof GeoBudgetSettingError || error instanceof settings.SettingUnreadableError) return error.getResponse();
|
|
591
604
|
console.error(`[settings] failed to reset "${key}"`, error);
|
|
592
605
|
return c.json({ message: "Reset failed" }, 500);
|
|
593
606
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Hono, type MiddlewareHandler } from "hono";
|
|
2
|
+
import { describeRoute } from "hono-openapi";
|
|
3
|
+
import { GeoBudgetInputSchema, GeoKeyInputSchema } from "../contracts/geo-admin";
|
|
4
|
+
import { type AuthContext, auth, v } from "../server";
|
|
5
|
+
import { GeoAdminError, geoAdmin } from "../services/geo/admin";
|
|
6
|
+
|
|
7
|
+
export const createAdminGeoRoutes = (
|
|
8
|
+
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
9
|
+
service: Pick<typeof geoAdmin, "status" | "usage" | "setKey" | "removeKey" | "saveBudget"> = geoAdmin,
|
|
10
|
+
) =>
|
|
11
|
+
new Hono<AuthContext>()
|
|
12
|
+
.use(authenticate)
|
|
13
|
+
.onError((error, c) => {
|
|
14
|
+
if (error instanceof GeoAdminError) return c.json(error.body, error.status);
|
|
15
|
+
if (c.req.raw.signal.aborted) return new Response(null, { status: 499 });
|
|
16
|
+
throw error;
|
|
17
|
+
})
|
|
18
|
+
.get(
|
|
19
|
+
"/",
|
|
20
|
+
describeRoute({ tags: ["Places and maps"], summary: "Show the Geoapify key state, plan and budgets" }),
|
|
21
|
+
auth.requireOAuthScope("read", "admin"),
|
|
22
|
+
async (c) => c.json(await service.status(c.get("user"))),
|
|
23
|
+
)
|
|
24
|
+
.get(
|
|
25
|
+
"/usage",
|
|
26
|
+
describeRoute({ tags: ["Places and maps"], summary: "Show this month's and today's geo credits with shares per function" }),
|
|
27
|
+
auth.requireOAuthScope("read", "admin"),
|
|
28
|
+
async (c) => c.json(await service.usage(c.get("user"))),
|
|
29
|
+
)
|
|
30
|
+
.put(
|
|
31
|
+
"/key",
|
|
32
|
+
describeRoute({
|
|
33
|
+
tags: ["Places and maps"],
|
|
34
|
+
summary: "Verify a Geoapify key with one provider lookup, then store it; a refused key changes nothing",
|
|
35
|
+
}),
|
|
36
|
+
auth.requireOAuthScope("write", "admin"),
|
|
37
|
+
v("json", GeoKeyInputSchema),
|
|
38
|
+
async (c) => c.json(await service.setKey(c.get("user"), c.req.valid("json").apiKey, c.req.raw.signal)),
|
|
39
|
+
)
|
|
40
|
+
.delete(
|
|
41
|
+
"/key",
|
|
42
|
+
describeRoute({ tags: ["Places and maps"], summary: "Remove the Geoapify key; places and maps turn off for everyone" }),
|
|
43
|
+
auth.requireOAuthScope("write", "admin"),
|
|
44
|
+
async (c) => c.json(await service.removeKey(c.get("user"))),
|
|
45
|
+
)
|
|
46
|
+
.put(
|
|
47
|
+
"/budget",
|
|
48
|
+
describeRoute({ tags: ["Places and maps"], summary: "Save the plan, the monthly budget and the daily limit per person" }),
|
|
49
|
+
auth.requireOAuthScope("write", "admin"),
|
|
50
|
+
v("json", GeoBudgetInputSchema),
|
|
51
|
+
async (c) => c.json(await service.saveBudget(c.get("user"), c.req.valid("json"))),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export default createAdminGeoRoutes();
|
|
@@ -6,7 +6,9 @@ import { type AuthContext, auth, v } from "../server";
|
|
|
6
6
|
import { PosixError, posix } from "../services/accounts/posix";
|
|
7
7
|
|
|
8
8
|
const Id = z.object({ id: z.uuid() });
|
|
9
|
-
const Configure = z
|
|
9
|
+
const Configure = z
|
|
10
|
+
.object({ config: LinuxIdentityConfigurationSchema, rangeReserved: z.boolean(), replaceUnreadable: z.boolean().optional() })
|
|
11
|
+
.strict();
|
|
10
12
|
|
|
11
13
|
export const createAdminLinuxIdentityRoutes = (
|
|
12
14
|
authenticate: MiddlewareHandler<AuthContext> = auth.requireRole("admin"),
|
|
@@ -32,12 +34,13 @@ export const createAdminLinuxIdentityRoutes = (
|
|
|
32
34
|
describeRoute({
|
|
33
35
|
tags: ["Linux identities"],
|
|
34
36
|
summary: "Configure automatic local Linux identity assignment; does not enable computer login",
|
|
37
|
+
description: "Unreadable stored configuration returns 409 setting_unreadable unless replaceUnreadable is explicitly true.",
|
|
35
38
|
}),
|
|
36
39
|
v("json", Configure),
|
|
37
40
|
async (c) => {
|
|
38
|
-
const { config, rangeReserved } = c.req.valid("json");
|
|
41
|
+
const { config, rangeReserved, replaceUnreadable } = c.req.valid("json");
|
|
39
42
|
if (config.enabled && !rangeReserved) throw new PosixError("range_confirmation_required", 400);
|
|
40
|
-
return c.json(await service.configure(c.get("user"), config));
|
|
43
|
+
return c.json(await service.configure(c.get("user"), config, { replaceUnreadable }));
|
|
41
44
|
},
|
|
42
45
|
)
|
|
43
46
|
.get(
|
|
@@ -17,15 +17,18 @@ import { type AuthContext, auth, jsonResponse, requiresAdmin } from "../server";
|
|
|
17
17
|
import { noTransform } from "../server/middleware/no-transform";
|
|
18
18
|
import { audit } from "../services/audit";
|
|
19
19
|
import { outgoingMailLog } from "../services/outgoing-mail/admin";
|
|
20
|
-
import { MAIL_RETENTION_KEYS,
|
|
20
|
+
import { MAIL_RETENTION_KEYS, readMailRetentionSettings } from "../services/outgoing-mail/retention";
|
|
21
21
|
import { type MailAuditContext, OutgoingMailError, outgoingMailStore } from "../services/outgoing-mail/store";
|
|
22
22
|
import { outgoingMailTest } from "../services/outgoing-mail/test-send";
|
|
23
23
|
import * as settings from "../services/settings";
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const retentionView = MailRetentionSchema.extend({ unreadable: z.record(z.string(), z.boolean()) });
|
|
26
|
+
const retentionInput = MailRetentionSchema.partial()
|
|
27
|
+
.extend({ replaceUnreadable: z.array(z.enum(MAIL_RETENTION_KEYS)).optional() })
|
|
28
|
+
.refine((value) => value.recordDays === undefined || value.contentDays === undefined || value.recordDays >= value.contentDays, {
|
|
29
|
+
message: "Record retention must be at least content retention.",
|
|
30
|
+
path: ["recordDays"],
|
|
31
|
+
});
|
|
29
32
|
|
|
30
33
|
const validate = <Target extends "param" | "json" | "query", Schema extends z.ZodType>(
|
|
31
34
|
target: Target,
|
|
@@ -113,9 +116,9 @@ export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<Au
|
|
|
113
116
|
summary: "Read outgoing mail retention",
|
|
114
117
|
tags: ["Outgoing mail"],
|
|
115
118
|
...requiresAdmin,
|
|
116
|
-
responses: { 200: jsonResponse(
|
|
119
|
+
responses: { 200: jsonResponse(retentionView, "Effective retention in days") },
|
|
117
120
|
}),
|
|
118
|
-
async (c) => c.json(await
|
|
121
|
+
async (c) => c.json(await readMailRetentionSettings()),
|
|
119
122
|
)
|
|
120
123
|
.put(
|
|
121
124
|
"/retention",
|
|
@@ -124,19 +127,30 @@ export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<Au
|
|
|
124
127
|
tags: ["Outgoing mail"],
|
|
125
128
|
...requiresAdmin,
|
|
126
129
|
responses: {
|
|
127
|
-
200: jsonResponse(
|
|
130
|
+
200: jsonResponse(retentionView, "Updated retention in days"),
|
|
128
131
|
400: jsonResponse(z.object({ code: z.literal("bad_input"), message: z.string() }), "Invalid retention"),
|
|
132
|
+
409: jsonResponse(
|
|
133
|
+
z.object({ code: z.literal("setting_unreadable"), message: z.string() }),
|
|
134
|
+
"Stored period needs explicit replacement",
|
|
135
|
+
),
|
|
129
136
|
},
|
|
130
137
|
}),
|
|
131
138
|
validate("json", retentionInput, "bad_input"),
|
|
132
139
|
async (c) => {
|
|
133
140
|
const next = c.req.valid("json");
|
|
134
|
-
const old = await readMailRetention();
|
|
135
141
|
await sql.begin(async (tx) => {
|
|
136
|
-
|
|
137
|
-
await
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended('outgoing-mail.retention-settings', 0))`;
|
|
143
|
+
const old = await readMailRetentionSettings(tx);
|
|
144
|
+
if ((next.recordDays ?? old.recordDays) < (next.contentDays ?? old.contentDays))
|
|
145
|
+
throw new OutgoingMailError("bad_input", "Record retention must be at least content retention.");
|
|
146
|
+
if (next.contentDays !== undefined)
|
|
147
|
+
await settings.set(MAIL_RETENTION_KEYS[0], next.contentDays, tx, {
|
|
148
|
+
replaceUnreadable: next.replaceUnreadable?.includes(MAIL_RETENTION_KEYS[0]) === true,
|
|
149
|
+
});
|
|
150
|
+
if (next.recordDays !== undefined)
|
|
151
|
+
await settings.set(MAIL_RETENTION_KEYS[1], next.recordDays, tx, {
|
|
152
|
+
replaceUnreadable: next.replaceUnreadable?.includes(MAIL_RETENTION_KEYS[1]) === true,
|
|
153
|
+
});
|
|
140
154
|
await audit.record(
|
|
141
155
|
{
|
|
142
156
|
...context(c),
|
|
@@ -149,7 +163,7 @@ export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<Au
|
|
|
149
163
|
);
|
|
150
164
|
});
|
|
151
165
|
await settings.invalidateSettingsCache(MAIL_RETENTION_KEYS);
|
|
152
|
-
return c.json(
|
|
166
|
+
return c.json(await readMailRetentionSettings());
|
|
153
167
|
},
|
|
154
168
|
)
|
|
155
169
|
.get(
|
|
@@ -245,7 +259,7 @@ export const createAdminOutgoingMailRoutes = (authenticate: MiddlewareHandler<Au
|
|
|
245
259
|
.put(
|
|
246
260
|
"/apps/:appId/log-access",
|
|
247
261
|
describeRoute({
|
|
248
|
-
summary: "
|
|
262
|
+
summary: "Replace application metadata and content mail log access",
|
|
249
263
|
tags: ["Outgoing mail"],
|
|
250
264
|
...requiresAdmin,
|
|
251
265
|
responses: {
|
|
@@ -1,20 +1,44 @@
|
|
|
1
1
|
import { submitAiTurnAction } from "../ai/runtime";
|
|
2
2
|
import { aiConversations } from "../ai/store";
|
|
3
|
-
import { APP_ACTION_LIMITS } from "../contracts/app-approval";
|
|
3
|
+
import { APP_ACTION_LIMITS, type AppActionSummary } from "../contracts/app-approval";
|
|
4
|
+
import { APPROVALS_PAGE_SIZE } from "../contracts/approvals";
|
|
4
5
|
import type { AppDeviceActions } from "../services/app-approval";
|
|
5
|
-
import { listPending } from "../services/approvals";
|
|
6
|
-
import { appActionSummary, appActionView } from "../services/approvals/device";
|
|
6
|
+
import { listPending, loadApprovalActionPolicies, pendingApprovalCard } from "../services/approvals";
|
|
7
|
+
import { appActionOffered, appActionSummary, appActionView } from "../services/approvals/device";
|
|
8
|
+
|
|
9
|
+
/** Pages of open requests one inbox poll reads at most while it skips browser-only questions. */
|
|
10
|
+
const INBOX_PAGES = 5;
|
|
7
11
|
|
|
8
12
|
export const createDeviceActions = (appName: (appId: string, locale: string) => string): AppDeviceActions => {
|
|
9
13
|
const view: AppDeviceActions["view"] = async ({ userId, issuer, actionId }) => {
|
|
10
14
|
const { items } = await listPending({ approverUserId: userId, id: actionId, limit: 1 });
|
|
11
|
-
|
|
15
|
+
const row = items[0];
|
|
16
|
+
if (!row) return null;
|
|
17
|
+
const policy = (await loadApprovalActionPolicies([row]))(row);
|
|
18
|
+
const mapped = await appActionView(row, { issuer, appName, policy });
|
|
19
|
+
return appActionOffered(mapped, pendingApprovalCard(row, appName, policy), policy) ? mapped : null;
|
|
12
20
|
};
|
|
13
21
|
return {
|
|
14
22
|
inbox: async ({ userId, issuer }) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
// Browser-only questions are skipped, so read on past them. One poll reads at most this many pages of open
|
|
24
|
+
// requests, newest first, which keeps each 5-second poll bounded.
|
|
25
|
+
const actions: AppActionSummary[] = [];
|
|
26
|
+
let count = 0;
|
|
27
|
+
let cursor: string | undefined;
|
|
28
|
+
for (let page = 0; page < INBOX_PAGES && actions.length < APP_ACTION_LIMITS.inboxActions; page += 1) {
|
|
29
|
+
const result = await listPending({ approverUserId: userId, limit: APPROVALS_PAGE_SIZE, ...(cursor ? { cursor } : {}) });
|
|
30
|
+
count = result.count;
|
|
31
|
+
const policyOf = await loadApprovalActionPolicies(result.items);
|
|
32
|
+
for (const row of result.items) {
|
|
33
|
+
const policy = policyOf(row);
|
|
34
|
+
const mapped = await appActionView(row, { issuer, appName, policy });
|
|
35
|
+
if (appActionOffered(mapped, pendingApprovalCard(row, appName, policy), policy)) actions.push(appActionSummary(mapped));
|
|
36
|
+
if (actions.length === APP_ACTION_LIMITS.inboxActions) break;
|
|
37
|
+
}
|
|
38
|
+
if (!result.nextCursor) break;
|
|
39
|
+
cursor = result.nextCursor;
|
|
40
|
+
}
|
|
41
|
+
return { actions, moreActions: Math.max(0, count - actions.length) };
|
|
18
42
|
},
|
|
19
43
|
view,
|
|
20
44
|
decide: async ({ userId, deviceId, issuer, actionId, digest, approved }) => {
|
|
@@ -3,7 +3,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
3
3
|
import { listCapabilities } from "../_internal/registry";
|
|
4
4
|
import { resolveAiChatRequestContext } from "../ai/request-context";
|
|
5
5
|
import type { ApprovalListQuery, ApprovalPage } from "../contracts/approvals";
|
|
6
|
-
import {
|
|
6
|
+
import { CapabilityActionReviewPresentationSchema, capabilityActionPolicy } from "../contracts/capabilities";
|
|
7
7
|
import type { AuthContext } from "../server/middleware/auth";
|
|
8
8
|
import { listPending, presentPendingApproval } from "../services/approvals/list";
|
|
9
9
|
import { logger } from "../services/logging";
|
|
@@ -16,8 +16,8 @@ export const loadPendingApprovals = async <T extends AuthContext>(c: Context<T>,
|
|
|
16
16
|
const apps = getLocalizedRuntimeContext(c).apps;
|
|
17
17
|
const needsPolicy = page.items.some((row) => {
|
|
18
18
|
if (row.storedReview && "sentence" in row.storedReview) return false;
|
|
19
|
-
const review =
|
|
20
|
-
return review.success && !review.data.ask;
|
|
19
|
+
const review = CapabilityActionReviewPresentationSchema.safeParse(row.details);
|
|
20
|
+
return review.success && "sentence" in review.data && !review.data.ask;
|
|
21
21
|
});
|
|
22
22
|
const capabilities = needsPolicy
|
|
23
23
|
? await listCapabilities().catch((error: unknown) => {
|
package/src/api/approvals.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { describeRoute } from "hono-openapi";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { toAiActionFailureResponse } from "../ai/http";
|
|
6
6
|
import { resolveAiChatRequestContext } from "../ai/request-context";
|
|
7
|
+
import { readReviewContent } from "../ai/review-content";
|
|
7
8
|
import { submitAiTurnAction } from "../ai/runtime";
|
|
8
9
|
import { aiConversations } from "../ai/store";
|
|
9
10
|
import {
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
ApprovalListQuerySchema,
|
|
15
16
|
ApprovalPageSchema,
|
|
16
17
|
} from "../contracts/approvals";
|
|
18
|
+
import { CapabilityReviewContentSchema } from "../contracts/capabilities";
|
|
17
19
|
import { ErrorResponseSchema } from "../contracts/shared";
|
|
18
20
|
import { rateLimit } from "../server";
|
|
19
21
|
import { respond } from "../server/api";
|
|
@@ -23,6 +25,7 @@ import { jsonResponse, requiresAuth } from "../server/middleware/openapi";
|
|
|
23
25
|
import { v } from "../server/middleware/validator";
|
|
24
26
|
import { ok } from "../server/services";
|
|
25
27
|
import { approvalDecisionChannel, countPending } from "../services/approvals";
|
|
28
|
+
import { readApprovalContent } from "../services/approvals/content";
|
|
26
29
|
import { decodeApprovalCursor } from "../services/approvals/cursor";
|
|
27
30
|
import { loadPendingApprovals } from "./approvals-loader";
|
|
28
31
|
|
|
@@ -30,6 +33,54 @@ export const approvalRoutes = new Hono<AuthContext>()
|
|
|
30
33
|
.use("*", noTransform())
|
|
31
34
|
.use(rateLimit())
|
|
32
35
|
.use(auth.requireRole("authenticated"))
|
|
36
|
+
.get(
|
|
37
|
+
"/:id/content",
|
|
38
|
+
describeRoute({
|
|
39
|
+
tags: ["Approvals"],
|
|
40
|
+
summary: "Read one complete pending approval snapshot",
|
|
41
|
+
...requiresAuth,
|
|
42
|
+
responses: {
|
|
43
|
+
200: jsonResponse(CapabilityReviewContentSchema, "Complete reviewed content"),
|
|
44
|
+
404: jsonResponse(ErrorResponseSchema, "Snapshot unavailable"),
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
v("param", z.object({ id: z.uuid() })),
|
|
48
|
+
v("query", z.object({ digest: z.string().regex(/^[a-f0-9]{64}$/) }).strict()),
|
|
49
|
+
async (c) => {
|
|
50
|
+
const ctx = await resolveAiChatRequestContext(c);
|
|
51
|
+
if (ctx instanceof Response) return ctx;
|
|
52
|
+
const content = await readApprovalContent({
|
|
53
|
+
...c.req.valid("param"),
|
|
54
|
+
...c.req.valid("query"),
|
|
55
|
+
approverUserId: ctx.ownerUserId,
|
|
56
|
+
signal: c.req.raw.signal,
|
|
57
|
+
});
|
|
58
|
+
c.header("Cache-Control", "private, no-store");
|
|
59
|
+
return content ? c.json(content) : c.json({ message: "This review is no longer available." }, 404);
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
.get(
|
|
63
|
+
"/conversations/:conversationId/turns/:turnId/reviews/:callId",
|
|
64
|
+
describeRoute({
|
|
65
|
+
tags: ["Approvals"],
|
|
66
|
+
summary: "Read one historical conversation review snapshot",
|
|
67
|
+
...requiresAuth,
|
|
68
|
+
responses: {
|
|
69
|
+
200: jsonResponse(CapabilityReviewContentSchema, "Complete reviewed content"),
|
|
70
|
+
404: jsonResponse(ErrorResponseSchema, "Snapshot unavailable"),
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
v("param", z.object({ conversationId: z.uuid(), turnId: z.uuid(), callId: z.string().min(1).max(200) })),
|
|
74
|
+
v("query", z.object({ digest: z.string().regex(/^[a-f0-9]{64}$/) }).strict()),
|
|
75
|
+
async (c) => {
|
|
76
|
+
const ctx = await resolveAiChatRequestContext(c);
|
|
77
|
+
if (ctx instanceof Response) return ctx;
|
|
78
|
+
const location = c.req.valid("param");
|
|
79
|
+
c.header("Cache-Control", "private, no-store");
|
|
80
|
+
const content = await readReviewContent(location, c.req.valid("query").digest, ctx.ownerUserId, c.req.raw.signal);
|
|
81
|
+
return content ? c.json(content) : c.json({ message: "This review is no longer available." }, 404);
|
|
82
|
+
},
|
|
83
|
+
)
|
|
33
84
|
.get(
|
|
34
85
|
"/",
|
|
35
86
|
describeRoute({
|
package/src/api/apps.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { describeRoute } from "hono-openapi";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { ErrorResponseSchema } from "../contracts";
|
|
5
5
|
import { type AuthContext, auth, jsonResponse, requiresAuth, v } from "../server";
|
|
6
|
+
import { appAccessAuthority } from "../services/app-access";
|
|
7
|
+
import { discoverableApps } from "../services/app-discovery";
|
|
6
8
|
import { visibleNavigationApps } from "../ssr/app-navigation";
|
|
7
9
|
import { getLocalizedRuntimeContext } from "../ssr/runtime";
|
|
8
10
|
|
|
@@ -35,9 +37,9 @@ export const appDiscoveryRoutes = new Hono<AuthContext>().use(auth.requireRole("
|
|
|
35
37
|
},
|
|
36
38
|
}),
|
|
37
39
|
v("query", AppListQuerySchema),
|
|
38
|
-
(c) => {
|
|
40
|
+
async (c) => {
|
|
39
41
|
const search = c.req.valid("query").search?.toLowerCase();
|
|
40
|
-
const items = visibleNavigationApps(getLocalizedRuntimeContext(c).apps, c.get("user"))
|
|
42
|
+
const items = visibleNavigationApps(await discoverableApps(appAccessAuthority(c), getLocalizedRuntimeContext(c).apps), c.get("user"))
|
|
41
43
|
.map((app) => ({
|
|
42
44
|
id: app.id,
|
|
43
45
|
name: app.name,
|
package/src/api/auth/schemas.ts
CHANGED
|
@@ -54,6 +54,9 @@ export const AuthResponseSchema = z.object({
|
|
|
54
54
|
user: UserSchema,
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
/** Password-change sign-in result; retry ending other sign-ins when signOutOthersFailed is true. */
|
|
58
|
+
export const PasswordChangeAuthResponseSchema = AuthResponseSchema.extend({ signOutOthersFailed: z.boolean() });
|
|
59
|
+
|
|
57
60
|
export const VerifyPasskeyAuthenticationSchema = z.object({
|
|
58
61
|
response: z.unknown(),
|
|
59
62
|
acceptedAgb: z.literal(true).optional(),
|