History

Enhance storage management and clarify deployment guidelines

- Updated `.env.example` to specify that `agentfs-local` is for self-hosted deployments only, while hosted Convex deployments should use `agentfs-cloudflare`. - Expanded `README.md` to include detailed environment variable descriptions for agent storage, including new variables for Cloudflare integration. - Improved the `StoragePanel` component to manage storage instances more effectively, including state handling for file previews and SQL queries. - Added new storage-related actions in `executorNode.ts` for listing directories, reading files, and querying SQL, ensuring better integration with the storage provider. - Enhanced validation and error handling in storage-related database functions to prevent unsupported configurations in hosted environments. - Updated tool contracts to include new storage capabilities and improved JSON schema handling for key-value operations.

Rhys Sullivan committed 8 months ago

Commit b43eea01 parent 7c7b2ca18 files · +2416 −2055Browse files

Unified · Split

18 files
  1. .env.example +2 −0
  2. executor/README.md +5 −0
  3. executor/apps/web/src/components/tools/storage.tsx +821 −130
  4. executor/apps/web/src/components/tools/view.tsx +3 −1
  5. executor/packages/database/convex/_generated/api.d.ts +97 −1837
  6. executor/packages/database/convex/database.ts +2 −0
  7. executor/packages/database/convex/database/storage_instances.ts +20 −0
  8. executor/packages/database/convex/database/tasks.ts +140 −1
  9. executor/packages/database/convex/executorNode.ts +195 −0
  10. executor/packages/database/src/database/validators.ts +6 −0
  11. executor/packages/database/src/runtime/base_tools.ts +136 −4
  12. executor/packages/database/src/runtime/storage_provider.ts +43 −3
  13. executor/packages/database/src/runtime/storage_tool_contracts.ts +265 −14
  14. executor/packages/database/src/runtime/storage_tools.ts +454 −27
  15. executor/packages/database/src/runtime/tool_call_resolution.ts +3 −1
  16. executor/packages/database/src/runtime/tool_invocation.ts +204 −35
  17. executor/packages/runner-sandbox-host/src/index.ts +5 −2
  18. executor/scripts/dev/storage/smoke.execute-flow.ts +15 −0
