@k2b/cloud 0.25.0 → 0.27.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 (93) hide show
  1. package/package.json +3 -3
  2. package/src/_internal/capabilities.ts +12 -0
  3. package/src/_internal/define-app.ts +8 -1
  4. package/src/_internal/process-identity.ts +7 -1
  5. package/src/_internal/registry-validation.ts +3 -0
  6. package/src/_internal/registry.ts +1 -0
  7. package/src/_internal/runtime-context.ts +1 -0
  8. package/src/access/GroupCoverage.tsx +175 -0
  9. package/src/access/PermissionEditor.tsx +119 -99
  10. package/src/access/messages.ts +30 -0
  11. package/src/ai/admin.ts +1 -0
  12. package/src/ai/approval-routes.ts +5 -5
  13. package/src/ai/browser-code-contracts.ts +14 -2
  14. package/src/ai/browser.ts +8 -1
  15. package/src/ai/capabilities.ts +115 -33
  16. package/src/ai/chat/blocks.tsx +86 -179
  17. package/src/ai/chat/builtin-tools.tsx +83 -48
  18. package/src/ai/chat/file-tools.tsx +4 -1
  19. package/src/ai/chat/live-turn.browser-harness.tsx +44 -0
  20. package/src/ai/chat/message-actions.tsx +6 -2
  21. package/src/ai/chat/message-utils.ts +17 -14
  22. package/src/ai/chat/messages.ts +330 -2
  23. package/src/ai/chat/presentation.tsx +278 -104
  24. package/src/ai/chat/tool-groups.ts +55 -35
  25. package/src/ai/chat/turn-layout.ts +141 -0
  26. package/src/ai/chat/turn-view.tsx +644 -0
  27. package/src/ai/client/controller.ts +60 -31
  28. package/src/ai/client/file-source.ts +20 -3
  29. package/src/ai/client/projection.ts +42 -6
  30. package/src/ai/code-mode-skill.ts +27 -27
  31. package/src/ai/code-runtime-tools.ts +10 -1
  32. package/src/ai/code-source-contracts.ts +54 -4
  33. package/src/ai/code-source-tools.ts +10 -3
  34. package/src/ai/credentials.ts +17 -3
  35. package/src/ai/data-analysis-skill.ts +2 -2
  36. package/src/ai/default-tools.ts +2 -2
  37. package/src/ai/executor.ts +177 -80
  38. package/src/ai/file-context.ts +14 -2
  39. package/src/ai/file-tools.ts +17 -3
  40. package/src/ai/files-store.ts +134 -11
  41. package/src/ai/grids-skill.ts +2 -2
  42. package/src/ai/index.ts +7 -0
  43. package/src/ai/memories.ts +14 -0
  44. package/src/ai/migrate.ts +125 -0
  45. package/src/ai/model-request-settings.ts +98 -0
  46. package/src/ai/protocol.ts +26 -4
  47. package/src/ai/provider-fetch.ts +67 -15
  48. package/src/ai/provider-retry.ts +105 -0
  49. package/src/ai/provider.ts +7 -1
  50. package/src/ai/quota-provider.ts +16 -7
  51. package/src/ai/request-headers.ts +117 -0
  52. package/src/ai/routes.ts +34 -6
  53. package/src/ai/runtime.ts +1 -1
  54. package/src/ai/settings.ts +19 -2
  55. package/src/ai/skill-seeds.ts +31 -3
  56. package/src/ai/skills.ts +26 -0
  57. package/src/ai/solid.ts +1 -1
  58. package/src/ai/store.ts +202 -59
  59. package/src/ai/stream.ts +182 -37
  60. package/src/ai/structured.ts +20 -5
  61. package/src/ai/system-prompt.ts +25 -0
  62. package/src/ai/timeline.ts +9 -11
  63. package/src/ai/tool-call-names.ts +45 -0
  64. package/src/ai/turn-policy.ts +247 -0
  65. package/src/ai/turn-timing.ts +31 -3
  66. package/src/ai/types.ts +36 -5
  67. package/src/api/admin-ai-quotas.ts +36 -1
  68. package/src/api/admin-core-settings.ts +16 -23
  69. package/src/api/admin-outgoing-mail.ts +62 -0
  70. package/src/api/index.ts +2 -0
  71. package/src/cli/admin/ai-quotas.ts +70 -1
  72. package/src/cli/admin/index.ts +6 -0
  73. package/src/cli/admin/outgoing-mail.ts +118 -0
  74. package/src/contracts/app.ts +2 -0
  75. package/src/contracts/index.ts +1 -0
  76. package/src/contracts/outgoing-mail.ts +77 -0
  77. package/src/contracts/registry.ts +4 -0
  78. package/src/services/index.ts +3 -0
  79. package/src/services/notifications/email.ts +16 -26
  80. package/src/services/outgoing-mail/index.ts +19 -0
  81. package/src/services/outgoing-mail/store.ts +286 -0
  82. package/src/services/outgoing-mail/test-send.ts +40 -0
  83. package/src/services/outgoing-mail/transport.ts +13 -0
  84. package/src/services/settings/core-settings.ts +1 -38
  85. package/src/services/settings/store.ts +5 -1
  86. package/src/shared/ai-model-request-settings.ts +21 -0
  87. package/src/shared/ai-platform-prompt.ts +1 -1
  88. package/src/shared/ai-request-options.ts +185 -0
  89. package/src/shared/app-presentation.ts +10 -2
  90. package/src/ssr/admin-navigation.ts +1 -1
  91. package/src/ssr/platform-messages.ts +2 -0
  92. package/src/ssr/workspace-navigation.ts +7 -1
  93. package/src/styles/effects.css +69 -0
