@k2b/cloud 0.29.0 → 0.30.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 +5 -3
- package/src/_internal/app-roles.ts +14 -0
- package/src/_internal/capabilities.ts +111 -3
- package/src/_internal/capability-sentences.ts +413 -0
- package/src/_internal/define-app.ts +10 -1
- package/src/_internal/registry-validation.ts +5 -0
- package/src/access/PermissionEditor.tsx +43 -24
- package/src/ai/app-skill-store.ts +298 -0
- package/src/ai/app-skills.ts +105 -0
- package/src/ai/browser.ts +10 -0
- package/src/ai/capabilities.ts +68 -9
- package/src/ai/chart-block.ts +380 -0
- package/src/ai/chat/action-sentences.ts +49 -0
- package/src/ai/chat/blocks.tsx +203 -185
- package/src/ai/chat/chart-block.tsx +79 -0
- package/src/ai/chat/live-turn.browser-harness.tsx +5 -1
- package/src/ai/chat/message-utils.ts +3 -0
- package/src/ai/chat/messages.ts +12 -26
- package/src/ai/chat/tool-disclosure.tsx +21 -14
- package/src/ai/chat/tool-groups.ts +64 -37
- package/src/ai/chat/tool-names.ts +2 -0
- package/src/ai/chat/turn-layout.ts +4 -4
- package/src/ai/chat/turn-view.tsx +141 -69
- package/src/ai/code-runtime-tools.ts +59 -44
- package/src/ai/default-tool-contracts.ts +198 -0
- package/src/ai/default-tools.ts +25 -1
- package/src/ai/executor.ts +19 -21
- package/src/ai/index.ts +9 -1
- package/src/ai/migrate.ts +59 -12
- package/src/ai/protocol.ts +9 -4
- package/src/ai/runtime-tools.ts +1 -0
- package/src/ai/skill-content.ts +54 -0
- package/src/ai/skills-routes.ts +22 -13
- package/src/ai/skills.ts +177 -231
- package/src/ai/store.ts +20 -13
- package/src/ai/tool-sentences.ts +38 -0
- package/src/ai/types.ts +10 -2
- package/src/api/admin-ai-skills.ts +35 -16
- package/src/api/admin-outgoing-mail.ts +18 -0
- package/src/api/fanout.ts +128 -0
- package/src/api/help.ts +8 -3
- package/src/api/mcp.ts +4 -1
- package/src/api/search.ts +31 -106
- package/src/api/widgets.ts +259 -100
- package/src/browser/CloudResourceSearch.browser-harness.tsx +1 -1
- package/src/browser/ndjson.ts +34 -0
- package/src/browser/reload.ts +46 -0
- package/src/browser/search-stream.ts +4 -26
- package/src/browser/widgets.ts +28 -0
- package/src/capabilities/server.ts +20 -2
- package/src/cli/access.ts +48 -14
- package/src/cli/admin/ai-skills.ts +59 -39
- package/src/cli/admin/outgoing-mail.ts +30 -2
- package/src/contracts/announcement-cookie.ts +62 -0
- package/src/contracts/announcements.ts +14 -52
- package/src/contracts/capabilities.ts +24 -0
- package/src/contracts/outgoing-mail.ts +56 -11
- package/src/contracts/pwa-paths.ts +7 -2
- package/src/contracts/pwa.ts +2 -7
- package/src/contracts/registry.ts +1 -0
- package/src/contracts/shared.ts +5 -0
- package/src/contracts/widgets.ts +20 -0
- package/src/index.ts +1 -0
- package/src/server/help.ts +8 -1
- package/src/services/accounts/users.ts +15 -0
- package/src/services/help/index.ts +18 -5
- package/src/services/help/types.ts +3 -2
- package/src/services/identity/constants.ts +2 -0
- package/src/services/identity/key-ring.ts +123 -42
- package/src/services/identity/runtime-config.ts +13 -2
- package/src/services/outbox.ts +24 -9
- package/src/services/outgoing-mail/index.ts +33 -5
- package/src/services/outgoing-mail/messages.ts +60 -19
- package/src/services/outgoing-mail/store.ts +63 -4
- package/src/shared/network-address.ts +1 -0
- package/src/ssr/AppLaunchpadPanel.tsx +12 -3
- package/src/ssr/GlobalAnnouncements.island.tsx +2 -2
- package/src/ssr/GlobalSearchDialog.tsx +1 -43
- package/src/ssr/GlobalSearchTrigger.island.tsx +12 -3
- package/src/ssr/LayoutHelpTrigger.island.tsx +1 -1
- package/src/ssr/PwaRuntime.island.tsx +1 -1
- package/src/ssr/app-navigation.ts +5 -7
- package/src/ssr/global-search-host.tsx +55 -0
- package/src/ssr/help.ts +3 -3
- package/src/ssr/rail-context.ts +4 -3
- package/src/ssr/runtime.ts +14 -1
- package/src/styles/effects.css +142 -0
- package/src/styles/utilities-markdown-table.css +5 -1
- package/src/ai/code-mode-skill.ts +0 -114
- package/src/ai/data-analysis-skill.ts +0 -10
- package/src/ai/grids-skill.ts +0 -158
- package/src/ai/skill-seeds.ts +0 -560
|
@@ -17,17 +17,34 @@ import {
|
|
|
17
17
|
import { markdown } from "../../shared";
|
|
18
18
|
import { AI_TURN_LEASE_MS, type AiTurnBlock } from "../protocol";
|
|
19
19
|
import type { AiTurnError } from "../types";
|
|
20
|
-
import {
|
|
20
|
+
import { toolOutcome, toolSubject } from "./action-sentences";
|
|
21
|
+
import {
|
|
22
|
+
type AiApprovalDecision,
|
|
23
|
+
ApprovalBlockView,
|
|
24
|
+
CompactionBlockView,
|
|
25
|
+
CompactToolRow,
|
|
26
|
+
SurveyToolView,
|
|
27
|
+
TextEditorToolView,
|
|
28
|
+
} from "./blocks";
|
|
21
29
|
import { CapabilityTablePreview } from "./capability-table";
|
|
30
|
+
import { CloudChartBlock } from "./chart-block";
|
|
22
31
|
import { PresentToolBlock } from "./file-tools";
|
|
23
32
|
import { useAiChatActions } from "./message-actions";
|
|
24
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
aiToolIcon,
|
|
35
|
+
capabilityErrorDescription,
|
|
36
|
+
displayToolName,
|
|
37
|
+
isCardToolName,
|
|
38
|
+
isChartToolName,
|
|
39
|
+
isRecord,
|
|
40
|
+
isSurveyToolName,
|
|
41
|
+
} from "./message-utils";
|
|
25
42
|
import { aiChatMessages } from "./messages";
|
|
26
43
|
import { AssistantMarkdownBlock } from "./primitives";
|
|
27
44
|
import { AiToolActivity, AiToolDisclosureProvider, type AiToolDisclosureState } from "./tool-disclosure";
|
|
28
|
-
import { type AiWorkEntry, countWorkSteps, groupWorkBlocks
|
|
45
|
+
import { type AiWorkEntry, type AiWorkItem, countWorkSteps, groupWorkBlocks } from "./tool-groups";
|
|
29
46
|
import { aiTurnErrorCanContinue, aiTurnErrorDescription } from "./turn-error";
|
|
30
|
-
import type { AiTurnAction, AiTurnLayout, AiTurnPhase, AiTurnResult } from "./turn-layout";
|
|
47
|
+
import type { AiTurnAction, AiTurnActionState, AiTurnLayout, AiTurnPhase, AiTurnResult } from "./turn-layout";
|
|
31
48
|
import { CloudCardBlock } from "./visual-tools";
|
|
32
49
|
|
|
33
50
|
type ToolBlock = Extract<AiTurnBlock, { kind: "tool" }>;
|
|
@@ -88,10 +105,12 @@ export const formatWorkClock = (ms: number): string => {
|
|
|
88
105
|
|
|
89
106
|
const basename = (value: string) => value.slice(value.lastIndexOf("/") + 1) || value;
|
|
90
107
|
|
|
108
|
+
/** What a call acts on: the file name of its path, or its name, title, or query as given. */
|
|
91
109
|
const toolTarget = (block: ToolBlock): string => {
|
|
92
110
|
const args = isRecord(block.args) ? block.args : {};
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
if (typeof args.path === "string" && args.path.trim()) return basename(args.path.trim());
|
|
112
|
+
const value = [args.name, args.title, args.query].find((entry) => typeof entry === "string" && entry.trim());
|
|
113
|
+
return typeof value === "string" ? value.trim() : "";
|
|
95
114
|
};
|
|
96
115
|
|
|
97
116
|
/** A step that runs longer than this shows its duration, so a long step does not look like a stalled one. */
|
|
@@ -124,6 +143,7 @@ const stepPhrase = (block: AiTurnBlock | null, t: Messages, withTargets: boolean
|
|
|
124
143
|
if (name === "present") return withTarget(t.stepDeliveringFile);
|
|
125
144
|
if (name === "code_present") return withTarget(t.stepPreparingView);
|
|
126
145
|
if (isCardToolName(name)) return t.stepPreparingCard;
|
|
146
|
+
if (isChartToolName(name)) return t.stepPreparingChart;
|
|
127
147
|
if (["code_write", "code_create", "code_update", "code_fork", "code_remove", "code_restore"].includes(name))
|
|
128
148
|
return withTarget(t.stepWritingCode);
|
|
129
149
|
if (["code_run", "code_action", "code_sql"].includes(name)) return withTarget(t.stepRunningCode);
|
|
@@ -234,6 +254,7 @@ function ThoughtRow(props: { block: Extract<AiTurnBlock, { kind: "thinking" }>;
|
|
|
234
254
|
blockId={props.block.id}
|
|
235
255
|
label={firstLine()}
|
|
236
256
|
icon="ti ti-bulb"
|
|
257
|
+
bodyInset={false}
|
|
237
258
|
renderBody={() => <p class="ai-turn-steps__thought">{props.block.text.trim()}</p>}
|
|
238
259
|
/>
|
|
239
260
|
</Show>
|
|
@@ -254,25 +275,26 @@ function WorkEntryRow(props: { entry: AiWorkEntry; busy: boolean }) {
|
|
|
254
275
|
);
|
|
255
276
|
}
|
|
256
277
|
|
|
257
|
-
|
|
278
|
+
/** Housekeeping folded into one row at step level. It opens in place: its steps follow at the same indent. */
|
|
279
|
+
function HousekeepingGroup(props: { id: string; entries: AiWorkEntry[]; active: boolean }) {
|
|
258
280
|
const locale = useLocale();
|
|
259
|
-
const tools = () => props.entries.filter((entry): entry is ToolBlock => entry.kind === "tool");
|
|
260
281
|
const last = () => props.entries.at(-1)!;
|
|
261
282
|
return (
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
<AiToolActivity
|
|
284
|
+
blockId={`group:${props.id}`}
|
|
285
|
+
label={aiChatMessages(locale()).groupLoad}
|
|
286
|
+
description={countWorkSteps(props.entries, locale())}
|
|
287
|
+
icon="ti ti-stack-2"
|
|
288
|
+
bodyInset={false}
|
|
289
|
+
busy={props.active && last().kind === "tool" && (last() as ToolBlock).status === "running"}
|
|
290
|
+
renderBody={() => (
|
|
291
|
+
<div class="ai-turn-steps">
|
|
270
292
|
<For each={props.entries}>
|
|
271
293
|
{(entry, index) => <WorkEntryRow entry={entry} busy={props.active && index() === props.entries.length - 1} />}
|
|
272
294
|
</For>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
/>
|
|
276
298
|
);
|
|
277
299
|
}
|
|
278
300
|
|
|
@@ -288,34 +310,46 @@ function WorkText(props: { text: string }) {
|
|
|
288
310
|
);
|
|
289
311
|
}
|
|
290
312
|
|
|
291
|
-
|
|
313
|
+
const workItemKey = (item: AiWorkItem): string =>
|
|
314
|
+
item.kind === "text" ? `text:${item.block.id}` : item.kind === "step" ? `step:${item.entry.id}` : `group:${item.id}`;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Expanded work, one indent below the work line and one size: intermediate texts as quiet paragraphs, the steps between
|
|
318
|
+
* them directly, and housekeeping folded at step level. A row shown while this list is open never folds away under the
|
|
319
|
+
* reader, also when reasoning or arguments that would fold it arrive later; the next time it opens, it folds like in
|
|
320
|
+
* history.
|
|
321
|
+
*/
|
|
292
322
|
function AiWorkSteps(props: { blocks: AiTurnBlock[]; active: boolean }) {
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
323
|
+
const shown = new Set<string>();
|
|
324
|
+
const items = createMemo(() => {
|
|
325
|
+
const work = groupWorkBlocks(props.blocks, shown);
|
|
326
|
+
for (const item of work) if (item.kind === "step") shown.add(item.entry.id);
|
|
327
|
+
return new Map(work.map((item) => [workItemKey(item), item]));
|
|
328
|
+
});
|
|
329
|
+
const keys = createMemo(() => [...items().keys()]);
|
|
298
330
|
return (
|
|
299
331
|
<div class="ai-turn-steps">
|
|
300
332
|
<For each={keys()}>
|
|
301
333
|
{(key, index) => {
|
|
302
|
-
const
|
|
334
|
+
const item = () => items().get(key);
|
|
303
335
|
const text = () => {
|
|
304
|
-
const value =
|
|
336
|
+
const value = item();
|
|
305
337
|
return value?.kind === "text" ? value.block.text : undefined;
|
|
306
338
|
};
|
|
307
|
-
const
|
|
308
|
-
const value =
|
|
309
|
-
return value?.kind === "
|
|
339
|
+
const step = () => {
|
|
340
|
+
const value = item();
|
|
341
|
+
return value?.kind === "step" ? value.entry : undefined;
|
|
342
|
+
};
|
|
343
|
+
const group = () => {
|
|
344
|
+
const value = item();
|
|
345
|
+
return value?.kind === "housekeeping" ? value : undefined;
|
|
310
346
|
};
|
|
347
|
+
const active = () => props.active && index() === keys().length - 1;
|
|
311
348
|
return (
|
|
312
349
|
<Switch>
|
|
313
|
-
<Match when={text()
|
|
314
|
-
|
|
315
|
-
</Match>
|
|
316
|
-
<Match when={steps()}>
|
|
317
|
-
{(value) => <WorkGroup id={value().id} entries={value().entries} active={props.active && index() === keys().length - 1} />}
|
|
318
|
-
</Match>
|
|
350
|
+
<Match when={text()}>{(value) => <WorkText text={value()} />}</Match>
|
|
351
|
+
<Match when={step()}>{(entry) => <WorkEntryRow entry={entry()} busy={active()} />}</Match>
|
|
352
|
+
<Match when={group()}>{(value) => <HousekeepingGroup id={value().id} entries={value().entries} active={active()} />}</Match>
|
|
319
353
|
</Switch>
|
|
320
354
|
);
|
|
321
355
|
}}
|
|
@@ -413,6 +447,9 @@ function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; r
|
|
|
413
447
|
<Match when={isCardToolName(current().name)}>
|
|
414
448
|
<CloudCardBlock args={current().args} />
|
|
415
449
|
</Match>
|
|
450
|
+
<Match when={isChartToolName(current().name)}>
|
|
451
|
+
<CloudChartBlock args={current().args} completed={current().status === "completed"} />
|
|
452
|
+
</Match>
|
|
416
453
|
<Match when={true}>
|
|
417
454
|
<Show when={!props.receipt()}>
|
|
418
455
|
<CapabilityResultRow block={current()} />
|
|
@@ -428,31 +465,51 @@ function AiTurnResultView(props: { result: Accessor<AiTurnResult | undefined>; r
|
|
|
428
465
|
);
|
|
429
466
|
}
|
|
430
467
|
|
|
431
|
-
|
|
468
|
+
type ReceiptState = Exclude<AiTurnActionState, "open" | "interaction">;
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* One line for an action in place 4, the same element while it runs and once it is done, rejected, failed, or not run.
|
|
472
|
+
* It says what happened in the owning app's words: its sentence for the outcome, such as "Moved report.pdf to the
|
|
473
|
+
* trash", else, once done, the application's own summary of the call. Without either it puts the state in the reader's
|
|
474
|
+
* language before what the call does, such as "Rejected: Write report.md", or before its title and target.
|
|
475
|
+
*/
|
|
476
|
+
function AiReceipt(props: { block: ToolBlock; state: ReceiptState; stopped: boolean }) {
|
|
432
477
|
const locale = useLocale();
|
|
433
478
|
const t = () => aiChatMessages(locale());
|
|
434
|
-
const block = () => props.
|
|
435
|
-
const
|
|
436
|
-
|
|
479
|
+
const block = () => props.block;
|
|
480
|
+
const subject = () => {
|
|
481
|
+
const sentence = toolSubject(block(), locale());
|
|
482
|
+
if (sentence) return sentence;
|
|
483
|
+
const title = block().presentation?.title ?? displayToolName(block().name, locale());
|
|
484
|
+
const target = toolTarget(block());
|
|
485
|
+
return target ? `${title} · ${target}` : title;
|
|
486
|
+
};
|
|
487
|
+
const capabilityAction = () => block().presentation?.kind === "capability";
|
|
488
|
+
const links = () => (props.state === "done" ? resultLinks(block()) : []);
|
|
437
489
|
const label = () => {
|
|
438
|
-
const state = props.
|
|
490
|
+
const state = props.state;
|
|
491
|
+
if (state === "running") return t().receiptRunning({ title: subject() });
|
|
439
492
|
if (state === "done") {
|
|
440
|
-
const summary = resultSummary(block());
|
|
441
|
-
if (summary) return summary;
|
|
442
493
|
// A tool that is not a Cloud action has no effect of its own to report; the receipt records the decision.
|
|
443
|
-
|
|
494
|
+
if (!capabilityAction()) return t().receiptApproved({ title: subject() });
|
|
495
|
+
return toolOutcome(block(), "done", locale()) || resultSummary(block()) || t().receiptDone({ title: subject() });
|
|
444
496
|
}
|
|
445
|
-
if (state === "rejected") return t().receiptRejected({ title:
|
|
446
|
-
if (state === "failed") return t().receiptFailed({ title:
|
|
447
|
-
|
|
497
|
+
if (state === "rejected") return toolOutcome(block(), "rejected", locale()) ?? t().receiptRejected({ title: subject() });
|
|
498
|
+
if (state === "failed") return t().receiptFailed({ title: subject() });
|
|
499
|
+
const notRun = toolOutcome(block(), "notRun", locale());
|
|
500
|
+
return notRun
|
|
501
|
+
? t().receiptNotRunSentence({ sentence: notRun, stopped: props.stopped })
|
|
502
|
+
: t().receiptNotRun({ title: subject(), stopped: props.stopped });
|
|
448
503
|
};
|
|
504
|
+
const declined = () => props.state === "rejected" || props.state === "not_run";
|
|
449
505
|
return (
|
|
450
506
|
<Chat.Activity
|
|
451
507
|
class="ai-turn-receipt"
|
|
452
|
-
icon={aiToolIcon(block().name, block().presentation?.appIcon)}
|
|
453
|
-
accent={block().presentation?.appAccent}
|
|
508
|
+
icon={declined() ? "ti ti-circle-off" : aiToolIcon(block().name, block().presentation?.appIcon)}
|
|
509
|
+
accent={declined() ? undefined : block().presentation?.appAccent}
|
|
454
510
|
label={label()}
|
|
455
|
-
|
|
511
|
+
busy={props.state === "running"}
|
|
512
|
+
description={props.state === "failed" ? capabilityErrorDescription(block().result) : undefined}
|
|
456
513
|
trailing={links().length > 0 ? <ResultLinks links={links()} /> : undefined}
|
|
457
514
|
/>
|
|
458
515
|
);
|
|
@@ -463,8 +520,26 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
463
520
|
let row!: HTMLDivElement;
|
|
464
521
|
// Set while focus is in this place, including after the focused control disappeared with the decided card.
|
|
465
522
|
let focusedHere = false;
|
|
523
|
+
// The decision the server accepted for a card here; its receipt replaces the card before the turn reports it. It
|
|
524
|
+
// belongs to the call it answered: code that asks again shows the next request on the same block as a new card.
|
|
525
|
+
const [decided, setDecided] = createSignal<AiApprovalDecision | null>(null);
|
|
466
526
|
const action = () => props.action();
|
|
467
527
|
const state = () => action()?.state;
|
|
528
|
+
const decision = () => {
|
|
529
|
+
const current = action();
|
|
530
|
+
const value = decided();
|
|
531
|
+
return current && value?.callId === current.block.callId ? value.approved : null;
|
|
532
|
+
};
|
|
533
|
+
const card = () => {
|
|
534
|
+
const current = action();
|
|
535
|
+
return current?.state === "open" && current.block.status === "awaiting_approval" && decision() === null ? current.block : undefined;
|
|
536
|
+
};
|
|
537
|
+
const receipt = (): ReceiptState | undefined => {
|
|
538
|
+
const current = action();
|
|
539
|
+
if (!current || card()) return undefined;
|
|
540
|
+
if (current.state === "open" && current.block.status === "awaiting_approval") return decision() ? "running" : "rejected";
|
|
541
|
+
return current.state === "open" || current.state === "interaction" ? undefined : current.state;
|
|
542
|
+
};
|
|
468
543
|
// Screen readers hear a waiting approval once, as one short line instead of the whole card, without moving focus.
|
|
469
544
|
createEffect(
|
|
470
545
|
on(state, (next, previous) => {
|
|
@@ -472,18 +547,22 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
472
547
|
if (next !== "open" || previous === "open" || !isLive(props.phase) || current?.block.status !== "awaiting_approval") return;
|
|
473
548
|
announce(
|
|
474
549
|
aiChatMessages(locale()).approvalNeeded({
|
|
475
|
-
title: current.block.presentation?.title ?? displayToolName(current.block.name, locale()),
|
|
550
|
+
title: toolSubject(current.block, locale()) ?? current.block.presentation?.title ?? displayToolName(current.block.name, locale()),
|
|
476
551
|
}),
|
|
477
552
|
);
|
|
478
553
|
}),
|
|
479
554
|
);
|
|
480
|
-
// A card the user decided becomes its receipt in place
|
|
555
|
+
// A card the user decided becomes its receipt in place, once, as the direct result of the decision. Focus stays on
|
|
556
|
+
// that place without scrolling.
|
|
481
557
|
createEffect(
|
|
482
|
-
on(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
558
|
+
on(
|
|
559
|
+
() => card() !== undefined,
|
|
560
|
+
(open, wasOpen) => {
|
|
561
|
+
if (!wasOpen || open || !focusedHere || typeof document === "undefined") return;
|
|
562
|
+
const focused = document.activeElement;
|
|
563
|
+
if (!focused || focused === document.body || row.contains(focused)) row.focus({ preventScroll: true });
|
|
564
|
+
},
|
|
565
|
+
),
|
|
487
566
|
);
|
|
488
567
|
return (
|
|
489
568
|
<div
|
|
@@ -500,11 +579,12 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
500
579
|
>
|
|
501
580
|
<Show when={action()}>
|
|
502
581
|
{(current) => (
|
|
503
|
-
<Switch
|
|
504
|
-
<Match when={
|
|
505
|
-
|
|
582
|
+
<Switch>
|
|
583
|
+
<Match when={card()}>{(block) => <ApprovalBlockView turnId={props.turnId} block={block()} onDecided={setDecided} />}</Match>
|
|
584
|
+
<Match when={receipt()}>
|
|
585
|
+
{(receiptState) => <AiReceipt block={current().block} state={receiptState()} stopped={props.phase === "stopped"} />}
|
|
506
586
|
</Match>
|
|
507
|
-
<Match when={
|
|
587
|
+
<Match when={true}>
|
|
508
588
|
<Switch
|
|
509
589
|
fallback={
|
|
510
590
|
<Chat.Activity
|
|
@@ -522,14 +602,6 @@ function AiTurnActionView(props: { action: Accessor<AiTurnAction | undefined>; t
|
|
|
522
602
|
</Match>
|
|
523
603
|
</Switch>
|
|
524
604
|
</Match>
|
|
525
|
-
<Match when={current().state === "running"}>
|
|
526
|
-
<Chat.Activity
|
|
527
|
-
label={current().block.presentation?.title ?? displayToolName(current().block.name, locale())}
|
|
528
|
-
icon={aiToolIcon(current().block.name, current().block.presentation?.appIcon)}
|
|
529
|
-
accent={current().block.presentation?.appAccent}
|
|
530
|
-
busy
|
|
531
|
-
/>
|
|
532
|
-
</Match>
|
|
533
605
|
</Switch>
|
|
534
606
|
)}
|
|
535
607
|
</Show>
|
|
@@ -3,8 +3,10 @@ import { z } from "zod";
|
|
|
3
3
|
import { readBoundedJson } from "../_internal/bounded-json";
|
|
4
4
|
import { getApp } from "../_internal/registry";
|
|
5
5
|
import type { RequestActor } from "../server";
|
|
6
|
+
import { IDENTITY_REFRESH_TIMEOUT_MS } from "../services/identity/constants";
|
|
6
7
|
import { signInvocationToken } from "../services/identity/invocation-token";
|
|
7
8
|
import { withActiveIdentitySigner } from "../services/identity/key-ring";
|
|
9
|
+
import { logger } from "../services/logging";
|
|
8
10
|
import { LOCALE_HEADER } from "../shared/locale";
|
|
9
11
|
import { TIMEZONE_COOKIE } from "../shared/time";
|
|
10
12
|
import { CodeToolFailure } from "./browser-code-contracts";
|
|
@@ -13,6 +15,9 @@ import { CODE_CAPABILITY_TOKEN_HEADER, codeCapabilityOperation } from "./code-ca
|
|
|
13
15
|
import { authorizeCodeExecution } from "./code-execution";
|
|
14
16
|
import { aiConversations } from "./store";
|
|
15
17
|
|
|
18
|
+
const log = logger("ai:code-runtime");
|
|
19
|
+
const ISSUANCE_TIMEOUT_MS = 5_000;
|
|
20
|
+
|
|
16
21
|
const Reply = z.object({
|
|
17
22
|
ok: z.literal(true),
|
|
18
23
|
data: z.object({
|
|
@@ -49,54 +54,64 @@ export const runManagedCodeTool =
|
|
|
49
54
|
await context.reportProgress?.(context.locale?.startsWith("de") ? "Ausführungshost verbinden" : "Connecting execution host");
|
|
50
55
|
const app = await getApp("assistant");
|
|
51
56
|
if (!app) throw new Error("Assistant code host is unavailable");
|
|
57
|
+
const issue = async (audience: { targetAppId: string; callingAppId: string; operation: string }) => {
|
|
58
|
+
// A token is signed before its request goes out, so a timed-out issuance
|
|
59
|
+
// never reached the host and is safe to repeat. Retry long enough for a
|
|
60
|
+
// stalled shared refresh to finish or fall back to the cached key. Every
|
|
61
|
+
// timeout used up a full attempt or ended a shared load, so this cannot spin.
|
|
62
|
+
const giveUpAt = Date.now() + IDENTITY_REFRESH_TIMEOUT_MS + ISSUANCE_TIMEOUT_MS;
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
return await withActiveIdentitySigner(
|
|
66
|
+
"invocation",
|
|
67
|
+
(signer) =>
|
|
68
|
+
signInvocationToken({
|
|
69
|
+
...audience,
|
|
70
|
+
schemaHash: null,
|
|
71
|
+
authority: {
|
|
72
|
+
sub: actor.user.id,
|
|
73
|
+
principal_type: "user",
|
|
74
|
+
access_subject_type: "user",
|
|
75
|
+
access_subject_id: actor.user.id,
|
|
76
|
+
credential_kind: "session",
|
|
77
|
+
scopes: [],
|
|
78
|
+
},
|
|
79
|
+
signer,
|
|
80
|
+
issuer: signer.issuer,
|
|
81
|
+
}),
|
|
82
|
+
{ signal: context.signal, timeoutMs: ISSUANCE_TIMEOUT_MS },
|
|
83
|
+
);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (context.signal.aborted || !(error instanceof DOMException && error.name === "TimeoutError")) throw error;
|
|
86
|
+
if (Date.now() < giveUpAt) continue;
|
|
87
|
+
log.warn("Code call gave up on identity issuance", { tool: name, error: error.message });
|
|
88
|
+
throw new Error(
|
|
89
|
+
context.locale?.startsWith("de")
|
|
90
|
+
? "Der Identitätsdienst hat nicht rechtzeitig geantwortet. Prüfe, ob dieser Aufruf schon gestartet ist, bevor du ihn erneut ausführst."
|
|
91
|
+
: "The identity service did not respond in time. Check whether this call already started before running it again.",
|
|
92
|
+
{ cause: error },
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
// Polls run every 250 ms for as long as the code runs. Reuse both tokens
|
|
98
|
+
// while they stay valid for at least 10 s, so a long run or an approval
|
|
99
|
+
// round trip does not depend on a fresh issuance for every poll.
|
|
100
|
+
const usable = (token: Awaited<ReturnType<typeof signInvocationToken>> | undefined) =>
|
|
101
|
+
token && token.claims.exp * 1000 - Date.now() >= 10_000 ? token : undefined;
|
|
102
|
+
let signed: Awaited<ReturnType<typeof signInvocationToken>> | undefined;
|
|
52
103
|
let callback: Awaited<ReturnType<typeof signInvocationToken>> | undefined;
|
|
53
104
|
const request = async (decision?: { id: string; approved: boolean }) => {
|
|
54
105
|
context.signal.throwIfAborted();
|
|
55
106
|
await authorizeCodeExecution(context.conversationId!, context.turnId!, actor.user.id);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
authority: {
|
|
65
|
-
sub: actor.user.id,
|
|
66
|
-
principal_type: "user",
|
|
67
|
-
access_subject_type: "user",
|
|
68
|
-
access_subject_id: actor.user.id,
|
|
69
|
-
credential_kind: "session",
|
|
70
|
-
scopes: [],
|
|
71
|
-
},
|
|
72
|
-
signer,
|
|
73
|
-
issuer: signer.issuer,
|
|
74
|
-
}),
|
|
75
|
-
{ signal: context.signal, timeoutMs: 5000 },
|
|
76
|
-
);
|
|
77
|
-
if (!callback || callback.claims.exp * 1000 - Date.now() < 10_000) {
|
|
78
|
-
callback = await withActiveIdentitySigner(
|
|
79
|
-
"invocation",
|
|
80
|
-
(signer) =>
|
|
81
|
-
signInvocationToken({
|
|
82
|
-
targetAppId: "core",
|
|
83
|
-
callingAppId: "assistant",
|
|
84
|
-
operation: codeCapabilityOperation(context.conversationId!, context.turnId!),
|
|
85
|
-
schemaHash: null,
|
|
86
|
-
authority: {
|
|
87
|
-
sub: actor.user.id,
|
|
88
|
-
principal_type: "user",
|
|
89
|
-
access_subject_type: "user",
|
|
90
|
-
access_subject_id: actor.user.id,
|
|
91
|
-
credential_kind: "session",
|
|
92
|
-
scopes: [],
|
|
93
|
-
},
|
|
94
|
-
signer,
|
|
95
|
-
issuer: signer.issuer,
|
|
96
|
-
}),
|
|
97
|
-
{ signal: context.signal, timeoutMs: 5000 },
|
|
98
|
-
);
|
|
99
|
-
}
|
|
107
|
+
signed = usable(signed) ?? (await issue({ targetAppId: "assistant", callingAppId: "core", operation: `tool:${name}` }));
|
|
108
|
+
callback =
|
|
109
|
+
usable(callback) ??
|
|
110
|
+
(await issue({
|
|
111
|
+
targetAppId: "core",
|
|
112
|
+
callingAppId: "assistant",
|
|
113
|
+
operation: codeCapabilityOperation(context.conversationId!, context.turnId!),
|
|
114
|
+
}));
|
|
100
115
|
const headers = new Headers({ authorization: `Bearer ${signed.token}`, "content-type": "application/json" });
|
|
101
116
|
headers.set(CODE_CAPABILITY_TOKEN_HEADER, callback.token);
|
|
102
117
|
if (context.locale) headers.set(LOCALE_HEADER, context.locale);
|