@k2b/cloud 0.41.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 +6 -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-timeout.ts +71 -0
- package/src/_internal/define-app.ts +145 -89
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/runtime-context.ts +1 -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 +7 -1
- 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 +202 -22
- package/src/ai/chat/action-text.ts +1 -1
- 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 +56 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/http.ts +1 -0
- package/src/ai/migrate.ts +13 -0
- package/src/ai/open-questions.ts +4 -8
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +69 -31
- 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 +168 -33
- package/src/ai/types.ts +10 -3
- 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/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- 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 +46 -38
- package/src/api/me-panel-consent.ts +8 -5
- 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 +5 -3
- 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.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +148 -54
- 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/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 +1 -1
- 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/posix.ts +21 -7
- 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/index.ts +14 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +11 -7
- 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 +9 -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/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- 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/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +3 -2
- 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 +6 -6
- 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 +3 -2
- package/src/ssr/index.ts +1 -1
- package/src/ssr/layout-context.ts +2 -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-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/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
|
@@ -138,6 +138,15 @@ export type CreateAiChatControllerOptions = {
|
|
|
138
138
|
|
|
139
139
|
const isAccessor = <T>(value: T | Accessor<T>): value is Accessor<T> => typeof value === "function";
|
|
140
140
|
|
|
141
|
+
class AiRequestError extends Error {
|
|
142
|
+
constructor(
|
|
143
|
+
message: string,
|
|
144
|
+
readonly status: number,
|
|
145
|
+
) {
|
|
146
|
+
super(message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
141
150
|
const readError = async (response: Response, fallback: string): Promise<string> => {
|
|
142
151
|
const body = await response.json().catch(() => null);
|
|
143
152
|
return body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : fallback;
|
|
@@ -190,6 +199,17 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
190
199
|
const initialProjection = options.initialDetail ? detailToProjection(options.initialDetail) : emptyProjection();
|
|
191
200
|
const [state, setState] = createStore<AiChatProjection>(initialProjection);
|
|
192
201
|
const completedDuringSend = new Map<string, string>();
|
|
202
|
+
let disposed = false;
|
|
203
|
+
const submissionBodies = new Map<string, string>();
|
|
204
|
+
// Only a confirmed draft-save response can give this submission its identity.
|
|
205
|
+
const [creationErrorStatus, setCreationErrorStatus] = createSignal<number | null>(null);
|
|
206
|
+
const [uncertainSubmissions, setUncertainSubmissions] = createSignal<Record<string, number>>({});
|
|
207
|
+
const clearUncertainSubmission = (conversationId: string) =>
|
|
208
|
+
setUncertainSubmissions((current) => {
|
|
209
|
+
const next = { ...current };
|
|
210
|
+
delete next[conversationId];
|
|
211
|
+
return next;
|
|
212
|
+
});
|
|
193
213
|
const [pendingSends, setPendingSends] = createSignal<Record<string, AiStoredMessage | undefined>>({});
|
|
194
214
|
const clearPendingSend = (conversationId: string) =>
|
|
195
215
|
setPendingSends((current) => {
|
|
@@ -251,7 +271,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
251
271
|
|
|
252
272
|
const request = async <T>(path: string, init: RequestInit, fallback: string): Promise<T> => {
|
|
253
273
|
const response = await fetch(url(path), { ...init, headers: { "Content-Type": "application/json", ...init.headers } });
|
|
254
|
-
if (!response.ok) throw new
|
|
274
|
+
if (!response.ok) throw new AiRequestError(await readError(response, fallback), response.status);
|
|
255
275
|
return (await response.json()) as T;
|
|
256
276
|
};
|
|
257
277
|
|
|
@@ -688,6 +708,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
688
708
|
|
|
689
709
|
const createConversation = async (
|
|
690
710
|
input: {
|
|
711
|
+
requestId?: string;
|
|
691
712
|
title?: string;
|
|
692
713
|
projectId?: string;
|
|
693
714
|
draft?: { content: AiDraftContentPart[] };
|
|
@@ -696,6 +717,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
696
717
|
) => {
|
|
697
718
|
const generation = ++conversationOpenGeneration;
|
|
698
719
|
clearErrors();
|
|
720
|
+
setCreationErrorStatus(null);
|
|
699
721
|
try {
|
|
700
722
|
const conversation = await request<AiConversation>(
|
|
701
723
|
`/conversations`,
|
|
@@ -714,6 +736,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
714
736
|
return conversation;
|
|
715
737
|
} catch (createError) {
|
|
716
738
|
if (generation === conversationOpenGeneration) {
|
|
739
|
+
setCreationErrorStatus(createError instanceof AiRequestError ? createError.status : null);
|
|
717
740
|
setGlobalError(createError instanceof Error ? createError.message : "Failed to create conversation");
|
|
718
741
|
}
|
|
719
742
|
return null;
|
|
@@ -766,7 +789,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
766
789
|
input: ComposerDraftInput,
|
|
767
790
|
conversationId: string | null,
|
|
768
791
|
): Promise<{ conversationId: string; content: AiDraftContentPart[]; revision: number } | null> => {
|
|
769
|
-
if (!conversationId) return null;
|
|
792
|
+
if (!conversationId || uncertainSubmissions()[conversationId] !== undefined) return null;
|
|
770
793
|
const conversation = isActiveConversation(conversationId) ? state.conversation : cache.get(conversationId)?.conversation;
|
|
771
794
|
if (!conversation) return null;
|
|
772
795
|
|
|
@@ -833,7 +856,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
833
856
|
return queueDraftOperation(async () => persistComposerDraft(input, await targetConversation));
|
|
834
857
|
};
|
|
835
858
|
|
|
836
|
-
const queuedAttempts = new Map<string, { key: string; files?: File[]; id: string
|
|
859
|
+
const queuedAttempts = new Map<string, { key: string; files?: File[]; id: string }>();
|
|
837
860
|
const queueMessage = (input: ComposerDraftInput & { modelProfileId?: string }): Promise<boolean> => {
|
|
838
861
|
if (!input.conversationId || !isComposerDraftSendable(input)) return Promise.resolve(false);
|
|
839
862
|
const conversationId = input.conversationId;
|
|
@@ -845,38 +868,46 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
845
868
|
storedFiles: input.storedFiles,
|
|
846
869
|
model: input.modelProfileId,
|
|
847
870
|
});
|
|
871
|
+
const previous = queuedAttempts.get(conversationId);
|
|
872
|
+
if (uncertainSubmissions()[conversationId] !== undefined) {
|
|
873
|
+
const same =
|
|
874
|
+
previous?.key === key &&
|
|
875
|
+
(previous.files ?? []).length === (input.files ?? []).length &&
|
|
876
|
+
(previous.files ?? []).every((file, index) => file === input.files?.[index]);
|
|
877
|
+
return same ? retrySubmission(conversationId).then((result) => result === "accepted") : Promise.resolve(false);
|
|
878
|
+
}
|
|
848
879
|
return queueDraftOperation(async () => {
|
|
880
|
+
if (uncertainSubmissions()[conversationId] !== undefined) return false;
|
|
881
|
+
let submittedRevision: number | undefined;
|
|
849
882
|
try {
|
|
850
|
-
const
|
|
851
|
-
const same =
|
|
852
|
-
previous?.key === key &&
|
|
853
|
-
(previous.files ?? []).length === (input.files ?? []).length &&
|
|
854
|
-
(previous.files ?? []).every((file, index) => file === input.files?.[index]);
|
|
855
|
-
const saved = same ? previous.saved : await persistComposerDraft(input, conversationId);
|
|
883
|
+
const saved = await persistComposerDraft(input, conversationId);
|
|
856
884
|
if (!saved) return false;
|
|
857
|
-
const queueId =
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}),
|
|
869
|
-
},
|
|
870
|
-
"Failed to queue message",
|
|
871
|
-
);
|
|
885
|
+
const queueId = crypto.randomUUID();
|
|
886
|
+
const body = JSON.stringify({
|
|
887
|
+
draftRevision: saved.revision,
|
|
888
|
+
queueId,
|
|
889
|
+
modelProfileId: input.modelProfileId,
|
|
890
|
+
clientToolIds: options.clientToolIds?.filter((id) => options.frontendTools?.[id]),
|
|
891
|
+
});
|
|
892
|
+
queuedAttempts.set(conversationId, { key, files: input.files, id: queueId });
|
|
893
|
+
submissionBodies.set(conversationId, body);
|
|
894
|
+
submittedRevision = saved.revision;
|
|
895
|
+
await request(`/conversations/${conversationId}/turns`, { method: "POST", body }, "Failed to queue message");
|
|
872
896
|
if (isActiveConversation(conversationId)) {
|
|
873
897
|
if ((state.conversation?.draft.revision ?? 0) <= saved.revision)
|
|
874
898
|
setState("conversation", "draft", { content: [], revision: saved.revision + 1, updatedAt: new Date().toISOString() });
|
|
875
899
|
} else invalidateInactiveCache(conversationId);
|
|
876
900
|
queuedAttempts.delete(conversationId);
|
|
901
|
+
submissionBodies.delete(conversationId);
|
|
877
902
|
if (isActiveConversation(conversationId)) setGlobalError(null);
|
|
878
903
|
return true;
|
|
879
904
|
} catch (error) {
|
|
905
|
+
if (submittedRevision !== undefined && !(error instanceof AiRequestError && error.status >= 400 && error.status < 500)) {
|
|
906
|
+
setUncertainSubmissions((current) => ({ ...current, [conversationId]: submittedRevision! }));
|
|
907
|
+
} else {
|
|
908
|
+
queuedAttempts.delete(conversationId);
|
|
909
|
+
submissionBodies.delete(conversationId);
|
|
910
|
+
}
|
|
880
911
|
setConversationError(conversationId, error instanceof Error ? error.message : "Failed to queue message");
|
|
881
912
|
return false;
|
|
882
913
|
}
|
|
@@ -888,7 +919,8 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
888
919
|
const targetConversation = input.conversationId ? Promise.resolve(input.conversationId) : ensureConversation();
|
|
889
920
|
return queueDraftOperation(async () => {
|
|
890
921
|
const conversationId = await targetConversation;
|
|
891
|
-
if (!conversationId || (isActiveConversation(conversationId) && running()))
|
|
922
|
+
if (!conversationId || uncertainSubmissions()[conversationId] !== undefined || (isActiveConversation(conversationId) && running()))
|
|
923
|
+
return false;
|
|
892
924
|
if (isActiveConversation(conversationId)) clearErrors();
|
|
893
925
|
const baseProjection = isActiveConversation(conversationId)
|
|
894
926
|
? { conversation: state.conversation, messages: [...state.messages], activeTurn: state.activeTurn }
|
|
@@ -928,6 +960,7 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
928
960
|
setRunStatusRaw("streaming");
|
|
929
961
|
}
|
|
930
962
|
|
|
963
|
+
let submittedRevision: number | undefined;
|
|
931
964
|
try {
|
|
932
965
|
const savedDraft = await persistComposerDraft(input, conversationId);
|
|
933
966
|
if (!savedDraft) {
|
|
@@ -956,18 +989,22 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
956
989
|
},
|
|
957
990
|
},
|
|
958
991
|
}));
|
|
992
|
+
submittedRevision = savedDraft.revision;
|
|
993
|
+
const submissionBody = JSON.stringify({
|
|
994
|
+
draftRevision: savedDraft.revision,
|
|
995
|
+
clientToolIds: options.clientToolIds?.filter((id) => options.frontendTools?.[id]),
|
|
996
|
+
modelProfileId: input.modelProfileId,
|
|
997
|
+
});
|
|
998
|
+
submissionBodies.set(conversationId, submissionBody);
|
|
959
999
|
const result = await request<SubmitTurnResult>(
|
|
960
1000
|
`/conversations/${conversationId}/turns`,
|
|
961
1001
|
{
|
|
962
1002
|
method: "POST",
|
|
963
|
-
body:
|
|
964
|
-
draftRevision: savedDraft.revision,
|
|
965
|
-
clientToolIds: options.clientToolIds?.filter((id) => options.frontendTools?.[id]),
|
|
966
|
-
modelProfileId: input.modelProfileId,
|
|
967
|
-
}),
|
|
1003
|
+
body: submissionBody,
|
|
968
1004
|
},
|
|
969
1005
|
"AI request failed",
|
|
970
1006
|
);
|
|
1007
|
+
submissionBodies.delete(conversationId);
|
|
971
1008
|
const alreadyFinished = completedDuringSend.get(conversationId) === result.turn.id;
|
|
972
1009
|
completedDuringSend.delete(conversationId);
|
|
973
1010
|
if (alreadyFinished) clearPendingSend(conversationId);
|
|
@@ -1006,7 +1043,13 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1006
1043
|
return true;
|
|
1007
1044
|
} catch (sendError) {
|
|
1008
1045
|
completedDuringSend.delete(conversationId);
|
|
1009
|
-
|
|
1046
|
+
// A 4xx on this first submission rejects it. Transport/5xx failures may have committed.
|
|
1047
|
+
if (
|
|
1048
|
+
submittedRevision !== undefined &&
|
|
1049
|
+
!(sendError instanceof AiRequestError && sendError.status >= 400 && sendError.status < 500)
|
|
1050
|
+
) {
|
|
1051
|
+
setUncertainSubmissions((current) => ({ ...current, [conversationId]: submittedRevision! }));
|
|
1052
|
+
} else clearPendingSend(conversationId);
|
|
1010
1053
|
if (invalidateInactiveCache(conversationId)) return false;
|
|
1011
1054
|
setState("messages", (prev) => prev.filter((message) => message.id !== optimistic.id));
|
|
1012
1055
|
cache.set(conversationId, { conversation: state.conversation, messages: state.messages, activeTurn: state.activeTurn });
|
|
@@ -1017,6 +1060,73 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1017
1060
|
});
|
|
1018
1061
|
};
|
|
1019
1062
|
|
|
1063
|
+
// Keep the local draft locked until its post-submission revision is available.
|
|
1064
|
+
// A positive lookup alone must not hydrate the composer from a stale cached draft.
|
|
1065
|
+
const finishAcceptedSubmission = async (conversationId: string): Promise<"accepted" | "unknown"> => {
|
|
1066
|
+
const revision = uncertainSubmissions()[conversationId];
|
|
1067
|
+
if (revision === undefined || disposed) return "unknown";
|
|
1068
|
+
if (isActiveConversation(conversationId)) {
|
|
1069
|
+
if (!(await refreshActiveConversation())) return "unknown";
|
|
1070
|
+
if (isActiveConversation(conversationId) && (state.conversation?.draft.revision ?? -1) <= revision) return "unknown";
|
|
1071
|
+
} else cache.delete(conversationId);
|
|
1072
|
+
if (disposed) return "unknown";
|
|
1073
|
+
submissionBodies.delete(conversationId);
|
|
1074
|
+
queuedAttempts.delete(conversationId);
|
|
1075
|
+
clearUncertainSubmission(conversationId);
|
|
1076
|
+
clearPendingSend(conversationId);
|
|
1077
|
+
if (isActiveConversation(conversationId)) setGlobalError(null);
|
|
1078
|
+
return "accepted";
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
/** Read only: a missing message in a history window never proves non-acceptance. */
|
|
1082
|
+
const reconcileSubmission = async (conversationId: string): Promise<"accepted" | "unknown" | "none"> => {
|
|
1083
|
+
const revision = uncertainSubmissions()[conversationId];
|
|
1084
|
+
if (revision === undefined) return "none";
|
|
1085
|
+
try {
|
|
1086
|
+
const queue = queuedAttempts.get(conversationId);
|
|
1087
|
+
let accepted: boolean;
|
|
1088
|
+
if (queue) {
|
|
1089
|
+
const pending = await request<Array<{ id: string }>>(
|
|
1090
|
+
`/conversations/${conversationId}/queue`,
|
|
1091
|
+
{},
|
|
1092
|
+
"Failed to check message status",
|
|
1093
|
+
);
|
|
1094
|
+
// Drained/cancelled entries disappear from this view. Absence never proves rejection;
|
|
1095
|
+
// an explicit frozen retry still uses the server's durable queue receipt.
|
|
1096
|
+
accepted = pending.some((message) => message.id === queue.id);
|
|
1097
|
+
} else {
|
|
1098
|
+
const result = await request<{ submission: { messageId: string; turnId: string | null } | null }>(
|
|
1099
|
+
`/conversations/${conversationId}/submission/${revision}`,
|
|
1100
|
+
{},
|
|
1101
|
+
"Failed to check message status",
|
|
1102
|
+
);
|
|
1103
|
+
accepted = Boolean(result.submission);
|
|
1104
|
+
}
|
|
1105
|
+
if (!accepted || disposed || uncertainSubmissions()[conversationId] !== revision) return "unknown";
|
|
1106
|
+
return await finishAcceptedSubmission(conversationId);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
setConversationError(conversationId, error instanceof Error ? error.message : "Failed to check message status");
|
|
1109
|
+
return "unknown";
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
|
|
1113
|
+
/** Explicit retry of the same frozen submission. Never saves or chooses another revision. */
|
|
1114
|
+
const retrySubmission = (conversationId: string) =>
|
|
1115
|
+
queueDraftOperation(async (): Promise<"accepted" | "unknown" | "none"> => {
|
|
1116
|
+
const resolved = await reconcileSubmission(conversationId);
|
|
1117
|
+
if (resolved !== "unknown" || disposed) return resolved;
|
|
1118
|
+
const body = submissionBodies.get(conversationId);
|
|
1119
|
+
if (!body) return "unknown";
|
|
1120
|
+
try {
|
|
1121
|
+
await request<SubmitTurnResult>(`/conversations/${conversationId}/turns`, { method: "POST", body }, "AI request failed");
|
|
1122
|
+
if (disposed) return "unknown";
|
|
1123
|
+
return await finishAcceptedSubmission(conversationId);
|
|
1124
|
+
} catch {
|
|
1125
|
+
// Even a conflict cannot turn the earlier lost response into a known rejection.
|
|
1126
|
+
return reconcileSubmission(conversationId);
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1020
1130
|
const submitSteer = async (input: { text: string; clientRequestId: string; blockId: string }) => {
|
|
1021
1131
|
const conversationId = activeConversationId();
|
|
1022
1132
|
const turn = state.activeTurn;
|
|
@@ -1426,7 +1536,10 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1426
1536
|
runFrontendTools();
|
|
1427
1537
|
}
|
|
1428
1538
|
|
|
1429
|
-
onCleanup(() =>
|
|
1539
|
+
onCleanup(() => {
|
|
1540
|
+
disposed = true;
|
|
1541
|
+
closeStream();
|
|
1542
|
+
});
|
|
1430
1543
|
|
|
1431
1544
|
const hasMoreHistory = () => {
|
|
1432
1545
|
const conversationId = activeConversationId();
|
|
@@ -1457,6 +1570,11 @@ export const createAiChatController = (options: CreateAiChatControllerOptions) =
|
|
|
1457
1570
|
openConversation,
|
|
1458
1571
|
refreshActiveConversation,
|
|
1459
1572
|
createConversation,
|
|
1573
|
+
/** HTTP status for the reported creation failure; null also covers an unknown network outcome. */
|
|
1574
|
+
creationErrorStatus,
|
|
1575
|
+
uncertainSubmissions,
|
|
1576
|
+
reconcileSubmission,
|
|
1577
|
+
retrySubmission,
|
|
1460
1578
|
saveDraft,
|
|
1461
1579
|
queueMessage,
|
|
1462
1580
|
send,
|
|
@@ -73,7 +73,14 @@ export async function* parseAiSse(response: Response, signal: AbortSignal): Asyn
|
|
|
73
73
|
.filter((line) => line.startsWith("data:"))
|
|
74
74
|
.map((line) => line.slice(5).trimStart())
|
|
75
75
|
.join("\n");
|
|
76
|
-
if (data)
|
|
76
|
+
if (data) {
|
|
77
|
+
const value: unknown = JSON.parse(data);
|
|
78
|
+
if (value && typeof value === "object" && "code" in value && value.code === "app_access_denied") {
|
|
79
|
+
const message = "message" in value && typeof value.message === "string" ? value.message : "Application access denied";
|
|
80
|
+
throw new AiStreamError("access_denied", message);
|
|
81
|
+
}
|
|
82
|
+
yield value as AiStreamEvent;
|
|
83
|
+
}
|
|
77
84
|
}
|
|
78
85
|
}
|
|
79
86
|
} finally {
|
|
@@ -133,8 +140,13 @@ export const subscribeAiStream = (input: {
|
|
|
133
140
|
if (stopped) break;
|
|
134
141
|
input.onEvent(event);
|
|
135
142
|
}
|
|
136
|
-
} catch {
|
|
143
|
+
} catch (error) {
|
|
137
144
|
if (stopped) return;
|
|
145
|
+
if (error instanceof AiStreamError) {
|
|
146
|
+
stopped = true;
|
|
147
|
+
input.onError?.(error);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
138
150
|
} finally {
|
|
139
151
|
clearConnectTimer();
|
|
140
152
|
if (activeAttempt === attempt) activeAttempt = null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Context, Hono } from "hono";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
4
|
+
import { withCapabilityRequestTimeout } from "../_internal/capability-timeout";
|
|
4
5
|
import { dispatchCapability } from "../api/capabilities";
|
|
5
6
|
import { dispatchCapabilityStream } from "../api/capability-streams";
|
|
6
7
|
import { CAPABILITY_MAX_REQUEST_BYTES } from "../contracts/capabilities";
|
|
@@ -37,30 +38,33 @@ export function createCodeCapabilityRoutes(
|
|
|
37
38
|
credentialKind: "session",
|
|
38
39
|
scopes: [],
|
|
39
40
|
});
|
|
40
|
-
const invoke = async (c: Context<AuthContext>, review = false) =>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
41
|
+
const invoke = async (c: Context<AuthContext>, review = false) =>
|
|
42
|
+
withCapabilityRequestTimeout(c, review ? "review" : c.req.param("kind") === "actions" ? "action" : "query", async () => {
|
|
43
|
+
const body = await readBoundedJson(c.req.raw, CAPABILITY_MAX_REQUEST_BYTES);
|
|
44
|
+
const input = body.ok ? Input.safeParse(body.data) : null;
|
|
45
|
+
if (!input?.success) return Response.json({ code: "VALIDATION_FAILED", message: "Invalid capability input" }, { status: 400 });
|
|
46
|
+
const conversation = await store.getConversation({ conversationId: c.req.param("conversationId")! });
|
|
47
|
+
const appId = c.req.param("appId")!;
|
|
48
|
+
const capabilityId = c.req.param("capabilityId")!;
|
|
49
|
+
if (conversation?.allowedTools && !conversation.allowedTools.includes(`${appId}.${capabilityId}`)) return denied();
|
|
50
|
+
const config = await store.getTurnRunConfig({ conversationId: c.req.param("conversationId")!, turnId: c.req.param("turnId")! });
|
|
51
|
+
if (!config || config.kind === "compact") return denied();
|
|
52
|
+
return (dependencies.dispatch ?? dispatchCapability)({
|
|
53
|
+
request: c.req.raw,
|
|
54
|
+
authority: turnAuthority(c),
|
|
55
|
+
origin: "assistant",
|
|
56
|
+
continuation: codeCapabilityOperation(c.req.param("conversationId")!, c.req.param("turnId")!),
|
|
57
|
+
mandate: config.mandate
|
|
58
|
+
? { mandateId: config.mandate.id, mandateRevision: config.mandate.revision, ownerAppId: "core" }
|
|
59
|
+
: undefined,
|
|
60
|
+
review,
|
|
61
|
+
kind: review || c.req.param("kind") === "actions" ? "actions" : "queries",
|
|
62
|
+
appId,
|
|
63
|
+
capabilityId,
|
|
64
|
+
input: input.data.input,
|
|
65
|
+
locale: getLocale(c),
|
|
66
|
+
});
|
|
62
67
|
});
|
|
63
|
-
};
|
|
64
68
|
return new Hono<AuthContext>()
|
|
65
69
|
.use("/:conversationId/:turnId/*", async (c, next) => {
|
|
66
70
|
await next();
|
|
@@ -4,7 +4,8 @@ import { readBoundedJson } from "../_internal/bounded-json";
|
|
|
4
4
|
import { getApp } from "../_internal/registry";
|
|
5
5
|
import type { RequestActor } from "../server";
|
|
6
6
|
import { IDENTITY_REFRESH_TIMEOUT_MS } from "../services/identity/constants";
|
|
7
|
-
import {
|
|
7
|
+
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
8
|
+
import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
|
|
8
9
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
9
10
|
import { logger } from "../services/logging";
|
|
10
11
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
@@ -62,11 +63,12 @@ export const runManagedCodeTool =
|
|
|
62
63
|
if (!runConfig || runConfig.kind === "compact" || Boolean(runConfig.background) !== Boolean(runConfig.mandate)) {
|
|
63
64
|
throw new Error("Code execution requires a valid turn and task-scoped background authority.");
|
|
64
65
|
}
|
|
65
|
-
const
|
|
66
|
+
const authority = await resolveAiCapabilityActor({
|
|
66
67
|
conversationId: context.conversationId,
|
|
67
68
|
persistedActor: context.actor,
|
|
68
69
|
store: aiConversations,
|
|
69
70
|
});
|
|
71
|
+
const { actor } = authority;
|
|
70
72
|
await authorizeCodeExecution(context.conversationId, context.turnId, actor.user.id);
|
|
71
73
|
await context.reportProgress?.(context.locale?.startsWith("de") ? "Ausführungshost verbinden" : "Connecting execution host");
|
|
72
74
|
const app = await getApp("assistant");
|
|
@@ -81,21 +83,17 @@ export const runManagedCodeTool =
|
|
|
81
83
|
try {
|
|
82
84
|
return await withActiveIdentitySigner(
|
|
83
85
|
"invocation",
|
|
84
|
-
(signer) =>
|
|
85
|
-
signInvocationToken(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
access_subject_id: actor.user.id,
|
|
93
|
-
credential_kind: "session",
|
|
94
|
-
scopes: [],
|
|
86
|
+
(signer, db) =>
|
|
87
|
+
signInvocationToken(
|
|
88
|
+
{
|
|
89
|
+
...audience,
|
|
90
|
+
schemaHash: null,
|
|
91
|
+
authority: invocationAuthorityFromRequest(authority),
|
|
92
|
+
signer,
|
|
93
|
+
issuer: signer.issuer,
|
|
95
94
|
},
|
|
96
|
-
signer,
|
|
97
|
-
|
|
98
|
-
}),
|
|
95
|
+
{ db, groupsAdmin: signer.groupsAdmin },
|
|
96
|
+
),
|
|
99
97
|
{ signal: context.signal, timeoutMs: ISSUANCE_TIMEOUT_MS },
|
|
100
98
|
);
|
|
101
99
|
} catch (error) {
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
completeCapabilityClaim,
|
|
9
9
|
markCapabilityClaimUncertain,
|
|
10
10
|
} from "../capabilities/claims";
|
|
11
|
-
import {
|
|
11
|
+
import { invocationAuthorityFromRequest } from "../services/identity/invocation-authority";
|
|
12
|
+
import { issueInvocationToken as signInvocationToken } from "../services/identity/issue-invocation";
|
|
12
13
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
13
14
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
14
15
|
import { resolveAiCapabilityActor } from "./capability-execution";
|
|
@@ -53,11 +54,12 @@ export function createCodeSourceTool(name: CodeSourceToolName) {
|
|
|
53
54
|
promptHint: "promptHint" in definition ? definition.promptHint : undefined,
|
|
54
55
|
}).server(async (input, context) => {
|
|
55
56
|
if (!context.conversationId) throw new Error("Code tools require an Assistant conversation.");
|
|
56
|
-
const
|
|
57
|
+
const authority = await resolveAiCapabilityActor({
|
|
57
58
|
conversationId: context.conversationId,
|
|
58
59
|
persistedActor: context.actor,
|
|
59
60
|
store: aiConversations,
|
|
60
61
|
});
|
|
62
|
+
const { actor } = authority;
|
|
61
63
|
const databaseOperations: Partial<Record<CodeSourceToolName, string>> = {
|
|
62
64
|
code_sql: "query",
|
|
63
65
|
code_database_read: "status",
|
|
@@ -97,23 +99,19 @@ export function createCodeSourceTool(name: CodeSourceToolName) {
|
|
|
97
99
|
await authorizeDatabase?.();
|
|
98
100
|
const signed = await withActiveIdentitySigner(
|
|
99
101
|
"invocation",
|
|
100
|
-
(signer) =>
|
|
101
|
-
signInvocationToken(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
access_subject_id: actor.user.id,
|
|
111
|
-
credential_kind: "session",
|
|
112
|
-
scopes: [],
|
|
102
|
+
(signer, db) =>
|
|
103
|
+
signInvocationToken(
|
|
104
|
+
{
|
|
105
|
+
targetAppId: "assistant",
|
|
106
|
+
callingAppId: "core",
|
|
107
|
+
operation: `tool:${name}`,
|
|
108
|
+
schemaHash: null,
|
|
109
|
+
authority: invocationAuthorityFromRequest(authority),
|
|
110
|
+
signer,
|
|
111
|
+
issuer: signer.issuer,
|
|
113
112
|
},
|
|
114
|
-
signer,
|
|
115
|
-
|
|
116
|
-
}),
|
|
113
|
+
{ db, groupsAdmin: signer.groupsAdmin },
|
|
114
|
+
),
|
|
117
115
|
{ signal, timeoutMs: 5000 },
|
|
118
116
|
);
|
|
119
117
|
const headers = new Headers({ authorization: `Bearer ${signed.token}`, "content-type": "application/json" });
|