@k2b/cloud 0.7.0 → 0.9.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.
Files changed (190) hide show
  1. package/package.json +5 -4
  2. package/scripts/README.md +10 -0
  3. package/scripts/build.ts +14 -1
  4. package/scripts/preload.ts +7 -0
  5. package/scripts/sync-recovery-smoke.ts +21 -6
  6. package/src/_internal/build-metadata.ts +8 -1
  7. package/src/_internal/capabilities.ts +35 -12
  8. package/src/_internal/define-app.ts +28 -11
  9. package/src/_internal/page-responses.ts +1 -1
  10. package/src/_internal/readiness.ts +4 -1
  11. package/src/_internal/runtime-context.ts +1 -1
  12. package/src/access/PermissionEditor.tsx +15 -9
  13. package/src/access/PrincipalPicker.tsx +9 -3
  14. package/src/access/ResourceApiKeys.tsx +1 -1
  15. package/src/access/ui.ts +1 -2
  16. package/src/ai/admin.ts +2 -2
  17. package/src/ai/assistant-models.ts +1 -1
  18. package/src/ai/audio-tool.ts +1 -1
  19. package/src/ai/browser-code-contracts.ts +174 -52
  20. package/src/ai/browser.ts +4 -2
  21. package/src/ai/capabilities.ts +3 -3
  22. package/src/ai/chat/blocks.tsx +127 -50
  23. package/src/ai/chat/builtin-tools.tsx +2 -4
  24. package/src/ai/chat/capability-table.tsx +1 -1
  25. package/src/ai/chat/composer-adapter.ts +48 -13
  26. package/src/ai/chat/message-utils.ts +18 -5
  27. package/src/ai/chat/presentation.tsx +24 -11
  28. package/src/ai/chat/tool-disclosure.tsx +6 -2
  29. package/src/ai/chat/tool-groups.ts +8 -2
  30. package/src/ai/chat-quotas.ts +2 -2
  31. package/src/ai/chat-task-contracts.ts +9 -13
  32. package/src/ai/chat-tasks.ts +39 -18
  33. package/src/ai/client/controller.ts +87 -38
  34. package/src/ai/client/projection.ts +13 -6
  35. package/src/ai/client/transport.ts +4 -2
  36. package/src/ai/code-capability-routes.ts +22 -27
  37. package/src/ai/code-capability-transport.ts +2 -1
  38. package/src/ai/code-execution.ts +53 -0
  39. package/src/ai/code-mode-skill.ts +9 -5
  40. package/src/ai/code-runtime-tools.ts +16 -3
  41. package/src/ai/code-source-contracts.ts +100 -32
  42. package/src/ai/code-source-tools.ts +49 -6
  43. package/src/ai/compaction.ts +3 -3
  44. package/src/ai/default-tools.ts +87 -14
  45. package/src/ai/dictation-runtime.ts +1 -1
  46. package/src/ai/draft-content.ts +27 -12
  47. package/src/ai/enrich.ts +1 -1
  48. package/src/ai/executor.ts +8 -12
  49. package/src/ai/fetch-file-tool.ts +2 -2
  50. package/src/ai/file-content-version.ts +6 -1
  51. package/src/ai/file-tools.ts +8 -2
  52. package/src/ai/files-store.ts +55 -17
  53. package/src/ai/grids-skill.ts +2 -0
  54. package/src/ai/http.ts +18 -7
  55. package/src/ai/index.ts +24 -25
  56. package/src/ai/inference-calls.ts +3 -4
  57. package/src/ai/live.ts +1 -1
  58. package/src/ai/markdown-pdf-tool.ts +2 -2
  59. package/src/ai/memories.ts +30 -27
  60. package/src/ai/memory-learning-runs.ts +1 -1
  61. package/src/ai/memory-learning.ts +5 -5
  62. package/src/ai/memory-tool.ts +1 -5
  63. package/src/ai/memory-workflow-evidence.ts +9 -4
  64. package/src/ai/message-queue.ts +23 -12
  65. package/src/ai/migrate.ts +6 -4
  66. package/src/ai/model-pricing.ts +4 -4
  67. package/src/ai/pdf-render-worker.ts +3 -2
  68. package/src/ai/projects-routes.ts +15 -40
  69. package/src/ai/projects.ts +18 -13
  70. package/src/ai/quota-provider.ts +6 -5
  71. package/src/ai/quota-report.ts +5 -5
  72. package/src/ai/quotas.ts +3 -3
  73. package/src/ai/routes.ts +60 -34
  74. package/src/ai/run-timeout.ts +3 -1
  75. package/src/ai/runtime-tools.ts +6 -7
  76. package/src/ai/runtime.ts +38 -20
  77. package/src/ai/settings.ts +1 -1
  78. package/src/ai/short-id.ts +4 -5
  79. package/src/ai/skill-search.ts +21 -10
  80. package/src/ai/skill-seeds.ts +16 -7
  81. package/src/ai/skill-tool.ts +6 -1
  82. package/src/ai/skills-routes.ts +32 -15
  83. package/src/ai/skills.ts +54 -28
  84. package/src/ai/store.ts +80 -47
  85. package/src/ai/structured.ts +6 -2
  86. package/src/ai/system-prompt.ts +7 -5
  87. package/src/ai/task-contracts.ts +84 -0
  88. package/src/ai/task-execution.ts +92 -0
  89. package/src/ai/todo-contracts.ts +24 -12
  90. package/src/ai/todo-tool.ts +16 -13
  91. package/src/ai/tools.ts +1 -1
  92. package/src/ai/transcription.ts +4 -4
  93. package/src/ai/turn-timing.ts +1 -1
  94. package/src/ai/ui.tsx +2 -1
  95. package/src/ai/usage.ts +1 -1
  96. package/src/ai/vision-tool.ts +38 -17
  97. package/src/api/admin-ai-quotas.ts +14 -8
  98. package/src/api/admin-ai-skills.ts +3 -3
  99. package/src/api/admin-linux-identities.ts +10 -4
  100. package/src/api/admin-rail.ts +2 -2
  101. package/src/api/app-approval.ts +2 -2
  102. package/src/api/auth/schemas.ts +7 -1
  103. package/src/api/auth.ts +4 -3
  104. package/src/api/capabilities.ts +45 -13
  105. package/src/api/index.ts +5 -5
  106. package/src/api/mcp.ts +5 -12
  107. package/src/browser/CloudResourceSearch.tsx +1 -1
  108. package/src/browser/command-bridge.ts +1 -1
  109. package/src/browser/command-shortcuts.ts +1 -1
  110. package/src/browser/commands.ts +8 -7
  111. package/src/browser/testing.ts +2 -1
  112. package/src/capabilities/client.ts +1 -1
  113. package/src/capabilities/command-link.ts +1 -1
  114. package/src/capabilities/server.ts +2 -1
  115. package/src/cli/admin/ai-quotas.ts +5 -5
  116. package/src/cli/admin/app-credentials.ts +7 -9
  117. package/src/cli/admin/index.ts +3 -3
  118. package/src/cli/admin/instance.ts +2 -2
  119. package/src/cli/admin/linux.ts +4 -2
  120. package/src/cli/admin/sync.ts +1 -1
  121. package/src/cli/capabilities.ts +42 -29
  122. package/src/config/define-env.ts +97 -0
  123. package/src/config/env.ts +147 -54
  124. package/src/config/index.ts +3 -1
  125. package/src/contracts/capabilities.ts +8 -1
  126. package/src/contracts/index.ts +5 -7
  127. package/src/contracts/registry.ts +2 -0
  128. package/src/contracts/shared.ts +12 -3
  129. package/src/desktop/index.ts +1 -0
  130. package/src/desktop/solid.tsx +1 -1
  131. package/src/index.ts +8 -2
  132. package/src/server/app-assets.ts +27 -0
  133. package/src/server/index.ts +3 -2
  134. package/src/server/ratelimit.ts +1 -1
  135. package/src/server/services/access-revision.ts +3 -3
  136. package/src/server/services/access.ts +3 -1
  137. package/src/services/accounts/notification-sender.ts +12 -2
  138. package/src/services/announcements/index.ts +4 -4
  139. package/src/services/app-approval.ts +11 -10
  140. package/src/services/auth-flows/magic-link.ts +13 -1
  141. package/src/services/identity/key-config.ts +6 -4
  142. package/src/services/identity/runtime-config.ts +6 -5
  143. package/src/services/ipa/posix.ts +1 -1
  144. package/src/services/ipa/sync.ts +1 -1
  145. package/src/services/logging/index.ts +1 -1
  146. package/src/services/logging/trace.ts +1 -1
  147. package/src/services/mandates/index.ts +5 -2
  148. package/src/services/mandates/policy.ts +106 -6
  149. package/src/services/notifications/batches.ts +1 -0
  150. package/src/services/pdf/gotenberg.ts +50 -14
  151. package/src/services/pdf/index.ts +11 -11
  152. package/src/services/public-http.ts +1 -1
  153. package/src/services/rail-shortcuts.ts +1 -1
  154. package/src/services/rail-snapshot.ts +1 -1
  155. package/src/services/request-cache-redis.ts +2 -1
  156. package/src/services/session/user.ts +1 -1
  157. package/src/services/settings/app.ts +5 -1
  158. package/src/services/settings/core-settings.ts +6 -8
  159. package/src/services/settings/index.ts +3 -3
  160. package/src/services/settings/store.ts +3 -3
  161. package/src/services/webauthn.ts +1 -1
  162. package/src/shared/index.ts +3 -6
  163. package/src/shared/markdown/extensions/info-blocks.ts +1 -1
  164. package/src/ssr/AdminSidebar.tsx +2 -2
  165. package/src/ssr/AppLaunchpad.island.tsx +12 -2
  166. package/src/ssr/AppLaunchpadPanel.tsx +4 -4
  167. package/src/ssr/GlobalSearchDialog.tsx +6 -6
  168. package/src/ssr/GlobalSearchTrigger.island.tsx +2 -2
  169. package/src/ssr/Layout.tsx +4 -3
  170. package/src/ssr/LayoutHeader.tsx +1 -1
  171. package/src/ssr/LayoutHelp.tsx +4 -6
  172. package/src/ssr/LayoutHelpTrigger.island.tsx +2 -2
  173. package/src/ssr/LayoutRail.tsx +3 -2
  174. package/src/ssr/MinimalLayout.tsx +1 -1
  175. package/src/ssr/MobileNavigation.tsx +2 -2
  176. package/src/ssr/ProfilePreferences.island.tsx +1 -1
  177. package/src/ssr/RailApps.island.tsx +7 -7
  178. package/src/ssr/admin-navigation.ts +6 -2
  179. package/src/ssr/index.ts +1 -0
  180. package/src/ssr/islands/index.ts +2 -2
  181. package/src/ssr/mobile-menu-history.ts +1 -1
  182. package/src/ssr/rail-navigation.ts +3 -2
  183. package/src/ssr/request-path.ts +12 -0
  184. package/src/workflows/ai/index.ts +2 -2
  185. package/src/workflows/ai/runtime.ts +16 -91
  186. package/src/workflows/ai/store.ts +1 -1
  187. package/src/workflows/ai/types.ts +6 -82
  188. package/src/workflows/store/index.ts +2 -2
  189. package/src/server/services/freeipa/test-certificates.ts +0 -44
  190. package/src/services/session/test-fixture.ts +0 -18