@@ -6,6 +6,8 @@ import type { RequestActor } from "../server";
6
6
  import { signInvocationToken } from "../services/identity/invocation-token";
7
7
  import { withActiveIdentitySigner } from "../services/identity/key-ring";
8
8
  import { LOCALE_HEADER } from "../shared/locale";
9
+ import { TIMEZONE_COOKIE } from "../shared/time";
10
+ import { CodeToolFailure } from "./browser-code-contracts";
9
11
  import { resolveAiCapabilityActor } from "./capability-execution";
10
12
  import { CODE_CAPABILITY_TOKEN_HEADER, codeCapabilityOperation } from "./code-capability-transport";
11
13
  import { authorizeCodeExecution } from "./code-execution";
@@ -25,6 +27,7 @@ type Context = ToolContext & {
25
27
  conversationId?: string;
26
28
  turnId?: string;
27
29
  locale?: string;
30
+ timeZone?: string;
28
31
  reportProgress?: (message: string) => Promise<void>;
29
32
  };
30
33
 
@@ -97,6 +100,7 @@ export const runManagedCodeTool =
97
100
  const headers = new Headers({ authorization: `Bearer ${signed.token}`, "content-type": "application/json" });
98
101
  headers.set(CODE_CAPABILITY_TOKEN_HEADER, callback.token);
99
102
  if (context.locale) headers.set(LOCALE_HEADER, context.locale);