.env.example+2−0
@@ -15,6 +15,8 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 14 unmodified lines
1515
1616 # ── Agent storage backend (optional) ──
1717 # Default: agentfs-local
18+# NOTE: agentfs-local is for self-hosted/single-host deployments only.
19+# Hosted Convex deployments must use AGENT_STORAGE_PROVIDER=agentfs-cloudflare.
1820 # AGENT_STORAGE_PROVIDER=agentfs-local
1921 # AGENT_STORAGE_ROOT=/tmp/executor-agentfs
2022 # For Cloudflare Durable Objects storage proxy (served by runner-sandbox-host):
executor/README.md+5−0
@@ -148,6 +148,10 @@ Important env vars (see root `.env.example` for the base template): · 147 unmodified lines
148148 - `CONVEX_URL`
149149 - `CONVEX_SITE_URL`
150150 - `DANGEROUSLY_ALLOW_LOCAL_VM` (`1` in local/self-hosted deployments to allow `local-bun` runtime; production leaves unset for Cloudflare-first behavior)
151+- Agent storage:
152+ - `AGENT_STORAGE_PROVIDER` (`agentfs-local` for self-hosted/single-host only; hosted Convex requires `agentfs-cloudflare`)
153+ - `AGENT_STORAGE_ROOT` (optional local storage root for `agentfs-local`)
154+ - `CLOUDFLARE_SANDBOX_RUN_URL` and `CLOUDFLARE_SANDBOX_AUTH_TOKEN` (required when using `agentfs-cloudflare`)
151155 - WorkOS (optional auth/org features):
152156 - `WORKOS_CLIENT_ID`
153157 - `WORKOS_API_KEY`
@@ -225,4 +229,5 @@ executor/ · 71 unmodified lines
225229 - `401` on `/mcp`: verify your bearer token issuer matches `MCP_AUTHORIZATION_SERVER`.
226230 - `401` on `/mcp/anonymous`: pass `API_KEY` from the setup card as `Authorization: Bearer <API_KEY>` (or `x-api-key`).
227231 - Web UI cannot load data: verify `CONVEX_URL` / `CONVEX_SITE_URL` and that Convex dev is running.
232+- `ENOENT` after `fs.write` on hosted Convex: `agentfs-local` is unsupported on hosted Convex because local filesystem state is not shared across workers. Use `AGENT_STORAGE_PROVIDER=agentfs-cloudflare`.
228233 - Release build missing managed artifacts: run `bun run build:release` and verify `executor/dist/release/` contains `executor-*.tar.gz`, `executor-web-*.tar.gz`, and `executor-runtime-*.tar.gz`.
executor/apps/web/src/components/tools/storage.tsx+821−130
@@ -1,12 +1,24 @@
11 "use client";
22
3−import { useMemo, useState } from "react";
4−import { Database, FolderOpen, Plus, Trash2, Power } from "lucide-react";
3+import { useEffect, useMemo, useState } from "react";
4+import { useAction } from "convex/react";
5+import {
6+ Database,
7+ FolderOpen,
8+ HardDrive,
9+ KeyRound,
10+ Play,
11+ Plus,
12+ Power,
13+ Table,
14+ Trash2,
15+} from "lucide-react";
516 import { Badge } from "@/components/ui/badge";
617 import { Button } from "@/components/ui/button";
718 import { Input } from "@/components/ui/input";
819 import { Skeleton } from "@/components/ui/skeleton";
920 import type { StorageDurability, StorageInstanceRecord, StorageScopeType } from "@/lib/types";
21+import { convexApi } from "@/lib/convex-api";
1022 import { cn } from "@/lib/utils";
1123
1224 type CreateStorageArgs = {
@@ -16,6 +28,49 @@ type CreateStorageArgs = { · 3 unmodified lines
1628 ttlHours?: number;
1729 };
1830
31+type StorageDirectoryEntry = {
32+ name: string;
33+ type: "file" | "directory" | "symlink" | "unknown";
34+ size?: number;
35+ mtime?: number;
36+};
37+
38+type StorageSqlResult = {
39+ mode: "read" | "write";
40+ rows?: Record<string, unknown>[];
41+ rowCount: number;
42+ changes?: number;
43+};
44+
45+type StorageSqlObject = {
46+ name: string;
47+ type: "table" | "view" | "unknown";
48+};
49+
50+const USER_TABLES_QUERY = [
51+ "SELECT name",
52+ "FROM sqlite_master",
53+ "WHERE type = 'table'",
54+ " AND name NOT LIKE 'sqlite_%'",
55+ " AND name NOT IN ('fs_config', 'fs_data', 'fs_dentry', 'fs_inode', 'fs_symlink', 'kv_store')",
56+ "ORDER BY name",
57+].join("\n");
58+
59+const ALL_OBJECTS_QUERY = "SELECT name, type FROM sqlite_master ORDER BY name";
60+const KV_DATA_QUERY = "SELECT key, value, updated_at FROM kv_store ORDER BY key LIMIT 200";
61+const FS_ENTRIES_QUERY = "SELECT * FROM fs_dentry LIMIT 200";
62+const SQL_OBJECTS_QUERY = "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY name";
63+
64+const INTERNAL_SQL_OBJECT_NAMES = new Set([
65+ "fs_config",
66+ "fs_data",
67+ "fs_dentry",
68+ "fs_inode",
69+ "fs_symlink",
70+ "kv_store",
71+ "sqlite_sequence",
72+]);
73+
1974 function prettyBytes(value?: number): string {
2075 if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2176 return "-";
@@ -39,7 +94,74 @@ function asLocalDate(value?: number): string { · 17 unmodified lines
3994 return new Date(value).toLocaleString();
4095 }
4196
97+function joinStoragePath(basePath: string, name: string): string {
98+ const base = (basePath.trim() || "/").replace(/\/+$/, "");
99+ if (!base || base === "/") {
100+ return `/${name}`;
101+ }
102+ return `${base}/${name}`;
103+}
104+
105+function previewJson(value: unknown): string {
106+ try {
107+ return JSON.stringify(value, null, 2);
108+ } catch {
109+ return String(value);
110+ }
111+}
112+
113+function truncateText(text: string, maxChars: number): string {
114+ if (text.length <= maxChars) {
115+ return text;
116+ }
117+ return `${text.slice(0, Math.max(1, maxChars))}\n\n...truncated...`;
118+}
119+
120+function sqlCellText(value: unknown): string {
121+ if (value === null) return "NULL";
122+ if (value === undefined) return "";
123+ if (typeof value === "string") return value;
124+ if (typeof value === "number" || typeof value === "boolean") return String(value);
125+ return previewJson(value);
126+}
127+
128+function collectSqlColumns(rows: Array<Record<string, unknown>>): string[] {
129+ const seen = new Set<string>();
130+ const columns: string[] = [];
131+ for (const row of rows) {
132+ for (const key of Object.keys(row)) {
133+ if (seen.has(key)) continue;
134+ seen.add(key);
135+ columns.push(key);
136+ }
137+ }
138+ return columns;
139+}
140+
141+function isInternalSqlObject(name: string): boolean {
142+ if (INTERNAL_SQL_OBJECT_NAMES.has(name)) {
143+ return true;
144+ }
145+ if (name.startsWith("fs_")) {
146+ return true;
147+ }
148+ return name.startsWith("sqlite_");
149+}
150+
151+function sqlObjectType(value: unknown): "table" | "view" | "unknown" {
152+ if (value === "table" || value === "view") {
153+ return value;
154+ }
155+ return "unknown";
156+}
157+
158+function escapeSqlIdentifier(value: string): string {
159+ return value.replaceAll('"', '""');
160+}
161+
42162 export function StoragePanel({
163+ workspaceId,
164+ sessionId,
43165 instances,
44166 loading,
45167 creating,
@@ -48,6 +170,8 @@ export function StoragePanel({ · 2 unmodified lines
48170 onClose,
49171 onDelete,
50172 }: {
173+ workspaceId?: string;
174+ sessionId?: string;
51175 instances: StorageInstanceRecord[];
52176 loading: boolean;
53177 creating: boolean;
@@ -60,12 +184,252 @@ export function StoragePanel({ · 6 unmodified lines
60184 const [durability, setDurability] = useState<StorageDurability>("ephemeral");
61185 const [purpose, setPurpose] = useState("");
62186 const [ttlHours, setTtlHours] = useState("24");
187+ const [selectedInstanceId, setSelectedInstanceId] = useState<string | undefined>(undefined);
188+ const [activeInspectorTab, setActiveInspectorTab] = useState<"fs" | "kv" | "sql">("fs");
189+
190+ const [fsPath, setFsPath] = useState("/");
191+ const [fsEntries, setFsEntries] = useState<StorageDirectoryEntry[]>([]);
192+ const [fsLoading, setFsLoading] = useState(false);
193+ const [fsError, setFsError] = useState<string | null>(null);
194+ const [filePreviewPath, setFilePreviewPath] = useState<string | null>(null);
195+ const [filePreviewContent, setFilePreviewContent] = useState<string>("");
196+ const [filePreviewBytes, setFilePreviewBytes] = useState<number | null>(null);
197+ const [filePreviewLoading, setFilePreviewLoading] = useState(false);
198+
199+ const [kvPrefix, setKvPrefix] = useState("");
200+ const [kvLimit, setKvLimit] = useState("100");
201+ const [kvItems, setKvItems] = useState<Array<{ key: string; value: unknown }>>([]);
202+ const [kvLoading, setKvLoading] = useState(false);
203+ const [kvError, setKvError] = useState<string | null>(null);
204+
205+ const [sqlText, setSqlText] = useState(USER_TABLES_QUERY);
206+ const [sqlMaxRows, setSqlMaxRows] = useState("200");
207+ const [sqlViewMode, setSqlViewMode] = useState<"table" | "json">("table");
208+ const [sqlResult, setSqlResult] = useState<StorageSqlResult | null>(null);
209+ const [sqlLoading, setSqlLoading] = useState(false);
210+ const [sqlError, setSqlError] = useState<string | null>(null);
211+ const [sqlObjects, setSqlObjects] = useState<StorageSqlObject[]>([]);
212+ const [sqlObjectsLoading, setSqlObjectsLoading] = useState(false);
213+ const [sqlObjectsError, setSqlObjectsError] = useState<string | null>(null);
214+ const [sqlShowInternalObjects, setSqlShowInternalObjects] = useState(false);
215+ const [selectedSqlObjectName, setSelectedSqlObjectName] = useState<string | null>(null);
216+
217+ const listDirectory = useAction(convexApi.executorNode.storageListDirectory);
218+ const readFileAction = useAction(convexApi.executorNode.storageReadFile);
219+ const listKv = useAction(convexApi.executorNode.storageListKv);
220+ const querySql = useAction(convexApi.executorNode.storageQuerySql);
63221
64222 const visibleInstances = useMemo(
65223 () => [...instances].sort((a, b) => b.updatedAt - a.updatedAt),
66224 [instances],
67225 );
68226
227+ const selectedInstance = useMemo(
228+ () => visibleInstances.find((instance) => instance.id === selectedInstanceId) ?? visibleInstances[0],
229+ [selectedInstanceId, visibleInstances],
230+ );
231+
232+ useEffect(() => {
233+ if (!selectedInstance && visibleInstances.length === 0) {
234+ setSelectedInstanceId(undefined);
235+ return;
236+ }
237+ if (!selectedInstance && visibleInstances.length > 0) {
238+ setSelectedInstanceId(visibleInstances[0]?.id);
239+ return;
240+ }
241+ if (selectedInstance && selectedInstanceId !== selectedInstance.id) {
242+ setSelectedInstanceId(selectedInstance.id);
243+ }
244+ }, [selectedInstance, selectedInstanceId, visibleInstances]);
245+
246+ const canInspect = Boolean(workspaceId && selectedInstance);
247+ const sqlRows = useMemo(() => (sqlResult?.rows ?? []) as Array<Record<string, unknown>>, [sqlResult]);
248+ const sqlColumns = useMemo(() => collectSqlColumns(sqlRows), [sqlRows]);
249+ const visibleSqlObjects = useMemo(
250+ () => sqlObjects.filter((entry) => sqlShowInternalObjects || !isInternalSqlObject(entry.name)),
251+ [sqlObjects, sqlShowInternalObjects],
252+ );
253+
254+ const refreshDirectory = async (nextPath?: string) => {
255+ if (!workspaceId || !selectedInstance) {
256+ return;
257+ }
258+
259+ const path = (nextPath ?? fsPath).trim() || "/";
260+ setFsLoading(true);
261+ setFsError(null);
262+ try {
263+ const result = await listDirectory({
264+ workspaceId: workspaceId as never,
265+ sessionId,
266+ instanceId: selectedInstance.id,
267+ path,
268+ });
269+ setFsPath(result.path);
270+ setFsEntries(result.entries as StorageDirectoryEntry[]);
271+ } catch (error) {
272+ setFsError(error instanceof Error ? error.message : "Failed to list directory");
273+ setFsEntries([]);
274+ } finally {
275+ setFsLoading(false);
276+ }
277+ };
278+
279+ const readFilePreview = async (path: string) => {
280+ if (!workspaceId || !selectedInstance) {
281+ return;
282+ }
283+
284+ setFilePreviewLoading(true);
285+ try {
286+ const result = await readFileAction({
287+ workspaceId: workspaceId as never,
288+ sessionId,
289+ instanceId: selectedInstance.id,
290+ path,
291+ encoding: "utf8",
292+ });
293+ setFilePreviewPath(result.path);
294+ setFilePreviewContent(truncateText(result.content, 20_000));
295+ setFilePreviewBytes(result.bytes);
296+ } catch (error) {
297+ setFilePreviewPath(path);
298+ setFilePreviewContent(error instanceof Error ? error.message : "Failed to read file");
299+ setFilePreviewBytes(null);
300+ } finally {
301+ setFilePreviewLoading(false);
302+ }
303+ };
304+
305+ const refreshKv = async () => {
306+ if (!workspaceId || !selectedInstance) {
307+ return;
308+ }
309+
310+ const parsedLimit = Number.parseInt(kvLimit, 10);
311+ setKvLoading(true);
312+ setKvError(null);
313+ try {
314+ const result = await listKv({
315+ workspaceId: workspaceId as never,
316+ sessionId,
317+ instanceId: selectedInstance.id,
318+ prefix: kvPrefix,
319+ limit: Number.isFinite(parsedLimit) ? parsedLimit : 100,
320+ });
321+ setKvItems(result.items as Array<{ key: string; value: unknown }>);
322+ } catch (error) {
323+ setKvError(error instanceof Error ? error.message : "Failed to list key-value entries");
324+ setKvItems([]);
325+ } finally {
326+ setKvLoading(false);
327+ }
328+ };
329+
330+ const runSql = async (queryOverride?: string) => {
331+ if (!workspaceId || !selectedInstance) {
332+ return;
333+ }
334+
335+ const sql = (queryOverride ?? sqlText).trim();
336+ if (!sql) {
337+ return;
338+ }
339+
340+ const parsedMaxRows = Number.parseInt(sqlMaxRows, 10);
341+ setSqlLoading(true);
342+ setSqlError(null);
343+ try {
344+ const result = await querySql({
345+ workspaceId: workspaceId as never,
346+ sessionId,
347+ instanceId: selectedInstance.id,
348+ sql,
349+ maxRows: Number.isFinite(parsedMaxRows) ? parsedMaxRows : 200,
350+ });
351+ if (queryOverride) {
352+ setSqlText(sql);
353+ }
354+ setSqlResult(result as StorageSqlResult);
355+ } catch (error) {
356+ setSqlError(error instanceof Error ? error.message : "Failed to query SQLite");
357+ setSqlResult(null);
358+ } finally {
359+ setSqlLoading(false);
360+ }
361+ };
362+
363+ const refreshSqlObjects = async () => {
364+ if (!workspaceId || !selectedInstance) {
365+ return [] as StorageSqlObject[];
366+ }
367+
368+ setSqlObjectsLoading(true);
369+ setSqlObjectsError(null);
370+ try {
371+ const result = await querySql({
372+ workspaceId: workspaceId as never,
373+ sessionId,
374+ instanceId: selectedInstance.id,
375+ sql: SQL_OBJECTS_QUERY,
376+ maxRows: 1000,
377+ });
378+
379+ const rows = ((result as StorageSqlResult).rows ?? []) as Array<Record<string, unknown>>;
380+ const objects = rows
381+ .map((row) => ({
382+ name: typeof row.name === "string" ? row.name : "",
383+ type: sqlObjectType(row.type),
384+ }))
385+ .filter((entry) => entry.name.length > 0);
386+
387+ setSqlObjects(objects);
388+ return objects;
389+ } catch (error) {
390+ setSqlObjectsError(error instanceof Error ? error.message : "Failed to list SQLite tables");
391+ setSqlObjects([]);
392+ return [] as StorageSqlObject[];
393+ } finally {
394+ setSqlObjectsLoading(false);
395+ }
396+ };
397+
398+ const openSqlObject = async (objectName: string) => {
399+ const parsedMaxRows = Number.parseInt(sqlMaxRows, 10);
400+ const limit = Number.isFinite(parsedMaxRows) ? Math.max(1, parsedMaxRows) : 200;
401+ setSelectedSqlObjectName(objectName);
402+ setSqlViewMode("table");
403+ await runSql(`SELECT * FROM "${escapeSqlIdentifier(objectName)}" LIMIT ${limit}`);
404+ };
405+
406+ useEffect(() => {
407+ if (!canInspect) {
408+ setFsEntries([]);
409+ setKvItems([]);
410+ setSqlResult(null);
411+ setSqlObjects([]);
412+ setSqlObjectsError(null);
413+ setSelectedSqlObjectName(null);
414+ return;
415+ }
416+
417+ void refreshDirectory("/");
418+ void refreshKv();
419+ void (async () => {
420+ const objects = await refreshSqlObjects();
421+ const preferred = objects.find((entry) => !isInternalSqlObject(entry.name)) ?? objects[0];
422+ if (preferred) {
423+ await openSqlObject(preferred.name);
424+ return;
425+ }
426+ await runSql();
427+ })();
428+ setFilePreviewPath(null);
429+ setFilePreviewContent("");
430+ setFilePreviewBytes(null);
431+ }, [canInspect, selectedInstance?.id]);
432+
69433 const submitCreate = async () => {
70434 const parsedTtl = Number.parseInt(ttlHours, 10);
71435 await onCreate({
@@ -78,159 +442,486 @@ export function StoragePanel({ · 6 unmodified lines
78442 };
79443
80444 return (
81− <section className="mx-auto flex h-full min-h-0 w-full max-w-6xl flex-col rounded-none border border-border/50 bg-card/30">
82− <div className="flex items-center justify-between border-b border-border/40 px-4 py-3 sm:px-5">
83− <div className="flex items-center gap-2">
84− <div className="flex h-7 w-7 items-center justify-center border border-border/60 bg-background/80">
85− <Database className="h-4 w-4 text-muted-foreground" />
445+ <section className="flex h-full min-h-0 w-full overflow-hidden rounded-none border border-border/50 bg-card/30">
446+ <aside className="flex h-full w-72 shrink-0 flex-col border-r border-border/40 lg:w-80 xl:w-[22rem]">
447+ <div className="flex items-center justify-between border-b border-border/40 px-3 py-2.5">
448+ <div className="flex items-center gap-2">
449+ <div className="flex h-7 w-7 items-center justify-center border border-border/60 bg-background/80">
450+ <Database className="h-4 w-4 text-muted-foreground" />
451+ </div>
452+ <h2 className="text-sm font-medium">Storage</h2>
86453 </div>
87− <h2 className="text-sm font-medium">Storage Instances</h2>
454+ <Badge variant="outline" className="h-5 px-2 text-[10px] font-mono uppercase tracking-wide text-muted-foreground">
455+ {visibleInstances.length}
456+ </Badge>
88457 </div>
89− <Badge variant="outline" className="h-5 px-2 text-[10px] font-mono uppercase tracking-wide text-muted-foreground">
90− {visibleInstances.length}
91− </Badge>
92− </div>
93458
94− <div className="border-b border-border/40 bg-background/40 px-4 py-3 sm:px-5">
95− <div className="grid gap-2 md:grid-cols-5">
96− <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
97− Scope
98− <select
99− className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"
100− value={scopeType}
101− onChange={(event) => {
102− const next = event.target.value as StorageScopeType;
103− setScopeType(next);
104− if (next !== "scratch") {
105− setDurability("durable");
106− }
107− }}
108− >
109− <option value="scratch">scratch</option>
110− <option value="account">account</option>
111− <option value="workspace">workspace</option>
112− <option value="organization">organization</option>
113− </select>
114− </label>
115−
116− <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
117− Durability
118− <select
119− className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"
120− value={durability}
121− onChange={(event) => setDurability(event.target.value as StorageDurability)}
122− >
123− <option value="ephemeral">ephemeral</option>
124− <option value="durable">durable</option>
125− </select>
126− </label>
127−
128− <label className="flex flex-col gap-1 text-[11px] text-muted-foreground md:col-span-2">
129− Purpose
130− <Input
131− value={purpose}
132− onChange={(event) => setPurpose(event.target.value)}
133− placeholder="scratch for repo indexing"
134− className="h-8 rounded-none text-xs"
135− />
136− </label>
137−
138− <div className="flex items-end gap-2">
139− <label className="flex flex-1 flex-col gap-1 text-[11px] text-muted-foreground">
140− TTL (hours)
459+ <div className="shrink-0 border-b border-border/40 bg-background/40 px-3 py-3">
460+ <div className="space-y-2">
461+ <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
462+ Scope
463+ <select
464+ className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"
465+ value={scopeType}
466+ onChange={(event) => {
467+ const next = event.target.value as StorageScopeType;
468+ setScopeType(next);
469+ if (next !== "scratch") {
470+ setDurability("durable");
471+ }
472+ }}
473+ >
474+ <option value="scratch">scratch</option>
475+ <option value="account">account</option>
476+ <option value="workspace">workspace</option>
477+ <option value="organization">organization</option>
478+ </select>
479+ </label>
480+
481+ <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
482+ Durability
483+ <select
484+ className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"
485+ value={durability}
486+ onChange={(event) => setDurability(event.target.value as StorageDurability)}
487+ >
488+ <option value="ephemeral">ephemeral</option>
489+ <option value="durable">durable</option>
490+ </select>
491+ </label>
492+
493+ <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
494+ Purpose
141495 <Input
142− value={ttlHours}
143− onChange={(event) => setTtlHours(event.target.value)}
144− disabled={durability !== "ephemeral"}
496+ value={purpose}
497+ onChange={(event) => setPurpose(event.target.value)}
498+ placeholder="scratch for repo indexing"
145499 className="h-8 rounded-none text-xs"
146500 />
147501 </label>
148− <Button size="sm" className="h-8 rounded-none text-xs" disabled={creating} onClick={() => void submitCreate()}>
149− <Plus className="mr-1.5 h-3.5 w-3.5" />
150− Create
151− </Button>
502+
503+ <div className="flex items-end gap-2">
504+ <label className="flex flex-1 flex-col gap-1 text-[11px] text-muted-foreground">
505+ TTL (hours)
506+ <Input
507+ value={ttlHours}
508+ onChange={(event) => setTtlHours(event.target.value)}
509+ disabled={durability !== "ephemeral"}
510+ className="h-8 rounded-none text-xs"
511+ />
512+ </label>
513+ <Button size="sm" className="h-8 rounded-none text-xs" disabled={creating} onClick={() => void submitCreate()}>
514+ <Plus className="mr-1.5 h-3.5 w-3.5" />
515+ Create
516+ </Button>
517+ </div>
152518 </div>
153519 </div>
154− </div>
155520
156− <div className="min-h-0 flex-1 overflow-y-auto p-4 sm:p-5">
157− {loading ? (
158− <div className="space-y-2">
159− {Array.from({ length: 4 }).map((_, i) => (
160− <Skeleton key={i} className="h-16 rounded-none" />
161− ))}
162− </div>
163− ) : visibleInstances.length === 0 ? (
164− <div className="flex h-full min-h-[260px] flex-col items-center justify-center gap-2 border border-dashed border-border/50 bg-background/50">
165− <div className="flex h-10 w-10 items-center justify-center border border-border/60 bg-muted/40">
166− <FolderOpen className="h-5 w-5 text-muted-foreground/50" />
521+ <div className="min-h-0 flex-1 overflow-y-auto p-2">
522+ {loading ? (
523+ <div className="space-y-2">
524+ {Array.from({ length: 4 }).map((_, i) => (
525+ <Skeleton key={i} className="h-16 rounded-none" />
526+ ))}
167527 </div>
168− <p className="text-sm text-muted-foreground">No storage instances yet</p>
169− <p className="text-[11px] text-muted-foreground/70 text-center max-w-md">
170− Create a scratch instance for short-lived runs or durable instances for shared state.
171− </p>
172− </div>
173− ) : (
174− <div className="space-y-2">
175− {visibleInstances.map((instance) => {
176− const busy = busyInstanceId === instance.id;
177− return (
178− <div
179− key={instance.id}
180− className={cn(
181− "group flex items-center gap-3 border border-border/50 bg-background/70 px-3 py-2.5 transition-colors",
182− busy ? "opacity-60" : "hover:border-border hover:bg-accent/20",
183− )}
184− style={{ boxShadow: "inset 2px 0 0 0 color-mix(in oklch, var(--border) 70%, transparent)" }}
528+ ) : visibleInstances.length === 0 ? (
529+ <div className="flex h-full min-h-[220px] flex-col items-center justify-center gap-2 border border-dashed border-border/50 bg-background/50 p-3">
530+ <div className="flex h-10 w-10 items-center justify-center border border-border/60 bg-muted/40">
531+ <FolderOpen className="h-5 w-5 text-muted-foreground/50" />
532+ </div>
533+ <p className="text-sm text-muted-foreground">No storage instances yet</p>
534+ </div>
535+ ) : (
536+ <div className="space-y-2">
537+ {visibleInstances.map((instance) => {
538+ const busy = busyInstanceId === instance.id;
539+ const active = selectedInstance?.id === instance.id;
540+ return (
541+ <div
542+ key={instance.id}
543+ className={cn(
544+ "group flex items-center gap-2 border px-2 py-2 transition-colors",
545+ active ? "border-primary/40 bg-primary/5" : "border-border/50 bg-background/70",
546+ busy ? "opacity-60" : "hover:border-border hover:bg-accent/20",
547+ )}
548+ >
549+ <button
550+ type="button"
551+ className="flex min-w-0 flex-1 items-center gap-2 text-left"
552+ onClick={() => setSelectedInstanceId(instance.id)}
553+ >
554+ <div className="flex h-7 w-7 shrink-0 items-center justify-center border border-border/60 bg-muted/50">
555+ <Database className="h-3.5 w-3.5 text-muted-foreground" />
556+ </div>
557+ <div className="min-w-0 flex-1">
558+ <p className="truncate text-[12px] font-medium">{instance.purpose || instance.id}</p>
559+ <p className="text-[10px] text-muted-foreground">
560+ {instance.scopeType} · {instance.durability} · {instance.status}
561+ </p>
562+ <p className="text-[10px] text-muted-foreground">
563+ {prettyBytes(instance.sizeBytes)} · {instance.fileCount ?? "-"} inode{instance.fileCount === 1 ? "" : "s"}
564+ </p>
565+ </div>
566+ </button>
567+ <div className="flex items-center gap-1">
568+ <Button
569+ variant="outline"
570+ size="sm"
571+ className="h-7 rounded-none px-2 text-[11px]"
572+ disabled={busy}
573+ onClick={() => void onClose(instance.id)}
574+ >
575+ <Power className="h-3 w-3" />
576+ </Button>
577+ <Button
578+ variant="outline"
579+ size="sm"
580+ className="h-7 rounded-none border-destructive/40 px-2 text-[11px] text-destructive hover:bg-destructive/10"
581+ disabled={busy}
582+ onClick={() => void onDelete(instance.id)}
583+ >
584+ <Trash2 className="h-3 w-3" />
585+ </Button>
586+ </div>
587+ </div>
588+ );
589+ })}
590+ </div>
591+ )}
592+ </div>
593+ </aside>
594+
595+ <div className="flex min-h-0 flex-1 min-w-0 flex-col bg-background/50">
596+ {selectedInstance ? (
597+ <>
598+ <div className="flex flex-wrap items-center justify-between gap-2 border-b border-border/40 px-4 py-2.5">
599+ <div>
600+ <p className="text-xs font-medium">Inspector</p>
601+ <p className="text-[11px] text-muted-foreground">{selectedInstance.id} · last used {asLocalDate(selectedInstance.lastSeenAt)}</p>
602+ </div>
603+ <div className="flex items-center gap-1">
604+ <Button
605+ variant={activeInspectorTab === "fs" ? "default" : "outline"}
606+ size="sm"
607+ className="h-7 rounded-none px-2 text-[11px]"
608+ onClick={() => setActiveInspectorTab("fs")}
609+ >
610+ <HardDrive className="mr-1 h-3 w-3" /> FS
611+ </Button>
612+ <Button
613+ variant={activeInspectorTab === "kv" ? "default" : "outline"}
614+ size="sm"
615+ className="h-7 rounded-none px-2 text-[11px]"
616+ onClick={() => setActiveInspectorTab("kv")}
185617 >
186− <div className="flex h-8 w-8 shrink-0 items-center justify-center border border-border/60 bg-muted/50 overflow-hidden">
187− <Database className="h-4 w-4 text-muted-foreground" />
618+ <KeyRound className="mr-1 h-3 w-3" /> KV
619+ </Button>
620+ <Button
621+ variant={activeInspectorTab === "sql" ? "default" : "outline"}
622+ size="sm"
623+ className="h-7 rounded-none px-2 text-[11px]"
624+ onClick={() => setActiveInspectorTab("sql")}
625+ >
626+ <Table className="mr-1 h-3 w-3" /> SQLite
627+ </Button>
628+ </div>
629+ </div>
630+
631+ {!canInspect ? (
632+ <div className="p-4 text-xs text-muted-foreground">Select a signed-in workspace session to inspect contents.</div>
633+ ) : null}
634+
635+ {activeInspectorTab === "fs" ? (
636+ <div className="space-y-2 p-4">
637+ <div className="flex items-center gap-2">
638+ <Input
639+ value={fsPath}
640+ onChange={(event) => setFsPath(event.target.value)}
641+ className="h-8 rounded-none text-xs"
642+ placeholder="/"
643+ />
644+ <Button
645+ size="sm"
646+ className="h-8 rounded-none text-xs"
647+ disabled={fsLoading || !canInspect}
648+ onClick={() => void refreshDirectory()}
649+ >
650+ Refresh
651+ </Button>
652+ </div>
653+ {fsError ? <p className="text-[11px] text-destructive">{fsError}</p> : null}
654+
655+ <div className="max-h-56 overflow-auto border border-border/40">
656+ {fsLoading ? (
657+ <div className="p-2 text-[11px] text-muted-foreground">Loading...</div>
658+ ) : fsEntries.length === 0 ? (
659+ <div className="p-2 text-[11px] text-muted-foreground">No entries</div>
660+ ) : (
661+ <div className="divide-y divide-border/30">
662+ {fsEntries.map((entry) => {
663+ const nextPath = joinStoragePath(fsPath, entry.name);
664+ return (
665+ <div key={`${entry.type}:${entry.name}`} className="flex items-center justify-between gap-2 px-2 py-1.5 text-[11px]">
666+ <div className="min-w-0">
667+ <p className="truncate font-medium">{entry.name}</p>
668+ <p className="text-muted-foreground">{entry.type} {typeof entry.size === "number" ? `- ${prettyBytes(entry.size)}` : ""}</p>
669+ </div>
670+ {entry.type === "directory" ? (
671+ <Button variant="outline" size="sm" className="h-6 rounded-none px-2 text-[10px]" onClick={() => void refreshDirectory(nextPath)}>
672+ Open
673+ </Button>
674+ ) : entry.type === "file" ? (
675+ <Button variant="outline" size="sm" className="h-6 rounded-none px-2 text-[10px]" onClick={() => void readFilePreview(nextPath)}>
676+ Read
677+ </Button>
678+ ) : null}
679+ </div>
680+ );
681+ })}
682+ </div>
683+ )}
684+ </div>
685+
686+ {filePreviewPath ? (
687+ <div className="border border-border/40">
688+ <div className="flex items-center justify-between border-b border-border/30 px-2 py-1 text-[11px]">
689+ <span className="truncate">{filePreviewPath}</span>
690+ <span className="text-muted-foreground">{filePreviewBytes !== null ? prettyBytes(filePreviewBytes) : "-"}</span>
691+ </div>
692+ <pre className="max-h-[28rem] overflow-auto p-2 text-[11px] leading-5">{filePreviewLoading ? "Loading..." : filePreviewContent}</pre>
188693 </div>
694+ ) : null}
695+ </div>
696+ ) : null}
189697
190− <div className="flex-1 min-w-0">
191− <div className="mb-1 flex items-center gap-1.5 flex-wrap">
192− <span className="text-sm font-medium truncate">{instance.purpose || instance.id}</span>
193− <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.scopeType}</Badge>
194− <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.durability}</Badge>
195− <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.provider}</Badge>
196− <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.status}</Badge>
698+ {activeInspectorTab === "kv" ? (
699+ <div className="space-y-2 p-4">
700+ <div className="flex items-center gap-2">
701+ <Input
702+ value={kvPrefix}
703+ onChange={(event) => setKvPrefix(event.target.value)}
704+ className="h-8 rounded-none text-xs"
705+ placeholder="prefix"
706+ />
707+ <Input
708+ value={kvLimit}
709+ onChange={(event) => setKvLimit(event.target.value)}
710+ className="h-8 w-24 rounded-none text-xs"
711+ placeholder="100"
712+ />
713+ <Button
714+ size="sm"
715+ className="h-8 rounded-none text-xs"
716+ disabled={kvLoading || !canInspect}
717+ onClick={() => void refreshKv()}
718+ >
719+ Refresh
720+ </Button>
721+ </div>
722+ {kvError ? <p className="text-[11px] text-destructive">{kvError}</p> : null}
723+
724+ <div className="max-h-[36rem] overflow-auto border border-border/40">
725+ {kvLoading ? (
726+ <div className="p-2 text-[11px] text-muted-foreground">Loading...</div>
727+ ) : kvItems.length === 0 ? (
728+ <div className="p-2 text-[11px] text-muted-foreground">No key-value entries</div>
729+ ) : (
730+ <div className="divide-y divide-border/30">
731+ {kvItems.map((item) => (
732+ <div key={item.key} className="px-2 py-1.5 text-[11px]">
733+ <p className="font-medium">{item.key}</p>
734+ <pre className="overflow-auto text-muted-foreground">{truncateText(previewJson(item.value), 4000)}</pre>
735+ </div>
736+ ))}
197737 </div>
738+ )}
739+ </div>
740+ </div>
741+ ) : null}
198742
199− <p className="text-[11px] text-muted-foreground">
200− {prettyBytes(instance.sizeBytes)} - {instance.fileCount ?? "-"} inode{instance.fileCount === 1 ? "" : "s"}
201− </p>
202− <p className="mt-0.5 text-[11px] text-muted-foreground">
203− Last used {asLocalDate(instance.lastSeenAt)}
204− {instance.expiresAt ? ` - expires ${asLocalDate(instance.expiresAt)}` : ""}
205− </p>
743+ {activeInspectorTab === "sql" ? (
744+ <div className="grid h-full min-h-0 gap-3 p-4 lg:grid-cols-[15rem_minmax(0,1fr)]">
745+ <div className="flex min-h-0 flex-col border border-border/40">
746+ <div className="flex items-center justify-between border-b border-border/30 px-2 py-1 text-[11px] text-muted-foreground">
747+ <span>Tables</span>
748+ <div className="flex items-center gap-1">
749+ <Button
750+ variant="outline"
751+ size="sm"
752+ className="h-6 rounded-none px-2 text-[10px]"
753+ onClick={() => setSqlShowInternalObjects((current) => !current)}
754+ >
755+ {sqlShowInternalObjects ? "Hide internal" : "Show internal"}
756+ </Button>
757+ <Button
758+ variant="outline"
759+ size="sm"
760+ className="h-6 rounded-none px-2 text-[10px]"
761+ onClick={() => void refreshSqlObjects()}
762+ disabled={sqlObjectsLoading}
763+ >
764+ Refresh
765+ </Button>
766+ </div>
206767 </div>
207768
208− <div className="flex items-center gap-1">
769+ {sqlObjectsError ? <p className="px-2 py-1 text-[11px] text-destructive">{sqlObjectsError}</p> : null}
770+
771+ <div className="min-h-0 flex-1 overflow-y-auto p-1">
772+ {sqlObjectsLoading ? (
773+ <p className="px-2 py-1 text-[11px] text-muted-foreground">Loading...</p>
774+ ) : visibleSqlObjects.length === 0 ? (
775+ <p className="px-2 py-1 text-[11px] text-muted-foreground">No tables</p>
776+ ) : (
777+ <div className="space-y-1">
778+ {visibleSqlObjects.map((entry) => (
779+ <Button
780+ key={entry.name}
781+ variant={selectedSqlObjectName === entry.name ? "default" : "outline"}
782+ size="sm"
783+ className="h-7 w-full justify-start rounded-none px-2 text-[11px]"
784+ onClick={() => void openSqlObject(entry.name)}
785+ >
786+ <span className="truncate">{entry.name}</span>
787+ </Button>
788+ ))}
789+ </div>
790+ )}
791+ </div>
792+ </div>
793+
794+ <div className="flex min-h-0 flex-col gap-2">
795+ <textarea
796+ value={sqlText}
797+ onChange={(event) => {
798+ setSqlText(event.target.value);
799+ setSelectedSqlObjectName(null);
800+ }}
801+ className="h-24 w-full resize-y rounded-none border border-border/60 bg-background px-2 py-1.5 font-mono text-xs text-foreground"
802+ placeholder="SELECT * FROM sqlite_master"
803+ />
804+ <div className="flex flex-wrap items-center gap-2">
805+ <Input
806+ value={sqlMaxRows}
807+ onChange={(event) => setSqlMaxRows(event.target.value)}
808+ className="h-8 w-24 rounded-none text-xs"
809+ placeholder="200"
810+ />
209811 <Button
210− variant="outline"
211812 size="sm"
212− className="h-7 rounded-none text-[11px]"
213− disabled={busy}
214− onClick={() => void onClose(instance.id)}
813+ className="h-8 rounded-none text-xs"
814+ disabled={sqlLoading || !canInspect}
815+ onClick={() => {
816+ setSelectedSqlObjectName(null);
817+ void runSql();
818+ }}
215819 >
216− <Power className="mr-1 h-3 w-3" />
217− Close
820+ <Play className="mr-1 h-3 w-3" /> Run
218821 </Button>
219− <Button
220− variant="outline"
221− size="sm"
222− className="h-7 rounded-none text-[11px] border-destructive/40 text-destructive hover:bg-destructive/10"
223− disabled={busy}
224− onClick={() => void onDelete(instance.id)}
225− >
226− <Trash2 className="mr-1 h-3 w-3" />
227− Delete
822+ <Button variant="outline" size="sm" className="h-8 rounded-none px-2 text-[11px]" onClick={() => { setSelectedSqlObjectName(null); void runSql(USER_TABLES_QUERY); }}>
823+ User tables
824+ </Button>
825+ <Button variant="outline" size="sm" className="h-8 rounded-none px-2 text-[11px]" onClick={() => { setSelectedSqlObjectName(null); void runSql(ALL_OBJECTS_QUERY); }}>
826+ All objects
827+ </Button>
828+ <Button variant="outline" size="sm" className="h-8 rounded-none px-2 text-[11px]" onClick={() => { setSelectedSqlObjectName(null); void runSql("PRAGMA table_info('kv_store')"); }}>
829+ KV schema
830+ </Button>
831+ <Button variant="outline" size="sm" className="h-8 rounded-none px-2 text-[11px]" onClick={() => { setSelectedSqlObjectName("kv_store"); void runSql(KV_DATA_QUERY); }}>
832+ KV data
833+ </Button>
834+ <Button variant="outline" size="sm" className="h-8 rounded-none px-2 text-[11px]" onClick={() => { setSelectedSqlObjectName("fs_dentry"); void runSql(FS_ENTRIES_QUERY); }}>
835+ FS entries
228836 </Button>
229837 </div>
838+ <p className="text-[11px] text-muted-foreground">
839+ Tip: click a table on the left to load rows instantly (no manual SQL needed).
840+ </p>
841+ {sqlError ? <p className="text-[11px] text-destructive">{sqlError}</p> : null}
842+
843+ <div className="min-h-0 flex-1 border border-border/40">
844+ {sqlLoading ? (
845+ <div className="p-2 text-[11px] text-muted-foreground">Running query...</div>
846+ ) : sqlResult ? (
847+ <div className="h-full min-h-0">
848+ <div className="flex items-center justify-between gap-2 border-b border-border/30 px-2 py-1 text-[11px] text-muted-foreground">
849+ <span>
850+ rows: {sqlResult.rowCount}
851+ {typeof sqlResult.changes === "number" ? ` - changes: ${sqlResult.changes}` : ""}
852+ {sqlColumns.length > 0 ? ` - columns: ${sqlColumns.length}` : ""}
853+ </span>
854+ <div className="flex items-center gap-1">
855+ <Button
856+ variant={sqlViewMode === "table" ? "default" : "outline"}
857+ size="sm"
858+ className="h-6 rounded-none px-2 text-[10px]"
859+ onClick={() => setSqlViewMode("table")}
860+ >
861+ Table
862+ </Button>
863+ <Button
864+ variant={sqlViewMode === "json" ? "default" : "outline"}
865+ size="sm"
866+ className="h-6 rounded-none px-2 text-[10px]"
867+ onClick={() => setSqlViewMode("json")}
868+ >
869+ JSON
870+ </Button>
871+ </div>
872+ </div>
873+
874+ {sqlViewMode === "json" ? (
875+ <pre className="h-[calc(100%-2rem)] overflow-auto p-2 text-[11px] leading-5">{previewJson(sqlRows)}</pre>
876+ ) : sqlRows.length === 0 ? (
877+ <div className="p-2 text-[11px] text-muted-foreground">Query returned no rows.</div>
878+ ) : (
879+ <div className="h-[calc(100%-2rem)] overflow-auto">
880+ <table className="min-w-full border-collapse text-[11px]">
881+ <thead className="sticky top-0 z-10 bg-muted/50">
882+ <tr>
883+ <th className="border-b border-r border-border/40 px-2 py-1 text-left font-medium text-muted-foreground">#</th>
884+ {sqlColumns.map((column) => (
885+ <th
886+ key={column}
887+ className="border-b border-r border-border/40 px-2 py-1 text-left font-medium text-muted-foreground last:border-r-0"
888+ >
889+ {column}
890+ </th>
891+ ))}
892+ </tr>
893+ </thead>
894+ <tbody>
895+ {sqlRows.map((row, rowIndex) => (
896+ <tr key={`row-${rowIndex}`} className="odd:bg-background even:bg-muted/10">
897+ <td className="border-b border-r border-border/30 px-2 py-1 align-top text-muted-foreground">{rowIndex + 1}</td>
898+ {sqlColumns.map((column) => (
899+ <td
900+ key={`row-${rowIndex}-${column}`}
901+ className="max-w-[420px] border-b border-r border-border/30 px-2 py-1 align-top last:border-r-0"
902+ >
903+ <div className="max-h-28 overflow-auto whitespace-pre-wrap break-words">
904+ {truncateText(sqlCellText(row[column]), 2000)}
905+ </div>
906+ </td>
907+ ))}
908+ </tr>
909+ ))}
910+ </tbody>
911+ </table>
912+ </div>
913+ )}
914+ </div>
915+ ) : (
916+ <div className="p-2 text-[11px] text-muted-foreground">Select a table from the left or run a query.</div>
917+ )}
918+ </div>
230919 </div>
231− );
232− })}
233− </div>
920+ </div>
921+ ) : null}
922+ </>
923+ ) : (
924+ <div className="p-4 text-[11px] text-muted-foreground">Select a storage instance to inspect.</div>
234925 )}
235926 </div>
236927 </section>
executor/apps/web/src/components/tools/view.tsx+3−1
@@ -558,8 +558,10 @@ export function ToolsView() { · 557 unmodified lines
558558 ) : null}
559559
560560 {activeTab === "storage" ? (
561− <div className="flex-1 min-h-0 overflow-y-auto p-4 md:p-6 lg:p-8">
561+ <div className="flex-1 min-h-0 overflow-hidden p-0">
562562 <StoragePanel
563+ workspaceId={context?.workspaceId}
564+ sessionId={context?.sessionId}
563565 instances={storageItems}
564566 loading={storageLoading}
565567 creating={storageCreateBusy}
executor/packages/database/convex/_generated/api.d.ts+97−1837
@@ -8,8 +8,95 @@ · 7 unmodified lines
88 * @module
99 */
1010
11−import type { FunctionReference } from "convex/server";
12−import type { GenericId as Id } from "convex/values";
11+import type * as accounts from "../accounts.js";
12+import type * as app from "../app.js";
13+import type * as auth from "../auth.js";
14+import type * as billing from "../billing.js";
15+import type * as billingInternal from "../billingInternal.js";
16+import type * as billingSync from "../billingSync.js";
17+import type * as credentialsNode from "../credentialsNode.js";
18+import type * as database from "../database.js";
19+import type * as database_anonymous_session from "../database/anonymous_session.js";
20+import type * as database_approvals from "../database/approvals.js";
21+import type * as database_credentials from "../database/credentials.js";
22+import type * as database_policies from "../database/policies.js";
23+import type * as database_storage_instances from "../database/storage_instances.js";
24+import type * as database_task_events from "../database/task_events.js";
25+import type * as database_tasks from "../database/tasks.js";
26+import type * as database_tool_calls from "../database/tool_calls.js";
27+import type * as database_tool_sources from "../database/tool_sources.js";
28+import type * as executor from "../executor.js";
29+import type * as executorNode from "../executorNode.js";
30+import type * as executorRuntimeNode from "../executorRuntimeNode.js";
31+import type * as http from "../http.js";
32+import type * as http_anonymous_auth from "../http/anonymous_auth.js";
33+import type * as http_mcp_auth from "../http/mcp_auth.js";
34+import type * as http_mcp_handler from "../http/mcp_handler.js";
35+import type * as http_mcp_service from "../http/mcp_service.js";
36+import type * as http_oauth_handlers from "../http/oauth_handlers.js";
37+import type * as http_openapi_handler from "../http/openapi_handler.js";
38+import type * as http_openapi_spec from "../http/openapi_spec.js";
39+import type * as http_openapi_spec_registry from "../http/openapi_spec_registry.js";
40+import type * as invites from "../invites.js";
41+import type * as migrations from "../migrations.js";
42+import type * as openApiSpecCache from "../openApiSpecCache.js";
43+import type * as organizationMembers from "../organizationMembers.js";
44+import type * as organizations from "../organizations.js";
45+import type * as runtimeCallbacks from "../runtimeCallbacks.js";
46+import type * as runtimeNode from "../runtimeNode.js";
47+import type * as toolRegistry from "../toolRegistry.js";
48+import type * as workspace from "../workspace.js";
49+import type * as workspaceAuthInternal from "../workspaceAuthInternal.js";
50+import type * as workspaces from "../workspaces.js";
51+
52+import type {
53+ ApiFromModules,
54+ FilterApi,
55+ FunctionReference,
56+} from "convex/server";
57+
58+declare const fullApi: ApiFromModules<{
59+ accounts: typeof accounts;
60+ app: typeof app;
61+ auth: typeof auth;
62+ billing: typeof billing;
63+ billingInternal: typeof billingInternal;
64+ billingSync: typeof billingSync;
65+ credentialsNode: typeof credentialsNode;
66+ database: typeof database;
67+ "database/anonymous_session": typeof database_anonymous_session;
68+ "database/approvals": typeof database_approvals;
69+ "database/credentials": typeof database_credentials;
70+ "database/policies": typeof database_policies;
71+ "database/storage_instances": typeof database_storage_instances;
72+ "database/task_events": typeof database_task_events;
73+ "database/tasks": typeof database_tasks;
74+ "database/tool_calls": typeof database_tool_calls;
75+ "database/tool_sources": typeof database_tool_sources;
76+ executor: typeof executor;
77+ executorNode: typeof executorNode;
78+ executorRuntimeNode: typeof executorRuntimeNode;
79+ http: typeof http;
80+ "http/anonymous_auth": typeof http_anonymous_auth;
81+ "http/mcp_auth": typeof http_mcp_auth;
82+ "http/mcp_handler": typeof http_mcp_handler;
83+ "http/mcp_service": typeof http_mcp_service;
84+ "http/oauth_handlers": typeof http_oauth_handlers;
85+ "http/openapi_handler": typeof http_openapi_handler;
86+ "http/openapi_spec": typeof http_openapi_spec;
87+ "http/openapi_spec_registry": typeof http_openapi_spec_registry;
88+ invites: typeof invites;
89+ migrations: typeof migrations;
90+ openApiSpecCache: typeof openApiSpecCache;
91+ organizationMembers: typeof organizationMembers;
92+ organizations: typeof organizations;
93+ runtimeCallbacks: typeof runtimeCallbacks;
94+ runtimeNode: typeof runtimeNode;
95+ toolRegistry: typeof toolRegistry;
96+ workspace: typeof workspace;
97+ workspaceAuthInternal: typeof workspaceAuthInternal;
98+ workspaces: typeof workspaces;
99+}>;
13100
14101 /**
15102 * A utility for referencing Convex functions in your app's public API.
@@ -19,556 +106,10 @@ import type { GenericId as Id } from "convex/values"; · 3 unmodified lines
19106 * const myFunctionReference = api.myModule.myFunction;
20107 * ```
21108 */
22−export declare const api: {
23− accounts: {
24− deleteCurrentAccount: FunctionReference<
25− "mutation",
26− "public",
27− { sessionId?: string },
28− any
29− >;
30− };
31− app: {
32− getClientConfig: FunctionReference<"query", "public", {}, any>;
33− getCurrentAccount: FunctionReference<
34− "query",
35− "public",
36− { sessionId?: string },
37− any
38− >;
39− };
40− auth: {
41− bootstrapCurrentWorkosAccount: FunctionReference<
42− "mutation",
43− "public",
44− {},
45− any
46− >;
47− };
48− billing: {
49− createCustomerPortal: FunctionReference<
50− "action",
51− "public",
52− {
53− organizationId: Id<"organizations">;
54− returnUrl?: string;
55− sessionId?: string;
56− },
57− { url: string }
58− >;
59− createSubscriptionCheckout: FunctionReference<
60− "action",
61− "public",
62− {
63− cancelUrl?: string;
64− organizationId: Id<"organizations">;
65− priceId: string;
66− sessionId?: string;
67− successUrl?: string;
68− },
69− any
70− >;
71− getSummary: FunctionReference<
72− "query",
73− "public",
74− { organizationId: Id<"organizations">; sessionId?: string },
75− any
76− >;
77− retrySeatSync: FunctionReference<
78− "mutation",
79− "public",
80− { organizationId: Id<"organizations">; sessionId?: string },
81− any
82− >;
83− };
84− credentialsNode: {
85− upsertCredential: FunctionReference<
86− "action",
87− "public",
88− {
89− accountId?: Id<"accounts">;
90− id?: string;
91− provider?: "local-convex" | "workos-vault";
92− scopeType?: "account" | "organization" | "workspace";
93− secretJson: Record<string, any>;
94− sessionId?: string;
95− sourceKey: string;
96− workspaceId: Id<"workspaces">;
97− },
98− any
99− >;
100− };
101− executor: {
102− createTask: FunctionReference<
103− "action",
104− "public",
105− {
106− accountId?: Id<"accounts">;
107− code: string;
108− metadata?: Record<string, any>;
109− runtimeId?: string;
110− sessionId?: string;
111− timeoutMs?: number;
112− waitForResult?: boolean;
113− workspaceId: Id<"workspaces">;
114− },
115− any
116− >;
117− resolveApproval: FunctionReference<
118− "mutation",
119− "public",
120− {
121− approvalId: string;
122− decision: "approved" | "denied";
123− reason?: string;
124− reviewerId?: string;
125− sessionId?: string;
126− workspaceId: Id<"workspaces">;
127− },
128− any
129− >;
130− };
131− executorNode: {
132− listToolsWithWarnings: FunctionReference<
133− "action",
134− "public",
135− {
136− accountId?: Id<"accounts">;
137− buildId?: string;
138− cursor?: string;
139− fetchAll?: boolean;
140− includeDetails?: boolean;
141− includeSourceMeta?: boolean;
142− limit?: number;
143− rebuildInventory?: boolean;
144− sessionId?: string;
145− source?: string;
146− sourceName?: string;
147− toolPaths?: Array<string>;
148− workspaceId: Id<"workspaces">;
149− },
150− any
151− >;
152− previewOpenApiSourceUpgrade: FunctionReference<
153− "action",
154− "public",
155− {
156− accountId?: Id<"accounts">;
157− config: Record<string, any>;
158− name: string;
159− sessionId?: string;
160− sourceId: string;
161− workspaceId: Id<"workspaces">;
162− },
163− any
164− >;
165− };
166− invites: {
167− create: FunctionReference<
168− "mutation",
169− "public",
170− {
171− email: string;
172− expiresInDays?: number;
173− organizationId: Id<"organizations">;
174− role: "owner" | "admin" | "member" | "billing_admin";
175− sessionId?: string;
176− },
177− any
178− >;
179− list: FunctionReference<
180− "query",
181− "public",
182− { organizationId: Id<"organizations">; sessionId?: string },
183− any
184− >;
185− revoke: FunctionReference<
186− "mutation",
187− "public",
188− {
189− inviteId: Id<"invites">;
190− organizationId: Id<"organizations">;
191− sessionId?: string;
192− },
193− any
194− >;
195− };
196− organizationMembers: {
197− list: FunctionReference<
198− "query",
199− "public",
200− { organizationId: Id<"organizations">; sessionId?: string },
201− any
202− >;
203− remove: FunctionReference<
204− "mutation",
205− "public",
206− {
207− accountId: Id<"accounts">;
208− organizationId: Id<"organizations">;
209− sessionId?: string;
210− },
211− any
212− >;
213− updateBillable: FunctionReference<
214− "mutation",
215− "public",
216− {
217− accountId: Id<"accounts">;
218− billable: boolean;
219− organizationId: Id<"organizations">;
220− sessionId?: string;
221− },
222− any
223− >;
224− updateRole: FunctionReference<
225− "mutation",
226− "public",
227− {
228− accountId: Id<"accounts">;
229− organizationId: Id<"organizations">;
230− role: "owner" | "admin" | "member" | "billing_admin";
231− sessionId?: string;
232− },
233− any
234− >;
235− };
236− organizations: {
237− create: FunctionReference<
238− "mutation",
239− "public",
240− { name: string; sessionId?: string },
241− any
242− >;
243− getNavigationState: FunctionReference<
244− "query",
245− "public",
246− { sessionId?: string },
247− any
248− >;
249− getOrganizationAccess: FunctionReference<
250− "query",
251− "public",
252− { organizationId: Id<"organizations">; sessionId?: string },
253− any
254− >;
255− listMine: FunctionReference<"query", "public", { sessionId?: string }, any>;
256− resolveWorkosOrganizationId: FunctionReference<
257− "query",
258− "public",
259− { organizationId: Id<"organizations">; sessionId?: string },
260− any
261− >;
262− };
263− runtimeCallbacks: {
264− completeRun: FunctionReference<
265− "mutation",
266− "public",
267− {
268− durationMs?: number;
269− error?: string;
270− exitCode?: number;
271− internalSecret: string;
272− runId: string;
273− status: "completed" | "failed" | "timed_out" | "denied";
274− },
275− any
276− >;
277− getApprovalStatus: FunctionReference<
278− "query",
279− "public",
280− { approvalId: string; internalSecret: string; runId: string },
281− any
282− >;
283− getTaskWatchStatus: FunctionReference<
284− "query",
285− "public",
286− { internalSecret: string; runId: string; workspaceId: Id<"workspaces"> },
287− any
288− >;
289− handleToolCall: FunctionReference<
290− "action",
291− "public",
292− {
293− callId: string;
294− input?: Record<string, any>;
295− internalSecret: string;
296− runId: string;
297− toolPath: string;
298− },
299− any
300− >;
301− };
302− workspace: {
303− bootstrapAnonymousSession: FunctionReference<
304− "mutation",
305− "public",
306− { accountId?: string; sessionId?: string },
307− any
308− >;
309− closeStorageInstance: FunctionReference<
310− "mutation",
311− "public",
312− { instanceId: string; sessionId?: string; workspaceId: Id<"workspaces"> },
313− any
314− >;
315− deleteStorageInstance: FunctionReference<
316− "mutation",
317− "public",
318− { instanceId: string; sessionId?: string; workspaceId: Id<"workspaces"> },
319− any
320− >;
321− deleteToolPolicyAssignment: FunctionReference<
322− "mutation",
323− "public",
324− { bindingId: string; sessionId?: string; workspaceId: Id<"workspaces"> },
325− any
326− >;
327− deleteToolPolicyRule: FunctionReference<
328− "mutation",
329− "public",
330− {
331− roleId: string;
332− ruleId: string;
333− sessionId?: string;
334− workspaceId: Id<"workspaces">;
335− },
336− any
337− >;
338− deleteToolPolicySet: FunctionReference<
339− "mutation",
340− "public",
341− { roleId: string; sessionId?: string; workspaceId: Id<"workspaces"> },
342− any
343− >;
344− deleteToolSource: FunctionReference<
345− "mutation",
346− "public",
347− { sessionId?: string; sourceId: string; workspaceId: Id<"workspaces"> },
348− any
349− >;
350− getMcpApiKey: FunctionReference<
351− "query",
352− "public",
353− { sessionId?: string; workspaceId: Id<"workspaces"> },
354− any
355− >;
356− getToolDetails: FunctionReference<
357− "mutation",
358− "public",
359− {
360− sessionId?: string;
361− toolPaths: Array<string>;
362− workspaceId: Id<"workspaces">;
363− },
364− any
365− >;
366− getToolInventoryProgress: FunctionReference<
367− "query",
368− "public",
369− { sessionId?: string; workspaceId: Id<"workspaces"> },
370− any
371− >;
372− listCredentials: FunctionReference<
373− "query",
374− "public",
375− { sessionId?: string; workspaceId: Id<"workspaces"> },
376− any
377− >;
378− listPendingApprovals: FunctionReference<
379− "query",
380− "public",
381− { sessionId?: string; workspaceId: Id<"workspaces"> },
382− any
383− >;
384− listStorageInstances: FunctionReference<
385− "query",
386− "public",
387− {
388− includeDeleted?: boolean;
389− scopeType?: "scratch" | "account" | "workspace" | "organization";
390− sessionId?: string;
391− workspaceId: Id<"workspaces">;
392− },
393− any
394− >;
395− listTasks: FunctionReference<
396− "query",
397− "public",
398− { sessionId?: string; workspaceId: Id<"workspaces"> },
399− any
400− >;
401− listToolPolicies: FunctionReference<
402− "query",
403− "public",
404− { sessionId?: string; workspaceId: Id<"workspaces"> },
405− any
406− >;
407− listToolPolicyAssignments: FunctionReference<
408− "query",
409− "public",
410− { roleId?: string; sessionId?: string; workspaceId: Id<"workspaces"> },
411− any
412− >;
413− listToolPolicyRules: FunctionReference<
414− "query",
415− "public",
416− { roleId: string; sessionId?: string; workspaceId: Id<"workspaces"> },
417− any
418− >;
419− listToolPolicySets: FunctionReference<
420− "query",
421− "public",
422− { sessionId?: string; workspaceId: Id<"workspaces"> },
423− any
424− >;
425− listToolSources: FunctionReference<
426− "query",
427− "public",
428− { sessionId?: string; workspaceId: Id<"workspaces"> },
429− any
430− >;
431− openStorageInstance: FunctionReference<
432− "mutation",
433− "public",
434− {
435− durability?: "ephemeral" | "durable";
436− instanceId?: string;
437− purpose?: string;
438− scopeType?: "scratch" | "account" | "workspace" | "organization";
439− sessionId?: string;
440− ttlHours?: number;
441− workspaceId: Id<"workspaces">;
442− },
443− any
444− >;
445− regenerateToolInventory: FunctionReference<
446− "mutation",
447− "public",
448− { sessionId?: string; workspaceId: Id<"workspaces"> },
449− any
450− >;
451− resolveCredential: FunctionReference<
452− "query",
453− "public",
454− {
455− accountId?: Id<"accounts">;
456− scopeType: "account" | "organization" | "workspace";
457− sessionId?: string;
458− sourceKey: string;
459− workspaceId: Id<"workspaces">;
460− },
461− any
462− >;
463− upsertCredential: FunctionReference<
464− "mutation",
465− "public",
466− {
467− accountId?: Id<"accounts">;
468− id?: string;
469− overridesJson?: Record<string, any>;
470− provider?: "local-convex" | "workos-vault";
471− scopeType?: "account" | "organization" | "workspace";
472− secretJson: Record<string, any>;
473− sessionId?: string;
474− sourceKey: string;
475− workspaceId: Id<"workspaces">;
476− },
477− any
478− >;
479− upsertToolPolicyAssignment: FunctionReference<
480− "mutation",
481− "public",
482− {
483− clientId?: string;
484− expiresAt?: number;
485− id?: string;
486− roleId: string;
487− scopeType?: "account" | "organization" | "workspace";
488− sessionId?: string;
489− status?: "active" | "disabled";
490− targetAccountId?: Id<"accounts">;
491− workspaceId: Id<"workspaces">;
492− },
493− any
494− >;
495− upsertToolPolicyRule: FunctionReference<
496− "mutation",
497− "public",
498− {
499− approvalMode?: "inherit" | "auto" | "required";
500− argumentConditions?: Array<{
501− key: string;
502− operator: "equals" | "contains" | "starts_with" | "not_equals";
503− value: string;
504− }>;
505− effect?: "allow" | "deny";
506− id?: string;
507− matchType?: "glob" | "exact";
508− priority?: number;
509− resourcePattern?: string;
510− roleId: string;
511− selectorType: "all" | "source" | "namespace" | "tool_path";
512− sessionId?: string;
513− sourceKey?: string;
514− workspaceId: Id<"workspaces">;
515− },
516− any
517− >;
518− upsertToolPolicySet: FunctionReference<
519− "mutation",
520− "public",
521− {
522− description?: string;
523− id?: string;
524− name: string;
525− sessionId?: string;
526− workspaceId: Id<"workspaces">;
527− },
528− any
529− >;
530− upsertToolSource: FunctionReference<
531− "mutation",
532− "public",
533− {
534− config: Record<string, any>;
535− enabled?: boolean;
536− id?: string;
537− name: string;
538− scopeType?: "organization" | "workspace";
539− sessionId?: string;
540− type: "mcp" | "openapi" | "graphql";
541− workspaceId: Id<"workspaces">;
542− },
543− any
544− >;
545− };
546− workspaces: {
547− create: FunctionReference<
548− "mutation",
549− "public",
550− {
551− iconStorageId?: Id<"_storage">;
552− name: string;
553− organizationId?: Id<"organizations">;
554− sessionId?: string;
555− },
556− any
557− >;
558− generateWorkspaceIconUploadUrl: FunctionReference<
559− "mutation",
560− "public",
561− { sessionId?: string },
562− any
563− >;
564− list: FunctionReference<
565− "query",
566− "public",
567− { organizationId?: Id<"organizations">; sessionId?: string },
568− any
569− >;
570− };
571−};
109+export declare const api: FilterApi<
110+ typeof fullApi,
111+ FunctionReference<any, "public">
112+>;
572113
573114 /**
574115 * A utility for referencing Convex functions in your app's internal API.
@@ -578,1291 +119,10 @@ export declare const api: { · 3 unmodified lines
578119 * const myFunctionReference = internal.myModule.myFunction;
579120 * ```
580121 */
581−export declare const internal: {
582− auth: {
583− authKitEvent: FunctionReference<
584− "mutation",
585− "internal",
586− { data: Record<string, any>; event: string },
587− null
588− >;
589− };
590− billingInternal: {
591− bumpSeatSyncVersion: FunctionReference<
592− "mutation",
593− "internal",
594− { organizationId: Id<"organizations"> },
595− any
596− >;
597− getBillingAccessForRequest: FunctionReference<
598− "query",
599− "internal",
600− { organizationId: Id<"organizations">; sessionId?: string },
601− any
602− >;
603− getSeatSyncSnapshot: FunctionReference<
604− "query",
605− "internal",
606− { organizationId: Id<"organizations"> },
607− any
608− >;
609− upsertCustomerLink: FunctionReference<
610− "mutation",
611− "internal",
612− { organizationId: Id<"organizations">; stripeCustomerId: string },
613− any
614− >;
615− upsertSeatState: FunctionReference<
616− "mutation",
617− "internal",
618− {
619− bumpVersion: boolean;
620− desiredSeats: number;
621− lastAppliedSeats: number | null;
622− organizationId: Id<"organizations">;
623− syncError: string | null;
624− },
625− any
626− >;
627− };
628− billingSync: {
629− syncSeatQuantity: FunctionReference<
630− "action",
631− "internal",
632− { expectedVersion: number; organizationId: Id<"organizations"> },
633− any
634− >;
635− };
636− credentialsNode: {
637− readVaultObject: FunctionReference<
638− "action",
639− "internal",
640− { apiKey?: string; objectId: string },
641− any
642− >;
643− };
644− database: {
645− anonymous_session: {
646− bootstrapAnonymousSession: FunctionReference<
647− "mutation",
648− "internal",
649− { accountId?: string; clientId?: string; sessionId?: string },
650− any
651− >;
652− };
653− approvals: {
654− createApproval: FunctionReference<
655− "mutation",
656− "internal",
657− {
658− id: string;
659− input?: Record<string, any>;
660− taskId: string;
661− toolPath: string;
662− },
663− any
664− >;
665− getApproval: FunctionReference<
666− "query",
667− "internal",
668− { approvalId: string },
669− any
670− >;
671− getApprovalInWorkspace: FunctionReference<
672− "query",
673− "internal",
674− { approvalId: string; workspaceId: Id<"workspaces"> },
675− any
676− >;
677− listApprovals: FunctionReference<
678− "query",
679− "internal",
680− {
681− status?: "pending" | "approved" | "denied";
682− workspaceId: Id<"workspaces">;
683− },
684− any
685− >;
686− listPendingApprovals: FunctionReference<
687− "query",
688− "internal",
689− { workspaceId: Id<"workspaces"> },
690− any
691− >;
692− resolveApproval: FunctionReference<
693− "mutation",
694− "internal",
695− {
696− approvalId: string;
697− decision: "approved" | "denied";
698− reason?: string;
699− reviewerId?: string;
700− },
701− any
702− >;
703− };
704− bootstrapAnonymousSession: FunctionReference<
705− "mutation",
706− "internal",
707− { accountId?: string; clientId?: string; sessionId?: string },
708− any
709− >;
710− closeStorageInstance: FunctionReference<
711− "mutation",
712− "internal",
713− {
714− accountId?: Id<"accounts">;
715− instanceId: string;
716− workspaceId: Id<"workspaces">;
717− },
718− any
719− >;
720− createApproval: FunctionReference<
721− "mutation",
722− "internal",
723− {
724− id: string;
725− input?: Record<string, any>;
726− taskId: string;
727− toolPath: string;
728− },
729− any
730− >;
731− createTask: FunctionReference<
732− "mutation",
733− "internal",
734− {
735− accountId?: Id<"accounts">;
736− clientId?: string;
737− code: string;
738− id: string;
739− metadata?: Record<string, any>;
740− runtimeId: string;
741− timeoutMs?: number;
742− workspaceId: Id<"workspaces">;
743− },
744− any
745− >;
746− createTaskEvent: FunctionReference<
747− "mutation",
748− "internal",
749− {
750− eventName: string;
751− payload: Record<string, any>;
752− taskId: string;
753− type: string;
754− },
755− any
756− >;
757− credentials: {
758− listCredentialProviders: FunctionReference<"query", "internal", {}, any>;
759− listCredentials: FunctionReference<
760− "query",
761− "internal",
762− { accountId?: Id<"accounts">; workspaceId: Id<"workspaces"> },
763− any
764− >;
765− resolveCredential: FunctionReference<
766− "query",
767− "internal",
768− {
769− accountId?: Id<"accounts">;
770− scopeType: "account" | "organization" | "workspace";
771− sourceKey: string;
772− workspaceId: Id<"workspaces">;
773− },
774− any
775− >;
776− upsertCredential: FunctionReference<
777− "mutation",
778− "internal",
779− {
780− accountId?: Id<"accounts">;
781− id?: string;
782− overridesJson?: Record<string, any>;
783− provider?: "local-convex" | "workos-vault";
784− scopeType?: "account" | "organization" | "workspace";
785− secretJson: Record<string, any>;
786− sourceKey: string;
787− workspaceId: Id<"workspaces">;
788− },
789− any
790− >;
791− };
792− deleteStorageInstance: FunctionReference<
793− "mutation",
794− "internal",
795− {
796− accountId?: Id<"accounts">;
797− instanceId: string;
798− workspaceId: Id<"workspaces">;
799− },
800− any
801− >;
802− deleteToolPolicyAssignment: FunctionReference<
803− "mutation",
804− "internal",
805− { bindingId: string; workspaceId: Id<"workspaces"> },
806− any
807− >;
808− deleteToolPolicyRule: FunctionReference<
809− "mutation",
810− "internal",
811− { roleId: string; ruleId: string; workspaceId: Id<"workspaces"> },
812− any
813− >;
814− deleteToolPolicySet: FunctionReference<
815− "mutation",
816− "internal",
817− { roleId: string; workspaceId: Id<"workspaces"> },
818− any
819− >;
820− deleteToolSource: FunctionReference<
821− "mutation",
822− "internal",
823− { sourceId: string; workspaceId: Id<"workspaces"> },
824− any
825− >;
826− finishToolCall: FunctionReference<
827− "mutation",
828− "internal",
829− {
830− callId: string;
831− error?: string;
832− status: "completed" | "failed" | "denied";
833− taskId: string;
834− },
835− any
836− >;
837− getApproval: FunctionReference<
838− "query",
839− "internal",
840− { approvalId: string },
841− any
842− >;
843− getApprovalInWorkspace: FunctionReference<
844− "query",
845− "internal",
846− { approvalId: string; workspaceId: Id<"workspaces"> },
847− any
848− >;
849− getStorageInstance: FunctionReference<
850− "query",
851− "internal",
852− {
853− accountId?: Id<"accounts">;
854− instanceId: string;
855− workspaceId: Id<"workspaces">;
856− },
857− any
858− >;
859− getTask: FunctionReference<"query", "internal", { taskId: string }, any>;
860− getTaskInWorkspace: FunctionReference<
861− "query",
862− "internal",
863− { taskId: string; workspaceId: Id<"workspaces"> },
864− any
865− >;
866− getToolCall: FunctionReference<
867− "query",
868− "internal",
869− { callId: string; taskId: string },
870− any
871− >;
872− listApprovals: FunctionReference<
873− "query",
874− "internal",
875− {
876− status?: "pending" | "approved" | "denied";
877− workspaceId: Id<"workspaces">;
878− },
879− any
880− >;
881− listCredentialProviders: FunctionReference<"query", "internal", {}, any>;
882− listCredentials: FunctionReference<
883− "query",
884− "internal",
885− { accountId?: Id<"accounts">; workspaceId: Id<"workspaces"> },
886− any
887− >;
888− listPendingApprovals: FunctionReference<
889− "query",
890− "internal",
891− { workspaceId: Id<"workspaces"> },
892− any
893− >;
894− listQueuedTaskIds: FunctionReference<
895− "query",
896− "internal",
897− { limit?: number },
898− any
899− >;
900− listRuntimeTargets: FunctionReference<"query", "internal", {}, any>;
901− listStorageInstances: FunctionReference<
902− "query",
903− "internal",
904− {
905− accountId?: Id<"accounts">;
906− includeDeleted?: boolean;
907− scopeType?: "scratch" | "account" | "workspace" | "organization";
908− workspaceId: Id<"workspaces">;
909− },
910− any
911− >;
912− listTaskEvents: FunctionReference<
913− "query",
914− "internal",
915− { taskId: string },
916− any
917− >;
918− listTasks: FunctionReference<
919− "query",
920− "internal",
921− { workspaceId: Id<"workspaces"> },
922− any
923− >;
924− listToolCalls: FunctionReference<
925− "query",
926− "internal",
927− { taskId: string },
928− any
929− >;
930− listToolPolicies: FunctionReference<
931− "query",
932− "internal",
933− { accountId?: Id<"accounts">; workspaceId: Id<"workspaces"> },
934− any
935− >;
936− listToolPolicyAssignments: FunctionReference<
937− "query",
938− "internal",
939− { roleId?: string; workspaceId: Id<"workspaces"> },
940− any
941− >;
942− listToolPolicyRules: FunctionReference<
943− "query",
944− "internal",
945− { roleId: string; workspaceId: Id<"workspaces"> },
946− any
947− >;
948− listToolPolicySets: FunctionReference<
949− "query",
950− "internal",
951− { workspaceId: Id<"workspaces"> },
952− any
953− >;
954− listToolSources: FunctionReference<
955− "query",
956− "internal",
957− { workspaceId: Id<"workspaces"> },
958− any
959− >;
960− markTaskFinished: FunctionReference<
961− "mutation",
962− "internal",
963− {
964− error?: string;
965− exitCode?: number;
966− status: "completed" | "failed" | "timed_out" | "denied";
967− taskId: string;
968− },
969− any
970− >;
971− markTaskRunning: FunctionReference<
972− "mutation",
973− "internal",
974− { taskId: string },
975− any
976− >;
977− openStorageInstance: FunctionReference<
978− "mutation",
979− "internal",
980− {
981− accountId?: Id<"accounts">;
982− durability?: "ephemeral" | "durable";
983− instanceId?: string;
984− purpose?: string;
985− scopeType?: "scratch" | "account" | "workspace" | "organization";
986− ttlHours?: number;
987− workspaceId: Id<"workspaces">;
988− },
989− any
990− >;
991− policies: {
992− deleteToolPolicyAssignment: FunctionReference<
993− "mutation",
994− "internal",
995− { bindingId: string; workspaceId: Id<"workspaces"> },
996− any
997− >;
998− deleteToolPolicyRule: FunctionReference<
999− "mutation",
1000− "internal",
1001− { roleId: string; ruleId: string; workspaceId: Id<"workspaces"> },
1002− any
1003− >;
1004− deleteToolPolicySet: FunctionReference<
1005− "mutation",
1006− "internal",
1007− { roleId: string; workspaceId: Id<"workspaces"> },
1008− any
1009− >;
1010− listRuntimeTargets: FunctionReference<"query", "internal", {}, any>;
1011− listToolPolicies: FunctionReference<
1012− "query",
1013− "internal",
1014− { accountId?: Id<"accounts">; workspaceId: Id<"workspaces"> },
1015− any
1016− >;
1017− listToolPolicyAssignments: FunctionReference<
1018− "query",
1019− "internal",
1020− { roleId?: string; workspaceId: Id<"workspaces"> },
1021− any
1022− >;
1023− listToolPolicyRules: FunctionReference<
1024− "query",
1025− "internal",
1026− { roleId: string; workspaceId: Id<"workspaces"> },
1027− any
1028− >;
1029− listToolPolicySets: FunctionReference<
1030− "query",
1031− "internal",
1032− { workspaceId: Id<"workspaces"> },
1033− any
1034− >;
1035− upsertToolPolicyAssignment: FunctionReference<
1036− "mutation",
1037− "internal",
1038− {
1039− clientId?: string;
1040− expiresAt?: number;
1041− id?: string;
1042− roleId: string;
1043− scopeType?: "account" | "organization" | "workspace";
1044− status?: "active" | "disabled";
1045− targetAccountId?: Id<"accounts">;
1046− workspaceId: Id<"workspaces">;
1047− },
1048− any
1049− >;
1050− upsertToolPolicyRule: FunctionReference<
1051− "mutation",
1052− "internal",
1053− {
1054− approvalMode?: "inherit" | "auto" | "required";
1055− argumentConditions?: Array<{
1056− key: string;
1057− operator: "equals" | "contains" | "starts_with" | "not_equals";
1058− value: string;
1059− }>;
1060− effect?: "allow" | "deny";
1061− id?: string;
1062− matchType?: "glob" | "exact";
1063− priority?: number;
1064− resourcePattern?: string;
1065− roleId: string;
1066− selectorType: "all" | "source" | "namespace" | "tool_path";
1067− sourceKey?: string;
1068− workspaceId: Id<"workspaces">;
1069− },
1070− any
1071− >;
1072− upsertToolPolicySet: FunctionReference<
1073− "mutation",
1074− "internal",
1075− {
1076− createdByAccountId?: Id<"accounts">;
1077− description?: string;
1078− id?: string;
1079− name: string;
1080− workspaceId: Id<"workspaces">;
1081− },
1082− any
1083− >;
1084− };
1085− resolveApproval: FunctionReference<
1086− "mutation",
1087− "internal",
1088− {
1089− approvalId: string;
1090− decision: "approved" | "denied";
1091− reason?: string;
1092− reviewerId?: string;
1093− },
1094− any
1095− >;
1096− resolveCredential: FunctionReference<
1097− "query",
1098− "internal",
1099− {
1100− accountId?: Id<"accounts">;
1101− scopeType: "account" | "organization" | "workspace";
1102− sourceKey: string;
1103− workspaceId: Id<"workspaces">;
1104− },
1105− any
1106− >;
1107− setToolCallPendingApproval: FunctionReference<
1108− "mutation",
1109− "internal",
1110− { approvalId: string; callId: string; taskId: string },
1111− any
1112− >;
1113− storage_instances: {
1114− closeStorageInstance: FunctionReference<
1115− "mutation",
1116− "internal",
1117− {
1118− accountId?: Id<"accounts">;
1119− instanceId: string;
1120− workspaceId: Id<"workspaces">;
1121− },
1122− any
1123− >;
1124− deleteStorageInstance: FunctionReference<
1125− "mutation",
1126− "internal",
1127− {
1128− accountId?: Id<"accounts">;
1129− instanceId: string;
1130− workspaceId: Id<"workspaces">;
1131− },
1132− any
1133− >;
1134− getStorageInstance: FunctionReference<
1135− "query",
1136− "internal",
1137− {
1138− accountId?: Id<"accounts">;
1139− instanceId: string;
1140− workspaceId: Id<"workspaces">;
1141− },
1142− any
1143− >;
1144− listStorageInstances: FunctionReference<
1145− "query",
1146− "internal",
1147− {
1148− accountId?: Id<"accounts">;
1149− includeDeleted?: boolean;
1150− scopeType?: "scratch" | "account" | "workspace" | "organization";
1151− workspaceId: Id<"workspaces">;
1152− },
1153− any
1154− >;
1155− openStorageInstance: FunctionReference<
1156− "mutation",
1157− "internal",
1158− {
1159− accountId?: Id<"accounts">;
1160− durability?: "ephemeral" | "durable";
1161− instanceId?: string;
1162− purpose?: string;
1163− scopeType?: "scratch" | "account" | "workspace" | "organization";
1164− ttlHours?: number;
1165− workspaceId: Id<"workspaces">;
1166− },
1167− any
1168− >;
1169− touchStorageInstance: FunctionReference<
1170− "mutation",
1171− "internal",
1172− {
1173− accountId?: Id<"accounts">;
1174− fileCount?: number;
1175− instanceId: string;
1176− provider?: "agentfs-local" | "agentfs-cloudflare";
1177− sizeBytes?: number;
1178− status?: "active" | "closed" | "deleted";
1179− workspaceId: Id<"workspaces">;
1180− },
1181− any
1182− >;
1183− };
1184− task_events: {
1185− createTaskEvent: FunctionReference<
1186− "mutation",
1187− "internal",
1188− {
1189− eventName: string;
1190− payload: Record<string, any>;
1191− taskId: string;
1192− type: string;
1193− },
1194− any
1195− >;
1196− listTaskEvents: FunctionReference<
1197− "query",
1198− "internal",
1199− { taskId: string },
1200− any
1201− >;
1202− };
1203− tasks: {
1204− createTask: FunctionReference<
1205− "mutation",
1206− "internal",
1207− {
1208− accountId?: Id<"accounts">;
1209− clientId?: string;
1210− code: string;
1211− id: string;
1212− metadata?: Record<string, any>;
1213− runtimeId: string;
1214− timeoutMs?: number;
1215− workspaceId: Id<"workspaces">;
1216− },
1217− any
1218− >;
1219− getTask: FunctionReference<"query", "internal", { taskId: string }, any>;
1220− getTaskInWorkspace: FunctionReference<
1221− "query",
1222− "internal",
1223− { taskId: string; workspaceId: Id<"workspaces"> },
1224− any
1225− >;
1226− listQueuedTaskIds: FunctionReference<
1227− "query",
1228− "internal",
1229− { limit?: number },
1230− any
1231− >;
1232− listTasks: FunctionReference<
1233− "query",
1234− "internal",
1235− { workspaceId: Id<"workspaces"> },
1236− any
1237− >;
1238− markTaskFinished: FunctionReference<
1239− "mutation",
1240− "internal",
1241− {
1242− error?: string;
1243− exitCode?: number;
1244− status: "completed" | "failed" | "timed_out" | "denied";
1245− taskId: string;
1246− },
1247− any
1248− >;
1249− markTaskRunning: FunctionReference<
1250− "mutation",
1251− "internal",
1252− { taskId: string },
1253− any
1254− >;
1255− };
1256− tool_calls: {
1257− finishToolCall: FunctionReference<
1258− "mutation",
1259− "internal",
1260− {
1261− callId: string;
1262− error?: string;
1263− status: "completed" | "failed" | "denied";
1264− taskId: string;
1265− },
1266− any
1267− >;
1268− getToolCall: FunctionReference<
1269− "query",
1270− "internal",
1271− { callId: string; taskId: string },
1272− any
1273− >;
1274− listToolCalls: FunctionReference<
1275− "query",
1276− "internal",
1277− { taskId: string },
1278− any
1279− >;
1280− setToolCallPendingApproval: FunctionReference<
1281− "mutation",
1282− "internal",
1283− { approvalId: string; callId: string; taskId: string },
1284− any
1285− >;
1286− upsertToolCallRequested: FunctionReference<
1287− "mutation",
1288− "internal",
1289− {
1290− callId: string;
1291− taskId: string;
1292− toolPath: string;
1293− workspaceId: Id<"workspaces">;
1294− },
1295− any
1296− >;
1297− };
1298− tool_sources: {
1299− deleteToolSource: FunctionReference<
1300− "mutation",
1301− "internal",
1302− { sourceId: string; workspaceId: Id<"workspaces"> },
1303− any
1304− >;
1305− listToolSources: FunctionReference<
1306− "query",
1307− "internal",
1308− { workspaceId: Id<"workspaces"> },
1309− any
1310− >;
1311− upsertToolSource: FunctionReference<
1312− "mutation",
1313− "internal",
1314− {
1315− config: Record<string, any>;
1316− enabled?: boolean;
1317− id?: string;
1318− name: string;
1319− scopeType?: "organization" | "workspace";
1320− type: "mcp" | "openapi" | "graphql";
1321− workspaceId: Id<"workspaces">;
1322− },
1323− any
1324− >;
1325− };
1326− touchStorageInstance: FunctionReference<
1327− "mutation",
1328− "internal",
1329− {
1330− accountId?: Id<"accounts">;
1331− fileCount?: number;
1332− instanceId: string;
1333− provider?: "agentfs-local" | "agentfs-cloudflare";
1334− sizeBytes?: number;
1335− status?: "active" | "closed" | "deleted";
1336− workspaceId: Id<"workspaces">;
1337− },
1338− any
1339− >;
1340− upsertCredential: FunctionReference<
1341− "mutation",
1342− "internal",
1343− {
1344− accountId?: Id<"accounts">;
1345− id?: string;
1346− overridesJson?: Record<string, any>;
1347− provider?: "local-convex" | "workos-vault";
1348− scopeType?: "account" | "organization" | "workspace";
1349− secretJson: Record<string, any>;
1350− sourceKey: string;
1351− workspaceId: Id<"workspaces">;
1352− },
1353− any
1354− >;
1355− upsertToolCallRequested: FunctionReference<
1356− "mutation",
1357− "internal",
1358− {
1359− callId: string;
1360− taskId: string;
1361− toolPath: string;
1362− workspaceId: Id<"workspaces">;
1363− },
1364− any
1365− >;
1366− upsertToolPolicyAssignment: FunctionReference<
1367− "mutation",
1368− "internal",
1369− {
1370− clientId?: string;
1371− expiresAt?: number;
1372− id?: string;
1373− roleId: string;
1374− scopeType?: "account" | "organization" | "workspace";
1375− status?: "active" | "disabled";
1376− targetAccountId?: Id<"accounts">;
1377− workspaceId: Id<"workspaces">;
1378− },
1379− any
1380− >;
1381− upsertToolPolicyRule: FunctionReference<
1382− "mutation",
1383− "internal",
1384− {
1385− approvalMode?: "inherit" | "auto" | "required";
1386− argumentConditions?: Array<{
1387− key: string;
1388− operator: "equals" | "contains" | "starts_with" | "not_equals";
1389− value: string;
1390− }>;
1391− effect?: "allow" | "deny";
1392− id?: string;
1393− matchType?: "glob" | "exact";
1394− priority?: number;
1395− resourcePattern?: string;
1396− roleId: string;
1397− selectorType: "all" | "source" | "namespace" | "tool_path";
1398− sourceKey?: string;
1399− workspaceId: Id<"workspaces">;
1400− },
1401− any
1402− >;
1403− upsertToolPolicySet: FunctionReference<
1404− "mutation",
1405− "internal",
1406− {
1407− createdByAccountId?: Id<"accounts">;
1408− description?: string;
1409− id?: string;
1410− name: string;
1411− workspaceId: Id<"workspaces">;
1412− },
1413− any
1414− >;
1415− upsertToolSource: FunctionReference<
1416− "mutation",
1417− "internal",
1418− {
1419− config: Record<string, any>;
1420− enabled?: boolean;
1421− id?: string;
1422− name: string;
1423− scopeType?: "organization" | "workspace";
1424− type: "mcp" | "openapi" | "graphql";
1425− workspaceId: Id<"workspaces">;
1426− },
1427− any
1428− >;
1429− };
1430− executor: {
1431− completeRuntimeRun: FunctionReference<
1432− "mutation",
1433− "internal",
1434− {
1435− durationMs?: number;
1436− error?: string;
1437− exitCode?: number;
1438− runId: string;
1439− status: "completed" | "failed" | "timed_out" | "denied";
1440− },
1441− any
1442− >;
1443− createTaskInternal: FunctionReference<
1444− "mutation",
1445− "internal",
1446− {
1447− accountId: Id<"accounts">;
1448− clientId?: string;
1449− code: string;
1450− metadata?: Record<string, any>;
1451− runtimeId?: string;
1452− scheduleAfterCreate?: boolean;
1453− timeoutMs?: number;
1454− workspaceId: Id<"workspaces">;
1455− },
1456− any
1457− >;
1458− resolveApprovalInternal: FunctionReference<
1459− "mutation",
1460− "internal",
1461− {
1462− approvalId: string;
1463− decision: "approved" | "denied";
1464− reason?: string;
1465− reviewerId?: string;
1466− workspaceId: Id<"workspaces">;
1467− },
1468− any
1469− >;
1470− };
1471− executorNode: {
1472− handleExternalToolCall: FunctionReference<
1473− "action",
1474− "internal",
1475− {
1476− callId: string;
1477− input?: Record<string, any>;
1478− runId: string;
1479− toolPath: string;
1480− },
1481− any
1482− >;
1483− listToolsInternal: FunctionReference<
1484− "action",
1485− "internal",
1486− {
1487− accountId?: Id<"accounts">;
1488− clientId?: string;
1489− workspaceId: Id<"workspaces">;
1490− },
1491− any
1492− >;
1493− listToolsWithWarningsInternal: FunctionReference<
1494− "action",
1495− "internal",
1496− {
1497− accountId?: Id<"accounts">;
1498− clientId?: string;
1499− workspaceId: Id<"workspaces">;
1500− },
1501− any
1502− >;
1503− rebuildToolInventoryInternal: FunctionReference<
1504− "action",
1505− "internal",
1506− {
1507− accountId?: Id<"accounts">;
1508− clientId?: string;
1509− workspaceId: Id<"workspaces">;
1510− },
1511− any
1512− >;
1513− runTask: FunctionReference<"action", "internal", { taskId: string }, any>;
1514− };
1515− executorRuntimeNode: {
1516− dispatchCloudflareWorker: FunctionReference<
1517− "action",
1518− "internal",
1519− { code: string; taskId: string; timeoutMs: number },
1520− any
1521− >;
1522− };
1523− invites: {
1524− deliverWorkosInvite: FunctionReference<
1525− "action",
1526− "internal",
1527− {
1528− expiresInDays?: number;
1529− inviteId: Id<"invites">;
1530− inviterWorkosUserId: string;
1531− roleSlug?: string;
1532− },
1533− any
1534− >;
1535− getInviteById: FunctionReference<
1536− "query",
1537− "internal",
1538− { inviteId: Id<"invites"> },
1539− any
1540− >;
1541− getInviteDeliveryContext: FunctionReference<
1542− "query",
1543− "internal",
1544− { inviteId: Id<"invites"> },
1545− any
1546− >;
1547− linkOrganizationToWorkos: FunctionReference<
1548− "mutation",
1549− "internal",
1550− { organizationId: Id<"organizations">; workosOrgId: string },
1551− any
1552− >;
1553− markInviteDelivered: FunctionReference<
1554− "mutation",
1555− "internal",
1556− { inviteId: Id<"invites">; providerInviteId: string },
1557− any
1558− >;
1559− markInviteDeliveryFailed: FunctionReference<
1560− "mutation",
1561− "internal",
1562− { errorMessage: string; inviteId: Id<"invites"> },
1563− any
1564− >;
1565− revokeWorkosInvite: FunctionReference<
1566− "action",
1567− "internal",
1568− { inviteId: Id<"invites">; providerInviteId: string },
1569− any
1570− >;
1571− };
1572− migrations: {
1573− run: FunctionReference<
1574− "mutation",
1575− "internal",
1576− {
1577− batchSize?: number;
1578− cursor?: string | null;
1579− dryRun?: boolean;
1580− fn?: string;
1581− next?: Array<string>;
1582− },
1583− any
1584− >;
1585− };
1586− openApiSpecCache: {
1587− getEntry: FunctionReference<
1588− "query",
1589− "internal",
1590− { maxAgeMs: number; specUrl: string; version: string },
1591− any
1592− >;
1593− putEntry: FunctionReference<
1594− "mutation",
1595− "internal",
1596− {
1597− sizeBytes: number;
1598− specUrl: string;
1599− storageId: Id<"_storage">;
1600− version: string;
1601− },
1602− any
1603− >;
1604− };
1605− runtimeNode: {
1606− compileExternalToolSource: FunctionReference<
1607− "action",
1608− "internal",
1609− { source: Record<string, any> },
1610− any
1611− >;
1612− executeLocalVm: FunctionReference<
1613− "action",
1614− "internal",
1615− { code: string; taskId: string; timeoutMs: number },
1616− any
1617− >;
1618− prepareOpenApiSpec: FunctionReference<
1619− "action",
1620− "internal",
1621− { includeDts?: boolean; sourceName: string; specUrl: string },
1622− any
1623− >;
1624− };
1625− toolRegistry: {
1626− beginBuild: FunctionReference<
1627− "mutation",
1628− "internal",
1629− { buildId: string; signature: string; workspaceId: Id<"workspaces"> },
1630− any
1631− >;
1632− deleteToolRegistryNamespacesPage: FunctionReference<
1633− "mutation",
1634− "internal",
1635− { buildId: string; cursor?: string; workspaceId: Id<"workspaces"> },
1636− any
1637− >;
1638− deleteToolRegistryPayloadsPage: FunctionReference<
1639− "mutation",
1640− "internal",
1641− { buildId: string; cursor?: string; workspaceId: Id<"workspaces"> },
1642− any
1643− >;
1644− deleteToolRegistryToolsPage: FunctionReference<
1645− "mutation",
1646− "internal",
1647− { buildId: string; cursor?: string; workspaceId: Id<"workspaces"> },
1648− any
1649− >;
1650− failBuild: FunctionReference<
1651− "mutation",
1652− "internal",
1653− { buildId: string; error?: string; workspaceId: Id<"workspaces"> },
1654− any
1655− >;
1656− finishBuild: FunctionReference<
1657− "mutation",
1658− "internal",
1659− { buildId: string; signature: string; workspaceId: Id<"workspaces"> },
1660− any
1661− >;
1662− getSerializedToolsByPaths: FunctionReference<
1663− "query",
1664− "internal",
1665− { buildId: string; paths: Array<string>; workspaceId: Id<"workspaces"> },
1666− any
1667− >;
1668− getState: FunctionReference<
1669− "query",
1670− "internal",
1671− { workspaceId: Id<"workspaces"> },
1672− any
1673− >;
1674− getToolByPath: FunctionReference<
1675− "query",
1676− "internal",
1677− { buildId: string; path: string; workspaceId: Id<"workspaces"> },
1678− any
1679− >;
1680− getToolsByNormalizedPath: FunctionReference<
1681− "query",
1682− "internal",
1683− {
1684− buildId: string;
1685− limit: number;
1686− normalizedPath: string;
1687− workspaceId: Id<"workspaces">;
1688− },
1689− any
1690− >;
1691− listNamespaces: FunctionReference<
1692− "query",
1693− "internal",
1694− { buildId: string; limit: number; workspaceId: Id<"workspaces"> },
1695− any
1696− >;
1697− listToolsByNamespace: FunctionReference<
1698− "query",
1699− "internal",
1700− {
1701− buildId: string;
1702− limit: number;
1703− namespace: string;
1704− workspaceId: Id<"workspaces">;
1705− },
1706− any
1707− >;
1708− listToolsBySourcePage: FunctionReference<
1709− "query",
1710− "internal",
1711− {
1712− buildId: string;
1713− cursor?: string;
1714− limit: number;
1715− source: string;
1716− workspaceId: Id<"workspaces">;
1717− },
1718− any
1719− >;
1720− listToolsPage: FunctionReference<
1721− "query",
1722− "internal",
1723− {
1724− buildId: string;
1725− cursor?: string;
1726− limit: number;
1727− workspaceId: Id<"workspaces">;
1728− },
1729− any
1730− >;
1731− pruneBuilds: FunctionReference<
1732− "action",
1733− "internal",
1734− { maxRetainedBuilds?: number; workspaceId: Id<"workspaces"> },
1735− any
1736− >;
1737− putNamespacesBatch: FunctionReference<
1738− "mutation",
1739− "internal",
1740− {
1741− buildId: string;
1742− namespaces: Array<{
1743− namespace: string;
1744− samplePaths: Array<string>;
1745− toolCount: number;
1746− }>;
1747− workspaceId: Id<"workspaces">;
1748− },
1749− any
1750− >;
1751− putToolsBatch: FunctionReference<
1752− "mutation",
1753− "internal",
1754− {
1755− buildId: string;
1756− tools: Array<{
1757− aliases: Array<string>;
1758− approval: "auto" | "required";
1759− description: string;
1760− displayInput?: string;
1761− displayOutput?: string;
1762− namespace: string;
1763− normalizedPath: string;
1764− path: string;
1765− preferredPath: string;
1766− previewInputKeys?: Array<string>;
1767− requiredInputKeys?: Array<string>;
1768− searchText: string;
1769− serializedToolJson: string;
1770− source?: string;
1771− typedRef?: {
1772− kind: "openapi_operation";
1773− operationId: string;
1774− sourceKey: string;
1775− };
1776− }>;
1777− workspaceId: Id<"workspaces">;
1778− },
1779− any
1780− >;
1781− scanNamespaceBuildsForPrune: FunctionReference<
1782− "query",
1783− "internal",
1784− { cursor?: string; workspaceId: Id<"workspaces"> },
1785− any
1786− >;
1787− searchTools: FunctionReference<
1788− "query",
1789− "internal",
1790− {
1791− buildId: string;
1792− limit: number;
1793− query: string;
1794− workspaceId: Id<"workspaces">;
1795− },
1796− any
1797− >;
1798− updateBuildMetadata: FunctionReference<
1799− "mutation",
1800− "internal",
1801− {
1802− buildId: string;
1803− openApiRefHintTables?: Array<{
1804− refs: Array<{ hint: string; key: string }>;
1805− sourceKey: string;
1806− }>;
1807− sourceAuthProfiles: Array<{
1808− header?: string;
1809− inferred: boolean;
1810− mode?: "account" | "organization" | "workspace";
1811− sourceKey: string;
1812− type: "none" | "bearer" | "apiKey" | "basic" | "mixed";
1813− }>;
1814− sourceQuality: Array<{
1815− argsQuality: number;
1816− overallQuality: number;
1817− partialUnknownArgsCount: number;
1818− partialUnknownReturnsCount: number;
1819− returnsQuality: number;
1820− sourceKey: string;
1821− toolCount: number;
1822− unknownArgsCount: number;
1823− unknownReturnsCount: number;
1824− }>;
1825− sourceToolCounts: Array<{ sourceName: string; toolCount: number }>;
1826− sourceVersions: Array<{
1827− sourceId: string;
1828− sourceName: string;
1829− updatedAt: number;
1830− }>;
1831− toolCount: number;
1832− typesStorageId?: Id<"_storage">;
1833− warnings: Array<string>;
1834− workspaceId: Id<"workspaces">;
1835− },
1836− any
1837− >;
1838− };
1839− workspaceAuthInternal: {
1840− getWorkspaceAccessForAccount: FunctionReference<
1841− "query",
1842− "internal",
1843− { accountId: Id<"accounts">; workspaceId: Id<"workspaces"> },
1844− any
1845− >;
1846− getWorkspaceAccessForAnonymousSubject: FunctionReference<
1847− "query",
1848− "internal",
1849− { accountId: string; workspaceId: Id<"workspaces"> },
1850− any
1851− >;
1852− getWorkspaceAccessForRequest: FunctionReference<
1853− "query",
1854− "internal",
1855− { sessionId?: string; workspaceId: Id<"workspaces"> },
1856− any
1857− >;
1858− getWorkspaceAccessForWorkosSubject: FunctionReference<
1859− "query",
1860− "internal",
1861− { subject: string; workspaceId: Id<"workspaces"> },
1862− any
1863− >;
1864− };
1865−};
122+export declare const internal: FilterApi<
123+ typeof fullApi,
124+ FunctionReference<any, "internal">
125+>;
1866126
1867127 export declare const components: {
1868128 workOSAuthKit: {
executor/packages/database/convex/database.ts+2−0
@@ -6,6 +6,8 @@ export { · 5 unmodified lines
66 getTaskInWorkspace,
77 markTaskRunning,
88 markTaskFinished,
9+ setTaskStorageDefaultInstance,
10+ trackTaskStorageAccess,
911 } from "./database/tasks";
1012 export {
1113 createApproval,
executor/packages/database/convex/database/storage_instances.ts+20−0
@@ -14,9 +14,29 @@ const MAX_EPHEMERAL_TTL_HOURS = 24 * 30; · 13 unmodified lines
1414
1515 function resolveStorageProvider(): "agentfs-local" | "agentfs-cloudflare" {
1616 const raw = (process.env.AGENT_STORAGE_PROVIDER ?? "").trim().toLowerCase();
17+
18+ const isHostedConvexDeployment = [process.env.CONVEX_URL, process.env.CONVEX_SITE_URL]
19+ .map((value) => value?.trim())
20+ .filter((value): value is string => Boolean(value))
21+ .some((value) => {
22+ try {
23+ const hostname = new URL(value).hostname.trim().toLowerCase();
24+ return hostname.endsWith(".convex.cloud") || hostname.endsWith(".convex.site");
25+ } catch {
26+ return false;
27+ }
28+ });
29+
1730 if (raw === "cloudflare" || raw === "agentfs-cloudflare") {
1831 return "agentfs-cloudflare";
1932 }
33+
34+ if (isHostedConvexDeployment) {
35+ throw new Error(
36+ "agentfs-local is not supported on hosted Convex deployments because filesystem state is not shared across workers. Set AGENT_STORAGE_PROVIDER=agentfs-cloudflare.",
37+ );
38+ }
39+
2040 return "agentfs-local";
2141 }
2242
executor/packages/database/convex/database/tasks.ts+140−1
@@ -2,10 +2,46 @@ import { v } from "convex/values"; · 1 unmodified line
22 import { internalMutation, internalQuery } from "../_generated/server";
33 import { mapTask } from "../../src/database/mappers";
44 import { getTaskDoc } from "../../src/database/readers";
5−import { completedTaskStatusValidator, jsonObjectValidator } from "../../src/database/validators";
5+import {
6+ completedTaskStatusValidator,
7+ jsonObjectValidator,
8+ storageAccessTypeValidator,
9+ storageScopeTypeValidator,
10+} from "../../src/database/validators";
611 import { DEFAULT_TASK_TIMEOUT_MS } from "../../src/task/constants";
712 import { isTerminalTaskStatus } from "../../src/task/status";
813
14+function asRecord(value: unknown): Record<string, unknown> {
15+ return value && typeof value === "object" && !Array.isArray(value)
16+ ? value as Record<string, unknown>
17+ : {};
18+}
19+
20+function asStringArray(value: unknown): string[] {
21+ if (!Array.isArray(value)) {
22+ return [];
23+ }
24+
25+ return value
26+ .filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
27+ .map((entry) => entry.trim());
28+}
29+
30+function pushUniqueString(items: string[], value: string, maxItems = 50): string[] {
31+ const normalized = value.trim();
32+ if (!normalized) {
33+ return items;
34+ }
35+
36+ const withoutExisting = items.filter((entry) => entry !== normalized);
37+ const next = [...withoutExisting, normalized];
38+ if (next.length <= maxItems) {
39+ return next;
40+ }
41+
42+ return next.slice(next.length - maxItems);
43+}
44+
945 export const createTask = internalMutation({
1046 args: {
1147 id: v.string(),
@@ -143,3 +179,106 @@ export const markTaskFinished = internalMutation({ · 131 unmodified lines
143179 return updated ? mapTask(updated) : null;
144180 },
145181 });
182+
183+export const setTaskStorageDefaultInstance = internalMutation({
184+ args: {
185+ taskId: v.string(),
186+ scopeType: storageScopeTypeValidator,
187+ instanceId: v.string(),
188+ setCurrent: v.optional(v.boolean()),
189+ },
190+ handler: async (ctx, args) => {
191+ const doc = await getTaskDoc(ctx, args.taskId);
192+ if (!doc) {
193+ return null;
194+ }
195+
196+ const now = Date.now();
197+ const metadata = asRecord(doc.metadata);
198+ const storage = asRecord(metadata.storage);
199+ const defaultsByScope = asRecord(storage.defaultInstanceByScope);
200+
201+ defaultsByScope[args.scopeType] = args.instanceId;
202+
203+ const nextStorage: Record<string, unknown> = {
204+ ...storage,
205+ defaultInstanceByScope: defaultsByScope,
206+ };
207+
208+ if (args.setCurrent !== false) {
209+ nextStorage.currentInstanceId = args.instanceId;
210+ nextStorage.currentScopeType = args.scopeType;
211+ }
212+
213+ await ctx.db.patch(doc._id, {
214+ metadata: {
215+ ...metadata,
216+ storage: nextStorage,
217+ },
218+ updatedAt: now,
219+ });
220+
221+ const updated = await getTaskDoc(ctx, args.taskId);
222+ return updated ? mapTask(updated) : null;
223+ },
224+});
225+
226+export const trackTaskStorageAccess = internalMutation({
227+ args: {
228+ taskId: v.string(),
229+ instanceId: v.string(),
230+ scopeType: v.optional(storageScopeTypeValidator),
231+ accessType: storageAccessTypeValidator,
232+ },
233+ handler: async (ctx, args) => {
234+ const doc = await getTaskDoc(ctx, args.taskId);
235+ if (!doc) {
236+ return null;
237+ }
238+
239+ const now = Date.now();
240+ const metadata = asRecord(doc.metadata);
241+ const storage = asRecord(metadata.storage);
242+
243+ const accessedInstanceIds = pushUniqueString(
244+ asStringArray(storage.accessedInstanceIds),
245+ args.instanceId,
246+ );
247+
248+ const nextStorage: Record<string, unknown> = {
249+ ...storage,
250+ accessedInstanceIds,
251+ lastAccessedInstanceId: args.instanceId,
252+ lastAccessedAt: now,
253+ };
254+
255+ if (args.accessType === "opened") {
256+ nextStorage.openedInstanceIds = pushUniqueString(
257+ asStringArray(storage.openedInstanceIds),
258+ args.instanceId,
259+ );
260+ }
261+
262+ if (args.accessType === "provided") {
263+ nextStorage.providedInstanceIds = pushUniqueString(
264+ asStringArray(storage.providedInstanceIds),
265+ args.instanceId,
266+ );
267+ }
268+
269+ if (args.scopeType) {
270+ nextStorage.lastAccessedScopeType = args.scopeType;
271+ }
272+
273+ await ctx.db.patch(doc._id, {
274+ metadata: {
275+ ...metadata,
276+ storage: nextStorage,
277+ },
278+ updatedAt: now,
279+ });
280+
281+ const updated = await getTaskDoc(ctx, args.taskId);
282+ return updated ? mapTask(updated) : null;
283+ },
284+});
executor/packages/database/convex/executorNode.ts+195−0
@@ -7,6 +7,7 @@ import type { · 6 unmodified lines
77 ToolDescriptor,
88 OpenApiSourceQuality,
99 SourceAuthProfile,
10+ StorageInstanceRecord,
1011 } from "../../core/src/types";
1112 import { isAdminRole } from "../../core/src/identity";
1213 import { requireCanonicalAccount } from "../src/runtime/account_auth";
@@ -22,6 +23,72 @@ import { jsonObjectValidator } from "../src/database/validators"; · 9 unmodified lines
2223 import { customAction } from "../../core/src/function-builders";
2324 import { encodeToolCallResultForTransport } from "../../core/src/tool-call-result-transport";
2425 import { previewOpenApiSourceUpgradeForContext, type OpenApiUpgradeDiffPreview } from "../src/runtime/tool_upgrade";
26+import { getStorageProvider, type StorageEncoding, type StorageProvider } from "../src/runtime/storage_provider";
27+
28+function isReadOnlySql(sql: string): boolean {
29+ const trimmed = sql.trim().toLowerCase();
30+ return trimmed.startsWith("select")
31+ || trimmed.startsWith("pragma")
32+ || trimmed.startsWith("explain")
33+ || trimmed.startsWith("with");
34+}
35+
36+async function resolveStorageInspectorContext(
37+ ctx: Parameters<typeof requireCanonicalAccount>[0],
38+ args: {
39+ workspaceId: Parameters<typeof requireCanonicalAccount>[1]["workspaceId"];
40+ accountId?: Parameters<typeof requireCanonicalAccount>[1]["accountId"];
41+ sessionId?: string;
42+ instanceId: string;
43+ },
44+): Promise<{
45+ accountId: Awaited<ReturnType<typeof requireCanonicalAccount>>["accountId"];
46+ instance: StorageInstanceRecord;
47+ provider: StorageProvider;
48+}> {
49+ const access = await requireCanonicalAccount(ctx, {
50+ workspaceId: args.workspaceId,
51+ sessionId: args.sessionId,
52+ accountId: args.accountId,
53+ });
54+
55+ const instance = await ctx.runQuery(internal.database.getStorageInstance, {
56+ workspaceId: args.workspaceId,
57+ accountId: access.accountId,
58+ instanceId: args.instanceId,
59+ }) as StorageInstanceRecord | null;
60+
61+ if (!instance) {
62+ throw new Error(`Storage instance not found or inaccessible: ${args.instanceId}`);
63+ }
64+
65+ return {
66+ accountId: access.accountId,
67+ instance,
68+ provider: getStorageProvider(instance.provider),
69+ };
70+}
71+
72+async function touchStorageInstance(
73+ ctx: Parameters<typeof requireCanonicalAccount>[0],
74+ args: {
75+ workspaceId: Parameters<typeof requireCanonicalAccount>[1]["workspaceId"];
76+ accountId: Awaited<ReturnType<typeof requireCanonicalAccount>>["accountId"];
77+ instance: StorageInstanceRecord;
78+ provider: StorageProvider;
79+ withUsage: boolean;
80+ },
81+) {
82+ const usage = args.withUsage ? await args.provider.usage(args.instance) : undefined;
83+ await ctx.runMutation(internal.database.touchStorageInstance, {
84+ workspaceId: args.workspaceId,
85+ accountId: args.accountId,
86+ instanceId: args.instance.id,
87+ provider: args.instance.provider,
88+ ...(usage?.sizeBytes !== undefined ? { sizeBytes: usage.sizeBytes } : {}),
89+ ...(usage?.fileCount !== undefined ? { fileCount: usage.fileCount } : {}),
90+ });
91+}
2592
2693 export const listToolsWithWarnings = customAction({
2794 method: "POST",
@@ -177,6 +244,134 @@ export const previewOpenApiSourceUpgrade = customAction({ · 149 unmodified lines
177244 },
178245 });
179246
247+export const storageListDirectory = customAction({
248+ method: "POST",
249+ args: {
250+ workspaceId: v.id("workspaces"),
251+ accountId: v.optional(v.id("accounts")),
252+ sessionId: v.optional(v.string()),
253+ instanceId: v.string(),
254+ path: v.optional(v.string()),
255+ },
256+ handler: async (ctx, args) => {
257+ const { accountId, instance, provider } = await resolveStorageInspectorContext(ctx, args);
258+ const path = args.path?.trim() || "/";
259+ const entries = await provider.readdir(instance, path);
260+ await touchStorageInstance(ctx, {
261+ workspaceId: args.workspaceId,
262+ accountId,
263+ instance,
264+ provider,
265+ withUsage: false,
266+ });
267+ return {
268+ instanceId: instance.id,
269+ path,
270+ entries,
271+ };
272+ },
273+});
274+
275+export const storageReadFile = customAction({
276+ method: "POST",
277+ args: {
278+ workspaceId: v.id("workspaces"),
279+ accountId: v.optional(v.id("accounts")),
280+ sessionId: v.optional(v.string()),
281+ instanceId: v.string(),
282+ path: v.string(),
283+ encoding: v.optional(v.union(v.literal("utf8"), v.literal("base64"))),
284+ },
285+ handler: async (ctx, args) => {
286+ const { accountId, instance, provider } = await resolveStorageInspectorContext(ctx, args);
287+ const encoding = (args.encoding ?? "utf8") as StorageEncoding;
288+ const file = await provider.readFile(instance, args.path, encoding);
289+ await touchStorageInstance(ctx, {
290+ workspaceId: args.workspaceId,
291+ accountId,
292+ instance,
293+ provider,
294+ withUsage: false,
295+ });
296+ return {
297+ instanceId: instance.id,
298+ path: args.path,
299+ encoding,
300+ content: file.content,
301+ bytes: file.bytes,
302+ };
303+ },
304+});
305+
306+export const storageListKv = customAction({
307+ method: "POST",
308+ args: {
309+ workspaceId: v.id("workspaces"),
310+ accountId: v.optional(v.id("accounts")),
311+ sessionId: v.optional(v.string()),
312+ instanceId: v.string(),
313+ prefix: v.optional(v.string()),
314+ limit: v.optional(v.number()),
315+ },
316+ handler: async (ctx, args) => {
317+ const { accountId, instance, provider } = await resolveStorageInspectorContext(ctx, args);
318+ const limit = Math.max(1, Math.min(500, Math.floor(args.limit ?? 100)));
319+ const prefix = args.prefix?.trim() ?? "";
320+ const items = await provider.kvList(instance, prefix, limit);
321+ await touchStorageInstance(ctx, {
322+ workspaceId: args.workspaceId,
323+ accountId,
324+ instance,
325+ provider,
326+ withUsage: false,
327+ });
328+ return {
329+ instanceId: instance.id,
330+ prefix,
331+ items,
332+ total: items.length,
333+ };
334+ },
335+});
336+
337+export const storageQuerySql = customAction({
338+ method: "POST",
339+ args: {
340+ workspaceId: v.id("workspaces"),
341+ accountId: v.optional(v.id("accounts")),
342+ sessionId: v.optional(v.string()),
343+ instanceId: v.string(),
344+ sql: v.string(),
345+ params: v.optional(v.array(v.union(v.string(), v.number(), v.boolean(), v.null()))),
346+ maxRows: v.optional(v.number()),
347+ },
348+ handler: async (ctx, args) => {
349+ if (!isReadOnlySql(args.sql)) {
350+ throw new Error("Storage inspector only allows read-only SQL (SELECT/PRAGMA/EXPLAIN/WITH)");
351+ }
352+
353+ const { accountId, instance, provider } = await resolveStorageInspectorContext(ctx, args);
354+ const maxRows = Math.max(1, Math.min(1000, Math.floor(args.maxRows ?? 200)));
355+ const result = await provider.sqliteQuery(instance, {
356+ sql: args.sql,
357+ params: args.params ?? [],
358+ mode: "read",
359+ maxRows,
360+ });
361+ await touchStorageInstance(ctx, {
362+ workspaceId: args.workspaceId,
363+ accountId,
364+ instance,
365+ provider,
366+ withUsage: false,
367+ });
368+ return {
369+ instanceId: instance.id,
370+ ...result,
371+ };
372+ },
373+});
374+
180375 export const handleExternalToolCall = internalAction({
181376 args: {
182377 runId: v.string(),
executor/packages/database/src/database/validators.ts+6−0
@@ -62,3 +62,9 @@ export const storageDurabilityValidator = v.union(v.literal("ephemeral"), v.lite · 61 unmodified lines
6262 export const storageInstanceStatusValidator = v.union(v.literal("active"), v.literal("closed"), v.literal("deleted"));
6363
6464 export const storageProviderValidator = v.union(v.literal("agentfs-local"), v.literal("agentfs-cloudflare"));
65+
66+export const storageAccessTypeValidator = v.union(
67+ v.literal("opened"),
68+ v.literal("provided"),
69+ v.literal("accessed"),
70+);
executor/packages/database/src/runtime/base_tools.ts+136−4
@@ -24,12 +24,18 @@ import { · 23 unmodified lines
2424 kvDeleteOutputJsonSchema,
2525 kvGetInputJsonSchema,
2626 kvGetOutputJsonSchema,
27+ kvIncrInputJsonSchema,
28+ kvIncrOutputJsonSchema,
2729 kvListInputJsonSchema,
2830 kvListOutputJsonSchema,
2931 kvSetInputJsonSchema,
3032 kvSetOutputJsonSchema,
3133 sqliteQueryInputJsonSchema,
3234 sqliteQueryOutputJsonSchema,
35+ sqliteCapabilitiesInputJsonSchema,
36+ sqliteCapabilitiesOutputJsonSchema,
37+ sqliteInsertRowsInputJsonSchema,
38+ sqliteInsertRowsOutputJsonSchema,
3339 storageCloseInputJsonSchema,
3440 storageCloseOutputJsonSchema,
3541 storageDeleteInputJsonSchema,
@@ -94,7 +100,7 @@ registerSystemTool( · 58 unmodified lines
94100
95101 registerSystemTool(
96102 "storage.open",
97− "Open an existing storage instance or create a new one when instanceId is omitted.",
103+ "Open an existing storage instance or create a new one when instanceId is omitted. Saves a default instance for this task; pass instanceId explicitly when you need to reuse the same storage across separate tasks/runs.",
98104 {
99105 inputSchema: storageOpenInputJsonSchema,
100106 outputSchema: storageOpenOutputJsonSchema,
@@ -130,7 +136,7 @@ registerSystemTool( · 29 unmodified lines
130136
131137 registerSystemTool(
132138 "fs.read",
133− "Read a file from a storage instance.",
139+ "Read a file from a storage instance. Reuses the current default instance when instanceId is omitted.",
134140 {
135141 inputSchema: fsReadInputJsonSchema,
136142 outputSchema: fsReadOutputJsonSchema,
@@ -139,7 +145,7 @@ registerSystemTool( · 2 unmodified lines
139145
140146 registerSystemTool(
141147 "fs.write",
142− "Write file contents into a storage instance.",
148+ "Write file contents into a storage instance. Reuses the current default instance when instanceId is omitted.",
143149 {
144150 inputSchema: fsWriteInputJsonSchema,
145151 outputSchema: fsWriteOutputJsonSchema,
@@ -200,6 +206,33 @@ registerSystemTool( · 54 unmodified lines
200206 },
201207 );
202208
209+registerSystemTool(
210+ "kv.put",
211+ "Alias for kv.set. Write a key-value entry into a storage instance.",
212+ {
213+ inputSchema: kvSetInputJsonSchema,
214+ outputSchema: kvSetOutputJsonSchema,
215+ },
216+);
217+
218+registerSystemTool(
219+ "kv.create",
220+ "Alias for kv.set. Create a key-value entry in a storage instance.",
221+ {
222+ inputSchema: kvSetInputJsonSchema,
223+ outputSchema: kvSetOutputJsonSchema,
224+ },
225+);
226+
227+registerSystemTool(
228+ "kv.update",
229+ "Alias for kv.set. Update a key-value entry in a storage instance.",
230+ {
231+ inputSchema: kvSetInputJsonSchema,
232+ outputSchema: kvSetOutputJsonSchema,
233+ },
234+);
235+
203236 registerSystemTool(
204237 "kv.list",
205238 "List key-value entries by prefix from a storage instance.",
@@ -209,6 +242,15 @@ registerSystemTool( · 3 unmodified lines
209242 },
210243 );
211244
245+registerSystemTool(
246+ "kv.keys",
247+ "Alias for kv.list. List keys by prefix from a storage instance.",
248+ {
249+ inputSchema: kvListInputJsonSchema,
250+ outputSchema: kvListOutputJsonSchema,
251+ },
252+);
253+
212254 registerSystemTool(
213255 "kv.delete",
214256 "Delete a key-value entry from a storage instance.",
@@ -218,11 +260,101 @@ registerSystemTool( · 3 unmodified lines
218260 },
219261 );
220262
263+registerSystemTool(
264+ "kv.del",
265+ "Alias for kv.delete. Delete a key-value entry from a storage instance.",
266+ {
267+ inputSchema: kvDeleteInputJsonSchema,
268+ outputSchema: kvDeleteOutputJsonSchema,
269+ },
270+);
271+
272+registerSystemTool(
273+ "kv.has",
274+ "Alias for kv.get. Check whether a key exists in a storage instance.",
275+ {
276+ inputSchema: kvGetInputJsonSchema,
277+ outputSchema: kvGetOutputJsonSchema,
278+ },
279+);
280+
281+registerSystemTool(
282+ "kv.exists",
283+ "Alias for kv.get. Check whether a key exists in a storage instance.",
284+ {
285+ inputSchema: kvGetInputJsonSchema,
286+ outputSchema: kvGetOutputJsonSchema,
287+ },
288+);
289+
290+registerSystemTool(
291+ "kv.value",
292+ "Alias for kv.get. Read a key-value entry from a storage instance.",
293+ {
294+ inputSchema: kvGetInputJsonSchema,
295+ outputSchema: kvGetOutputJsonSchema,
296+ },
297+);
298+
299+registerSystemTool(
300+ "kv.incr",
301+ "Increment a numeric key-value entry and return the updated value. Creates the key from initial when missing.",
302+ {
303+ inputSchema: kvIncrInputJsonSchema,
304+ outputSchema: kvIncrOutputJsonSchema,
305+ },
306+);
307+
308+registerSystemTool(
309+ "kv.decr",
310+ "Decrement a numeric key-value entry and return the updated value. Creates the key from initial when missing.",
311+ {
312+ inputSchema: kvIncrInputJsonSchema,
313+ outputSchema: kvIncrOutputJsonSchema,
314+ },
315+);
316+
221317 registerSystemTool(
222318 "sqlite.query",
223− "Run SQL queries against the storage instance database.",
319+ "Run SQL against storage-backed SQLite. Use mode='write' for mutating SQL. For multi-task persistence, pass instanceId explicitly. For bulk inserts, chunk into smaller batches or use json_each(?) with a JSON payload to avoid SQLite bind-variable limits.",
224320 {
225321 inputSchema: sqliteQueryInputJsonSchema,
226322 outputSchema: sqliteQueryOutputJsonSchema,
227323 },
228324 );
325+
326+registerSystemTool(
327+ "sqlite.exec",
328+ "Alias for sqlite.query. Run SQL against storage-backed SQLite with the same guidance on mode, instanceId reuse, and bulk inserts via chunking or json_each(?).",
329+ {
330+ inputSchema: sqliteQueryInputJsonSchema,
331+ outputSchema: sqliteQueryOutputJsonSchema,
332+ },
333+);
334+
335+registerSystemTool(
336+ "sqlite.capabilities",
337+ "Return SQLite execution limits and import guidance for the active storage provider.",
338+ {
339+ inputSchema: sqliteCapabilitiesInputJsonSchema,
340+ outputSchema: sqliteCapabilitiesOutputJsonSchema,
341+ },
342+);
343+
344+registerSystemTool(
345+ "sqlite.insert_rows",
346+ "Insert rows into a table with automatic chunking under bind-variable limits. Use for bulk imports instead of huge VALUES clauses.",
347+ {
348+ inputSchema: sqliteInsertRowsInputJsonSchema,
349+ outputSchema: sqliteInsertRowsOutputJsonSchema,
350+ },
351+);
352+
353+registerSystemTool(
354+ "sqlite.bulk_insert",
355+ "Alias for sqlite.insert_rows. Bulk insert rows with automatic chunking.",
356+ {
357+ inputSchema: sqliteInsertRowsInputJsonSchema,
358+ outputSchema: sqliteInsertRowsOutputJsonSchema,
359+ },
360+);
executor/packages/database/src/runtime/storage_provider.ts+43−3
@@ -351,15 +351,19 @@ class AgentFsLocalStorageProvider implements StorageProvider { · 350 unmodified lines
351351 async kvList(instance: StorageInstanceRecord, prefix: string, limit: number): Promise<Array<{ key: string; value: unknown }>> {
352352 return await this.withDatabase(instance, false, async (db) => {
353353 await this.ensureKvTable(db);
354+ const escapedPrefix = prefix
355+ .replaceAll("^", "^^")
356+ .replaceAll("%", "^%")
357+ .replaceAll("_", "^_");
354358 const rows = rowsFromSqlResult(
355359 db.exec(
356− "SELECT key, value FROM kv_store WHERE key LIKE ? ESCAPE '\\\\' ORDER BY key LIMIT ?",
357− [`${prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`, limit],
360+ "SELECT key, value FROM kv_store WHERE key LIKE ? ESCAPE '^' ORDER BY key LIMIT ?",
361+ [`${escapedPrefix}%`, limit],
358362 ),
359363 );
360364 return rows.map((row) => ({
361365 key: String(row.key ?? ""),
362− value: typeof row.value === "string" ? JSON.parse(row.value) : undefined,
366+ value: typeof row.value === "string" ? JSON.parse(row.value) : null,
363367 }));
364368 });
365369 }
@@ -553,6 +557,40 @@ let localProviderSingleton: AgentFsLocalStorageProvider | null = null; · 187 unmodified lines
553557 let cloudflareProviderSingleton: CloudflareRemoteStorageProvider | null = null;
554558 let cloudflareStorageProbeStarted = false;
555559
560+function isHostedConvexHostname(hostname: string): boolean {
561+ const normalized = hostname.trim().toLowerCase();
562+ return normalized.endsWith(".convex.cloud") || normalized.endsWith(".convex.site");
563+}
564+
565+function isHostedConvexDeployment(): boolean {
566+ const candidates = [process.env.CONVEX_URL, process.env.CONVEX_SITE_URL]
567+ .map((value) => value?.trim())
568+ .filter((value): value is string => Boolean(value));
569+
570+ for (const candidate of candidates) {
571+ try {
572+ const url = new URL(candidate);
573+ if (isHostedConvexHostname(url.hostname)) {
574+ return true;
575+ }
576+ } catch {
577+ continue;
578+ }
579+ }
580+
581+ return false;
582+}
583+
584+function assertLocalProviderSupported() {
585+ if (!isHostedConvexDeployment()) {
586+ return;
587+ }
588+
589+ throw new Error(
590+ "agentfs-local is not supported on hosted Convex deployments because filesystem state is not shared across workers. Use AGENT_STORAGE_PROVIDER=agentfs-cloudflare, or run self-hosted Convex for agentfs-local.",
591+ );
592+}
593+
556594 function startCloudflareStorageProbe(endpoint: string, token: string) {
557595 if (cloudflareStorageProbeStarted) {
558596 return;
@@ -594,6 +632,8 @@ function startCloudflareStorageProbe(endpoint: string, token: string) { · 35 unmodified lines
594632 }
595633
596634 function getLocalProvider(): AgentFsLocalStorageProvider {
635+ assertLocalProviderSupported();
636+
597637 if (!localProviderSingleton) {
598638 const baseDir = (process.env.AGENT_STORAGE_ROOT ?? "").trim() || "/tmp/executor-agentfs";
599639 localProviderSingleton = new AgentFsLocalStorageProvider(baseDir);
executor/packages/database/src/runtime/storage_tool_contracts.ts+265−14
@@ -1,11 +1,64 @@
11 import { z } from "zod";
22
33 type JsonSchema = Record<string, unknown>;
4+type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
5+
6+function isRecord(value: unknown): value is Record<string, unknown> {
7+ return typeof value === "object" && value !== null && !Array.isArray(value);
8+}
9+
10+function mergeSchemaWithFallback(schema: unknown, fallback: JsonSchema): JsonSchema {
11+ if (!isRecord(schema)) {
12+ return fallback;
13+ }
14+
15+ const merged: JsonSchema = { ...schema };
16+
17+ if (typeof fallback.description === "string" && typeof merged.description !== "string") {
18+ merged.description = fallback.description;
19+ }
20+
21+ const schemaProps = isRecord(merged.properties) ? { ...merged.properties } : {};
22+ const fallbackProps = isRecord(fallback.properties) ? fallback.properties : {};
23+ for (const [key, fallbackProp] of Object.entries(fallbackProps)) {
24+ const currentProp = schemaProps[key];
25+ if (!isRecord(currentProp)) {
26+ schemaProps[key] = fallbackProp;
27+ continue;
28+ }
29+
30+ if (isRecord(fallbackProp)) {
31+ schemaProps[key] = {
32+ ...fallbackProp,
33+ ...currentProp,
34+ ...(typeof fallbackProp.description === "string" && typeof currentProp.description !== "string"
35+ ? { description: fallbackProp.description }
36+ : {}),
37+ };
38+ }
39+ }
40+ if (Object.keys(schemaProps).length > 0) {
41+ merged.properties = schemaProps;
42+ }
43+
44+ if (!Array.isArray(merged.required) && Array.isArray(fallback.required)) {
45+ merged.required = fallback.required;
46+ }
47+ if (!("additionalProperties" in merged) && "additionalProperties" in fallback) {
48+ merged.additionalProperties = fallback.additionalProperties;
49+ }
50+
51+ return merged;
52+}
453
554 function toJsonSchema(schema: z.ZodTypeAny, fallback: JsonSchema): JsonSchema {
655 const maybeToJsonSchema = (z as unknown as { toJSONSchema?: (value: z.ZodTypeAny) => unknown }).toJSONSchema;
756 if (typeof maybeToJsonSchema === "function") {
8− return maybeToJsonSchema(schema) as JsonSchema;
57+ try {
58+ return mergeSchemaWithFallback(maybeToJsonSchema(schema), fallback);
59+ } catch {
60+ return fallback;
61+ }
962 }
1063
1164 return fallback;
@@ -16,6 +69,15 @@ export const storageDurabilitySchema = z.enum(["ephemeral", "durable"]); · 4 unmodified lines
1669 export const storageStatusSchema = z.enum(["active", "closed", "deleted"]);
1770 export const storageProviderSchema = z.enum(["agentfs-local", "agentfs-cloudflare"]);
1871
72+const jsonValueSchema: z.ZodType<JsonValue> = z.lazy(() => z.union([
73+ z.string(),
74+ z.number(),
75+ z.boolean(),
76+ z.null(),
77+ z.array(jsonValueSchema),
78+ z.record(jsonValueSchema),
79+]));
80+
1981 export const storageInstanceSchema = z.object({
2082 id: z.string(),
2183 scopeType: storageScopeSchema,
@@ -165,12 +227,12 @@ export const kvGetOutputSchema = z.object({ · 143 unmodified lines
165227 instanceId: z.string(),
166228 key: z.string(),
167229 found: z.boolean(),
168− value: z.unknown().optional(),
230+ value: jsonValueSchema.optional(),
169231 });
170232
171233 export const kvSetInputSchema = fsAccessSchema.extend({
172234 key: z.string(),
173− value: z.unknown(),
235+ value: jsonValueSchema,
174236 });
175237
176238 export const kvSetOutputSchema = z.object({
@@ -186,7 +248,7 @@ export const kvListInputSchema = fsAccessSchema.extend({ · 9 unmodified lines
186248
187249 export const kvListOutputSchema = z.object({
188250 instanceId: z.string(),
189− items: z.array(z.object({ key: z.string(), value: z.unknown() })),
251+ items: z.array(z.object({ key: z.string(), value: jsonValueSchema })),
190252 total: z.number(),
191253 });
192254
@@ -200,6 +262,20 @@ export const kvDeleteOutputSchema = z.object({ · 7 unmodified lines
200262 ok: z.boolean(),
201263 });
202264
265+export const kvIncrInputSchema = fsAccessSchema.extend({
266+ key: z.string(),
267+ by: z.coerce.number().optional(),
268+ initial: z.coerce.number().optional(),
269+});
270+
271+export const kvIncrOutputSchema = z.object({
272+ instanceId: z.string(),
273+ key: z.string(),
274+ by: z.number(),
275+ previous: z.number(),
276+ value: z.number(),
277+});
278+
203279 export const sqliteQueryInputSchema = fsAccessSchema.extend({
204280 sql: z.string(),
205281 params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
@@ -210,11 +286,55 @@ export const sqliteQueryInputSchema = fsAccessSchema.extend({ · 4 unmodified lines
210286 export const sqliteQueryOutputSchema = z.object({
211287 instanceId: z.string(),
212288 mode: z.enum(["read", "write"]),
213− rows: z.array(z.record(z.unknown())).optional(),
289+ rows: z.array(z.record(jsonValueSchema)).optional(),
214290 rowCount: z.number(),
215291 changes: z.number().optional(),
216292 });
217293
294+const sqliteScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
295+
296+export const sqliteCapabilitiesInputSchema = fsAccessSchema.extend({});
297+
298+export const sqliteCapabilitiesOutputSchema = z.object({
299+ instanceId: z.string(),
300+ provider: storageProviderSchema,
301+ maxBindVariables: z.number(),
302+ supportsJsonEach: z.boolean(),
303+ supportsInsertRowsTool: z.boolean(),
304+ guidance: z.array(z.string()),
305+});
306+
307+export const sqliteInsertRowsInputSchema = fsAccessSchema.extend({
308+ table: z.string(),
309+ columns: z.array(z.string()).min(1),
310+ rows: z.array(z.array(sqliteScalarSchema)).min(1),
311+ onConflict: z.enum(["none", "ignore", "replace"]).optional(),
312+ chunkSize: z.coerce.number().optional(),
313+});
314+
315+export const sqliteInsertRowsOutputSchema = z.object({
316+ instanceId: z.string(),
317+ table: z.string(),
318+ columns: z.array(z.string()),
319+ rowsReceived: z.number(),
320+ rowsProcessed: z.number(),
321+ chunkCount: z.number(),
322+ rowsPerChunk: z.number(),
323+ maxBindVariables: z.number(),
324+ changes: z.number(),
325+});
326+
327+const jsonValueJsonSchema = {
328+ oneOf: [
329+ { type: "string" },
330+ { type: "number" },
331+ { type: "boolean" },
332+ { type: "null" },
333+ { type: "array", items: {} },
334+ { type: "object", additionalProperties: {} },
335+ ],
336+};
337+
218338 export const storageOpenInputJsonSchema = toJsonSchema(storageOpenInputSchema, {
219339 type: "object",
220340 properties: {
@@ -280,8 +400,15 @@ export const storageDeleteOutputJsonSchema = storageCloseOutputJsonSchema; · 59 unmodified lines
280400
281401 function fsAccessJsonProperties() {
282402 return {
283− instanceId: { type: "string" },
284− scopeType: { type: "string", enum: ["scratch", "account", "workspace", "organization"] },
403+ instanceId: {
404+ type: "string",
405+ description: "Storage instance identifier. Recommended for cross-task/cross-run persistence.",
406+ },
407+ scopeType: {
408+ type: "string",
409+ enum: ["scratch", "account", "workspace", "organization"],
410+ description: "Scope used to open/select a default instance when instanceId is omitted.",
411+ },
285412 };
286413 }
287414
@@ -438,7 +565,7 @@ export const kvGetOutputJsonSchema = toJsonSchema(kvGetOutputSchema, { · 150 unmodified lines
438565 instanceId: { type: "string" },
439566 key: { type: "string" },
440567 found: { type: "boolean" },
441− value: {},
568+ value: jsonValueJsonSchema,
442569 },
443570 required: ["instanceId", "key", "found"],
444571 additionalProperties: false,
@@ -449,7 +576,7 @@ export const kvSetInputJsonSchema = toJsonSchema(kvSetInputSchema, { · 4 unmodified lines
449576 properties: {
450577 ...fsAccessJsonProperties(),
451578 key: { type: "string" },
452− value: {},
579+ value: jsonValueJsonSchema,
453580 },
454581 required: ["key", "value"],
455582 additionalProperties: false,
@@ -480,7 +607,18 @@ export const kvListOutputJsonSchema = toJsonSchema(kvListOutputSchema, { · 24 unmodified lines
480607 type: "object",
481608 properties: {
482609 instanceId: { type: "string" },
483− items: { type: "array", items: { type: "object" } },
610+ items: {
611+ type: "array",
612+ items: {
613+ type: "object",
614+ properties: {
615+ key: { type: "string" },
616+ value: jsonValueJsonSchema,
617+ },
618+ required: ["key", "value"],
619+ additionalProperties: false,
620+ },
621+ },
484622 total: { type: "number" },
485623 },
486624 required: ["instanceId", "items", "total"],
@@ -508,19 +646,52 @@ export const kvDeleteOutputJsonSchema = toJsonSchema(kvDeleteOutputSchema, { · 21 unmodified lines
508646 additionalProperties: false,
509647 });
510648
649+export const kvIncrInputJsonSchema = toJsonSchema(kvIncrInputSchema, {
650+ type: "object",
651+ properties: {
652+ ...fsAccessJsonProperties(),
653+ key: { type: "string" },
654+ by: { type: "number" },
655+ initial: { type: "number" },
656+ },
657+ required: ["key"],
658+ additionalProperties: false,
659+});
660+
661+export const kvIncrOutputJsonSchema = toJsonSchema(kvIncrOutputSchema, {
662+ type: "object",
663+ properties: {
664+ instanceId: { type: "string" },
665+ key: { type: "string" },
666+ by: { type: "number" },
667+ previous: { type: "number" },
668+ value: { type: "number" },
669+ },
670+ required: ["instanceId", "key", "by", "previous", "value"],
671+ additionalProperties: false,
672+});
673+
511674 export const sqliteQueryInputJsonSchema = toJsonSchema(sqliteQueryInputSchema, {
512675 type: "object",
676+ description:
677+ "Execute SQL on a storage-backed SQLite database. Use instanceId to target the same database across separate tasks or sessions.",
513678 properties: {
514679 ...fsAccessJsonProperties(),
515− sql: { type: "string" },
680+ sql: { type: "string", description: "SQL statement text to execute." },
516681 params: {
517682 type: "array",
683+ description:
684+ "Positional SQL parameters. Keep per-call bind count modest (for large inserts, batch rows into smaller chunks or pass one JSON payload and expand with json_each(?)).",
518685 items: {
519686 oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }, { type: "null" }],
520687 },
521688 },
522− mode: { type: "string", enum: ["read", "write"] },
523− maxRows: { type: "number" },
689+ mode: {
690+ type: "string",
691+ enum: ["read", "write"],
692+ description: "Use 'write' for CREATE/INSERT/UPDATE/DELETE statements.",
693+ },
694+ maxRows: { type: "number", description: "Read mode row cap for result payload size." },
524695 },
525696 required: ["sql"],
526697 additionalProperties: false,
@@ -531,10 +702,90 @@ export const sqliteQueryOutputJsonSchema = toJsonSchema(sqliteQueryOutputSchema, · 4 unmodified lines
531702 properties: {
532703 instanceId: { type: "string" },
533704 mode: { type: "string", enum: ["read", "write"] },
534− rows: { type: "array", items: { type: "object" } },
705+ rows: { type: "array", items: { type: "object", additionalProperties: jsonValueJsonSchema } },
535706 rowCount: { type: "number" },
536707 changes: { type: "number" },
537708 },
538709 required: ["instanceId", "mode", "rowCount"],
539710 additionalProperties: false,
540711 });
712+
713+export const sqliteCapabilitiesInputJsonSchema = toJsonSchema(sqliteCapabilitiesInputSchema, {
714+ type: "object",
715+ description: "Get provider-specific SQLite execution capabilities and batching guidance.",
716+ properties: {
717+ ...fsAccessJsonProperties(),
718+ },
719+ additionalProperties: false,
720+});
721+
722+export const sqliteCapabilitiesOutputJsonSchema = toJsonSchema(sqliteCapabilitiesOutputSchema, {
723+ type: "object",
724+ properties: {
725+ instanceId: { type: "string" },
726+ provider: { type: "string", enum: ["agentfs-local", "agentfs-cloudflare"] },
727+ maxBindVariables: { type: "number" },
728+ supportsJsonEach: { type: "boolean" },
729+ supportsInsertRowsTool: { type: "boolean" },
730+ guidance: { type: "array", items: { type: "string" } },
731+ },
732+ required: [
733+ "instanceId",
734+ "provider",
735+ "maxBindVariables",
736+ "supportsJsonEach",
737+ "supportsInsertRowsTool",
738+ "guidance",
739+ ],
740+ additionalProperties: false,
741+});
742+
743+export const sqliteInsertRowsInputJsonSchema = toJsonSchema(sqliteInsertRowsInputSchema, {
744+ type: "object",
745+ description:
746+ "Insert tabular rows with automatic chunking to stay under SQLite bind-variable limits. Prefer this over huge VALUES(...) statements.",
747+ properties: {
748+ ...fsAccessJsonProperties(),
749+ table: { type: "string", description: "Target table name." },
750+ columns: { type: "array", items: { type: "string" }, minItems: 1 },
751+ rows: {
752+ type: "array",
753+ minItems: 1,
754+ items: {
755+ type: "array",
756+ items: { oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }, { type: "null" }] },
757+ },
758+ },
759+ onConflict: { type: "string", enum: ["none", "ignore", "replace"] },
760+ chunkSize: { type: "number", description: "Optional max rows per insert chunk." },
761+ },
762+ required: ["table", "columns", "rows"],
763+ additionalProperties: false,
764+});
765+
766+export const sqliteInsertRowsOutputJsonSchema = toJsonSchema(sqliteInsertRowsOutputSchema, {
767+ type: "object",
768+ properties: {
769+ instanceId: { type: "string" },
770+ table: { type: "string" },
771+ columns: { type: "array", items: { type: "string" } },
772+ rowsReceived: { type: "number" },
773+ rowsProcessed: { type: "number" },
774+ chunkCount: { type: "number" },
775+ rowsPerChunk: { type: "number" },
776+ maxBindVariables: { type: "number" },
777+ changes: { type: "number" },
778+ },
779+ required: [
780+ "instanceId",
781+ "table",
782+ "columns",
783+ "rowsReceived",
784+ "rowsProcessed",
785+ "chunkCount",
786+ "rowsPerChunk",
787+ "maxBindVariables",
788+ "changes",
789+ ],
790+ additionalProperties: false,
791+});
executor/packages/database/src/runtime/storage_tools.ts+454−27
@@ -19,10 +19,16 @@ import { · 18 unmodified lines
1919 kvDeleteOutputSchema,
2020 kvGetInputSchema,
2121 kvGetOutputSchema,
22+ kvIncrInputSchema,
23+ kvIncrOutputSchema,
2224 kvListInputSchema,
2325 kvListOutputSchema,
2426 kvSetInputSchema,
2527 kvSetOutputSchema,
28+ sqliteCapabilitiesInputSchema,
29+ sqliteCapabilitiesOutputSchema,
30+ sqliteInsertRowsInputSchema,
31+ sqliteInsertRowsOutputSchema,
2632 sqliteQueryInputSchema,
2733 sqliteQueryOutputSchema,
2834 storageCloseInputSchema,
@@ -35,6 +41,14 @@ import { · 6 unmodified lines
3541 storageOpenOutputSchema,
3642 } from "./storage_tool_contracts";
3743
44+type StorageScopeType = "scratch" | "account" | "workspace" | "organization";
45+
46+type TaskStorageDefaults = {
47+ currentInstanceId?: string;
48+ currentScopeType?: StorageScopeType;
49+ byScope: Partial<Record<StorageScopeType, string>>;
50+};
51+
3852 const STORAGE_SYSTEM_TOOLS = new Set([
3953 "storage.open",
4054 "storage.list",
@@ -48,11 +62,27 @@ const STORAGE_SYSTEM_TOOLS = new Set([ · 7 unmodified lines
4862 "fs.remove",
4963 "kv.get",
5064 "kv.set",
65+ "kv.put",
66+ "kv.create",
67+ "kv.update",
5168 "kv.list",
69+ "kv.keys",
5270 "kv.delete",
71+ "kv.del",
72+ "kv.has",
73+ "kv.exists",
74+ "kv.value",
75+ "kv.incr",
76+ "kv.decr",
5377 "sqlite.query",
78+ "sqlite.exec",
79+ "sqlite.capabilities",
80+ "sqlite.insert_rows",
81+ "sqlite.bulk_insert",
5482 ]);
5583
84+const SQLITE_MAX_BIND_VARIABLES = 100;
85+
5686 function toInputRecord(input: unknown): Record<string, unknown> {
5787 if (!input || typeof input !== "object" || Array.isArray(input)) {
5888 return {};
@@ -70,6 +100,107 @@ function normalizeScopeType(value: unknown): undefined | "scratch" | "account" | · 11 unmodified lines
70100 return undefined;
71101 }
72102
103+function isReadOnlySql(sql: string): boolean {
104+ const trimmed = sql.trim().toLowerCase();
105+ return trimmed.startsWith("select")
106+ || trimmed.startsWith("pragma")
107+ || trimmed.startsWith("explain")
108+ || trimmed.startsWith("with");
109+}
110+
111+function toErrorMessage(error: unknown): string {
112+ if (error instanceof Error && typeof error.message === "string") {
113+ return error.message;
114+ }
115+ return String(error);
116+}
117+
118+function estimateSqlBindCount(sql: string): number {
119+ let inSingleQuote = false;
120+ let inDoubleQuote = false;
121+ let count = 0;
122+
123+ for (let index = 0; index < sql.length; index += 1) {
124+ const char = sql[index];
125+ if (char === "'" && !inDoubleQuote) {
126+ inSingleQuote = !inSingleQuote;
127+ continue;
128+ }
129+ if (char === '"' && !inSingleQuote) {
130+ inDoubleQuote = !inDoubleQuote;
131+ continue;
132+ }
133+ if (!inSingleQuote && !inDoubleQuote && char === "?") {
134+ count += 1;
135+ }
136+ }
137+
138+ return count;
139+}
140+
141+function decorateSqliteError(error: unknown, args: {
142+ sql: string;
143+ params: Array<string | number | boolean | null>;
144+ instanceId: string;
145+}): Error {
146+ const original = toErrorMessage(error);
147+ const normalized = original.toLowerCase();
148+
149+ if (normalized.includes("too many sql variables")) {
150+ const bindCount = estimateSqlBindCount(args.sql);
151+ const paramsCount = args.params.length;
152+ return new Error(
153+ [
154+ original,
155+ `sqlite.query guidance: this statement used ${paramsCount} params and ~${bindCount} '?' placeholders.`,
156+ "Use smaller batches, or prefer JSON batching: INSERT ... SELECT ... FROM json_each(?) with one JSON payload param.",
157+ "Keep using instanceId to write/read the same database across task runs.",
158+ ].join(" "),
159+ );
160+ }
161+
162+ if (normalized.includes("no such table")) {
163+ return new Error(
164+ [
165+ original,
166+ `sqlite.query guidance: table lookup happened in instanceId=${args.instanceId}.`,
167+ "If the table was created in another run, pass that exact instanceId explicitly.",
168+ ].join(" "),
169+ );
170+ }
171+
172+ if (normalized.includes("timed out") || normalized.includes("timeout")) {
173+ return new Error(
174+ [
175+ original,
176+ "sqlite.query guidance: reduce batch size and split long imports into multiple calls.",
177+ "For bulk inserts, use json_each(?) payload batches instead of very large VALUES(...) statements.",
178+ ].join(" "),
179+ );
180+ }
181+
182+ return error instanceof Error ? error : new Error(original);
183+}
184+
185+function assertSqlIdentifier(identifier: string, label: string): string {
186+ const trimmed = identifier.trim();
187+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed)) {
188+ throw new Error(`${label} must be a valid SQLite identifier`);
189+ }
190+ return trimmed;
191+}
192+
193+function quoteSqlIdentifier(identifier: string): string {
194+ return `"${identifier.replaceAll('"', '""')}"`;
195+}
196+
197+function assertFiniteNumber(value: unknown, label: string): number {
198+ if (typeof value !== "number" || !Number.isFinite(value)) {
199+ throw new Error(`${label} must be a finite number`);
200+ }
201+ return value;
202+}
203+
73204 function normalizeInstanceId(value: unknown): string | undefined {
74205 if (typeof value !== "string") return undefined;
75206 const normalized = value.trim();
@@ -85,6 +216,84 @@ function normalizeInputPayload(input: unknown): Record<string, unknown> { · 9 unmodified lines
85216 };
86217 }
87218
219+function asRecord(value: unknown): Record<string, unknown> {
220+ return value && typeof value === "object" && !Array.isArray(value)
221+ ? value as Record<string, unknown>
222+ : {};
223+}
224+
225+function parseTaskStorageDefaults(metadata: unknown): TaskStorageDefaults {
226+ const root = asRecord(metadata);
227+ const storage = asRecord(root.storage);
228+ const byScopeRaw = asRecord(storage.defaultInstanceByScope);
229+ const byScope: Partial<Record<StorageScopeType, string>> = {};
230+
231+ for (const scopeType of ["scratch", "account", "workspace", "organization"] as const) {
232+ const value = normalizeInstanceId(byScopeRaw[scopeType]);
233+ if (value) {
234+ byScope[scopeType] = value;
235+ }
236+ }
237+
238+ return {
239+ currentInstanceId: normalizeInstanceId(storage.currentInstanceId),
240+ currentScopeType: normalizeScopeType(storage.currentScopeType),
241+ byScope,
242+ };
243+}
244+
245+async function getTaskStorageDefaults(
246+ ctx: ActionCtx,
247+ task: TaskRecord,
248+): Promise<TaskStorageDefaults> {
249+ const latest = await ctx.runQuery(internal.database.getTask, { taskId: task.id });
250+ if (!latest) {
251+ return { byScope: {} };
252+ }
253+
254+ return parseTaskStorageDefaults((latest as TaskRecord).metadata);
255+}
256+
257+async function saveTaskStorageDefault(
258+ ctx: ActionCtx,
259+ task: TaskRecord,
260+ scopeType: StorageScopeType,
261+ instanceId: string,
262+ setCurrent = true,
263+ accessType: "opened" | "provided" | "accessed" = "accessed",
264+) {
265+ await ctx.runMutation(internal.database.setTaskStorageDefaultInstance, {
266+ taskId: task.id,
267+ scopeType,
268+ instanceId,
269+ setCurrent,
270+ });
271+
272+ await ctx.runMutation(internal.database.trackTaskStorageAccess, {
273+ taskId: task.id,
274+ instanceId,
275+ scopeType,
276+ accessType,
277+ });
278+}
279+
280+async function trackTaskStorageAccess(
281+ ctx: ActionCtx,
282+ task: TaskRecord,
283+ args: {
284+ instanceId: string;
285+ scopeType?: StorageScopeType;
286+ accessType: "opened" | "provided" | "accessed";
287+ },
288+) {
289+ await ctx.runMutation(internal.database.trackTaskStorageAccess, {
290+ taskId: task.id,
291+ instanceId: args.instanceId,
292+ scopeType: args.scopeType,
293+ accessType: args.accessType,
294+ });
295+}
296+
88297 async function openStorageInstanceForTask(
89298 ctx: ActionCtx,
90299 task: TaskRecord,
@@ -114,7 +323,7 @@ async function resolveStorageInstance( · 23 unmodified lines
114323 payload: Record<string, unknown>,
115324 ): Promise<StorageInstanceRecord> {
116325 const requestedInstanceId = normalizeInstanceId(payload.instanceId);
117− const scopeType = normalizeScopeType(payload.scopeType);
326+ const requestedScopeType = normalizeScopeType(payload.scopeType);
118327
119328 if (requestedInstanceId) {
120329 const existing = await ctx.runQuery(internal.database.getStorageInstance, {
@@ -129,13 +338,55 @@ async function resolveStorageInstance( · 8 unmodified lines
129338 const reopened = await openStorageInstanceForTask(ctx, task, {
130339 instanceId: requestedInstanceId,
131340 });
341+ await saveTaskStorageDefault(ctx, task, reopened.scopeType, reopened.id, true, "provided");
132342 return reopened;
133343 }
134344
135− return await openStorageInstanceForTask(ctx, task, {
136− scopeType,
137− purpose: "auto",
345+ const defaults = await getTaskStorageDefaults(ctx, task);
346+
347+ const candidateIds: string[] = [];
348+ if (requestedScopeType) {
349+ const forScope = defaults.byScope[requestedScopeType];
350+ if (forScope) {
351+ candidateIds.push(forScope);
352+ }
353+ if (defaults.currentScopeType === requestedScopeType && defaults.currentInstanceId) {
354+ candidateIds.push(defaults.currentInstanceId);
355+ }
356+ } else {
357+ if (defaults.currentInstanceId) {
358+ candidateIds.push(defaults.currentInstanceId);
359+ }
360+ if (defaults.byScope.scratch) {
361+ candidateIds.push(defaults.byScope.scratch);
362+ }
363+ }
364+
365+ const uniqueCandidateIds = [...new Set(candidateIds)];
366+ for (const candidateId of uniqueCandidateIds) {
367+ const existing = await ctx.runQuery(internal.database.getStorageInstance, {
368+ workspaceId: task.workspaceId,
369+ accountId: task.accountId,
370+ instanceId: candidateId,
371+ }) as StorageInstanceRecord | null;
372+ if (!existing) {
373+ continue;
374+ }
375+
376+ const reopened = await openStorageInstanceForTask(ctx, task, {
377+ instanceId: candidateId,
378+ });
379+ await saveTaskStorageDefault(ctx, task, reopened.scopeType, reopened.id, true, "accessed");
380+ return reopened;
381+ }
382+
383+ const fallbackScopeType = requestedScopeType ?? defaults.currentScopeType ?? "scratch";
384+
385+ const created = await openStorageInstanceForTask(ctx, task, {
386+ scopeType: fallbackScopeType,
138387 });
388+ await saveTaskStorageDefault(ctx, task, created.scopeType, created.id, true, "opened");
389+ return created;
139390 }
140391
141392 async function touchInstance(
@@ -167,14 +418,43 @@ export async function runStorageSystemTool( · 25 unmodified lines
167418 input: unknown,
168419 ): Promise<unknown> {
169420 const payload = normalizeInputPayload(input);
170−
171− if (toolPath === "storage.open") {
421+ const normalizedToolPath = toolPath === "kv.put"
422+ ? "kv.set"
423+ : toolPath === "kv.create"
424+ ? "kv.set"
425+ : toolPath === "kv.update"
426+ ? "kv.set"
427+ : toolPath === "kv.del"
428+ ? "kv.delete"
429+ : toolPath === "kv.has"
430+ ? "kv.get"
431+ : toolPath === "kv.exists"
432+ ? "kv.get"
433+ : toolPath === "kv.value"
434+ ? "kv.get"
435+ : toolPath === "kv.keys"
436+ ? "kv.list"
437+ : toolPath === "sqlite.exec"
438+ ? "sqlite.query"
439+ : toolPath === "sqlite.bulk_insert"
440+ ? "sqlite.insert_rows"
441+ : toolPath;
442+
443+ if (normalizedToolPath === "storage.open") {
172444 const parsed = storageOpenInputSchema.parse(payload);
173445 const instance = await openStorageInstanceForTask(ctx, task, parsed);
446+ await saveTaskStorageDefault(
447+ ctx,
448+ task,
449+ instance.scopeType,
450+ instance.id,
451+ true,
452+ parsed.instanceId ? "provided" : "opened",
453+ );
174454 return storageOpenOutputSchema.parse({ instance });
175455 }
176456
177− if (toolPath === "storage.list") {
457+ if (normalizedToolPath === "storage.list") {
178458 const parsed = storageListInputSchema.parse(payload);
179459 const instances = await ctx.runQuery(internal.database.listStorageInstances, {
180460 workspaceId: task.workspaceId,
@@ -189,17 +469,24 @@ export async function runStorageSystemTool( · 8 unmodified lines
189469 });
190470 }
191471
192− if (toolPath === "storage.close") {
472+ if (normalizedToolPath === "storage.close") {
193473 const parsed = storageCloseInputSchema.parse(payload);
194474 const instance = await ctx.runMutation(internal.database.closeStorageInstance, {
195475 workspaceId: task.workspaceId,
196476 accountId: task.accountId,
197477 instanceId: parsed.instanceId,
198478 });
479+
480+ await trackTaskStorageAccess(ctx, task, {
481+ instanceId: parsed.instanceId,
482+ scopeType: instance?.scopeType,
483+ accessType: "provided",
484+ });
485+
199486 return storageCloseOutputSchema.parse({ instance });
200487 }
201488
202− if (toolPath === "storage.delete") {
489+ if (normalizedToolPath === "storage.delete") {
203490 const parsed = storageDeleteInputSchema.parse(payload);
204491 const existing = await ctx.runQuery(internal.database.getStorageInstance, {
205492 workspaceId: task.workspaceId,
@@ -221,10 +508,17 @@ export async function runStorageSystemTool( · 15 unmodified lines
221508 accountId: task.accountId,
222509 instanceId: parsed.instanceId,
223510 });
511+
512+ await trackTaskStorageAccess(ctx, task, {
513+ instanceId: parsed.instanceId,
514+ scopeType: existing?.scopeType,
515+ accessType: "provided",
516+ });
517+
224518 return storageDeleteOutputSchema.parse({ instance });
225519 }
226520
227− if (toolPath === "fs.read") {
521+ if (normalizedToolPath === "fs.read") {
228522 const parsed = fsReadInputSchema.parse(payload);
229523 const instance = await resolveStorageInstance(ctx, task, payload);
230524 const provider = getStorageProvider(instance.provider);
@@ -240,7 +534,7 @@ export async function runStorageSystemTool( · 9 unmodified lines
240534 });
241535 }
242536
243− if (toolPath === "fs.write") {
537+ if (normalizedToolPath === "fs.write") {
244538 const parsed = fsWriteInputSchema.parse(payload);
245539 const instance = await resolveStorageInstance(ctx, task, payload);
246540 const provider = getStorageProvider(instance.provider);
@@ -254,7 +548,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
254548 });
255549 }
256550
257− if (toolPath === "fs.readdir") {
551+ if (normalizedToolPath === "fs.readdir") {
258552 const parsed = fsReaddirInputSchema.parse(payload);
259553 const instance = await resolveStorageInstance(ctx, task, payload);
260554 const provider = getStorageProvider(instance.provider);
@@ -268,7 +562,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
268562 });
269563 }
270564
271− if (toolPath === "fs.stat") {
565+ if (normalizedToolPath === "fs.stat") {
272566 const parsed = fsStatInputSchema.parse(payload);
273567 const instance = await resolveStorageInstance(ctx, task, payload);
274568 const provider = getStorageProvider(instance.provider);
@@ -281,7 +575,7 @@ export async function runStorageSystemTool( · 6 unmodified lines
281575 });
282576 }
283577
284− if (toolPath === "fs.mkdir") {
578+ if (normalizedToolPath === "fs.mkdir") {
285579 const parsed = fsMkdirInputSchema.parse(payload);
286580 const instance = await resolveStorageInstance(ctx, task, payload);
287581 const provider = getStorageProvider(instance.provider);
@@ -294,7 +588,7 @@ export async function runStorageSystemTool( · 6 unmodified lines
294588 });
295589 }
296590
297− if (toolPath === "fs.remove") {
591+ if (normalizedToolPath === "fs.remove") {
298592 const parsed = fsRemoveInputSchema.parse(payload);
299593 const instance = await resolveStorageInstance(ctx, task, payload);
300594 const provider = getStorageProvider(instance.provider);
@@ -310,7 +604,7 @@ export async function runStorageSystemTool( · 9 unmodified lines
310604 });
311605 }
312606
313− if (toolPath === "kv.get") {
607+ if (normalizedToolPath === "kv.get") {
314608 const parsed = kvGetInputSchema.parse(payload);
315609 const instance = await resolveStorageInstance(ctx, task, payload);
316610 const provider = getStorageProvider(instance.provider);
@@ -324,7 +618,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
324618 });
325619 }
326620
327− if (toolPath === "kv.set") {
621+ if (normalizedToolPath === "kv.set") {
328622 const parsed = kvSetInputSchema.parse(payload);
329623 const instance = await resolveStorageInstance(ctx, task, payload);
330624 const provider = getStorageProvider(instance.provider);
@@ -337,7 +631,7 @@ export async function runStorageSystemTool( · 6 unmodified lines
337631 });
338632 }
339633
340− if (toolPath === "kv.list") {
634+ if (normalizedToolPath === "kv.list") {
341635 const parsed = kvListInputSchema.parse(payload);
342636 const instance = await resolveStorageInstance(ctx, task, payload);
343637 const provider = getStorageProvider(instance.provider);
@@ -351,7 +645,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
351645 });
352646 }
353647
354− if (toolPath === "kv.delete") {
648+ if (normalizedToolPath === "kv.delete") {
355649 const parsed = kvDeleteInputSchema.parse(payload);
356650 const instance = await resolveStorageInstance(ctx, task, payload);
357651 const provider = getStorageProvider(instance.provider);
@@ -364,18 +658,151 @@ export async function runStorageSystemTool( · 6 unmodified lines
364658 });
365659 }
366660
367− if (toolPath === "sqlite.query") {
661+ if (normalizedToolPath === "kv.incr" || normalizedToolPath === "kv.decr") {
662+ const parsed = kvIncrInputSchema.parse(payload);
663+ const instance = await resolveStorageInstance(ctx, task, payload);
664+ const provider = getStorageProvider(instance.provider);
665+
666+ const existing = await provider.kvGet(instance, parsed.key);
667+ const initial = assertFiniteNumber(parsed.initial ?? 0, "initial");
668+ const previous = existing === undefined
669+ ? initial
670+ : assertFiniteNumber(existing, `kv value at '${parsed.key}'`);
671+
672+ const rawBy = assertFiniteNumber(parsed.by ?? 1, "by");
673+ const by = normalizedToolPath === "kv.decr" ? -Math.abs(rawBy) : rawBy;
674+ const value = previous + by;
675+
676+ await provider.kvSet(instance, parsed.key, value);
677+ await touchInstance(ctx, task, instance, provider, true);
678+
679+ return kvIncrOutputSchema.parse({
680+ instanceId: instance.id,
681+ key: parsed.key,
682+ by,
683+ previous,
684+ value,
685+ });
686+ }
687+
688+ if (normalizedToolPath === "sqlite.capabilities") {
689+ sqliteCapabilitiesInputSchema.parse(payload);
690+ const instance = await resolveStorageInstance(ctx, task, payload);
691+ const provider = getStorageProvider(instance.provider);
692+ await touchInstance(ctx, task, instance, provider, false);
693+ return sqliteCapabilitiesOutputSchema.parse({
694+ instanceId: instance.id,
695+ provider: instance.provider,
696+ maxBindVariables: SQLITE_MAX_BIND_VARIABLES,
697+ supportsJsonEach: true,
698+ supportsInsertRowsTool: true,
699+ guidance: [
700+ "Prefer sqlite.insert_rows for bulk tabular inserts.",
701+ "Keep bind params per statement under maxBindVariables.",
702+ "For very large payloads, use one JSON payload param and expand with json_each(?).",
703+ "Pass instanceId explicitly to reuse the same database across task runs.",
704+ ],
705+ });
706+ }
707+
708+ if (normalizedToolPath === "sqlite.insert_rows") {
709+ const parsed = sqliteInsertRowsInputSchema.parse(payload);
710+ const instance = await resolveStorageInstance(ctx, task, payload);
711+ const provider = getStorageProvider(instance.provider);
712+
713+ const table = assertSqlIdentifier(parsed.table, "table");
714+ const columns = parsed.columns.map((column, index) => assertSqlIdentifier(column, `columns[${index}]`));
715+ if (new Set(columns).size !== columns.length) {
716+ throw new Error("columns must be unique");
717+ }
718+
719+ const rows = parsed.rows;
720+ for (let index = 0; index < rows.length; index += 1) {
721+ if (rows[index].length !== columns.length) {
722+ throw new Error(`rows[${index}] has ${rows[index].length} values but expected ${columns.length}`);
723+ }
724+ }
725+
726+ const maxRowsPerChunk = Math.max(1, Math.floor(SQLITE_MAX_BIND_VARIABLES / Math.max(1, columns.length)));
727+ const requestedChunkSize = typeof parsed.chunkSize === "number" && Number.isFinite(parsed.chunkSize)
728+ ? Math.max(1, Math.floor(parsed.chunkSize))
729+ : maxRowsPerChunk;
730+ const rowsPerChunk = Math.max(1, Math.min(maxRowsPerChunk, requestedChunkSize));
731+
732+ const conflictClause = parsed.onConflict === "ignore"
733+ ? " OR IGNORE"
734+ : parsed.onConflict === "replace"
735+ ? " OR REPLACE"
736+ : "";
737+
738+ const quotedTable = quoteSqlIdentifier(table);
739+ const quotedColumns = columns.map(quoteSqlIdentifier).join(", ");
740+ const placeholderRow = `(${columns.map(() => "?").join(", ")})`;
741+
742+ let totalChanges = 0;
743+ let chunkCount = 0;
744+ for (let start = 0; start < rows.length; start += rowsPerChunk) {
745+ const chunk = rows.slice(start, start + rowsPerChunk);
746+ const values = chunk.map(() => placeholderRow).join(", ");
747+ const params = chunk.flat();
748+ const sql = `INSERT${conflictClause} INTO ${quotedTable} (${quotedColumns}) VALUES ${values}`;
749+
750+ let writeResult;
751+ try {
752+ writeResult = await provider.sqliteQuery(instance, {
753+ sql,
754+ params,
755+ mode: "write",
756+ maxRows: 1,
757+ });
758+ } catch (error) {
759+ throw decorateSqliteError(error, {
760+ sql,
761+ params,
762+ instanceId: instance.id,
763+ });
764+ }
765+
766+ totalChanges += Number(writeResult.changes ?? 0);
767+ chunkCount += 1;
768+ }
769+
770+ await touchInstance(ctx, task, instance, provider, true);
771+ return sqliteInsertRowsOutputSchema.parse({
772+ instanceId: instance.id,
773+ table,
774+ columns,
775+ rowsReceived: rows.length,
776+ rowsProcessed: rows.length,
777+ chunkCount,
778+ rowsPerChunk,
779+ maxBindVariables: SQLITE_MAX_BIND_VARIABLES,
780+ changes: totalChanges,
781+ });
782+ }
783+
784+ if (normalizedToolPath === "sqlite.query") {
368785 const parsed = sqliteQueryInputSchema.parse(payload);
369786 const instance = await resolveStorageInstance(ctx, task, payload);
370787 const provider = getStorageProvider(instance.provider);
371− const mode = parsed.mode ?? "read";
788+ const mode = parsed.mode ?? (isReadOnlySql(parsed.sql) ? "read" : "write");
372789 const maxRows = Math.max(1, Math.min(1_000, Math.floor(parsed.maxRows ?? 200)));
373− const result = await provider.sqliteQuery(instance, {
374− sql: parsed.sql,
375− params: parsed.params ?? [],
376− mode,
377− maxRows,
378− });
790+ const params = parsed.params ?? [];
791+ let result;
792+ try {
793+ result = await provider.sqliteQuery(instance, {
794+ sql: parsed.sql,
795+ params,
796+ mode,
797+ maxRows,
798+ });
799+ } catch (error) {
800+ throw decorateSqliteError(error, {
801+ sql: parsed.sql,
802+ params,
803+ instanceId: instance.id,
804+ });
805+ }
379806 await touchInstance(ctx, task, instance, provider, mode === "write");
380807 return sqliteQueryOutputSchema.parse({
381808 instanceId: instance.id,
executor/packages/database/src/runtime/tool_call_resolution.ts+3−1
@@ -159,7 +159,9 @@ function unknownToolErrorMessage(toolPath: string, suggestions: string[]): strin · 158 unmodified lines
159159 : "";
160160 const queryHint = toolPath.split(".").filter(Boolean).join(" ");
161161 const discoverHint = `\nTry: const found = await tools.discover({ query: "${queryHint}", depth: 1, limit: 12 });`;
162− return `Unknown tool: ${toolPath}${suggestionText}${discoverHint}`;
162+ const catalogHint = `\nOr: const list = await tools.catalog.tools({ query: "${queryHint}", limit: 20 });`;
163+ const proxyHint = "\nNote: tools.* uses a lazy proxy, so property introspection (Object.keys/typeof on arbitrary paths) is not a reliable availability check.";
164+ return `Unknown tool: ${toolPath}${suggestionText}${discoverHint}${catalogHint}${proxyHint}`;
163165 }
164166
165167 export async function resolveToolForCall(
executor/packages/database/src/runtime/tool_invocation.ts+204−35
@@ -12,6 +12,10 @@ import type { · 11 unmodified lines
1212 ToolRunContext,
1313 } from "../../../core/src/types";
1414 import { executeSerializedTool, parseSerializedTool } from "../../../core/src/tool/source-serialization";
15+import {
16+ compactArgTypeHintFromSchema,
17+ compactReturnTypeHintFromSchema,
18+} from "../../../core/src/type-hints";
1519 import { describeError } from "../../../core/src/utils";
1620 import {
1721 decodeToolCallControlSignal,
@@ -239,6 +243,53 @@ async function listRegistryNamespaces( · 221 unmodified lines
239243 return parsed.success ? parsed.data : [];
240244 }
241245
246+function toolNamespace(path: string): string {
247+ const normalized = path.trim().toLowerCase();
248+ if (!normalized) return "default";
249+ const [head] = normalized.split(".");
250+ return head && head.length > 0 ? head : "default";
251+}
252+
253+function matchesToolQuery(
254+ query: string,
255+ args: { path: string; description?: string; source?: string },
256+): boolean {
257+ const normalizedQuery = query.trim().toLowerCase();
258+ if (!normalizedQuery) return true;
259+
260+ const haystack = `${args.path} ${args.description ?? ""} ${args.source ?? ""}`.toLowerCase();
261+ return normalizedQuery
262+ .split(/\s+/)
263+ .filter((token) => token.length > 0)
264+ .every((token) => haystack.includes(token));
265+}
266+
267+function getToolHints(tool: ToolDefinition): { inputHint: string; outputHint: string } {
268+ const hintedInput = typeof tool.typing?.inputHint === "string" ? tool.typing.inputHint.trim() : "";
269+ const hintedOutput = typeof tool.typing?.outputHint === "string" ? tool.typing.outputHint.trim() : "";
270+
271+ const inputHint = hintedInput.length > 0
272+ ? hintedInput
273+ : compactArgTypeHintFromSchema(tool.typing?.inputSchema ?? {});
274+ const outputHint = hintedOutput.length > 0
275+ ? hintedOutput
276+ : compactReturnTypeHintFromSchema(tool.typing?.outputSchema ?? {});
277+
278+ return {
279+ inputHint: inputHint.trim().length > 0 ? inputHint : "{}",
280+ outputHint: outputHint.trim().length > 0 ? outputHint : "unknown",
281+ };
282+}
283+
284+function toBaseDiscoveryTyping(tool: ToolDefinition): DiscoveryTypingPayload {
285+ const inputSchemaJson = toSchemaJson(tool.typing?.inputSchema);
286+ const outputSchemaJson = toSchemaJson(tool.typing?.outputSchema);
287+ return {
288+ ...(inputSchemaJson ? { inputSchemaJson } : {}),
289+ ...(outputSchemaJson ? { outputSchemaJson } : {}),
290+ };
291+}
292+
242293 async function searchRegistryTools(
243294 ctx: ActionCtx,
244295 args: {
@@ -403,13 +454,12 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 158 unmodified lines
403454 accountId: task.accountId,
404455 clientId: task.clientId,
405456 });
406− if (buildIdResult.isErr()) {
407− throw buildIdResult.error;
408− }
409− const buildId = buildIdResult.value;
410− const state = await ctx.runQuery(internal.toolRegistry.getState, {
411− workspaceId: task.workspaceId,
412− }) as unknown as { openApiRefHintTables?: unknown } | null;
457+ const buildId = buildIdResult.isErr() ? undefined : buildIdResult.value;
458+ const state = buildId
459+ ? await ctx.runQuery(internal.toolRegistry.getState, {
460+ workspaceId: task.workspaceId,
461+ }) as unknown as { openApiRefHintTables?: unknown } | null
462+ : null;
413463 const refHintLookup = buildOpenApiRefHintLookup(state?.openApiRefHintTables);
414464
415465 const payload = typeof input === "string"
@@ -439,11 +489,45 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 23 unmodified lines
439489 const parsedInput = catalogNamespacesInputSchema.safeParse(payload);
440490 const limitInput = parsedInput.success ? parsedInput.data.limit : undefined;
441491 const limit = Math.max(1, Math.min(200, Number(limitInput ?? 200)));
442− const namespaces = await listRegistryNamespaces(ctx, {
443− workspaceId: task.workspaceId,
444− buildId,
445− limit,
446− });
492+ const registryNamespaces = buildId
493+ ? await listRegistryNamespaces(ctx, {
494+ workspaceId: task.workspaceId,
495+ buildId,
496+ limit: 200,
497+ })
498+ : [];
499+
500+ const namespaceMap = new Map<string, { toolCount: number; samplePaths: string[] }>();
501+ for (const entry of registryNamespaces) {
502+ namespaceMap.set(entry.namespace, {
503+ toolCount: entry.toolCount,
504+ samplePaths: [...entry.samplePaths],
505+ });
506+ }
507+
508+ for (const baseTool of baseTools.values()) {
509+ if (!isAllowed(baseTool.path, baseTool.approval, baseTool.source)) {
510+ continue;
511+ }
512+
513+ const namespace = toolNamespace(baseTool.path);
514+ const current = namespaceMap.get(namespace) ?? { toolCount: 0, samplePaths: [] };
515+ current.toolCount += 1;
516+ if (!current.samplePaths.includes(baseTool.path)) {
517+ current.samplePaths.push(baseTool.path);
518+ }
519+ namespaceMap.set(namespace, current);
520+ }
521+
522+ const namespaces = [...namespaceMap.entries()]
523+ .map(([namespace, meta]) => ({
524+ namespace,
525+ toolCount: meta.toolCount,
526+ samplePaths: [...meta.samplePaths].sort((a, b) => a.localeCompare(b)).slice(0, 3),
527+ }))
528+ .sort((a, b) => a.namespace.localeCompare(b.namespace))
529+ .slice(0, limit);
530+
447531 const output = catalogNamespacesOutputSchema.parse({
448532 namespaces,
449533 total: namespaces.length,
@@ -459,29 +543,47 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 9 unmodified lines
459543 const limit = Math.max(1, Math.min(200, Number(limitInput ?? 50)));
460544 const includeSchemas = parsedInput.success ? (parsedInput.data.includeSchemas ?? false) : false;
461545
462− const raw = query
463− ? await searchRegistryTools(ctx, {
464− workspaceId: task.workspaceId,
465− buildId,
466− query,
467− limit,
468− includeSchemas,
469− })
470− : namespace
471− ? await listRegistryToolsByNamespace(ctx, {
546+ const raw = !buildId
547+ ? []
548+ : query
549+ ? await searchRegistryTools(ctx, {
472550 workspaceId: task.workspaceId,
473551 buildId,
474− namespace,
552+ query,
475553 limit,
476554 includeSchemas,
477555 })
478− : [];
556+ : namespace
557+ ? await listRegistryToolsByNamespace(ctx, {
558+ workspaceId: task.workspaceId,
559+ buildId,
560+ namespace,
561+ limit,
562+ includeSchemas,
563+ })
564+ : [];
565+
566+ const baseMatches = [...baseTools.values()]
567+ .filter((baseTool) => {
568+ if (namespace && toolNamespace(baseTool.path) !== namespace) {
569+ return false;
570+ }
571+ if (query && !matchesToolQuery(query, {
572+ path: baseTool.path,
573+ description: baseTool.description,
574+ source: baseTool.source,
575+ })) {
576+ return false;
577+ }
578+
579+ return isAllowed(baseTool.path, baseTool.approval, baseTool.source);
580+ })
581+ .sort((left, right) => left.path.localeCompare(right.path));
479582
480583 const refHintTable: Record<string, string> = {};
481− const results = raw
584+ const registryResults = raw
482585 .filter((entry) => !namespace || String(entry.preferredPath ?? entry.path ?? "").toLowerCase().startsWith(`${namespace}.`))
483586 .filter((entry) => isAllowed(entry.path, entry.approval, entry.source))
484− .slice(0, limit)
485587 .map((entry) => {
486588 const preferredPath = entry.preferredPath ?? entry.path;
487589 const discoveryTyping = resolveEntryDiscoveryTyping(entry, refHintLookup);
@@ -504,6 +606,34 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 16 unmodified lines
504606 };
505607 });
506608
609+ const baseResults = baseMatches.map((entry) => {
610+ const hints = getToolHints(entry);
611+ const typing = toBaseDiscoveryTyping(entry);
612+ return {
613+ path: entry.path,
614+ source: entry.source,
615+ approval: entry.approval,
616+ description: entry.description,
617+ ...(includeSchemas
618+ ? (Object.keys(typing).length > 0 ? { typing } : {})
619+ : {
620+ inputHint: hints.inputHint,
621+ outputHint: hints.outputHint,
622+ }),
623+ };
624+ });
625+
626+ const mergedByPath = new Map<string, (typeof registryResults)[number]>();
627+ for (const entry of baseResults) {
628+ mergedByPath.set(entry.path, entry);
629+ }
630+ for (const entry of registryResults) {
631+ if (!mergedByPath.has(entry.path)) {
632+ mergedByPath.set(entry.path, entry);
633+ }
634+ }
635+ const results = [...mergedByPath.values()].slice(0, limit);
636+
507637 const output = catalogToolsOutputSchema.parse({
508638 ...(Object.keys(refHintTable).length > 0 ? { refHintTable } : {}),
509639 results,
@@ -519,20 +649,31 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 9 unmodified lines
519649 const limit = Math.max(1, Math.min(50, Number(limitInput ?? 8)));
520650 const compact = parsedInput.success ? (parsedInput.data.compact ?? true) : true;
521651 const includeSchemas = parsedInput.success ? (parsedInput.data.includeSchemas ?? false) : false;
522− const hits = await searchRegistryTools(ctx, {
523− workspaceId: task.workspaceId,
524− buildId,
525− query,
526− limit: Math.max(limit * 2, limit),
527− includeSchemas,
528− });
652+ const hits = buildId
653+ ? await searchRegistryTools(ctx, {
654+ workspaceId: task.workspaceId,
655+ buildId,
656+ query,
657+ limit: Math.max(limit * 2, limit),
658+ includeSchemas,
659+ })
660+ : [];
661+
662+ const baseHits = [...baseTools.values()]
663+ .filter((baseTool) => matchesToolQuery(query, {
664+ path: baseTool.path,
665+ description: baseTool.description,
666+ source: baseTool.source,
667+ }))
668+ .filter((baseTool) => isAllowed(baseTool.path, baseTool.approval, baseTool.source))
669+ .sort((left, right) => left.path.localeCompare(right.path));
529670
530671 const filtered = hits
531672 .filter((entry) => isAllowed(entry.path, entry.approval, entry.source))
532− .slice(0, limit);
673+ .slice(0, Math.max(limit * 2, limit));
533674
534675 const refHintTable: Record<string, string> = {};
535− const results = filtered.map((entry) => {
676+ const registryResults = filtered.map((entry) => {
536677 const preferredPath = entry.preferredPath ?? entry.path;
537678 const description = compact ? String(entry.description ?? "").split("\n")[0] : entry.description;
538679 const discoveryTyping = resolveEntryDiscoveryTyping(entry, refHintLookup);
@@ -555,6 +696,34 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 16 unmodified lines
555696 };
556697 });
557698
699+ const baseResults = baseHits.map((entry) => {
700+ const hints = getToolHints(entry);
701+ const typing = toBaseDiscoveryTyping(entry);
702+ return {
703+ path: entry.path,
704+ source: entry.source,
705+ approval: entry.approval,
706+ description: compact ? String(entry.description ?? "").split("\n")[0] : entry.description,
707+ ...(includeSchemas
708+ ? (Object.keys(typing).length > 0 ? { typing } : {})
709+ : {
710+ inputHint: hints.inputHint,
711+ outputHint: hints.outputHint,
712+ }),
713+ };
714+ });
715+
716+ const mergedByPath = new Map<string, (typeof registryResults)[number]>();
717+ for (const entry of baseResults) {
718+ mergedByPath.set(entry.path, entry);
719+ }
720+ for (const entry of registryResults) {
721+ if (!mergedByPath.has(entry.path)) {
722+ mergedByPath.set(entry.path, entry);
723+ }
724+ }
725+ const results = [...mergedByPath.values()].slice(0, limit);
726+
558727 const bestPath = results[0]?.path ?? null;
559728 const output = discoverOutputSchema.parse({
560729 bestPath,
executor/packages/runner-sandbox-host/src/index.ts+5−2
@@ -320,10 +320,13 @@ export class AgentFSStorageObject extends DurableObject<Env> { · 319 unmodified lines
320320 const limit = typeof payload.limit === "number" && Number.isFinite(payload.limit)
321321 ? Math.max(1, Math.min(500, Math.floor(payload.limit)))
322322 : 100;
323− const escaped = prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
323+ const escaped = prefix
324+ .replaceAll("^", "^^")
325+ .replaceAll("%", "^%")
326+ .replaceAll("_", "^_");
324327 const rows = this.ctx.storage.sql
325328 .exec<{ key: string; value: string }>(
326− "SELECT key, value FROM kv_store WHERE key LIKE ? ESCAPE '\\\\' ORDER BY key LIMIT ?",
329+ "SELECT key, value FROM kv_store WHERE key LIKE ? ESCAPE '^' ORDER BY key LIMIT ?",
327330 `${escaped}%`,
328331 limit,
329332 )
executor/scripts/dev/storage/smoke.execute-flow.ts+15−0
@@ -81,6 +81,15 @@ function parseWorkersDevUrl(output: string): string | null { · 80 unmodified lines
8181 return match ? match[0] : null;
8282 }
8383
84+function isHostedConvexUrl(url: string): boolean {
85+ try {
86+ const hostname = new URL(url).hostname.trim().toLowerCase();
87+ return hostname.endsWith(".convex.cloud") || hostname.endsWith(".convex.site");
88+ } catch {
89+ return false;
90+ }
91+}
92+
8493 async function deployCloudflareWorkerAndGetUrl(): Promise<string> {
8594 console.log("[storage:smoke:execute-flow] deploying runner-sandbox-host...");
8695 const output = await $`bun run --cwd packages/runner-sandbox-host deploy`
@@ -172,6 +181,12 @@ async function run(): Promise<void> { · 85 unmodified lines
172181 const requestedRuntime: RuntimeId = options.runtimeId
173182 ?? (process.env.CLOUDFLARE_SANDBOX_RUN_URL?.trim() ? "cloudflare-worker-loader" : "local-bun");
174183
184+ if (requestedRuntime === "local-bun" && isHostedConvexUrl(convexUrl)) {
185+ throw new Error(
186+ "local-bun storage smoke is self-host only. Hosted Convex deployments must use AGENT_STORAGE_PROVIDER=agentfs-cloudflare and the cloudflare-worker-loader runtime.",
187+ );
188+ }
189+
175190 await maybeDeployConvex(options.deployConvex);
176191
177192 if (requestedRuntime === "cloudflare-worker-loader") {