@@ -27,8 +27,11 @@ export const emptyProjection = (conversation: AiConversation | null = null): AiC
27
27
  export const messagesWithPendingSend = (messages: AiStoredMessage[], pending?: AiStoredMessage): AiStoredMessage[] => {
28
28
  if (!pending) return messages;
29
29
  const revision = pending.meta?.submittedDraftRevision;
30
- const confirmed = messages.some(message => message.id === pending.id ||
31
- (revision !== undefined && message.message.role === "user" && message.meta?.submittedDraftRevision === revision));
30
+ const confirmed = messages.some(
31
+ (message) =>
32
+ message.id === pending.id ||
33
+ (revision !== undefined && message.message.role === "user" && message.meta?.submittedDraftRevision === revision),
34
+ );
32
35
  return confirmed ? messages : [...messages, pending];
33
36
  };
34
37
 
@@ -84,9 +87,9 @@ export const mergeActiveTurn = (previous: AiActiveTurn | null, incoming: AiActiv
84
87
  if (previous.attempt !== incoming.attempt) return incoming.attempt > previous.attempt ? incoming : previous;
85
88
  if (incoming.seq < previous.seq) return previous;
86
89
  // Locally submitted steering may not have reached the snapshot yet.
87
- const pendingSteers = previous.blocks.filter(block => block.kind === "steer_message" && block.status !== "consumed");
88
- const known = new Set(incoming.blocks.map(block => block.id));
89
- const blocks = [...incoming.blocks, ...pendingSteers.filter(block => !known.has(block.id))];
90
+ const pendingSteers = previous.blocks.filter((block) => block.kind === "steer_message" && block.status !== "consumed");
91
+ const known = new Set(incoming.blocks.map((block) => block.id));
92
+ const blocks = [...incoming.blocks, ...pendingSteers.filter((block) => !known.has(block.id))];
90
93
  return { ...incoming, blocks, status: deriveStatus(blocks) };
91
94
  };
92
95
 
@@ -171,7 +174,11 @@ export const reduceWireEvent = (state: AiChatProjection, event: AiWireEvent): Ai
171
174
 
172
175
  if (event.type === "message_saved") {
173
176
  if (!active || active.turnId !== event.turnId || !isNewerWireEvent(event, active)) return state;
174
- return { ...state, messages: mergeMessages(state.messages, [event.message]), activeTurn: { ...active, seq: event.seq, attempt: event.attempt } };
177
+ return {
178
+ ...state,
179
+ messages: mergeMessages(state.messages, [event.message]),
180
+ activeTurn: { ...active, seq: event.seq, attempt: event.attempt },
181
+ };
175
182
  }
176
183
 
177
184
  // block_set / block_delta