103
+ if (context.timeZone) headers.set("cookie", `${TIMEZONE_COOKIE}=${encodeURIComponent(context.timeZone)}`);
100
104
  const response = await fetch(new URL(`/_internal/assistant/tools/${name}`, app.baseUrl), {
101
105
  method: "POST",
102
106
  headers,
@@ -160,7 +164,12 @@ export async function waitForManagedCodeCall(
160
164
  seen.add(approval.id);
161
165
  if (approval.decision === null) await request({ id: approval.id, approved });
162
166
  }
163
- if (state.status === "done") return z.json().parse(state.result);
167
+ if (state.status === "done") {
168
+ // Nessi records a thrown error as a failed tool result the model can act on.
169
+ const failure = CodeToolFailure.safeParse(state.result);
170
+ if (failure.success) throw new Error([failure.data.error, failure.data.guidance].filter(Boolean).join(" "));
171
+ return z.json().parse(state.result);
172
+ }
164
173
  if (state.status === "lost")
165
174
  throw new Error("The isolated code host was lost. The call was not replayed; inspect saved data before starting a new run.");
166
175
  await new Promise<void>((resolve, reject) => {
@@ -48,7 +48,53 @@ const CodeWriteInput = Id.extend({
48
48
  .max(64),
49
49
  });
50
50
 
51
+ const DbName = z
52
+ .string()
53
+ .regex(/^[A-Za-z][A-Za-z0-9_]*$/)
54
+ .max(63);
55
+ const DbColumn = z
56
+ .object({
57
+ name: DbName.refine(
58
+ (name) => !["id", "created_at", "updated_at", "created_by", "updated_by"].includes(name.toLowerCase()),
59
+ "Omit Cloud-managed columns.",
60
+ ),
61
+ type: z.enum(["text", "integer", "real", "boolean", "json", "date", "datetime"]),
62
+ not_null: z.boolean().optional(),
63
+ unique: z.boolean().optional(),
64
+ index: z.boolean().optional(),
65
+ })
66
+ .strict();
67
+ const TableWrite = z.enum(["everyone", "own", "managers"]);
68
+ const DatabaseSchemaInput = z.discriminatedUnion("operation", [
69
+ Id.extend({
70
+ operation: z.literal("tables.create"),
71
+ name: DbName,
72
+ columns: z.array(DbColumn).min(1).max(1000),
73
+ write: TableWrite.default("everyone"),
74
+ }),
75
+ Id.extend({
76
+ operation: z.literal("tables.update"),
77
+ table: DbName,
78
+ changes: z
79
+ .object({
80
+ write: TableWrite.optional(),
81
+ rename: DbName.optional(),
82
+ add_columns: z.array(DbColumn).optional(),
83
+ drop_columns: z.array(DbName).optional(),
84
+ rename_columns: z.record(DbName, DbName).optional(),
85
+ })
86
+ .strict(),
87
+ }),
88
+ Id.extend({ operation: z.literal("schema.get"), table: DbName }),
89
+ Id.extend({ operation: z.literal("tables.list") }),
90
+ ]);
91
+
51
92
  export const CODE_SOURCE_TOOLS = {
93
+ code_database: {
94
+ description:
95
+ "Manage app tables and schema. Manage permission required. Table write rules are everyone (default), own (only creators update/delete), or managers. Cloud sets created_by/updated_by from trusted identity. Schema belongs here, never in runtime scripts.",
96
+ input: DatabaseSchemaInput,
97
+ },
52
98
  code_files: {
53
99
  description:
54
100
  "List authorized files in one explicit chat, Project or App shared store. Returns locations, metadata and nextAfter. Use code_file_stat to obtain the opaque version before copying; does not load bytes into model context.",
@@ -118,18 +164,20 @@ export const CODE_SOURCE_TOOLS = {
118
164
  },
119
165
  code_storage_list: {
120
166
  description:
121
- "Inspect an App's shared files or JSON keys for administration. Manage required. Returns storageRevision, counts and a page of key/bytes/mediaType/version metadata; no file contents. Follow nextAfter.",
167
+ "Inspect shared files/JSON with Manage, or only your personal JSON with scope user and Use. Returns storageRevision, counts and a page of key/bytes/mediaType/version metadata; no file contents. Follow nextAfter.",
122
168
  input: Id.extend({
169
+ scope: z.enum(["shared", "user"]).default("shared").describe("user shows only the signed-in viewer’s personal JSON data."),
123
170
  area: z.enum(["files", "kv"]),
124
171
  after: z.string().max(240).default(""),
125
172
  limit: z.number().int().min(1).max(1000).default(100),
126
- }),
173
+ }).refine((input) => input.scope !== "user" || input.area === "kv", "Personal storage supports JSON keys only."),
127
174
  },
128
175
  code_storage_delete: {
129
176
  description:
130
- "Delete one shared file/JSON key, or clear an explicit storage area, with fresh user review. Manage required. Source, publications and database are preserved. Read code_storage_list first.",
177
+ "Delete one shared file/JSON key or clear an area with fresh review and Manage. Scope user deletes only your personal JSON and requires Use. Source, publications and database are preserved. Read code_storage_list first.",
131
178
  review: true,
132
179
  input: Id.extend({
180
+ scope: z.enum(["shared", "user"]).default("shared"),
133
181
  area: z.enum(["files", "kv", "all"]),
134
182
  key: z.string().min(1).max(240).optional().describe("Delete this exact key; omit to clear the entire area. Not valid with area all."),
135
183
  expectedStorageRevision: z
@@ -137,7 +185,9 @@ export const CODE_SOURCE_TOOLS = {
137
185
  .int()
138
186
  .positive()
139
187
  .describe("storageRevision from code_storage_list; concurrent writes invalidate the review."),
140
- }).refine((input) => input.area !== "all" || input.key === undefined, "all clears both areas and cannot select a key"),
188
+ })
189
+ .refine((input) => input.area !== "all" || input.key === undefined, "all clears both areas and cannot select a key")
190
+ .refine((input) => input.scope !== "user" || input.area === "kv", "Personal storage supports JSON keys only."),
141
191
  },
142
192
  code_access_read: {
143
193
  description:
@@ -27,6 +27,7 @@ const WRITE_TOOLS = new Set([
27
27
  "code_publish",
28
28
  "code_restore",
29
29
  "code_database_export",
30
+ "code_database",
30
31
  ...Object.entries(CODE_SOURCE_TOOLS)
31
32
  .filter(([, definition]) => "review" in definition && definition.review)
32
33
  .map(([name]) => name),
@@ -62,12 +63,17 @@ export function createCodeSourceTool(name: CodeSourceToolName) {
62
63
  code_database_clear: "clear",
63
64
  code_database_reset: "reset",
64
65
  };
65
- const databaseOperation = databaseOperations[name];
66
+ const databaseOperation =
67
+ name === "code_database" ? z.object({ operation: z.string() }).parse(input).operation : databaseOperations[name];
66
68
  let authorizeDatabase: (() => Promise<void>) | undefined;
67
69
  if (context.turnId && databaseOperation) {
68
70
  const config = await aiConversations.getTurnRunConfig({ conversationId: context.conversationId, turnId: context.turnId });
69
71
  if (config?.kind !== "compact" && config?.background) {
70
- const { id } = z.object({ id: z.string() }).parse(input);
72
+ const {
73
+ id,
74
+ table,
75
+ name: tableName,
76
+ } = z.object({ id: z.string(), table: z.string().optional(), name: z.string().optional() }).parse(input);
71
77
  const conversationId = context.conversationId,
72
78
  turnId = context.turnId;
73
79
  authorizeDatabase = async () => {
@@ -76,7 +82,7 @@ export function createCodeSourceTool(name: CodeSourceToolName) {
76
82
  await aiChatTasks.authorizeRuntime({
77
83
  mandate: config.mandate,
78
84
  kind: "database",
79
- input: { resourceId: id, operation: databaseOperation },
85
+ input: { resourceId: id, operation: databaseOperation, table: table ?? tableName },
80
86
  });
81
87
  };
82
88
  }
@@ -163,6 +169,7 @@ export const createCodeSourceTools = () => [
163
169
  createCodeSourceTool("code_file_stat"),
164
170
  createCodeSourceTool("code_file_copy"),
165
171
  createCodeSourceTool("code_database_export"),
172
+ createCodeSourceTool("code_database"),
166
173
  createCodeSourceTool("code_manage_read"),
167
174
  createCodeSourceTool("code_delete"),
168
175
  createCodeSourceTool("code_database_read"),
@@ -1,6 +1,7 @@
1
1
  import { sql } from "bun";
2
2
  import { toPgTextArray } from "../services/postgres";
3
3
  import { decryptValue, encryptValue } from "../services/settings/crypto";
4
+ import type { AiRequestHeaderPatch } from "./request-headers";
4
5
 
5
6
  type SqlClient = typeof sql;
6
7
 
@@ -90,7 +91,12 @@ export const pruneAiCredentials = async (keepProfileIds: readonly string[], db:
90
91
  */
91
92
  export const splitAiProfileCredentials = (
92
93
  rawValue: unknown,
93
- ): { profilesJson: string; credentials: Array<{ profileId: string; secret: string }>; profileIds: string[] } | null => {
94
+ ): {
95
+ profilesJson: string;
96
+ credentials: Array<{ profileId: string; secret: string }>;
97
+ profileIds: string[];
98
+ requestHeaders: AiRequestHeaderPatch[];
99
+ } | null => {
94
100
  if (typeof rawValue !== "string") return null;
95
101
  const text = rawValue;
96
102
  let parsed: unknown;
@@ -104,21 +110,29 @@ export const splitAiProfileCredentials = (
104
110
  const profiles: unknown[] = [];
105
111
  const credentials: Array<{ profileId: string; secret: string }> = [];
106
112
  const profileIds: string[] = [];
113
+ const requestHeaders: AiRequestHeaderPatch[] = [];
107
114
 
108
115
  for (const entry of parsed) {
109
116
  if (!entry || typeof entry !== "object") {
110
117
  profiles.push(entry);
111
118
  continue;
112
119
  }
113
- const { apiKey, credentialSetting: _legacy, ...profile } = entry as Record<string, unknown>;
120
+ const {
121
+ apiKey,
122
+ requestHeaders: headerPatch,
123
+ requestHeaderNames: _headerNames,
124
+ credentialSetting: _legacy,
125
+ ...profile
126
+ } = entry as Record<string, unknown>;
114
127
  const profileId = typeof profile.id === "string" ? profile.id.trim() : "";
115
128
  if (profileId) {
116
129
  profile.id = profileId;
117
130
  profileIds.push(profileId);
131
+ if (headerPatch !== undefined) requestHeaders.push({ profileId, patch: headerPatch });
118
132
  if (typeof apiKey === "string" && apiKey.trim()) credentials.push({ profileId, secret: apiKey.trim() });
119
133
  }
120
134
  profiles.push(profile);
121
135
  }
122
136
 
123
- return { profilesJson: JSON.stringify(profiles), credentials, profileIds };
137
+ return { profilesJson: JSON.stringify(profiles), credentials, profileIds, requestHeaders };
124
138
  };
@@ -2,9 +2,9 @@
2
2
  import type { AiSkillTemplate } from "./skills";
3
3
  export const ASSISTANT_DATA_ANALYSIS_SKILL = {
4
4
  "key": "assistant:data-analysis",
5
- "version": 6,
5
+ "version": 7,
6
6
  "name": "assistant-data-analysis",
7
7
  "description": "Analyze source data, explain metrics and comparisons, and build evidence-backed reports or interactive dashboards in Assistant Code Mode. Use for multi-step analysis, data exploration, and dashboards; a simple chart only needs the Code Mode charts reference.",
8
- "instructions": "# Analyze data and deliver an inspectable result\n\nStart with the question the reader needs to answer. Choose a direct answer,\nan interactive visualization in this chat, an exported file, or a reusable Studio app accordingly.\nFor a one-time visual analysis, prefer a chat visualization; filters and buttons\ndo not by themselves require a Studio App.\nLoad `assistant-code-mode` for execution and read its `references/analytics.md`\nfor the built-in UI. Loading this skill does not install a library or grant access.\n\n## Keep a working plan\n\nFor work with several real steps, use `todo_write` to keep a short chat plan.\nReplace the full `todos` list each time; give each item a stable `id`, actionable\n`content`, and `status` (`pending`, `in_progress`, `completed`, or `cancelled`).\nAt most one step is active. Update as work changes, including user corrections;\nmark a step completed only after doing and checking it. Preserve exact commands\nwhen they matter. Skip this tool for a simple calculation or conversational reply.\n\nFor analysis, useful steps are reconcile source data, build the analysis, and\nverify totals, filters and conclusions. A rendered dashboard alone is not proof\nthat its numbers are correct. Keep blocked source work open and explain why.\n\n## Establish the data\n\nIdentify the actual source, unit of observation, time window, timezone, and\nlatest complete period. Read files or discover the relevant Cloud capabilities\nbefore selecting fields. External APIs use `http.fetch`; credentials are entered\nonly through the trusted `code_secret` dialog.\n\nInspect a bounded sample, missing values, duplicate keys, types, and coverage.\nReconcile totals and join cardinalities before drawing conclusions. Keep raw\nnumbers separate from display formatting. Distinguish zero from unavailable data;\nstate exclusions and denominator choices. Do not substitute fixture data for a\nblocked source unless the user requested a mockup.\n\nKeep the source query or transformation in the saved source or an accompanying\nfile so another run can reproduce the result. Preserve the source identity and\nretrieval timestamp, without credentials or credential-bearing URLs.\n\n## Build one consistent analysis\n\nDerive charts, metrics, and tables from the same reviewed data. Aggregate large\ninputs before crossing the UI bridge. Chart Explorer rows identify selectable\nentities with stable string keys; derived bins and groups need explicit mark\nmappings. Shared keys across charts mean the same entity, not equal values.\n\nChoose the simplest chart that answers the question. Use tables for exact\nrecords, bars for category comparisons, lines for ordered trends, and distributions\nfor spread. State units, comparison windows, and whether a percent is a fraction\nor an already scaled number. Do not imply causation from correlation.\n\nLead reports with the finding, then supporting evidence and limitations.\nLead dashboards with the important measurements, then trends and diagnostic\nbreakdowns. Use shared filters only when they affect all claimed views. Keep the\ninitial view useful without interaction. Avoid unrelated metrics added merely to\nfill a grid.\n\nAttach source context to Explorer data: `mode`, `asOf`, `sources`, and relevant\n`status`/`note`. A timestamp records when the data was retrieved; it does not prove\nthat the upstream source is complete. Label partial or fixture data visibly.\n\n## Validate before delivery\n\nRun the actual source with `code_run`. Inspect the result; use `code_interact`\nwith structured UI events to test filters, chart/table switching, selection,\nempty results, and recovery from a failed load. Reconcile displayed values with\nthe reviewed totals and check that filters describe the data actually displayed.\nA successful schema validation does not establish analytical correctness.\n\nValidate the actual presented source (the saved revision for Apps), rather than pasting its formulas into a\nsecond test script. Keep an independent expectation from the input data: row\ncounts, unmatched joins, totals and representative boundary cases. For targets,\nstate their grain (for example month × region) and aggregate each target once;\nmultiple selected regions must sum their distinct targets. Compare inspected raw\nKPI values and plotted series with independent expectations. A formatted value\nmatching after rounding does not validate the raw ratio. Never round fractions\nbefore passing them to percent-formatted controls. Preserve precision\nuntil display formatting. Test reset, one/multiple/all selections, empty results,\nand complete versus partial periods. A newly generated timestamp is not source\nfreshness: keep the real retrieval or file-snapshot timestamp stable.\n\nFor a Studio App data snapshot, export the validated dataset with `files.save` and\n`code_export`, then copy its exact path/version into the resource with\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nObtain the exact reference with `code_file_stat`; importing private files into\nApp source receives fresh review.\nImport that file in the app. Never rebuild a truncated dataset by copying tool\noutput. Keep transformations and source identity alongside the snapshot.\n\nHuman approval and uncertain HTTP outcomes follow the Code Mode HTTP contract.\nDo not replay an external mutation to refresh a chart. Separate local filtering\nfrom external loading; an Apply button can avoid a request for every slider move.\n\n## Save, share, and hand off\n\nFor chat visualizations, test `code_run` and relevant `code_interact` controls,\nthen deliver with `code_present({runId,title})`. A successful run alone is not\nvisible to the user. Retain source identity, reviewed input data and their real\nretrieval timestamp. Put writes and external reloads behind explicit buttons;\nopening an old result must not repeat earlier actions. The user can download the\ncurrent view as PDF/HTML or an individual chart as SVG.\n\nFor reusable Studio Apps, reuse one Cloud resource for later revisions of the same report or dashboard.\nSave its source, test that revision, and use the normal Code Mode publication\nworkflow when publication is requested. Publishing a version and granting reader\naccess are separate operations. Preserve existing access; a dashboard request\ndoes not authorize broadening it. Personal secrets are never copied to readers.\n\nA published source version is not automatically a frozen data snapshot. A frozen\nreport must retain the reviewed data explicitly in its authorized resource or\noutput file. A live app must implement its loader and display the retrieval time;\nloading once is not continuous monitoring. No background refresh exists unless\nimplemented through an appropriate supported workflow.\n\nPresent the chat visualization, resource link or exported file, the data timestamp, and material\ncoverage limitations. Say whether it is a snapshot or reloads from its sources.\nIf publication fails, retain the tested resource and report the failed stage;\ndo not claim success or create a different public destination. Sites-specific\nhosting, access policies, and editor storage are not part of this Cloud workflow.",
8
+ "instructions": "# Analyze data and deliver an inspectable result\n\nStart with the question the reader needs to answer. Choose a direct answer,\nan interactive visualization in this chat, an exported file, or a reusable Studio app accordingly.\nFor a one-time visual analysis, prefer a chat visualization; filters and buttons\ndo not by themselves require a Studio App.\nLoad `assistant-code-mode` for execution and read its `references/analytics.md`\nfor the built-in UI. Loading this skill does not install a library or grant access.\n\n## Keep a working plan\n\nFor work with several real steps, use `todo_write` to keep a short chat plan.\nReplace the full `todos` list each time; give each item a stable `id`, actionable\n`content`, and `status` (`pending`, `in_progress`, `completed`, or `cancelled`).\nAt most one step is active. Update as work changes, including user corrections;\nmark a step completed only after doing and checking it. Preserve exact commands\nwhen they matter. Skip this tool for a simple calculation or conversational reply.\n\nFor analysis, useful steps are reconcile source data, build the analysis, and\nverify totals, filters and conclusions. A rendered dashboard alone is not proof\nthat its numbers are correct. Keep blocked source work open and explain why.\n\n## Establish the data\n\nIdentify the actual source, unit of observation, time window, timezone, and\nlatest complete period. Read files or discover the relevant Cloud capabilities\nbefore selecting fields. External APIs use `cloud.http.fetch`; credentials are entered\nonly through the trusted `code_secret` dialog.\n\nInspect a bounded sample, missing values, duplicate keys, types, and coverage.\nReconcile totals and join cardinalities before drawing conclusions. Keep raw\nnumbers separate from display formatting. Distinguish zero from unavailable data;\nstate exclusions and denominator choices. Do not substitute fixture data for a\nblocked source unless the user requested a mockup.\n\nKeep the source query or transformation in the saved source or an accompanying\nfile so another run can reproduce the result. Preserve the source identity and\nretrieval timestamp, without credentials or credential-bearing URLs.\n\n## Build one consistent analysis\n\nDerive charts, metrics, and tables from the same reviewed data. Aggregate large\ninputs before crossing the UI bridge. Chart Explorer rows identify selectable\nentities with stable string keys; derived bins and groups need explicit mark\nmappings. Shared keys across charts mean the same entity, not equal values.\n\nChoose the simplest chart that answers the question. Use tables for exact\nrecords, bars for category comparisons, lines for ordered trends, and distributions\nfor spread. State units, comparison windows, and whether a percent is a fraction\nor an already scaled number. Do not imply causation from correlation.\n\nLead reports with the finding, then supporting evidence and limitations.\nLead dashboards with the important measurements, then trends and diagnostic\nbreakdowns. Use shared filters only when they affect all claimed views. Keep the\ninitial view useful without interaction. Avoid unrelated metrics added merely to\nfill a grid.\n\nAttach source context to Explorer data: `mode`, `asOf`, `sources`, and relevant\n`status`/`note`. A timestamp records when the data was retrieved; it does not prove\nthat the upstream source is complete. Label partial or fixture data visibly.\n\n## Validate before delivery\n\nRun the actual source with `code_run`. Inspect the result; use `code_interact`\nwith structured UI events to test filters, chart/table switching, selection,\nempty results, and recovery from a failed load. Reconcile displayed values with\nthe reviewed totals and check that filters describe the data actually displayed.\nA successful schema validation does not establish analytical correctness.\n\nValidate the actual presented source (the saved revision for Apps), rather than pasting its formulas into a\nsecond test script. Keep an independent expectation from the input data: row\ncounts, unmatched joins, totals and representative boundary cases. For targets,\nstate their grain (for example month × region) and aggregate each target once;\nmultiple selected regions must sum their distinct targets. Compare inspected raw\nKPI values and plotted series with independent expectations. A formatted value\nmatching after rounding does not validate the raw ratio. Never round fractions\nbefore passing them to percent-formatted controls. Preserve precision\nuntil display formatting. Test reset, one/multiple/all selections, empty results,\nand complete versus partial periods. A newly generated timestamp is not source\nfreshness: keep the real retrieval or file-snapshot timestamp stable.\n\nFor a Studio App data snapshot, export the validated dataset with `cloud.download` and\n`code_export`, then copy its exact path/version into the resource with\n`code_write({id,expectedRevision,files:[{path:\"data.json\",fromFile:reference}]})`.\nObtain the exact reference with `code_file_stat`; importing private files into\nApp source receives fresh review.\nImport that file in the app. Never rebuild a truncated dataset by copying tool\noutput. Keep transformations and source identity alongside the snapshot.\n\nHuman approval and uncertain HTTP outcomes follow the Code Mode HTTP contract.\nDo not replay an external mutation to refresh a chart. Separate local filtering\nfrom external loading; an Apply button can avoid a request for every slider move.\n\n## Save, share, and hand off\n\nFor chat visualizations, test `code_run` and relevant `code_interact` controls,\nthen deliver with `code_present({runId,title})`. A successful run alone is not\nvisible to the user. Retain source identity, reviewed input data and their real\nretrieval timestamp. Put writes and external reloads behind explicit buttons;\nopening an old result must not repeat earlier actions. The user can download the\ncurrent view as PDF/HTML or an individual chart as SVG.\n\nFor reusable Studio Apps, reuse one Cloud resource for later revisions of the same report or dashboard.\nSave its source, test that revision, and use the normal Code Mode publication\nworkflow when publication is requested. Publishing a version and granting reader\naccess are separate operations. Preserve existing access; a dashboard request\ndoes not authorize broadening it. Personal secrets are never copied to readers.\n\nA published source version is not automatically a frozen data snapshot. A frozen\nreport must retain the reviewed data explicitly in its authorized resource or\noutput file. A live app must implement its loader and display the retrieval time;\nloading once is not continuous monitoring. No background refresh exists unless\nimplemented through an appropriate supported workflow.\n\nPresent the chat visualization, resource link or exported file, the data timestamp, and material\ncoverage limitations. Say whether it is a snapshot or reloads from its sources.\nIf publication fails, retain the tested resource and report the failed stage;\ndo not claim success or create a different public destination. Sites-specific\nhosting, access policies, and editor storage are not part of this Cloud workflow.",
9
9
  "extraFrontmatter": {}
10
10
  } satisfies AiSkillTemplate;
@@ -110,7 +110,7 @@ export const createCloudAiCodeTools = () => [
110
110
  defineAiTool({
111
111
  name: "code_secret",
112
112
  description:
113
- "Open a trusted Secret input dialog. The user enters the value directly into encrypted Assistant storage; only configured/name returns. Never ask for a credential in chat, survey, app controls or code_interact. Personal secrets are scoped to this chat or resource and bound to the exact HTTPS origin, header and prefix. Use secret(name,{prefix}) in http.fetch headers; load assistant-code-mode for HTTP details. Web UI required for secret entry; stored secrets also work from CLI.",
113
+ "Open a trusted Secret input dialog. The user enters the value directly into encrypted Assistant storage; only configured/name returns. Never ask for a credential in chat, survey, app controls or code_interact. Personal secrets are scoped to this chat or resource and bound to the exact HTTPS origin, header and prefix. Use cloud.http.secret(name,{prefix}) in cloud.http.fetch headers; load assistant-code-mode for HTTP details. Web UI required for secret entry; stored secrets also work from CLI.",
114
114
  inputSchema: CodeSecretInput,
115
115
  outputSchema: z.object({ configured: z.boolean(), name: z.string() }),
116
116
  approval: "never",
@@ -128,7 +128,7 @@ export const createCloudAiCodeTools = () => [
128
128
  promptHint:
129
129
  "For file analysis, data transformations or combining Cloud data, run a short one-off script with code_run. Load assistant-code-mode for its runtime APIs; no saved app is required.",
130
130
  description:
131
- "Run a saved resource id OR one-off code in the isolated worker. Optional resourceId binds one-off code to existing app data (Manage required), without editing its source. Includes capabilities.run(name,input) to chain Cloud capabilities in JavaScript; load assistant-code-mode for its runtime APIs. Scripts accept current chat inputPaths; app test runs use them only as explicit picker fixtures. Returns output, logs and UI state for agent inspection only; use code_present to show a one-off visualization to the user. Local test storage is temporary; shared data, database writes and capability effects are real and keep normal permissions and approvals.",
131
+ "Run a saved resource id OR one-off code in the isolated worker. Optional resourceId binds one-off code to existing app data (Manage required), without editing its source. Includes cloud.capabilities.run(name,input) to chain Cloud capabilities in JavaScript; load assistant-code-mode for its runtime APIs. Scripts accept current chat inputPaths; app test runs use them only as explicit picker fixtures. Returns output, logs and UI state for agent inspection only; use code_present to show a one-off visualization to the user. Test runs use the app’s real shared and personal data; database writes and capability effects keep normal permissions and approvals.",
132
132
  inputSchema: CodeRunInput,
133
133
  outputSchema: z.json(),
134
134
  approval: "never",