@@ -21,8 +21,10 @@ const CONNECT_TIMEOUT_MS = 10_000;
21
21
  export async function* parseAiSse(response: Response, signal: AbortSignal): AsyncGenerator<AiStreamEvent> {
22
22
  const reader = response.body?.getReader();
23
23
  if (!reader) return;
24
- const cancel = () => { void reader.cancel().catch(() => undefined); };
25
- signal.addEventListener("abort", cancel, {once:true});
24
+ const cancel = () => {
25
+ void reader.cancel().catch(() => undefined);
26
+ };
27
+ signal.addEventListener("abort", cancel, { once: true });
26
28
  if (signal.aborted) cancel();
27
29
  const decoder = new TextDecoder();
28
30
  let buffer = "";
@@ -6,7 +6,9 @@ import { dispatchCapabilityStream } from "../api/capability-streams";
6
6
  import { CAPABILITY_MAX_REQUEST_BYTES } from "../contracts/capabilities";
7
7
  import { type AuthContext, getLocale, type RequestAuthority } from "../server";
8
8
  import { requireInvocation } from "../server/middleware/invocation";
9
+ import { getMandate } from "../services/mandates";
9
10
  import { codeCapabilityOperation } from "./code-capability-transport";
11
+ import { authorizeCodeExecution } from "./code-execution";
10
12
  import { aiConversations } from "./store";
11
13
 
12
14
  const Input = z
@@ -15,19 +17,21 @@ const Input = z
15
17
  .refine((value) => Object.hasOwn(value, "input"));
16
18
  const denied = () => Response.json({ code: "ACCESS_DENIED", message: "Code execution is no longer authorized" }, { status: 403 });
17
19
 
18
- /** Core-only continuation of a foreground code turn, not a general invocation exchange. */
20
+ /** Core-only continuation of a code turn, not a general invocation exchange. */
19
21
  export function createCodeCapabilityRoutes(
20
22
  dependencies: {
21
23
  invocation?: Parameters<typeof requireInvocation>[1];
22
- store?: Pick<typeof aiConversations, "getConversation" | "getActiveTurn" | "getTurnRunConfig">;
24
+ store?: Pick<typeof aiConversations, "getConversation" | "getTurn" | "getActiveTurn" | "getTurnRunConfig">;
25
+ getMandate?: typeof getMandate;
23
26
  dispatch?: typeof dispatchCapability;
24
27
  stream?: typeof dispatchCapabilityStream;
25
28
  } = {},
26
29
  ) {
27
30
  const store = dependencies.store ?? aiConversations;
28
- // This Core-only exchange resumes the verified foreground user turn. Ordinary
31
+ // This Core-only exchange resumes the verified turn; background calls also pass
32
+ // their persisted mandate to the dispatcher. Ordinary
29
33
  // app invocations remain non-exchangeable; this is not an interactive cookie.
30
- const foregroundTurnAuthority = (c: Context<AuthContext>): RequestAuthority => ({
34
+ const turnAuthority = (c: Context<AuthContext>): RequestAuthority => ({
31
35
  actor: c.get("actor"),
32
36
  accessSubject: c.get("accessSubject"),
33
37
  credentialKind: "session",
@@ -41,11 +45,14 @@ export function createCodeCapabilityRoutes(
41
45
  const appId = c.req.param("appId")!;
42
46
  const capabilityId = c.req.param("capabilityId")!;
43
47
  if (conversation?.allowedTools && !conversation.allowedTools.includes(`${appId}.${capabilityId}`)) return denied();
48
+ const config = await store.getTurnRunConfig({ conversationId: c.req.param("conversationId")!, turnId: c.req.param("turnId")! });
49
+ if (!config || config.kind === "compact") return denied();
44
50
  return (dependencies.dispatch ?? dispatchCapability)({
45
51
  request: c.req.raw,
46
- authority: foregroundTurnAuthority(c),
52
+ authority: turnAuthority(c),
47
53
  origin: "assistant",
48
54
  continuation: codeCapabilityOperation(c.req.param("conversationId")!, c.req.param("turnId")!),
55
+ mandate: config.mandate ? { mandateId: config.mandate.id, mandateRevision: config.mandate.revision, ownerAppId: "core" } : undefined,
49
56
  review,
50
57
  kind: review || c.req.param("kind") === "actions" ? "actions" : "queries",
51
58
  appId,
@@ -75,36 +82,24 @@ export function createCodeCapabilityRoutes(
75
82
  return denied();
76
83
  const conversationId = c.req.param("conversationId")!;
77
84
  const turnId = c.req.param("turnId")!;
78
- const [conversation, active, config] = await Promise.all([
79
- store.getConversation({ conversationId, ownerUserId: actor.user.id }),
80
- store.getActiveTurn({ conversationId }),
81
- store.getTurnRunConfig({ conversationId, turnId }),
82
- ]);
83
- if (
84
- !conversation ||
85
- conversation.createdByUserId !== actor.user.id ||
86
- conversation.archivedAt ||
87
- !active ||
88
- active.turn.id !== turnId ||
89
- active.turn.cancelRequestedAt ||
90
- !["running", "waiting_for_action"].includes(active.turn.status) ||
91
- !config ||
92
- config.kind === "compact" ||
93
- config.background ||
94
- config.mandate
95
- )
85
+ try {
86
+ await authorizeCodeExecution(conversationId, turnId, actor.user.id, { store, getMandate: dependencies.getMandate ?? getMandate });
87
+ } catch {
96
88
  return denied();
89
+ }
97
90
  await next();
98
91
  })
99
92
  .post("/:conversationId/:turnId/capabilities/v1/actions/:appId/:capabilityId/review", (c) => invoke(c, true))
100
93
  .post("/:conversationId/:turnId/capabilities/v1/:kind{queries|actions}/:appId/:capabilityId", (c) => invoke(c))
101
- .post("/:conversationId/:turnId/capabilities/v1/streams/:verb", (c) =>
102
- (dependencies.stream ?? dispatchCapabilityStream)(c.req.raw, foregroundTurnAuthority(c), c.req.param("verb"), {
94
+ .post("/:conversationId/:turnId/capabilities/v1/streams/:verb", async (c) => {
95
+ const config = await store.getTurnRunConfig({ conversationId: c.req.param("conversationId")!, turnId: c.req.param("turnId")! });
96
+ if (!config || config.kind === "compact" || config.mandate) return denied();
97
+ return (dependencies.stream ?? dispatchCapabilityStream)(c.req.raw, turnAuthority(c), c.req.param("verb"), {
103
98
  continuation: codeCapabilityOperation(c.req.param("conversationId")!, c.req.param("turnId")!),
104
99
  allow: async ({ appId, capabilityId }) => {
105
100
  const conversation = await store.getConversation({ conversationId: c.req.param("conversationId")! });
106
101
  return !!conversation && (!conversation.allowedTools || conversation.allowedTools.includes(`${appId}.${capabilityId}`));
107
102
  },
108
- }),
109
- );
103
+ });
104
+ });
110
105
  }
@@ -1,4 +1,5 @@
1
1
  import type { CapabilityCaller } from "../capabilities/server";
2
+ import { env } from "../config/env";
2
3
 
3
4
  export const CODE_CAPABILITY_TOKEN_HEADER = "x-cloud-code-capability-token";
4
5
  export const codeCapabilityOperation = (conversationId: string, turnId: string) => `code-capabilities:${conversationId}:${turnId}`;
@@ -8,7 +9,7 @@ export const codeCapabilityPath = (conversationId: string, turnId: string) => `/
8
9
  export function createCodeCapabilityTransport(
9
10
  current: () => { conversationId: string; turnId: string; token: string },
10
11
  ): NonNullable<CapabilityCaller["transport"]> {
11
- const configured = process.env.CLOUD_CORE_INTERNAL_ORIGIN;
12
+ const configured = env.CLOUD_CORE_INTERNAL_ORIGIN;
12
13
  if (!configured) throw new Error("CLOUD_CORE_INTERNAL_ORIGIN is required for the code host");
13
14
  const origin = new URL(configured);
14
15
  if (!["http:", "https:"].includes(origin.protocol)) throw new Error("Invalid Core origin");
@@ -0,0 +1,53 @@
1
+ import { getMandate } from "../services/mandates";
2
+ import { aiConversations } from "./store";
3
+
4
+ /** Trusted run context. Never accept a mandate or background flag from sandbox input. */
5
+ export async function authorizeCodeExecution(
6
+ conversationId: string,
7
+ turnId: string,
8
+ userId: string,
9
+ dependencies: {
10
+ store: Pick<typeof aiConversations, "getConversation" | "getTurn" | "getActiveTurn" | "getTurnRunConfig">;
11
+ getMandate: typeof getMandate;
12
+ } = { store: aiConversations, getMandate },
13
+ ) {
14
+ const { store } = dependencies;
15
+ const [conversation, turn, config] = await Promise.all([
16
+ store.getConversation({ conversationId, ownerUserId: userId }),
17
+ store.getTurn({ conversationId, turnId }),
18
+ store.getTurnRunConfig({ conversationId, turnId }),
19
+ ]);
20
+ if (
21
+ !conversation ||
22
+ conversation.createdByUserId !== userId ||
23
+ conversation.archivedAt ||
24
+ !turn ||
25
+ turn.cancelRequestedAt ||
26
+ !["running", "waiting_for_action"].includes(turn.status) ||
27
+ !config ||
28
+ config.kind === "compact" ||
29
+ Boolean(config.background) !== Boolean(config.mandate)
30
+ )
31
+ throw new Error("Code execution is no longer authorized");
32
+ if (config.background && config.mandate) {
33
+ // chat-tasks persists the mandate binding; background.taskId is a public short ID,
34
+ // whereas mandate.workloadId is the internal task UUID.
35
+ const mandate = await dependencies.getMandate(config.mandate.id);
36
+ if (
37
+ !mandate ||
38
+ mandate.ownerAppId !== "core" ||
39
+ mandate.workloadType !== "ai.chat-task" ||
40
+ mandate.subject.type !== "user" ||
41
+ mandate.subject.id !== userId ||
42
+ mandate.state !== "active" ||
43
+ mandate.revision !== config.mandate.revision ||
44
+ !mandate.confirmedAt ||
45
+ (mandate.expiresAt && Date.parse(mandate.expiresAt) <= Date.now())
46
+ )
47
+ throw new Error("Background task authority changed. Update the task in the normal chat.");
48
+ } else {
49
+ const active = await store.getActiveTurn({ conversationId });
50
+ if (active?.turn.id !== turnId) throw new Error("The code host's conversation turn is no longer active");
51
+ }
52
+ return { conversation, turn, config };
53
+ }
@@ -4,16 +4,20 @@ import type { AiSkillTemplate } from "./skills";
4
4
 
5
5
  export const ASSISTANT_CODE_MODE_SKILL = {
6
6
  "key": "assistant:code-mode",
7
- "version": 47,
7
+ "version": 50,
8
8
  "name": "assistant-code-mode",
9
9
  "description": "Inspect and transform unfamiliar data, analyze files, compare results across Cloud apps, or build and improve interactive and agent-only Apps in Assistant Studio. Use for quick code experiments, data analysis, file generation, resource SQL queries and combining discovered Cloud capabilities. For plain arithmetic or date offsets, answer directly or use calculate.",
10
- "instructions": "# Assistant code mode\n\nChoose the smallest useful result: one-off answer, exported file, or reusable\nStudio App. Apps may expose agent actions, a display-only dashboard, or both.\nPersistence is optional. One-off scripts stay in their chat and cannot be shared. Reuse an\nexisting Cloud feature when it fits. For a\nquick reading of an uploaded PDF or Office document, `read_file` can return\nMarkdown; use code for exact cells, calculations, original PDF text or positions.\n\n## Start from the contract\n\nLoad the needed `code_*` tools individually through `load_tools` and read their\ninput schemas. They are Assistant tools, not capabilities or functions inside\ncode. Discover other Cloud operations before using `capabilities.run`.\n\nRuntime namespaces are globals: no imports or package installation are needed.\nOnly relative imports of the resource's own source files are supported. There is\nno DOM or native network access. Before using a namespace, read its reference\nbelow for signatures, options and return values. Do not invent methods or infer\nan API from a familiar library. For discovered Cloud capabilities and external\nAPIs, obtain their actual contracts separately.\n\nInspect supplied data before joining, filtering or calculating: column names,\ntypes, units, date ranges and missing values. Ask only for decisions or inputs\nthat cannot be established from available evidence. For several real steps,\nkeep a short `todo_write` plan and update it as work changes; skip ceremony for a\nsmall experiment. A failed experiment should change the next hypothesis.\n\n## First file script\n\nPass exact current-chat manifest paths as `code_run.inputPaths`, and this entry\nas `code_run.code` for a small CSV:\n\n```js\nexport default async () => {\n const [input] = await files.list();\n if (!input) throw new Error(\"Select a CSV input.\");\n const rows = await sheet.fromCsv(await files.read(input.name));\n return { rows: rows.length, columns: Object.keys(rows[0] ?? {}), sample: rows.slice(0, 3) };\n};\n```\n\n`input.name` is the full path, such as `/sales.csv`; pass it unchanged to\n`files.read`, which returns a `File`. CSV rows are objects keyed by headers:\n`rows[0]` is already data. Do not drop it. For older Excel CSVs, use\n`sheet.fromCsv(file, {encoding:\"windows-1252\"})`. Inspect actual headings first.\nFor a tiny experiment without files, `export default () => ({answer:42})` suffices.\nEach run has fresh variables. No saved resource or UI is required.\n\n## Reference routing\n\nRead only the rows relevant to the task. Each link describes its own complete\nsupported surface; links within references add related workflows when needed.\n\n| Task / API | Read |\n| --- | --- |\n| Source entry, input/output files, pickers, CSV, IDs | [Runtime and files](/skills/assistant-code-mode/references/runtime.md) |\n| Inspect PDF pages, read PDF text/positions or XLSX/ODS cells | [Documents](/skills/assistant-code-mode/references/documents.md) |\n| Generate a PDF, embed attachments, combine invoice HTML and XML | [PDF generation](/skills/assistant-code-mode/references/pdf.md) |\n| Exact amounts, taxes, allocation, localized money | [Money](/skills/assistant-code-mode/references/money.md) |\n| Export DATEV bookings or SEPA transfers | [DATEV and SEPA](/skills/assistant-code-mode/references/finance.md) |\n| Parse a CAMT bank report | [Bank reports](/skills/assistant-code-mode/references/camt.md) |\n| Calculate, create or read electronic invoices/XML/PDF attachments | [Electronic invoices](/skills/assistant-code-mode/references/einvoice.md) |\n| Controls, layouts and dialogs | [UI and dialogs](/skills/assistant-code-mode/references/ui.md), [Analytics UI](/skills/assistant-code-mode/references/analytics.md) |\n| Chart types, series and axes | [Charts](/skills/assistant-code-mode/references/charts.md) |\n| Long processing, progress, cancellation | [Background work](/skills/assistant-code-mode/references/work.md) |\n| Persist JSON or files locally/shared | [Storage](/skills/assistant-code-mode/references/storage.md) |\n| Copy files between chats, Projects and Apps | [File transfers](/skills/assistant-code-mode/references/files.md) |\n| Resource SQL, schema, row CRUD, imports | [Database](/skills/assistant-code-mode/references/database.md) |\n| Discovered Cloud queries/actions | [Capability calls](/skills/assistant-code-mode/references/capabilities.md) |\n| External HTTPS and personal secrets | [HTTP and secrets](/skills/assistant-code-mode/references/http.md) |\n| Call a published App action; declare handlers | [App actions](/skills/assistant-code-mode/references/app-actions.md) |\n| Reuse work across chats, create or edit an App | [Source workflow](/skills/assistant-code-mode/references/source-workflow.md) |\n| Publish, restore, copy | [Publishing](/skills/assistant-code-mode/references/publishing.md) |\n| Find recipients or change App/Skill sharing | [Access](/skills/assistant-code-mode/references/access.md) |\n| Inspect, export, clear server data, or delete an App | [Management](/skills/assistant-code-mode/references/management.md) |\n| Execute, inspect, interact, export, stop, diagnose errors | [Run and debug](/skills/assistant-code-mode/references/debugging.md) |\n| Unfamiliar inputs or cross-app investigation | [Investigation](/skills/assistant-code-mode/references/investigation.md) |\n| Complete app starters | [Examples](/skills/assistant-code-mode/references/examples.md) |\n\nFor a new app, read Source workflow and the closest complete example before\nwriting source, plus only the API references it uses. For analytical reports or\ndashboards, also load `assistant-data-analysis` for metrics and source validation.\n\n## Choose the delivery\n\nFor a one-off chart, calculator, or interactive analysis in this conversation,\nuse `code_run({code,inputPaths})`, test the controls, then\n`code_present({runId,title})`. Read [Chat visualizations](/skills/assistant-code-mode/references/chat.md).\nA successful run is visible to the agent only; present it before saying the\nuser can see it. No saved App or chat file is necessary.\n\nUse a Studio App when the user needs an independently accessible, reusable\napplication. Use `files.save`, `code_export`, and `present` when the requested\nresult is a file. These are separate delivery choices.\n\n## Verify and deliver\n\nRun the actual source (the saved revision for Apps) and test relevant controls with IDs returned by\n`code_run`/`code_interact`, including invalid inputs and picker fixtures. Creating,\ncompiling or saving source does not verify behavior. If `work.status` is\n`running`, wait with `code_inspect({runId,waitMs:30000})`; do not restart the job.\nInspect only when the returned snapshot needs more detail. Errors and\n`outputTruncated` are not successful complete results.\n\nFor a CSV, call `await files.save(sheet.toCsv(rows), \"result.csv\")` inside code.\nThen call the **tool** `code_export` with the returned `runId` and captured file\nname, and `present` its returned chat path. `files.save` returns no path.\nReuse exported data via its path/version rather than retyping truncated output.\nReconcile row counts, exclusions and totals before reporting findings.\n\nOpen GUI apps with `code_open`. Saving or testing does\nnot replace a user's already-running app. Stop runs no longer needed that retain\nUI, jobs or output files. Never claim an unexecuted result is verified.\n\nAgent execution runs independently of the user's tab. Agent local storage is\ntemporary; shared storage, database writes and external actions are real, even\nin tests. Cancellation and source restore do not undo them. Apps select local\nfiles explicitly; they never gain implicit access to chat attachments. Use\n`code_secret` for credentials, never chat or app controls. Honor normal access\nand approval decisions; availability is not authorization for unrelated actions.",
10
+ "instructions": "# Assistant code mode\n\nChoose the smallest useful result: one-off answer, exported file, or reusable\nStudio App. Apps may expose agent actions, a display-only dashboard, or both.\nPersistence is optional. One-off scripts stay in their chat and cannot be shared. Reuse an\nexisting Cloud feature when it fits. For a\nquick reading of an uploaded PDF or Office document, `read_file` can return\nMarkdown; use code for exact cells, calculations, original PDF text or positions.\n\n## Start from the contract\n\nLoad the needed `code_*` tools individually through `load_tools` and read their\ninput schemas. They are Assistant tools, not capabilities or functions inside\ncode. Discover other Cloud operations before using `capabilities.run`.\n\nRuntime namespaces are globals: no imports or package installation are needed.\nOnly relative imports of the resource's own source files are supported. There is\nno DOM or native network access. Before using a namespace, read its reference\nbelow for signatures, options and return values. Do not invent methods or infer\nan API from a familiar library. For discovered Cloud capabilities and external\nAPIs, obtain their actual contracts separately.\n\nInspect supplied data before joining, filtering or calculating: column names,\ntypes, units, date ranges and missing values. Ask only for decisions or inputs\nthat cannot be established from available evidence. For several real steps,\nkeep a short `todo_write` plan and update it as work changes; skip ceremony for a\nsmall experiment. A failed experiment should change the next hypothesis.\n\n## First file script\n\nPass exact current-chat manifest paths as `code_run.inputPaths`, and this entry\nas `code_run.code` for a small CSV:\n\n```js\nexport default async () => {\n const [input] = await files.list();\n if (!input) throw new Error(\"Select a CSV input.\");\n const rows = await sheet.fromCsv(await files.read(input.name));\n return { rows: rows.length, columns: Object.keys(rows[0] ?? {}), sample: rows.slice(0, 3) };\n};\n```\n\n`input.name` is the full path, such as `/sales.csv`; pass it unchanged to\n`files.read`, which returns a `File`. CSV rows are objects keyed by headers:\n`rows[0]` is already data. Do not drop it. For older Excel CSVs, use\n`sheet.fromCsv(file, {encoding:\"windows-1252\"})`. Inspect actual headings first.\nFor a tiny experiment without files, `export default () => ({answer:42})` suffices.\nEach run has fresh variables. No saved resource or UI is required.\n\n## Reference routing\n\nRead only the rows relevant to the task. Each link describes its own complete\nsupported surface; links within references add related workflows when needed.\n\n| Task / API | Read |\n| --- | --- |\n| Source entry, input/output files, pickers, CSV, IDs | [Runtime and files](/skills/assistant-code-mode/references/runtime.md) |\n| Inspect PDF pages, read PDF text/positions or XLSX/ODS cells | [Documents](/skills/assistant-code-mode/references/documents.md) |\n| Generate a PDF, embed attachments, combine invoice HTML and XML | [PDF generation](/skills/assistant-code-mode/references/pdf.md) |\n| Exact amounts, taxes, allocation, localized money | [Money](/skills/assistant-code-mode/references/money.md) |\n| Export DATEV bookings or SEPA transfers | [DATEV and SEPA](/skills/assistant-code-mode/references/finance.md) |\n| Parse a CAMT bank report | [Bank reports](/skills/assistant-code-mode/references/camt.md) |\n| Calculate, create or read electronic invoices/XML/PDF attachments | [Electronic invoices](/skills/assistant-code-mode/references/einvoice.md) |\n| Controls, layouts and dialogs | [UI and dialogs](/skills/assistant-code-mode/references/ui.md), [Analytics UI](/skills/assistant-code-mode/references/analytics.md) |\n| Chart types, series and axes | [Charts](/skills/assistant-code-mode/references/charts.md) |\n| Long processing, progress, cancellation | [Background work](/skills/assistant-code-mode/references/work.md) |\n| Persist JSON or files locally/shared | [Storage](/skills/assistant-code-mode/references/storage.md) |\n| Copy files between stores; list and download Filesv2 beside Grids documents | [File transfers](/skills/assistant-code-mode/references/files.md) |\n| Resource SQL, schema, row CRUD, imports | [Database](/skills/assistant-code-mode/references/database.md) |\n| Generate text, classify data or extract structured fields | [AI calculations](/skills/assistant-code-mode/references/ai.md) |\n| Discovered Cloud queries/actions | [Capability calls](/skills/assistant-code-mode/references/capabilities.md) |\n| External HTTPS and personal secrets | [HTTP and secrets](/skills/assistant-code-mode/references/http.md) |\n| Call a published App action; declare handlers | [App actions](/skills/assistant-code-mode/references/app-actions.md) |\n| Reuse work across chats, create or edit an App | [Source workflow](/skills/assistant-code-mode/references/source-workflow.md) |\n| Publish, restore, copy | [Publishing](/skills/assistant-code-mode/references/publishing.md) |\n| Find recipients or change App/Skill sharing | [Access](/skills/assistant-code-mode/references/access.md) |\n| Inspect, export, clear server data, or delete an App | [Management](/skills/assistant-code-mode/references/management.md) |\n| Execute, inspect, interact, export, stop, diagnose errors | [Run and debug](/skills/assistant-code-mode/references/debugging.md) |\n| Unfamiliar inputs or cross-app investigation | [Investigation](/skills/assistant-code-mode/references/investigation.md) |\n| Complete app starters | [Examples](/skills/assistant-code-mode/references/examples.md) |\n\nFor a new app, read Source workflow and the closest complete example before\nwriting source, plus only the API references it uses. For analytical reports or\ndashboards, also load `assistant-data-analysis` for metrics and source validation.\n\n## Choose the delivery\n\nFor a one-off chart, calculator, or interactive analysis in this conversation,\nuse `code_run({code,inputPaths})`, test the controls, then\n`code_present({runId,title})`. Read [Chat visualizations](/skills/assistant-code-mode/references/chat.md).\nA successful run is visible to the agent only; present it before saying the\nuser can see it. No saved App or chat file is necessary.\n\nUse a Studio App when the user needs an independently accessible, reusable\napplication. Use `files.save`, `code_export`, and `present` when the requested\nresult is a file. These are separate delivery choices.\n\n## Verify and deliver\n\nRun the actual source (the saved revision for Apps) and test relevant controls with IDs returned by\n`code_run`/`code_interact`, including invalid inputs and picker fixtures. Creating,\ncompiling or saving source does not verify behavior. If `work.status` is\n`running`, wait with `code_inspect({runId,waitMs:30000})`; do not restart the job.\nInspect only when the returned snapshot needs more detail. Errors and\n`outputTruncated` are not successful complete results.\n\nFor a CSV, call `await files.save(sheet.toCsv(rows), \"result.csv\")` inside code.\nThen call the **tool** `code_export` with the returned `runId` and captured file\nname, and `present` its returned chat path. `files.save` returns no path.\nReuse exported data via its path/version rather than retyping truncated output.\nReconcile row counts, exclusions and totals before reporting findings.\n\nOpen GUI apps with `code_open`. Saving or testing does\nnot replace a user's already-running app. Stop runs no longer needed that retain\nUI, jobs or output files. Never claim an unexecuted result is verified.\n\nAgent execution runs independently of the user's tab. Agent local storage is\ntemporary; shared storage, database writes and external actions are real, even\nin tests. Cancellation and source restore do not undo them. Apps select local\nfiles explicitly; they never gain implicit access to chat attachments. Use\n`code_secret` for credentials, never chat or app controls. Honor normal access\nand approval decisions; availability is not authorization for unrelated actions.",
11
11
  "extraFrontmatter": {},
12
12
  "references": [
13
13
  {
14
14
  "path": "references/access.md",
15
15
  "content": "# Share an App or a linked Skill\n\nRead this reference only to inspect or change sharing. Calling a published\nApp action needs Use, not Manage, and does not need access-management tools.\n\n1. Find the App with `code_list` and load `code_access_read` and\n `code_access_change` through `load_tools`.\n2. If the recipient is unknown, discover `core.entities.search` and read its\n schema. Search by name, optionally restricted to `user` or `group`; follow\n its cursor when needed. Reuse the returned `principal` exactly. This search\n follows Accounts visibility; an absent result is not permission to guess IDs.\n3. Call `code_access_read({id})`. It requires Manage and returns current\n `grants`, `levels`, supported `principalTypes`, and `accessRevision`.\n4. Add one grant with `code_access_change({id, expectedAccessRevision,\n principal, permission:\"read\"})`. `read` means Use; `admin` means Manage.\n To change an existing grant, pass its `accessId` instead of `principal`.\n To revoke it, use that `accessId` with `permission:null`.\n5. The tool presents the exact App, recipient, and before/after permission for\n fresh user review. No `confirmed` flag or remembered approval is supported.\n Read grants again to verify the result. A conflict means the grants changed:\n inspect and prepare a new review. Do not retry an unknown mutation blindly.\n\nStudio Apps support users, groups, `{type:\"authenticated\"}` and\n`{type:\"public\"}`. Public only accepts `permission:\"read\"`; public Manage and\nservice-account grants are rejected. Never replace an unavailable recipient\nwith a broader one. The last manager cannot be removed.\nPublishing and sharing remain separate; Use executes only published source.\n\n## Skills are separate\n\nA Skill may explain when and how to invoke an App, but grants never propagate\nbetween them. For a requested reusable workflow, offer a Skill that references\nthe App ID and actions; load `skill-creator` only if creating or editing those\ninstructions is useful. Many Apps need no Skill, and many Skills need no code.\n\nFor Skill sharing, discover `core.ai.skill.access.read` and\n`core.ai.skill.access.change`. Read current grants with `{skillId}`; change one\nusing `{skillId, expectedAccessRevision, principal, permission}` or an existing\n`accessId`. Skill levels are `read`, `write`, and `admin`; `null` revokes an\nexisting grant. These capabilities also require Manage, fresh review, and the\ncurrent grants revision, and preserve the last administrator.\n\nTell the user when recipients can access only one of a linked Skill and App.\nPrepare each requested grant separately; never implicitly share the other.\n\n## Public and standalone apps\n\n`code_access_read` also returns `runnerHref` and `publicLevels:[\"read\"]`.\nThe standalone URL is `/app/assistant/apps/ID/run`. It always runs the current\npublication, including for managers. Share this URL, not a chat workspace URL.\nA private app requires sign-in and app access. Publication never grants access.\n\nBefore requesting a public grant, explain that visitors can use local computation,\nfile pickers, downloads and browser-local storage, but cannot use the app database,\nserver files/KV, personal secrets, server HTTP/PDF or protected Cloud actions.\nBeing signed in does not remove these restrictions: server features require an\nexplicit user, group or authenticated grant. Never execute as the app owner.\nSource and data embedded in the published code become public; do not embed secrets.\nA public grant does not expose the app's draft, history or administration.\nRemoving the grant or unpublishing prevents new loads; downloaded code cannot be recalled.\n\nFor a public calculator, use local inputs and downloads. For an internal dashboard\nusing shared data, grant the intended users or groups access instead. Explain when\nan existing app depends on server features before sharing it publicly.\n\nCloud administrators can add the runner URL as a Link shortcut in the navigation\nsettings. Shortcut audience controls visibility and never grants app access.\n"
16
16
  },
17
+ {
18
+ "path": "references/ai.md",
19
+ "content": "# AI calculations\n\nUse the global `ai` namespace for text generation, classification and extracting\nstructured values from supplied data. These are server-side calculations, not\nagents: no tools, browsing, chat history, memories or files are loaded implicitly.\nRead the required data first and pass it as `input`. Use ordinary code for exact\narithmetic, filtering and aggregation.\n\nAll methods return Promises. They work in Code Mode experiments, interactive chat\npresentations and authenticated Studio app runs. Public or local-only runners\ncannot use them. Calls use the executing user's permitted model and personal\nchat allowance; no provider key is exposed to source code. Omit `modelProfileId`\nto use the user's available default, or pass a verified allowed model ID.\nStopping the run cancels pending calls. Errors reject the Promise; catch them\nwhen the user should be able to retry. Do not retry indefinitely.\n\n## Methods\n\nCommon options: `prompt` (1–20,000 characters), `input` (JSON data), optional\n`modelProfileId`. Separate instructions in `prompt` from untrusted data in\n`input`. All output is validated; model output may still be factually wrong.\n\n- `await ai.generateText({ prompt, input?, modelProfileId?, maxOutputChars? })`\n returns a string. `maxOutputChars` is 1–20,000, default 4,000.\n- `await ai.classify({ prompt, input, choices, modelProfileId? })` returns exactly\n one of 2–50 unique choice strings (each at most 200 characters).\n- `await ai.classifyMany({ prompt, input, choices, minChoices?, maxChoices?, modelProfileId? })`\n returns a unique subset in declared choice order. Defaults: minimum 0, maximum\n the number of choices. Include an `other` choice if a single classification\n must support uncertainty; use an empty subset for no matches in multi-choice.\n- `await ai.extractData({ prompt, input, fields, modelProfileId? })` returns an\n object with only the declared fields. Declare 1–40 fields with unique `name`,\n `type` and `description`. Names start with a letter and contain only letters,\n digits and underscores (maximum 80 characters). Types: `text`, `number`,\n `boolean`, `date_time`, `enum`. Fields are required by default; set\n `required: false` to permit omission. Dates are ISO timestamps with timezone.\n Enum fields require 1–50 `choices`; text fields may set `maxLength` (1–20,000).\n Descriptions are at most 500 characters. This is a bounded field definition,\n not arbitrary JSON Schema.\n\n```js\nexport default async () => {\n const category = await ai.classify({\n prompt: \"Classify the feedback by its main purpose.\",\n input: \"Where can I download my invoice?\",\n choices: [\"praise\", \"problem\", \"question\", \"other\"],\n });\n const summary = await ai.generateText({\n prompt: \"Summarize the feedback in one short German sentence.\",\n input: \"Where can I download my invoice?\",\n maxOutputChars: 300,\n });\n return { category, summary };\n};\n```\n\nIn interactive views, call AI on an explicit action and show pending/error\nfeedback. Keep the result in app state; do not repeat inference on each render,\nslider movement or table selection. For many records, choose bounded batches\nand report progress. `code_run` tests execute real AI calls and consume allowance.\nNever send generated text or change domain data automatically just because AI\nreturned a value; use the appropriate permission-aware capability separately.\n"
20
+ },
17
21
  {
18
22
  "path": "references/analytics.md",
19
23
  "content": "# Analytics UI\n\nCreate an interactive analysis with the built-in UI API:\n\n```js\nexport default () => {\n const rows = [{ id: \"north\", region: \"North\", revenue: 1200 }];\n const explorer = ui.chartExplorer({\n id: \"revenue\", label: \"Revenue by region\",\n data: {\n rowKey: \"id\", rows,\n chart: { kind: \"bar\", category: \"region\", value: \"revenue\" },\n context: {\n mode: \"snapshot\", asOf: \"2026-09-13T12:00:00Z\",\n sources: [{ label: \"Example fixture\" }], status: \"fixture\",\n note: \"Demonstration data, not business results.\"\n }\n },\n columns: [\n { key: \"region\", label: \"Region\" },\n { key: \"revenue\", label: \"Revenue\", sortable: true,\n format: { type: \"currency\", currency: \"EUR\" } }\n ]\n });\n ui.grid({ children: [explorer] });\n};\n```\n\n## Controls and handles\n\nThe UI uses one options object per control. Common options: `id`, `label`,\n`description`, `disabled`, `loading`. IDs must be unique, at most 80 characters.\n\n| Constructor | Required options / callbacks | Handle updates |\n| --- | --- | --- |\n| `ui.stat` | `label`; optional numeric/null `value` (default null), `format`, `trend: number[]` | `setValue`, `setOptions`, `setLoading` |\n| `ui.text` | `value`; optional `markdown: true` | `setValue(text)` |\n| `ui.button` | `label`, `onClick`; optional `variant` | `setOptions`, `setLoading`, `setDisabled` |\n| `ui.filePicker` | `label`, `onChange(files)`; optional `accept`, `multiple` | `setLoading`, `setDisabled` |\n| `ui.input` | `value`; optional `placeholder`, `onChange(string)` | `setValue`, `getValue`, `setOptions`, `setLoading`, `setDisabled` |\n| `ui.select` | `value`, `options: [{value,label}]`, optional `onChange(string)` | same |\n| `ui.multiSelect` | `value: string[]`, `options`, optional `onChange(string[])` | same |\n| `ui.number` | `value: number or null`; optional `min`, `max`, `step`, `onChange` | same |\n| `ui.slider` | `value`, `min`, `max`; optional `step`, `onChange(number)` | same |\n| `ui.dateRange` | `value: {start,end}`; each ISO date or null; optional `onChange` | same |\n| `ui.table` | `rows`, `rowKey`, `columns`; optional `onSelect(row or null)` | `setData`, `setColumns`, `select(key or null)` |\n| `ui.chart` | `data: {options, marks?, formats?}`; optional `onSelect(key)` | `setData`, `setOptions`, `select`, `setLoading` |\n| `ui.chartExplorer` | `data`, `columns`; optional `onSelect(row or null)`, `onViewChange(\"chart\" or \"table\")` | `setData`, `setOptions`, `select`, `setLoading` |\n\nButton variants are `primary`, `secondary` (default), `ghost`, `text`, and\n`danger`. `number.onChange` receives `number | null`; `dateRange.onChange`\nreceives `{start: string | null, end: string | null}`. `filePicker.onChange`\nalways receives `File[]`, even with `multiple: false`; cancelling does not call\nit. All handles have an `id`; layout handles expose only that ID.\n\n`setOptions(patch)` updates constructor properties without replacing callbacks\nor IDs. For `chartExplorer`, the allowed keys are only `label`, `description`,\n`columns`, and `view`; for `chart`, use common options, `selectedKey`, and `cursor?: string`, with\n`setData` for chart data. Control/stat/button patches use their respective\nconstructor properties. `chartExplorer` accepts initial `view: \"chart\" | \"table\"`\n(default `\"chart\"`). Tables, charts and Explorers accept `selectedKey: string | null`\n(default null); their `select(keyOrNull)` sets or clears selection. A standalone\nchart's `setData` clears selection; table/Explorer updates retain valid keys.\n\nSetters never invoke user callbacks. Handles do not have generic\n`set`, `upsert`, or `remove`. Replace reviewed row arrays with `setData`.\nDate ranges are calendar dates, not timestamps; choose timezone and inclusivity\nexplicitly when translating a range into a query.\n\n`ui.row`, `ui.column`, and `ui.grid` take `{children: handles[]}`.\nGrid additionally accepts `minWidth` in pixels (160–1200; default 320), wrapping\nto fit narrow viewports. `ui.section` adds `label` and optional `description`.\nEach handle belongs to one layout. UI handles are not serializable entry output.\nUse `ui.modal` for trusted dialogs.\n\n## Data, formats, and charts\n\nRows contain scalar values and require unique nonempty string keys in `rowKey`.\nColumns use `{key,label,sortable?,align?,format?}` for both tables and Explorers.\nSorting compares raw values; nulls sort last. Formats apply in the host locale:\n\n- `{type:\"number\", maximumFractionDigits?}`\n- `{type:\"currency\", currency:\"EUR\", maximumFractionDigits?}`\n- `{type:\"percent\", input:\"fraction\" or \"percent\", maximumFractionDigits?}`\n- `{type:\"date\", timeZone:\"Europe/Berlin\", style?:\"short\"|\"medium\"|\"long\"}`\n\nDates require epoch milliseconds. Numeric formats reject strings; convert source\nvalues deliberately. Null displays as an unavailable value rather than zero.\n\nExplorer chart mappings support `bar`, `pie`, `donut` with `category`/`value`,\nand `line`, `scatter` with `x`/`y` and optional `series` fields. Each row maps to\none mark. Pie and donut values must be positive. There is no implicit aggregation.\nLine X values may be finite numbers or nonempty category labels such as months;\nlabels keep their first-occurrence order across series. Do not mix these types.\nScatter X and all Y/value fields must be finite numbers. The worker validates\nthese mappings before returning a ready state, including after filter updates.\nMapped charts reserve axis space for formatted numbers. Long bar labels are\nshortened on the axis; keep the full label column in tooltips and tables.\n\nFor all 14 kinds use `{options, marks, formats?}`. `options` uses the strict\n[Charts](charts.md) schema. Each mark is:\n`{role,index,seriesIndex?,key,rowKey,reference?,tooltip?:{title?,rows:[{label,value}]}}`.\n`role` is `point`, `item`, `bin`, `box`, `outlier`, `value`, `cell`, or\n`interval`; indices are zero-based. The role/index identifies the renderer datum\nin the current chart input (see the role table in [Charts](charts.md)). Every\nrendered mark needs one mapping; every `rowKey` must exist in the Explorer rows.\nHistogram bin indices identify computed bins; boxplot boxes identify groups and\noutliers identify observations. Prepare summary rows for these derived entities.\nDifferent current/reference marks may point to one comparison row.\n\nFor standalone `ui.chart`, marks are optional; supply them to enable selection\ncallbacks and controlled selection. Tooltips remain inspectable without them.\n`formats` maps raw datum field names (`x`, `y`, `value`, `delta`, etc.) to formats.\nAxes accept increasing `domain: [min,max]` for stable comparisons. No arbitrary\nSVG, HTML, JSX, or callbacks cross into host rendering.\n\n## Shared exploration\n\n`ui.explorer({id?,label?,snapshot,steps?,series?,comparison?,load})` owns multiple charts.\nA snapshot is `{request,charts:{[name]:explorerData}}`. Requests are\n`{step?,visibleKeys?,referenceStep?}`; omitted visible keys means all, `[]` means\nnone. Steps and series controls use `{key,label}` arrays. A reference requires a\ncurrent step. The loader must implement aggregation and comparison explicitly.\n\nMount each chart once with `group.chart(name,{label,columns,...})` and include\nthe group handle with its chart handles in a layout. The group shows filter,\nrefresh, and retry controls. Set `comparison: true` only when the loader implements\nreference data; it enables comparison controls. Shared row keys link selection, and line\ncharts share their inspection cursor. Comparison controls do not calculate deltas.\n\n`load(request,{signal})` returns a full `{request,charts}` snapshot. Return every\nconfigured chart and the matching request. New requests cancel obsolete loads;\nlate results are ignored. Changes commit together, retain valid selections, and\nclear unavailable selections. Failed loads retain the previous displayed data.\nCallbacks execute in the worker; honor the signal when doing asynchronous work.\nAborting a loader does not undo an already issued HTTP or capability call. The\ncurrent HTTP adapter has no per-call signal option; late results are ignored,\nwhile a pending server call retains its normal consent and deadline.\n\nRepeated `setRequest` calls with the same filters do not reload existing data.\n`refresh` and `retry` explicitly request a fresh load.\n\nGroup handle methods:\n\n- `chart(name, {columns, label?, description?, view?, ...})` mounts a named chart\n once; its handle has only `id`, `select(keyOrNull)`, and `setOptions(patch)`.\n Update its data through the group snapshot.\n- `await setRequest(request)` replaces filters, rather than merging them.\n- `await refresh()` or `await retry()` reloads the current desired filters.\n- `await pinReference()` pins the currently displayed step; it takes no argument.\n `await clearReference()` removes it. Both may call the loader.\n- `select(keyOrNull)` sets shared selection; `setData(snapshot)` synchronously\n replaces all chart data and filters, cancelling obsolete loading.\n- `cancel()` cancels loading and keeps the displayed snapshot.\n\nLoad failures are retained as group error state, rather than thrown from\n`setRequest`/`refresh`; inspect the resulting state. Local filtering requires no network call.\nAn external HTTP load still needs approval. A slider over data steps is not a\nsubstitute for an explicit Apply button when each change has an external effect.\n\n## Inspection, budgets, and delivery\n\n`code_interact` uses `event` for a typed UI event:\n`{type:\"change\",value:...}`, `{type:\"select\",key:\"row-id\"}`, or\n`{type:\"view\",value:\"table\"}`. Group events are\n`{type:\"request\",request:{step:\"month\"}}` and `{type:\"refresh\"}`.\nUse `code_inspect({runId,nodeId,offset,limit})` for bounded rows and controls.\n\nFor a short known sequence, use\n`code_interact({runId,steps:[{id:\"region\",event:{type:\"change\",value:\"north\"}},{id:\"apply\"}]})`.\nA batch accepts up to three sequential steps and returns one final snapshot.\nIt stops at an error, modal, or unfinished background work; check `completedSteps`\nand `nextStep` before continuing. Do not mix `steps` with top-level `id`, `event`,\nor `answer`. Use separate calls when the next action depends on inspecting data.\n\nExisting budgets still apply: 300 UI nodes, 1,000 rows/data entries per chart,\nand the 16 MiB bridge budget. Group data and multiple views also count toward\ntransport bytes. Aggregate before rendering; the host does not fetch hidden rows.\n\nUse `ui.stat` for KPIs so raw numbers remain inspectable and formatting follows\nthe host locale. Use null for unavailable ratios. Pass the full raw value to\n`setValue`: for a margin, `profit / revenue`, never `Math.round(ratio * 100) / 100`.\nA fraction such as 0.449550499 must remain that fraction; its percent format\ncontrols visible digits. Validate the raw `value` from inspection against an\nindependent calculation, not only a rounded screenshot or formatted string.\n\nSource context contains `mode:\"snapshot\"|\"live\"`, ISO `asOf`, `sources: [{label, href?, description?}]` with optional HTTPS links, and optional `status`/`note`. `status` is exactly `complete`, `partial`, or `fixture`; it does not accept\n`validated`. Partial or fixture status requires a note. Use a full ISO timestamp\nfor `asOf` (including time and Z), captured once during data preparation. Never put keys or credential-bearing URLs in\nprovenance. This context records claims; it does not validate the underlying data.\nRead the `assistant-data-analysis` skill for analytical validation and delivery.\n\n## Local shared-filter example\n\n```js\nexport default () => {\n const source = [\n { id: \"north\", name: \"North\", january: 10, february: 12 },\n { id: \"south\", name: \"South\", january: 8, february: 9 }\n ];\n const build = request => {\n const rows = source.filter(row => request.visibleKeys === undefined ||\n request.visibleKeys.includes(row.id)).map(row => ({\n id: row.id, name: row.name, value: row[request.step]\n }));\n return { request, charts: { revenue: {\n rowKey: \"id\", rows,\n chart: { kind: \"bar\", category: \"name\", value: \"value\" }\n } } };\n };\n const group = ui.explorer({\n label: \"Example monthly totals\",\n snapshot: build({step:\"january\"}),\n steps: [{key:\"january\",label:\"January\"},{key:\"february\",label:\"February\"}],\n series: source.map(row => ({key:row.id,label:row.name})),\n load: request => {\n if (request.referenceStep) throw new Error(\"This example does not implement comparisons.\");\n return build(request);\n }\n });\n const chart = group.chart(\"revenue\", {\n label: \"Example totals\", columns: [\n {key:\"name\",label:\"Region\"}, {key:\"value\",label:\"Total\",sortable:true}\n ]\n });\n ui.column({children:[group,chart]});\n};\n```\n\nComparison controls are disabled by default. The example also rejects an\nunsupported reference defensively rather than displaying unchanged values as a comparison. For comparisons, return rows containing both\nvalues and prepare distinct current/reference marks pointing to the same row key.\n"
@@ -28,7 +32,7 @@ export const ASSISTANT_CODE_MODE_SKILL = {
28
32
  },
29
33
  {
30
34
  "path": "references/capabilities.md",
31
- "content": "# Combine Cloud capabilities\n\nDiscover the actual capability through the normal capability search and load its\ninput contract before writing code. Try a read query directly when that helps\nunderstand its result. Never guess a capability name, input field, or result path.\n\nFor comparisons or analysis, a one-off script can call several discovered read\ncapabilities, normalize their results, and return a compact comparison. Inspect\npagination, identifiers, units and date ranges before joining or totaling data.\nUse a fresh short script for another question; no saved app is required. A\nsample is not evidence that all records were fetched. Shared writes and actions\nremain real even when the script is exploratory.\n\nInside a script or app, call:\n\n```ts\nconst result = await capabilities.run(\"app.capability\", { /* documented input */ });\nconst data = result.data;\n```\n\nThe name and input must match the discovered capability. The result is the\ncapability result envelope, including `data` and any supplied references or\nfiles. Inspect its documented shape before chaining it into another call.\nAwait dependent calls in order. Catch failures when the task has a useful\nrecovery; do not swallow them and report success.\n\nThe current user's Cloud permissions still apply. Read queries and actions\nconfigured without approval run directly. Other actions request real user\napproval through the chat or app host. An eligible action can offer “always\nallow” for its defined scope. Existing remembered approvals are reused.\nScripts cannot approve their own requests; `code_interact` is not an approval\nmechanism. Declined calls throw. Respect the decision and do not retry through\nanother route. A chat's allowed-tools restriction also applies to calls from\nits scripts.\n\nFailures reject the promise; the runtime removes the transport `{ok, data}`\nwrapper. The returned object is the capability's own envelope (`data`, `refs`,\nfiles when supplied), not a second transport wrapper.\n\nUse the user's current request to decide which effects are appropriate. The\navailability of a tool is not a reason to invoke unrelated actions.\n\nWhen the user runs a saved resource they do not manage, every capability call\nrequires explicit consent, including queries and actions normally needing no\napproval. The dialog identifies the resource and explains that returned data\ncan be stored in shared files or its database. Personal remembered approvals\ndo not apply, and these calls cannot create a personal always-allow rule.\nDenial must leave a useful message; do not retry unchanged or bypass consent.\n\n## Binary content\n\nSome discovered operations return a `stream` beside `data`. This is the one\nbinary path for any app: files, invoice PDFs, audio, and imports use the same\nmechanism. Never invent a download URL or put file bytes in capability JSON.\n\n```ts\nconst source = await capabilities.run(\"example.content.read\", {id: sourceId});\nconst file = await capabilities.streams.read(source.stream); // File\n// Analyze file with the documented CSV, Excel, PDF or binary helpers.\nconst output = new Blob([\"name,total\\nAlice,42\\n\"], {type:\"text/csv\"});\nconst target = await capabilities.run(\"example.content.create\", {\n path: \"totals.csv\", size: output.size, mediaType: output.type,\n});\nconst receipt = await capabilities.streams.write(target.stream, output);\n```\n\nThe names and fields above illustrate the flow; discover the installed app's\nactual contract. Streams are tied to this run's capability calls. Preserve the\nreturned descriptor unchanged. Reads return a `File`; writes accept a `Blob`,\nstring, `ArrayBuffer` or `Uint8Array`. The payload must exactly match the approved\nbyte size. The runtime accepts at most 50 MiB per payload, 250 MiB of transfers\nper run and 64 stream references. Do not split a larger file to bypass a limit.\n\nAfter an interrupted write, call `capabilities.streams.status(target.stream)`.\nA completed result is `{state:\"completed\", result: <capability envelope>}`;\n`open` means it has not completed and `aborted` means it cannot continue.\nUse `capabilities.streams.abort(target.stream)` to discard an unfinished upload.\nNever blindly repeat a write or claim success from a missing response. Stream\nreferences expire; request a fresh read when needed. A fresh write is a new\nAction and must follow the normal approval process.\n\nFilesv2 publishes discovery/listing and cursor-based search, `content.read`,\n`content.create`, folder creation, rename, move, copy, trash, and restore. Use\nexact returned base IDs and entry references. Overwriting requires current\n`expectedRevision`; default to creating a new output name. Follow `next` until\nnull when an analysis needs every entry. Trash remains recoverable; no permanent\ndelete capability is exposed.\n"
35
+ "content": "# Combine Cloud capabilities\n\nDiscover the actual capability through the normal capability search and load its\ninput contract before writing code. Try a read query directly when that helps\nunderstand its result. Never guess a capability name, input field, or result path.\n\nFor comparisons or analysis, a one-off script can call several discovered read\ncapabilities, normalize their results, and return a compact comparison. Inspect\npagination, identifiers, units and date ranges before joining or totaling data.\nUse a fresh short script for another question; no saved app is required. A\nsample is not evidence that all records were fetched. Shared writes and actions\nremain real even when the script is exploratory.\n\nInside a script or app, call:\n\n```ts\nconst result = await capabilities.run(\"app.capability\", { /* documented input */ });\nconst data = result.data;\n```\n\nThe name and input must match the discovered capability. The result is the\ncapability result envelope, including `data` and any supplied references or\nfiles. Inspect its documented shape before chaining it into another call.\nAwait dependent calls in order. Catch failures when the task has a useful\nrecovery; do not swallow them and report success.\n\nThe current user's Cloud permissions still apply. Read queries and actions\nconfigured without approval run directly. Other actions request real user\napproval through the chat or app host. An eligible action can offer “always\nallow” for its defined scope. Existing remembered approvals are reused.\nScripts cannot approve their own requests; `code_interact` is not an approval\nmechanism. Declined calls throw. Respect the decision and do not retry through\nanother route. A chat's allowed-tools restriction also applies to calls from\nits scripts.\n\nFailures reject the promise; the runtime removes the transport `{ok, data}`\nwrapper. The returned object is the capability's own envelope (`data`, `refs`,\nfiles when supplied), not a second transport wrapper.\n\nUse the user's current request to decide which effects are appropriate. The\navailability of a tool is not a reason to invoke unrelated actions.\n\nWhen the user runs a saved resource they do not manage, every capability call\nrequires explicit consent, including queries and actions normally needing no\napproval. The dialog identifies the resource and explains that returned data\ncan be stored in shared files or its database. Personal remembered approvals\ndo not apply, and these calls cannot create a personal always-allow rule.\nDenial must leave a useful message; do not retry unchanged or bypass consent.\n\n## Binary content\n\nSome discovered operations return a `stream` beside `data`. This is the one\nbinary processing path for any app: files, invoice PDFs, audio, and imports use the same\nmechanism. Never invent a download URL or put file bytes in capability JSON.\n\n```ts\nconst source = await capabilities.run(\"example.content.read\", {id: sourceId});\nconst file = await capabilities.streams.read(source.stream); // File\n// Analyze file with the documented CSV, Excel, PDF or binary helpers.\nconst output = new Blob([\"name,total\\nAlice,42\\n\"], {type:\"text/csv\"});\nconst target = await capabilities.run(\"example.content.create\", {\n path: \"totals.csv\", size: output.size, mediaType: output.type,\n});\nconst receipt = await capabilities.streams.write(target.stream, output);\n```\n\nThe names and fields above illustrate the flow; discover the installed app's\nactual contract. Streams are tied to this run's capability calls. Preserve the\nreturned descriptor unchanged. Reads return a `File`; writes accept a `Blob`,\nstring, `ArrayBuffer` or `Uint8Array`. The payload must exactly match the approved\nbyte size. The runtime accepts at most 50 MiB per payload, 250 MiB of transfers\nper run and 64 stream references. Do not split a larger file to bypass a limit.\n\nAfter an interrupted write while the same turn is active, call\n`capabilities.streams.status(target.stream)`.\nA completed result is `{state:\"completed\", result: <capability envelope>}`;\n`open` means it has not completed and `aborted` means it cannot continue.\nUse `capabilities.streams.abort(target.stream)` to discard an unfinished upload.\nNever blindly repeat a write or claim success from a missing response. Stream\nreferences expire and are bound to the current conversation and foreground\nturn. They stop working when that turn is canceled or ends; another turn cannot\nreuse them. After a stopped turn, inspect the destination before preparing a\nnew write: stopping does not undo a committed file. Request a fresh read when needed. A fresh write is a new\nAction and must follow the normal approval process.\n\nFilesv2 publishes discovery/listing and cursor-based search, `content.read`,\n`content.create`, folder creation, rename, move, copy, trash, and restore. Use\nexact returned base IDs and entry references. Overwriting requires current\n`expectedRevision`; default to creating a new output name. Follow `next` until\nnull when an analysis needs every entry. Trash remains recoverable; no permanent\ndelete capability is exposed.\n\n\nFor a user download from a Studio list, Filesv2 also provides an on-demand\n`content.download` lease. Keep resource refs in lists and request the URL only\nwhen selected; follow [Filesv2 and Grids downloads](files.md#list-filesv2-files-beside-grids-documents)\nfor expiry, permissions and error recovery.\n"
32
36
  },
33
37
  {
34
38
  "path": "references/charts.md",
@@ -60,7 +64,7 @@ export const ASSISTANT_CODE_MODE_SKILL = {
60
64
  },
61
65
  {
62
66
  "path": "references/files.md",
63
- "content": "# Explicit file references and transfers\n\nFiles belong to a chat, Project, or App shared store. A location is\n`{scope:\"chat\"|\"project\"|\"app\",id,path}`. A reference adds an opaque string\n`version`. The current chat ID is supplied as `Chat:` in the platform context;\nuse it for `scope:\"chat\"` rather than guessing an ID. Reuse returned locations and references exactly; access to a location\nnever grants access to its whole store or to another resource.\n\nLoad only `code_files`, `code_file_stat`, and `code_file_copy` as needed.\n\n1. `code_files({scope,id,after?:string,limit?:number})` returns `container`,\n `items:[{location,path,size,mediaType}]`, and `nextAfter`. Limit defaults to\n 100, maximum 1,000. Continue with `nextAfter` until null.\n2. `code_file_stat({file:location})` returns `{exists:false}` or\n `{exists:true,reference,size,mediaType}`. It does not print file bytes.\n3. `code_file_copy({source:reference,destination:location,expectedVersion})`\n copies bytes on the server. For a new destination use `expectedVersion:null`;\n replacing a file requires its exact current version from `code_file_stat`.\n The result contains the destination `reference,size,mediaType`.\n\nEvery copy receives fresh user review with the exact source, destination, and\noverwrite scope. App and Project files may be readable by other authorized\nusers; copying a private chat attachment there is an explicit disclosure.\nRejection does not copy anything. Source versions, destination versions, current\npermissions, and the destination's byte limits are checked again during execution.\nAfter a conflict, inspect current state and prepare a new review before retrying.\n\nApp file reads and writes require Use, not Manage. Project files require Read\nfor sources and Write for destinations. Chat files require ownership. Transfers\nwork without a running UI and do not mount other chat attachments implicitly.\n\nFor example, inspect an invoice attachment, copy its reference into the target\nApp's shared file store, then call the App's published `importFiles` action with\nthe destination key expected by its discovered input schema. Do not pass a chat\npath to an App and assume it can read it. Action handlers see only their explicit\ninput and authorized App data. Use [App actions](app-actions.md) for discovery.\n\nFor small UTF-8 files that should become App source, use\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nThis is a reviewed import with the same source references, not a chat-only\nspecial case. Source file/bundle limits still apply; keep larger or private\nruntime data in shared files or the database instead of embedding it in source.\n\nKnown rejections return `CONFLICT` for an occupied or changed destination and\n`STORAGE_FULL` for a destination byte limit. No destination bytes were written.\nChoose another path or reduce the file size, then prepare a new review.\n"
67
+ "content": "# Explicit file references and transfers\n\nFiles belong to a chat, Project, or App shared store. A location is\n`{scope:\"chat\"|\"project\"|\"app\",id,path}`. A reference adds an opaque string\n`version`. The current chat ID is supplied as `Chat:` in the platform context;\nuse it for `scope:\"chat\"` rather than guessing an ID. Reuse returned locations and references exactly; access to a location\nnever grants access to its whole store or to another resource.\n\nLoad only `code_files`, `code_file_stat`, and `code_file_copy` as needed.\n\n1. `code_files({scope,id,after?:string,limit?:number})` returns `container`,\n `items:[{location,path,size,mediaType}]`, and `nextAfter`. Limit defaults to\n 100, maximum 1,000. Continue with `nextAfter` until null.\n2. `code_file_stat({file:location})` returns `{exists:false}` or\n `{exists:true,reference,size,mediaType}`. It does not print file bytes.\n3. `code_file_copy({source:reference,destination:location,expectedVersion})`\n copies bytes on the server. For a new destination use `expectedVersion:null`;\n replacing a file requires its exact current version from `code_file_stat`.\n The result contains the destination `reference,size,mediaType`.\n\nEvery copy receives fresh user review with the exact source, destination, and\noverwrite scope. App and Project files may be readable by other authorized\nusers; copying a private chat attachment there is an explicit disclosure.\nRejection does not copy anything. Source versions, destination versions, current\npermissions, and the destination's byte limits are checked again during execution.\nAfter a conflict, inspect current state and prepare a new review before retrying.\n\nApp file reads and writes require Use, not Manage. Project files require Read\nfor sources and Write for destinations. Chat files require ownership. Transfers\nwork without a running UI and do not mount other chat attachments implicitly.\n\nFor example, inspect an invoice attachment, copy its reference into the target\nApp's shared file store, then call the App's published `importFiles` action with\nthe destination key expected by its discovered input schema. Do not pass a chat\npath to an App and assume it can read it. Action handlers see only their explicit\ninput and authorized App data. Use [App actions](app-actions.md) for discovery.\n\nFor small UTF-8 files that should become App source, use\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nThis is a reviewed import with the same source references, not a chat-only\nspecial case. Source file/bundle limits still apply; keep larger or private\nruntime data in shared files or the database instead of embedding it in source.\n\nKnown rejections return `CONFLICT` for an occupied or changed destination and\n`STORAGE_FULL` for a destination byte limit. No destination bytes were written.\nChoose another path or reduce the file size, then prepare a new review.\n\n\n## List Filesv2 files beside Grids documents\n\nDiscover the installed contracts first. Call `filesv2.bases.list`, then\n`filesv2.entry.list` for a folder or `filesv2.entry.search-in-base` for names\nbelow a known path. Keep the filters unchanged and follow `data.next` as\n`after` until null, even after an empty page. Each `data.items` entry includes\nits own `{type:\"filesv2.entry\",id}` ref and file metadata. Keep that ref in the\nStudio list, alongside the `grids.document` refs from `grids.document.list`.\nUse both `type` and `id` as the identity; dispatch each type to its own\noperations. Grids uses its own `page` cursor, not Filesv2's `data.next`.\n\nFilesv2 refs identify a base and path, including long paths; they do not grant\naccess or pin a content version. Moving or renaming changes the ref, and\nreplacing bytes at the same path keeps it. Refresh metadata when needed.\nNever construct storage URLs or turn files into public shares for this flow.\n\nOnly on a user's download request, call `filesv2.content.download` with the\nselected Filesv2 ref's exact `id`. Its `data` is `{url,method:\"GET\",expires}`.\nOffer that returned URL unchanged to the requesting user; it is a private\nbearer credential, not a stable resource link. It expires after 60 seconds;\nuse the returned `expires` timestamp and request a fresh lease when needed.\nDo not prefetch leases for list rows, persist them in App/shared data, or\ninclude them in logs. Do not send Cloud cookies or authorization headers to\nthe storage host. Grids documents keep their authenticated download path from\ntheir canonical reader; never send a `grids.document` ID to Filesv2.\n\nEach lease request checks the current user's storage and Unix permissions.\nA 403 means access is denied; a 404 means the ref or file is missing or the\nbase is no longer visible. Refresh the list and do not bypass the denial.\n`not_file` (400) means a folder was selected. `identity_changed` (409) requires\nrefreshing access/identity state before trying again. Storage unavailability is an\nerror, not an empty list. If a lease expires or a transfer fails, discard the\nURL and request a fresh lease through the same capability; if that is denied,\nstop. Revoking Cloud access prevents new leases; an already issued bearer\nlease can remain usable until expiry, subject to storage checks.\n\nFor analysis inside code, use `filesv2.content.read` and\n`capabilities.streams.read` instead of fetching a bearer URL. See\n[Capability calls](capabilities.md) for binary budgets and consent rules.\n"
64
68
  },
65
69
  {
66
70
  "path": "references/finance.md",
@@ -108,7 +112,7 @@ export const ASSISTANT_CODE_MODE_SKILL = {
108
112
  },
109
113
  {
110
114
  "path": "references/work.md",
111
- "content": "# Background work and cancellation\n\nNormal control callbacks and startup have a 15-second watchdog. Input reads and\nfile pickers pause it; agent tool calls still have a bounded outer deadline\n([details](debugging.md)). For folder\nprocessing, long computations, and imports, start one background job. Ordinary\ncontrols remain available while it runs; a second job is rejected until it ends.\n\n```js\nexport default () => {\n const status = ui.text({value:\"Choose a folder\"});\n ui.button({label:\"Start\", onClick: async () => {\n const selected = await files.openFolder();\n if (!selected.length) return;\n work.run(async job => {\n for (let index = 0; index < selected.length; index++) {\n await job.checkpoint();\n // Process selected[index]; close documents in finally.\n job.progress(index + 1, selected.length, files.path(selected[index]));\n status.setValue(`Processed ${index + 1} of ${selected.length}`);\n }\n return { processed: selected.length };\n });\n }});\n ui.button({label:\"Cancel\", onClick: () => work.cancel()});\n};\n```\n\nDo not await `job.done` in a GUI button: its callback would remain pending for the entire job. For a headless script, use\n`const job = work.run(async context => { /* ... */ }); return await job.done;`.\n`work.run(callback)` returns `{done: Promise<result>, cancel(): void}`;\n`work.cancel()` cancels the active job. Both cancel methods return immediately;\n`done` rejects on failure or cancellation. A job's returned value becomes the\nrun output. Do not return the job handle.\n\n`context.signal` is aborted by cancellation. `await context.checkpoint()` yields\nthe worker event loop and throws if cancelled. Call it between files/batches\nand inside long CPU loops. `context.progress(completed, total?, label?)` reports\nbounded progress in inspection. Progress is coalesced; it is not a log per row.\nErrors reach the console. Use `try/finally` to release documents. Cancellation\ncannot undo completed database writes or Cloud actions; summarize partial work\nand use stable import keys to make a deliberate retry safe. Cancellation is\ncooperative: an in-flight capability or database request may finish before the\nnext checkpoint. Use Stop to terminate a blocked run; inspect effects before\nretrying.\n\nThe worker sends a heartbeat while its event loop responds. A worker that stops\nresponding for 15 seconds is terminated. This watchdog is not a total job limit.\nThe user's Stop action and `code_stop` can also terminate a stuck worker.\n\n`code_run` and `code_interact` may return while background work is running.\nCheck the inspection result’s `work.status` (not a worker-global property): `running`, `completed`, `cancelled`, or `error`. Use\n`code_inspect({runId, waitMs: 30000})` to wait for completion and obtain current\nprogress, output, and errors. It returns after at most 30 seconds; a remaining\n`running` status is not success. Modal/approval waits return promptly. Do not\nrestart the job just because it takes time. CLI `--steps-file` can use the same\ninspection step. Keep the execution host open until the job finishes.\n\nUpdate compact status while processing. Populate result tables in pages or at\nbatch boundaries; do not rebuild thousands of table rows for every progress\nincrement. UI updates are coalesced to 100 ms, and each table remains bounded.\n"
115
+ "content": "# Background work and cancellation\n\nNormal control callbacks and startup have a 15-second watchdog. Input reads and\nfile pickers pause it; agent tool calls still have a bounded outer deadline\n([details](debugging.md)). For folder\nprocessing, long computations, and imports, start one background job. Ordinary\ncontrols remain available while it runs; a second job is rejected until it ends.\n\n```js\nexport default () => {\n const status = ui.text({value:\"Choose a folder\"});\n ui.button({label:\"Start\", onClick: async () => {\n const selected = await files.openFolder();\n if (!selected.length) return;\n work.run(async job => {\n for (let index = 0; index < selected.length; index++) {\n await job.checkpoint();\n // Process selected[index]; close documents in finally.\n job.progress(index + 1, selected.length, files.path(selected[index]));\n status.setValue(`Processed ${index + 1} of ${selected.length}`);\n }\n return { processed: selected.length };\n });\n }});\n ui.button({label:\"Cancel\", onClick: () => work.cancel()});\n};\n```\n\nDo not await `job.done` in a GUI button: its callback would remain pending for the entire job. For a headless script, use\n`const job = work.run(async context => { /* ... */ }); return await job.done;`.\n`work.run(callback)` returns `{done: Promise<result>, cancel(): void}`;\n`work.cancel()` cancels the active job. Both cancel methods return immediately;\n`done` rejects on failure or cancellation. A job's returned value becomes the\nrun output. Do not return the job handle.\n\n`context.signal` is aborted by cancellation. `await context.checkpoint()` yields\nthe worker event loop and throws if cancelled. Call it between files/batches\nand inside long CPU loops. `context.progress(completed, total?, label?)` reports\nbounded progress in inspection. Progress is coalesced; it is not a log per row.\nErrors reach the console. Use `try/finally` to release documents. Cancellation\ncannot undo completed database writes or Cloud actions; summarize partial work\nand use stable import keys to make a deliberate retry safe. Cancellation is\ncooperative: an in-flight capability or database request may finish before the\nnext checkpoint. Use Stop to terminate a blocked run; inspect effects before\nretrying.\n\nThe worker sends a heartbeat while its event loop responds. A worker that stops\nresponding for 15 seconds is terminated. This watchdog is not a total job limit.\nThe user's Stop action and `code_stop` can also terminate a stuck worker.\n\n`code_run` and `code_interact` may return while background work is running.\nCheck the inspection result’s `work.status` (not a worker-global property): `running`, `completed`, `cancelled`, or `error`. Use\n`code_inspect({runId, waitMs: 30000})` to wait for completion and obtain current\nprogress, output, and errors. It returns after at most 30 seconds; a remaining\n`running` status is not success. Modal/approval waits return promptly. Do not\nrestart the job just because it takes time. CLI `--steps-file` can use the same\ninspection step. Keep the execution host open until the job finishes.\n\nUpdate compact status while processing. Populate result tables in pages or at\nbatch boundaries; do not rebuild thousands of table rows for every progress\nincrement. UI updates are coalesced to 100 ms, and each table remains bounded.\n\n## Scheduled Assistant tasks\n\nScheduled tasks can use Code Mode without an open user tab. Each turn owns a\nseparate server execution host; it does not borrow the foreground chat's run.\nLoad the scheduled-tasks Skill when creating or changing the task.\n\nUse existing chat input paths, compute, call `ai`, use task-approved capabilities,\nand export results to the chat. The host applies the task's confirmed grants and\nfixed inputs automatically to capability calls. Do not call an authorization API\nor supply a mandate ID in code. Personal remembered approvals do not apply.\n\nThere is no user to answer a modal, enter secrets or open a local file picker.\nHTTP and RSQL are available through the normal APIs with task grants. In the\nsame grants list use `{kind:\"http\",fixedInput:{origin:\"https://api.example.com\",method:\"GET\"}}`\nor `{kind:\"database\",fixedInput:{resourceId:\"aBc234\"}}`. HTTP can also fix an exact\n`url`; database grants can fix `operation` and `table`. A resource-only database\ngrant covers connecting and subsequent reads/writes; an operation-specific grant\nneeds a separate `connect` grant. Empty fixedInput explicitly allows all supported\ntargets and operations within the user's current access. The task cannot expand\nits own grants. Existing HTTP secrets remain server-side; new secret entry needs\nthe normal chat. Shared app storage retains its usual resource checks.\nCapability binary streams remain unavailable. If an operation is outside the task grant,\nexplain what is missing in the result; ask the user to adjust the task in its\nnormal chat. Do not bypass a denied capability through another transport.\n\nAI helpers use background accounting. Revocation, task grant changes and turn\ncancellation stop further host requests. Already completed effects are not\nrolled back, and host loss never replays an uncertain write automatically.\n\nDatabase maintenance tools `code_database_clear` and `code_database_reset` use the same task grants (`operation: \"clear\"` or `\"reset\"`). They still require Manage access and the current generation/data revision; preapprove these destructive operations only when the user explicitly requests them.\n"
112
116
  }
113
117
  ]
114
118
  } satisfies AiSkillTemplate;
@@ -8,6 +8,7 @@ import { withActiveIdentitySigner } from "../services/identity/key-ring";
8
8
  import { LOCALE_HEADER } from "../shared/locale";
9
9
  import { resolveAiCapabilityActor } from "./capability-execution";
10
10
  import { CODE_CAPABILITY_TOKEN_HEADER, codeCapabilityOperation } from "./code-capability-transport";
11
+ import { authorizeCodeExecution } from "./code-execution";
11
12
  import { aiConversations } from "./store";
12
13
 
13
14
  const Reply = z.object({
@@ -33,20 +34,22 @@ export const runManagedCodeTool =
33
34
  async (args: unknown, context: Context): Promise<z.infer<ReturnType<typeof z.json>>> => {
34
35
  if (!context.conversationId || !context.turnId) throw new Error("Code execution requires an active Assistant turn");
35
36
  const runConfig = await aiConversations.getTurnRunConfig({ conversationId: context.conversationId, turnId: context.turnId });
36
- if (!runConfig || (runConfig.kind !== "compact" && (runConfig.background || runConfig.mandate))) {
37
- throw new Error("Code execution is unavailable for background tasks until the code host supports task-scoped authority.");
37
+ if (!runConfig || runConfig.kind === "compact" || Boolean(runConfig.background) !== Boolean(runConfig.mandate)) {
38
+ throw new Error("Code execution requires a valid turn and task-scoped background authority.");
38
39
  }
39
40
  const { actor } = await resolveAiCapabilityActor({
40
41
  conversationId: context.conversationId,
41
42
  persistedActor: context.actor,
42
43
  store: aiConversations,
43
44
  });
45
+ await authorizeCodeExecution(context.conversationId, context.turnId, actor.user.id);
44
46
  await context.reportProgress?.(context.locale?.startsWith("de") ? "Ausführungshost verbinden" : "Connecting execution host");
45
47
  const app = await getApp("assistant");
46
48
  if (!app) throw new Error("Assistant code host is unavailable");
47
49
  let callback: Awaited<ReturnType<typeof signInvocationToken>> | undefined;
48
50
  const request = async (decision?: { id: string; approved: boolean }) => {
49
51
  context.signal.throwIfAborted();
52
+ await authorizeCodeExecution(context.conversationId!, context.turnId!, actor.user.id);
50
53
  const signed = await withActiveIdentitySigner(
51
54
  "invocation",
52
55
  (signer) =>
@@ -109,7 +112,17 @@ export const runManagedCodeTool =
109
112
  throw new Error("Code host request failed; inspect the existing call before starting another execution");
110
113
  return Reply.parse(body.data).data;
111
114
  };
112
- return waitForManagedCodeCall(request, context);
115
+ return waitForManagedCodeCall(
116
+ request,
117
+ runConfig.background
118
+ ? {
119
+ ...context,
120
+ requestApproval: async () => {
121
+ throw new Error("Background code cannot request interactive approval. Update task grants in the normal chat.");
122
+ },
123
+ }
124
+ : context,
125
+ );
113
126
  };
114
127
 
115
128
  /** Ordered reviews are replayed through Nessi before consuming a durable result. */