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
15 15
16# ── Agent storage backend (optional) ──16# ── Agent storage backend (optional) ──
17# Default: agentfs-local17# 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.
18# AGENT_STORAGE_PROVIDER=agentfs-local20# AGENT_STORAGE_PROVIDER=agentfs-local
19# AGENT_STORAGE_ROOT=/tmp/executor-agentfs21# AGENT_STORAGE_ROOT=/tmp/executor-agentfs
20# For Cloudflare Durable Objects storage proxy (served by runner-sandbox-host):22# 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
148 - `CONVEX_URL`148 - `CONVEX_URL`
149 - `CONVEX_SITE_URL`149 - `CONVEX_SITE_URL`
150 - `DANGEROUSLY_ALLOW_LOCAL_VM` (`1` in local/self-hosted deployments to allow `local-bun` runtime; production leaves unset for Cloudflare-first behavior)150 - `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`)
151- WorkOS (optional auth/org features):155- WorkOS (optional auth/org features):
152 - `WORKOS_CLIENT_ID`156 - `WORKOS_CLIENT_ID`
153 - `WORKOS_API_KEY`157 - `WORKOS_API_KEY`
@@ -225,4 +229,5 @@ executor/ · 71 unmodified lines
225- `401` on `/mcp`: verify your bearer token issuer matches `MCP_AUTHORIZATION_SERVER`.229- `401` on `/mcp`: verify your bearer token issuer matches `MCP_AUTHORIZATION_SERVER`.
226- `401` on `/mcp/anonymous`: pass `API_KEY` from the setup card as `Authorization: Bearer <API_KEY>` (or `x-api-key`).230- `401` on `/mcp/anonymous`: pass `API_KEY` from the setup card as `Authorization: Bearer <API_KEY>` (or `x-api-key`).
227- Web UI cannot load data: verify `CONVEX_URL` / `CONVEX_SITE_URL` and that Convex dev is running.231- 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`.
228- 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`.233- 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 @@
1"use client";1"use client";
2 2
3import { useMemo, useState } from "react";3import { useEffect, useMemo, useState } from "react";
4import { Database, FolderOpen, Plus, Trash2, Power } from "lucide-react";4import { useAction } from "convex/react";
5import {
6 Database,
7 FolderOpen,
8 HardDrive,
9 KeyRound,
10 Play,
11 Plus,
12 Power,
13 Table,
14 Trash2,
15} from "lucide-react";
5import { Badge } from "@/components/ui/badge";16import { Badge } from "@/components/ui/badge";
6import { Button } from "@/components/ui/button";17import { Button } from "@/components/ui/button";
7import { Input } from "@/components/ui/input";18import { Input } from "@/components/ui/input";
8import { Skeleton } from "@/components/ui/skeleton";19import { Skeleton } from "@/components/ui/skeleton";
9import type { StorageDurability, StorageInstanceRecord, StorageScopeType } from "@/lib/types";20import type { StorageDurability, StorageInstanceRecord, StorageScopeType } from "@/lib/types";
21import { convexApi } from "@/lib/convex-api";
10import { cn } from "@/lib/utils";22import { cn } from "@/lib/utils";
11 23
12type CreateStorageArgs = {24type CreateStorageArgs = {
@@ -16,6 +28,49 @@ type CreateStorageArgs = { · 3 unmodified lines
16 ttlHours?: number;28 ttlHours?: number;
17};29};
18 30
31type StorageDirectoryEntry = {
32 name: string;
33 type: "file" | "directory" | "symlink" | "unknown";
34 size?: number;
35 mtime?: number;
36};
37
38type StorageSqlResult = {
39 mode: "read" | "write";
40 rows?: Record<string, unknown>[];
41 rowCount: number;
42 changes?: number;
43};
44
45type StorageSqlObject = {
46 name: string;
47 type: "table" | "view" | "unknown";
48};
49
50const 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
59const ALL_OBJECTS_QUERY = "SELECT name, type FROM sqlite_master ORDER BY name";
60const KV_DATA_QUERY = "SELECT key, value, updated_at FROM kv_store ORDER BY key LIMIT 200";
61const FS_ENTRIES_QUERY = "SELECT * FROM fs_dentry LIMIT 200";
62const SQL_OBJECTS_QUERY = "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view') ORDER BY name";
63
64const 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
19function prettyBytes(value?: number): string {74function prettyBytes(value?: number): string {
20 if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {75 if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
21 return "-";76 return "-";
@@ -39,7 +94,74 @@ function asLocalDate(value?: number): string { · 17 unmodified lines
39 return new Date(value).toLocaleString();94 return new Date(value).toLocaleString();
40}95}
41 96
97function 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
105function previewJson(value: unknown): string {
106 try {
107 return JSON.stringify(value, null, 2);
108 } catch {
109 return String(value);
110 }
111}
112
113function 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
120function 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
128function 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
141function 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
151function sqlObjectType(value: unknown): "table" | "view" | "unknown" {
152 if (value === "table" || value === "view") {
153 return value;
154 }
155 return "unknown";
156}
157
158function escapeSqlIdentifier(value: string): string {
159 return value.replaceAll('"', '""');
160}
161
42export function StoragePanel({162export function StoragePanel({
163 workspaceId,
164 sessionId,
43 instances,165 instances,
44 loading,166 loading,
45 creating,167 creating,
@@ -48,6 +170,8 @@ export function StoragePanel({ · 2 unmodified lines
48 onClose,170 onClose,
49 onDelete,171 onDelete,
50}: {172}: {
173 workspaceId?: string;
174 sessionId?: string;
51 instances: StorageInstanceRecord[];175 instances: StorageInstanceRecord[];
52 loading: boolean;176 loading: boolean;
53 creating: boolean;177 creating: boolean;
@@ -60,12 +184,252 @@ export function StoragePanel({ · 6 unmodified lines
60 const [durability, setDurability] = useState<StorageDurability>("ephemeral");184 const [durability, setDurability] = useState<StorageDurability>("ephemeral");
61 const [purpose, setPurpose] = useState("");185 const [purpose, setPurpose] = useState("");
62 const [ttlHours, setTtlHours] = useState("24");186 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);
63 221
64 const visibleInstances = useMemo(222 const visibleInstances = useMemo(
65 () => [...instances].sort((a, b) => b.updatedAt - a.updatedAt),223 () => [...instances].sort((a, b) => b.updatedAt - a.updatedAt),
66 [instances],224 [instances],
67 );225 );
68 226
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
69 const submitCreate = async () => {433 const submitCreate = async () => {
70 const parsedTtl = Number.parseInt(ttlHours, 10);434 const parsedTtl = Number.parseInt(ttlHours, 10);
71 await onCreate({435 await onCreate({
@@ -78,159 +442,486 @@ export function StoragePanel({ · 6 unmodified lines
78 };442 };
79 443
80 return (444 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">445 <section className="flex h-full min-h-0 w-full overflow-hidden 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">446 <aside className="flex h-full w-72 shrink-0 flex-col border-r border-border/40 lg:w-80 xl:w-[22rem]">
83 <div className="flex items-center gap-2">447 <div className="flex items-center justify-between border-b border-border/40 px-3 py-2.5">
84 <div className="flex h-7 w-7 items-center justify-center border border-border/60 bg-background/80">448 <div className="flex items-center gap-2">
85 <Database className="h-4 w-4 text-muted-foreground" />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>
86 </div>453 </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>
88 </div>457 </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>
93 458
94 <div className="border-b border-border/40 bg-background/40 px-4 py-3 sm:px-5">459 <div className="shrink-0 border-b border-border/40 bg-background/40 px-3 py-3">
95 <div className="grid gap-2 md:grid-cols-5">460 <div className="space-y-2">
96 <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">461 <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
97 Scope462 Scope
98 <select463 <select
99 className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"464 className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"
100 value={scopeType}465 value={scopeType}
101 onChange={(event) => {466 onChange={(event) => {
102 const next = event.target.value as StorageScopeType;467 const next = event.target.value as StorageScopeType;
103 setScopeType(next);468 setScopeType(next);
104 if (next !== "scratch") {469 if (next !== "scratch") {
105 setDurability("durable");470 setDurability("durable");
106 }471 }
107 }}472 }}
108 >473 >
109 <option value="scratch">scratch</option>474 <option value="scratch">scratch</option>
110 <option value="account">account</option>475 <option value="account">account</option>
111 <option value="workspace">workspace</option>476 <option value="workspace">workspace</option>
112 <option value="organization">organization</option>477 <option value="organization">organization</option>
113 </select>478 </select>
114 </label>479 </label>
115 480
116 <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">481 <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
117 Durability482 Durability
118 <select483 <select
119 className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"484 className="h-8 rounded-none border border-border/60 bg-background px-2 text-xs text-foreground"
120 value={durability}485 value={durability}
121 onChange={(event) => setDurability(event.target.value as StorageDurability)}486 onChange={(event) => setDurability(event.target.value as StorageDurability)}
122 >487 >
123 <option value="ephemeral">ephemeral</option>488 <option value="ephemeral">ephemeral</option>
124 <option value="durable">durable</option>489 <option value="durable">durable</option>
125 </select>490 </select>
126 </label>491 </label>
127 492
128 <label className="flex flex-col gap-1 text-[11px] text-muted-foreground md:col-span-2">493 <label className="flex flex-col gap-1 text-[11px] text-muted-foreground">
129 Purpose494 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)
141 <Input495 <Input
142 value={ttlHours}496 value={purpose}
143 onChange={(event) => setTtlHours(event.target.value)}497 onChange={(event) => setPurpose(event.target.value)}
144 disabled={durability !== "ephemeral"}498 placeholder="scratch for repo indexing"
145 className="h-8 rounded-none text-xs"499 className="h-8 rounded-none text-xs"
146 />500 />
147 </label>501 </label>
148 <Button size="sm" className="h-8 rounded-none text-xs" disabled={creating} onClick={() => void submitCreate()}>502
149 <Plus className="mr-1.5 h-3.5 w-3.5" />503 <div className="flex items-end gap-2">
150 Create504 <label className="flex flex-1 flex-col gap-1 text-[11px] text-muted-foreground">
151 </Button>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>
152 </div>518 </div>
153 </div>519 </div>
154 </div>
155 520
156 <div className="min-h-0 flex-1 overflow-y-auto p-4 sm:p-5">521 <div className="min-h-0 flex-1 overflow-y-auto p-2">
157 {loading ? (522 {loading ? (
158 <div className="space-y-2">523 <div className="space-y-2">
159 {Array.from({ length: 4 }).map((_, i) => (524 {Array.from({ length: 4 }).map((_, i) => (
160 <Skeleton key={i} className="h-16 rounded-none" />525 <Skeleton key={i} className="h-16 rounded-none" />
161 ))}526 ))}
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" />
167 </div>527 </div>
168 <p className="text-sm text-muted-foreground">No storage instances yet</p>528 ) : visibleInstances.length === 0 ? (
169 <p className="text-[11px] text-muted-foreground/70 text-center max-w-md">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">
170 Create a scratch instance for short-lived runs or durable instances for shared state.530 <div className="flex h-10 w-10 items-center justify-center border border-border/60 bg-muted/40">
171 </p>531 <FolderOpen className="h-5 w-5 text-muted-foreground/50" />
172 </div>532 </div>
173 ) : (533 <p className="text-sm text-muted-foreground">No storage instances yet</p>
174 <div className="space-y-2">534 </div>
175 {visibleInstances.map((instance) => {535 ) : (
176 const busy = busyInstanceId === instance.id;536 <div className="space-y-2">
177 return (537 {visibleInstances.map((instance) => {
178 <div538 const busy = busyInstanceId === instance.id;
179 key={instance.id}539 const active = selectedInstance?.id === instance.id;
180 className={cn(540 return (
181 "group flex items-center gap-3 border border-border/50 bg-background/70 px-3 py-2.5 transition-colors",541 <div
182 busy ? "opacity-60" : "hover:border-border hover:bg-accent/20",542 key={instance.id}
183 )}543 className={cn(
184 style={{ boxShadow: "inset 2px 0 0 0 color-mix(in oklch, var(--border) 70%, transparent)" }}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")}
185 >617 >
186 <div className="flex h-8 w-8 shrink-0 items-center justify-center border border-border/60 bg-muted/50 overflow-hidden">618 <KeyRound className="mr-1 h-3 w-3" /> KV
187 <Database className="h-4 w-4 text-muted-foreground" />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>
188 </div>693 </div>
694 ) : null}
695 </div>
696 ) : null}
189 697
190 <div className="flex-1 min-w-0">698 {activeInspectorTab === "kv" ? (
191 <div className="mb-1 flex items-center gap-1.5 flex-wrap">699 <div className="space-y-2 p-4">
192 <span className="text-sm font-medium truncate">{instance.purpose || instance.id}</span>700 <div className="flex items-center gap-2">
193 <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.scopeType}</Badge>701 <Input
194 <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.durability}</Badge>702 value={kvPrefix}
195 <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.provider}</Badge>703 onChange={(event) => setKvPrefix(event.target.value)}
196 <Badge variant="outline" className="text-[9px] font-mono uppercase tracking-wider">{instance.status}</Badge>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 ))}
197 </div>737 </div>
738 )}
739 </div>
740 </div>
741 ) : null}
198 742
199 <p className="text-[11px] text-muted-foreground">743 {activeInspectorTab === "sql" ? (
200 {prettyBytes(instance.sizeBytes)} - {instance.fileCount ?? "-"} inode{instance.fileCount === 1 ? "" : "s"}744 <div className="grid h-full min-h-0 gap-3 p-4 lg:grid-cols-[15rem_minmax(0,1fr)]">
201 </p>745 <div className="flex min-h-0 flex-col border border-border/40">
202 <p className="mt-0.5 text-[11px] text-muted-foreground">746 <div className="flex items-center justify-between border-b border-border/30 px-2 py-1 text-[11px] text-muted-foreground">
203 Last used {asLocalDate(instance.lastSeenAt)}747 <span>Tables</span>
204 {instance.expiresAt ? ` - expires ${asLocalDate(instance.expiresAt)}` : ""}748 <div className="flex items-center gap-1">
205 </p>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>
206 </div>767 </div>
207 768
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 />
209 <Button811 <Button
210 variant="outline"
211 size="sm"812 size="sm"
212 className="h-7 rounded-none text-[11px]"813 className="h-8 rounded-none text-xs"
213 disabled={busy}814 disabled={sqlLoading || !canInspect}
214 onClick={() => void onClose(instance.id)}815 onClick={() => {
816 setSelectedSqlObjectName(null);
817 void runSql();
818 }}
215 >819 >
216 <Power className="mr-1 h-3 w-3" />820 <Play className="mr-1 h-3 w-3" /> Run
217 Close
218 </Button>821 </Button>
219 <Button822 <Button variant="outline" size="sm" className="h-8 rounded-none px-2 text-[11px]" onClick={() => { setSelectedSqlObjectName(null); void runSql(USER_TABLES_QUERY); }}>
220 variant="outline"823 User tables
221 size="sm"824 </Button>
222 className="h-7 rounded-none text-[11px] border-destructive/40 text-destructive hover:bg-destructive/10"825 <Button variant="outline" size="sm" className="h-8 rounded-none px-2 text-[11px]" onClick={() => { setSelectedSqlObjectName(null); void runSql(ALL_OBJECTS_QUERY); }}>
223 disabled={busy}826 All objects
224 onClick={() => void onDelete(instance.id)}827 </Button>
225 >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')"); }}>
226 <Trash2 className="mr-1 h-3 w-3" />829 KV schema
227 Delete830 </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
228 </Button>836 </Button>
229 </div>837 </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>
230 </div>919 </div>
231 );920 </div>
232 })}921 ) : null}
233 </div>922 </>
923 ) : (
924 <div className="p-4 text-[11px] text-muted-foreground">Select a storage instance to inspect.</div>
234 )}925 )}
235 </div>926 </div>
236 </section>927 </section>
executor/apps/web/src/components/tools/view.tsx+3−1
@@ -558,8 +558,10 @@ export function ToolsView() { · 557 unmodified lines
558 ) : null}558 ) : null}
559 559
560 {activeTab === "storage" ? (560 {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">
562 <StoragePanel562 <StoragePanel
563 workspaceId={context?.workspaceId}
564 sessionId={context?.sessionId}
563 instances={storageItems}565 instances={storageItems}
564 loading={storageLoading}566 loading={storageLoading}
565 creating={storageCreateBusy}567 creating={storageCreateBusy}
executor/packages/database/convex/_generated/api.d.ts+97−1837
@@ -8,8 +8,95 @@ · 7 unmodified lines
8 * @module8 * @module
9 */9 */
10 10
11import type { FunctionReference } from "convex/server";11import type * as accounts from "../accounts.js";
12import type { GenericId as Id } from "convex/values";12import type * as app from "../app.js";
13import type * as auth from "../auth.js";
14import type * as billing from "../billing.js";
15import type * as billingInternal from "../billingInternal.js";
16import type * as billingSync from "../billingSync.js";
17import type * as credentialsNode from "../credentialsNode.js";
18import type * as database from "../database.js";
19import type * as database_anonymous_session from "../database/anonymous_session.js";
20import type * as database_approvals from "../database/approvals.js";
21import type * as database_credentials from "../database/credentials.js";
22import type * as database_policies from "../database/policies.js";
23import type * as database_storage_instances from "../database/storage_instances.js";
24import type * as database_task_events from "../database/task_events.js";
25import type * as database_tasks from "../database/tasks.js";
26import type * as database_tool_calls from "../database/tool_calls.js";
27import type * as database_tool_sources from "../database/tool_sources.js";
28import type * as executor from "../executor.js";
29import type * as executorNode from "../executorNode.js";
30import type * as executorRuntimeNode from "../executorRuntimeNode.js";
31import type * as http from "../http.js";
32import type * as http_anonymous_auth from "../http/anonymous_auth.js";
33import type * as http_mcp_auth from "../http/mcp_auth.js";
34import type * as http_mcp_handler from "../http/mcp_handler.js";
35import type * as http_mcp_service from "../http/mcp_service.js";
36import type * as http_oauth_handlers from "../http/oauth_handlers.js";
37import type * as http_openapi_handler from "../http/openapi_handler.js";
38import type * as http_openapi_spec from "../http/openapi_spec.js";
39import type * as http_openapi_spec_registry from "../http/openapi_spec_registry.js";
40import type * as invites from "../invites.js";
41import type * as migrations from "../migrations.js";
42import type * as openApiSpecCache from "../openApiSpecCache.js";
43import type * as organizationMembers from "../organizationMembers.js";
44import type * as organizations from "../organizations.js";
45import type * as runtimeCallbacks from "../runtimeCallbacks.js";
46import type * as runtimeNode from "../runtimeNode.js";
47import type * as toolRegistry from "../toolRegistry.js";
48import type * as workspace from "../workspace.js";
49import type * as workspaceAuthInternal from "../workspaceAuthInternal.js";
50import type * as workspaces from "../workspaces.js";
51
52import type {
53 ApiFromModules,
54 FilterApi,
55 FunctionReference,
56} from "convex/server";
57
58declare 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}>;
13 100
14/**101/**
15 * A utility for referencing Convex functions in your app's public API.102 * 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
19 * const myFunctionReference = api.myModule.myFunction;106 * const myFunctionReference = api.myModule.myFunction;
20 * ```107 * ```
21 */108 */
22export declare const api: {109export declare const api: FilterApi<
23 accounts: {110 typeof fullApi,
24 deleteCurrentAccount: FunctionReference<111 FunctionReference<any, "public">
25 "mutation",112>;
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};
572 113
573/**114/**
574 * A utility for referencing Convex functions in your app's internal API.115 * A utility for referencing Convex functions in your app's internal API.
@@ -578,1291 +119,10 @@ export declare const api: { · 3 unmodified lines
578 * const myFunctionReference = internal.myModule.myFunction;119 * const myFunctionReference = internal.myModule.myFunction;
579 * ```120 * ```
580 */121 */
581export declare const internal: {122export declare const internal: FilterApi<
582 auth: {123 typeof fullApi,
583 authKitEvent: FunctionReference<124 FunctionReference<any, "internal">
584 "mutation",125>;
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};
1866 126
1867export declare const components: {127export declare const components: {
1868 workOSAuthKit: {128 workOSAuthKit: {
executor/packages/database/convex/database.ts+2−0
@@ -6,6 +6,8 @@ export { · 5 unmodified lines
6 getTaskInWorkspace,6 getTaskInWorkspace,
7 markTaskRunning,7 markTaskRunning,
8 markTaskFinished,8 markTaskFinished,
9 setTaskStorageDefaultInstance,
10 trackTaskStorageAccess,
9} from "./database/tasks";11} from "./database/tasks";
10export {12export {
11 createApproval,13 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
14 14
15function resolveStorageProvider(): "agentfs-local" | "agentfs-cloudflare" {15function resolveStorageProvider(): "agentfs-local" | "agentfs-cloudflare" {
16 const raw = (process.env.AGENT_STORAGE_PROVIDER ?? "").trim().toLowerCase();16 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
17 if (raw === "cloudflare" || raw === "agentfs-cloudflare") {30 if (raw === "cloudflare" || raw === "agentfs-cloudflare") {
18 return "agentfs-cloudflare";31 return "agentfs-cloudflare";
19 }32 }
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
20 return "agentfs-local";40 return "agentfs-local";
21}41}
22 42
executor/packages/database/convex/database/tasks.ts+140−1
@@ -2,10 +2,46 @@ import { v } from "convex/values"; · 1 unmodified line
2import { internalMutation, internalQuery } from "../_generated/server";2import { internalMutation, internalQuery } from "../_generated/server";
3import { mapTask } from "../../src/database/mappers";3import { mapTask } from "../../src/database/mappers";
4import { getTaskDoc } from "../../src/database/readers";4import { getTaskDoc } from "../../src/database/readers";
5import { completedTaskStatusValidator, jsonObjectValidator } from "../../src/database/validators";5import {
6 completedTaskStatusValidator,
7 jsonObjectValidator,
8 storageAccessTypeValidator,
9 storageScopeTypeValidator,
10} from "../../src/database/validators";
6import { DEFAULT_TASK_TIMEOUT_MS } from "../../src/task/constants";11import { DEFAULT_TASK_TIMEOUT_MS } from "../../src/task/constants";
7import { isTerminalTaskStatus } from "../../src/task/status";12import { isTerminalTaskStatus } from "../../src/task/status";
8 13
14function asRecord(value: unknown): Record<string, unknown> {
15 return value && typeof value === "object" && !Array.isArray(value)
16 ? value as Record<string, unknown>
17 : {};
18}
19
20function 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
30function 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
9export const createTask = internalMutation({45export const createTask = internalMutation({
10 args: {46 args: {
11 id: v.string(),47 id: v.string(),
@@ -143,3 +179,106 @@ export const markTaskFinished = internalMutation({ · 131 unmodified lines
143 return updated ? mapTask(updated) : null;179 return updated ? mapTask(updated) : null;
144 },180 },
145});181});
182
183export 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
226export 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
7 ToolDescriptor,7 ToolDescriptor,
8 OpenApiSourceQuality,8 OpenApiSourceQuality,
9 SourceAuthProfile,9 SourceAuthProfile,
10 StorageInstanceRecord,
10} from "../../core/src/types";11} from "../../core/src/types";
11import { isAdminRole } from "../../core/src/identity";12import { isAdminRole } from "../../core/src/identity";
12import { requireCanonicalAccount } from "../src/runtime/account_auth";13import { requireCanonicalAccount } from "../src/runtime/account_auth";
@@ -22,6 +23,72 @@ import { jsonObjectValidator } from "../src/database/validators"; · 9 unmodified lines
22import { customAction } from "../../core/src/function-builders";23import { customAction } from "../../core/src/function-builders";
23import { encodeToolCallResultForTransport } from "../../core/src/tool-call-result-transport";24import { encodeToolCallResultForTransport } from "../../core/src/tool-call-result-transport";
24import { previewOpenApiSourceUpgradeForContext, type OpenApiUpgradeDiffPreview } from "../src/runtime/tool_upgrade";25import { previewOpenApiSourceUpgradeForContext, type OpenApiUpgradeDiffPreview } from "../src/runtime/tool_upgrade";
26import { getStorageProvider, type StorageEncoding, type StorageProvider } from "../src/runtime/storage_provider";
27
28function 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
36async 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
72async 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}
25 92
26export const listToolsWithWarnings = customAction({93export const listToolsWithWarnings = customAction({
27 method: "POST",94 method: "POST",
@@ -177,6 +244,134 @@ export const previewOpenApiSourceUpgrade = customAction({ · 149 unmodified lines
177 },244 },
178});245});
179 246
247export 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
275export 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
306export 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
337export 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
180export const handleExternalToolCall = internalAction({375export const handleExternalToolCall = internalAction({
181 args: {376 args: {
182 runId: v.string(),377 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
62export const storageInstanceStatusValidator = v.union(v.literal("active"), v.literal("closed"), v.literal("deleted"));62export const storageInstanceStatusValidator = v.union(v.literal("active"), v.literal("closed"), v.literal("deleted"));
63 63
64export const storageProviderValidator = v.union(v.literal("agentfs-local"), v.literal("agentfs-cloudflare"));64export const storageProviderValidator = v.union(v.literal("agentfs-local"), v.literal("agentfs-cloudflare"));
65
66export 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
24 kvDeleteOutputJsonSchema,24 kvDeleteOutputJsonSchema,
25 kvGetInputJsonSchema,25 kvGetInputJsonSchema,
26 kvGetOutputJsonSchema,26 kvGetOutputJsonSchema,
27 kvIncrInputJsonSchema,
28 kvIncrOutputJsonSchema,
27 kvListInputJsonSchema,29 kvListInputJsonSchema,
28 kvListOutputJsonSchema,30 kvListOutputJsonSchema,
29 kvSetInputJsonSchema,31 kvSetInputJsonSchema,
30 kvSetOutputJsonSchema,32 kvSetOutputJsonSchema,
31 sqliteQueryInputJsonSchema,33 sqliteQueryInputJsonSchema,
32 sqliteQueryOutputJsonSchema,34 sqliteQueryOutputJsonSchema,
35 sqliteCapabilitiesInputJsonSchema,
36 sqliteCapabilitiesOutputJsonSchema,
37 sqliteInsertRowsInputJsonSchema,
38 sqliteInsertRowsOutputJsonSchema,
33 storageCloseInputJsonSchema,39 storageCloseInputJsonSchema,
34 storageCloseOutputJsonSchema,40 storageCloseOutputJsonSchema,
35 storageDeleteInputJsonSchema,41 storageDeleteInputJsonSchema,
@@ -94,7 +100,7 @@ registerSystemTool( · 58 unmodified lines
94 100
95registerSystemTool(101registerSystemTool(
96 "storage.open",102 "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.",
98 {104 {
99 inputSchema: storageOpenInputJsonSchema,105 inputSchema: storageOpenInputJsonSchema,
100 outputSchema: storageOpenOutputJsonSchema,106 outputSchema: storageOpenOutputJsonSchema,
@@ -130,7 +136,7 @@ registerSystemTool( · 29 unmodified lines
130 136
131registerSystemTool(137registerSystemTool(
132 "fs.read",138 "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.",
134 {140 {
135 inputSchema: fsReadInputJsonSchema,141 inputSchema: fsReadInputJsonSchema,
136 outputSchema: fsReadOutputJsonSchema,142 outputSchema: fsReadOutputJsonSchema,
@@ -139,7 +145,7 @@ registerSystemTool( · 2 unmodified lines
139 145
140registerSystemTool(146registerSystemTool(
141 "fs.write",147 "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.",
143 {149 {
144 inputSchema: fsWriteInputJsonSchema,150 inputSchema: fsWriteInputJsonSchema,
145 outputSchema: fsWriteOutputJsonSchema,151 outputSchema: fsWriteOutputJsonSchema,
@@ -200,6 +206,33 @@ registerSystemTool( · 54 unmodified lines
200 },206 },
201);207);
202 208
209registerSystemTool(
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
218registerSystemTool(
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
227registerSystemTool(
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
203registerSystemTool(236registerSystemTool(
204 "kv.list",237 "kv.list",
205 "List key-value entries by prefix from a storage instance.",238 "List key-value entries by prefix from a storage instance.",
@@ -209,6 +242,15 @@ registerSystemTool( · 3 unmodified lines
209 },242 },
210);243);
211 244
245registerSystemTool(
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
212registerSystemTool(254registerSystemTool(
213 "kv.delete",255 "kv.delete",
214 "Delete a key-value entry from a storage instance.",256 "Delete a key-value entry from a storage instance.",
@@ -218,11 +260,101 @@ registerSystemTool( · 3 unmodified lines
218 },260 },
219);261);
220 262
263registerSystemTool(
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
272registerSystemTool(
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
281registerSystemTool(
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
290registerSystemTool(
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
299registerSystemTool(
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
308registerSystemTool(
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
221registerSystemTool(317registerSystemTool(
222 "sqlite.query",318 "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.",
224 {320 {
225 inputSchema: sqliteQueryInputJsonSchema,321 inputSchema: sqliteQueryInputJsonSchema,
226 outputSchema: sqliteQueryOutputJsonSchema,322 outputSchema: sqliteQueryOutputJsonSchema,
227 },323 },
228);324);
325
326registerSystemTool(
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
335registerSystemTool(
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
344registerSystemTool(
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
353registerSystemTool(
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
351 async kvList(instance: StorageInstanceRecord, prefix: string, limit: number): Promise<Array<{ key: string; value: unknown }>> {351 async kvList(instance: StorageInstanceRecord, prefix: string, limit: number): Promise<Array<{ key: string; value: unknown }>> {
352 return await this.withDatabase(instance, false, async (db) => {352 return await this.withDatabase(instance, false, async (db) => {
353 await this.ensureKvTable(db);353 await this.ensureKvTable(db);
354 const escapedPrefix = prefix
355 .replaceAll("^", "^^")
356 .replaceAll("%", "^%")
357 .replaceAll("_", "^_");
354 const rows = rowsFromSqlResult(358 const rows = rowsFromSqlResult(
355 db.exec(359 db.exec(
356 "SELECT key, value FROM kv_store WHERE key LIKE ? ESCAPE '\\\\' ORDER BY key LIMIT ?",360 "SELECT key, value FROM kv_store WHERE key LIKE ? ESCAPE '^' ORDER BY key LIMIT ?",
357 [`${prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`, limit],361 [`${escapedPrefix}%`, limit],
358 ),362 ),
359 );363 );
360 return rows.map((row) => ({364 return rows.map((row) => ({
361 key: String(row.key ?? ""),365 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,
363 }));367 }));
364 });368 });
365 }369 }
@@ -553,6 +557,40 @@ let localProviderSingleton: AgentFsLocalStorageProvider | null = null; · 187 unmodified lines
553let cloudflareProviderSingleton: CloudflareRemoteStorageProvider | null = null;557let cloudflareProviderSingleton: CloudflareRemoteStorageProvider | null = null;
554let cloudflareStorageProbeStarted = false;558let cloudflareStorageProbeStarted = false;
555 559
560function isHostedConvexHostname(hostname: string): boolean {
561 const normalized = hostname.trim().toLowerCase();
562 return normalized.endsWith(".convex.cloud") || normalized.endsWith(".convex.site");
563}
564
565function 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
584function 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
556function startCloudflareStorageProbe(endpoint: string, token: string) {594function startCloudflareStorageProbe(endpoint: string, token: string) {
557 if (cloudflareStorageProbeStarted) {595 if (cloudflareStorageProbeStarted) {
558 return;596 return;
@@ -594,6 +632,8 @@ function startCloudflareStorageProbe(endpoint: string, token: string) { · 35 unmodified lines
594}632}
595 633
596function getLocalProvider(): AgentFsLocalStorageProvider {634function getLocalProvider(): AgentFsLocalStorageProvider {
635 assertLocalProviderSupported();
636
597 if (!localProviderSingleton) {637 if (!localProviderSingleton) {
598 const baseDir = (process.env.AGENT_STORAGE_ROOT ?? "").trim() || "/tmp/executor-agentfs";638 const baseDir = (process.env.AGENT_STORAGE_ROOT ?? "").trim() || "/tmp/executor-agentfs";
599 localProviderSingleton = new AgentFsLocalStorageProvider(baseDir);639 localProviderSingleton = new AgentFsLocalStorageProvider(baseDir);
executor/packages/database/src/runtime/storage_tool_contracts.ts+265−14
@@ -1,11 +1,64 @@
1import { z } from "zod";1import { z } from "zod";
2 2
3type JsonSchema = Record<string, unknown>;3type JsonSchema = Record<string, unknown>;
4type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
5
6function isRecord(value: unknown): value is Record<string, unknown> {
7 return typeof value === "object" && value !== null && !Array.isArray(value);
8}
9
10function 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}
4 53
5function toJsonSchema(schema: z.ZodTypeAny, fallback: JsonSchema): JsonSchema {54function toJsonSchema(schema: z.ZodTypeAny, fallback: JsonSchema): JsonSchema {
6 const maybeToJsonSchema = (z as unknown as { toJSONSchema?: (value: z.ZodTypeAny) => unknown }).toJSONSchema;55 const maybeToJsonSchema = (z as unknown as { toJSONSchema?: (value: z.ZodTypeAny) => unknown }).toJSONSchema;
7 if (typeof maybeToJsonSchema === "function") {56 if (typeof maybeToJsonSchema === "function") {
8 return maybeToJsonSchema(schema) as JsonSchema;57 try {
58 return mergeSchemaWithFallback(maybeToJsonSchema(schema), fallback);
59 } catch {
60 return fallback;
61 }
9 }62 }
10 63
11 return fallback;64 return fallback;
@@ -16,6 +69,15 @@ export const storageDurabilitySchema = z.enum(["ephemeral", "durable"]); · 4 unmodified lines
16export const storageStatusSchema = z.enum(["active", "closed", "deleted"]);69export const storageStatusSchema = z.enum(["active", "closed", "deleted"]);
17export const storageProviderSchema = z.enum(["agentfs-local", "agentfs-cloudflare"]);70export const storageProviderSchema = z.enum(["agentfs-local", "agentfs-cloudflare"]);
18 71
72const 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
19export const storageInstanceSchema = z.object({81export const storageInstanceSchema = z.object({
20 id: z.string(),82 id: z.string(),
21 scopeType: storageScopeSchema,83 scopeType: storageScopeSchema,
@@ -165,12 +227,12 @@ export const kvGetOutputSchema = z.object({ · 143 unmodified lines
165 instanceId: z.string(),227 instanceId: z.string(),
166 key: z.string(),228 key: z.string(),
167 found: z.boolean(),229 found: z.boolean(),
168 value: z.unknown().optional(),230 value: jsonValueSchema.optional(),
169});231});
170 232
171export const kvSetInputSchema = fsAccessSchema.extend({233export const kvSetInputSchema = fsAccessSchema.extend({
172 key: z.string(),234 key: z.string(),
173 value: z.unknown(),235 value: jsonValueSchema,
174});236});
175 237
176export const kvSetOutputSchema = z.object({238export const kvSetOutputSchema = z.object({
@@ -186,7 +248,7 @@ export const kvListInputSchema = fsAccessSchema.extend({ · 9 unmodified lines
186 248
187export const kvListOutputSchema = z.object({249export const kvListOutputSchema = z.object({
188 instanceId: z.string(),250 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 })),
190 total: z.number(),252 total: z.number(),
191});253});
192 254
@@ -200,6 +262,20 @@ export const kvDeleteOutputSchema = z.object({ · 7 unmodified lines
200 ok: z.boolean(),262 ok: z.boolean(),
201});263});
202 264
265export const kvIncrInputSchema = fsAccessSchema.extend({
266 key: z.string(),
267 by: z.coerce.number().optional(),
268 initial: z.coerce.number().optional(),
269});
270
271export 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
203export const sqliteQueryInputSchema = fsAccessSchema.extend({279export const sqliteQueryInputSchema = fsAccessSchema.extend({
204 sql: z.string(),280 sql: z.string(),
205 params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),281 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
210export const sqliteQueryOutputSchema = z.object({286export const sqliteQueryOutputSchema = z.object({
211 instanceId: z.string(),287 instanceId: z.string(),
212 mode: z.enum(["read", "write"]),288 mode: z.enum(["read", "write"]),
213 rows: z.array(z.record(z.unknown())).optional(),289 rows: z.array(z.record(jsonValueSchema)).optional(),
214 rowCount: z.number(),290 rowCount: z.number(),
215 changes: z.number().optional(),291 changes: z.number().optional(),
216});292});
217 293
294const sqliteScalarSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
295
296export const sqliteCapabilitiesInputSchema = fsAccessSchema.extend({});
297
298export 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
307export 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
315export 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
327const 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
218export const storageOpenInputJsonSchema = toJsonSchema(storageOpenInputSchema, {338export const storageOpenInputJsonSchema = toJsonSchema(storageOpenInputSchema, {
219 type: "object",339 type: "object",
220 properties: {340 properties: {
@@ -280,8 +400,15 @@ export const storageDeleteOutputJsonSchema = storageCloseOutputJsonSchema; · 59 unmodified lines
280 400
281function fsAccessJsonProperties() {401function fsAccessJsonProperties() {
282 return {402 return {
283 instanceId: { type: "string" },403 instanceId: {
284 scopeType: { type: "string", enum: ["scratch", "account", "workspace", "organization"] },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 },
285 };412 };
286}413}
287 414
@@ -438,7 +565,7 @@ export const kvGetOutputJsonSchema = toJsonSchema(kvGetOutputSchema, { · 150 unmodified lines
438 instanceId: { type: "string" },565 instanceId: { type: "string" },
439 key: { type: "string" },566 key: { type: "string" },
440 found: { type: "boolean" },567 found: { type: "boolean" },
441 value: {},568 value: jsonValueJsonSchema,
442 },569 },
443 required: ["instanceId", "key", "found"],570 required: ["instanceId", "key", "found"],
444 additionalProperties: false,571 additionalProperties: false,
@@ -449,7 +576,7 @@ export const kvSetInputJsonSchema = toJsonSchema(kvSetInputSchema, { · 4 unmodified lines
449 properties: {576 properties: {
450 ...fsAccessJsonProperties(),577 ...fsAccessJsonProperties(),
451 key: { type: "string" },578 key: { type: "string" },
452 value: {},579 value: jsonValueJsonSchema,
453 },580 },
454 required: ["key", "value"],581 required: ["key", "value"],
455 additionalProperties: false,582 additionalProperties: false,
@@ -480,7 +607,18 @@ export const kvListOutputJsonSchema = toJsonSchema(kvListOutputSchema, { · 24 unmodified lines
480 type: "object",607 type: "object",
481 properties: {608 properties: {
482 instanceId: { type: "string" },609 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 },
484 total: { type: "number" },622 total: { type: "number" },
485 },623 },
486 required: ["instanceId", "items", "total"],624 required: ["instanceId", "items", "total"],
@@ -508,19 +646,52 @@ export const kvDeleteOutputJsonSchema = toJsonSchema(kvDeleteOutputSchema, { · 21 unmodified lines
508 additionalProperties: false,646 additionalProperties: false,
509});647});
510 648
649export 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
661export 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
511export const sqliteQueryInputJsonSchema = toJsonSchema(sqliteQueryInputSchema, {674export const sqliteQueryInputJsonSchema = toJsonSchema(sqliteQueryInputSchema, {
512 type: "object",675 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.",
513 properties: {678 properties: {
514 ...fsAccessJsonProperties(),679 ...fsAccessJsonProperties(),
515 sql: { type: "string" },680 sql: { type: "string", description: "SQL statement text to execute." },
516 params: {681 params: {
517 type: "array",682 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(?)).",
518 items: {685 items: {
519 oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }, { type: "null" }],686 oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }, { type: "null" }],
520 },687 },
521 },688 },
522 mode: { type: "string", enum: ["read", "write"] },689 mode: {
523 maxRows: { type: "number" },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." },
524 },695 },
525 required: ["sql"],696 required: ["sql"],
526 additionalProperties: false,697 additionalProperties: false,
@@ -531,10 +702,90 @@ export const sqliteQueryOutputJsonSchema = toJsonSchema(sqliteQueryOutputSchema, · 4 unmodified lines
531 properties: {702 properties: {
532 instanceId: { type: "string" },703 instanceId: { type: "string" },
533 mode: { type: "string", enum: ["read", "write"] },704 mode: { type: "string", enum: ["read", "write"] },
534 rows: { type: "array", items: { type: "object" } },705 rows: { type: "array", items: { type: "object", additionalProperties: jsonValueJsonSchema } },
535 rowCount: { type: "number" },706 rowCount: { type: "number" },
536 changes: { type: "number" },707 changes: { type: "number" },
537 },708 },
538 required: ["instanceId", "mode", "rowCount"],709 required: ["instanceId", "mode", "rowCount"],
539 additionalProperties: false,710 additionalProperties: false,
540});711});
712
713export 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
722export 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
743export 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
766export 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
19 kvDeleteOutputSchema,19 kvDeleteOutputSchema,
20 kvGetInputSchema,20 kvGetInputSchema,
21 kvGetOutputSchema,21 kvGetOutputSchema,
22 kvIncrInputSchema,
23 kvIncrOutputSchema,
22 kvListInputSchema,24 kvListInputSchema,
23 kvListOutputSchema,25 kvListOutputSchema,
24 kvSetInputSchema,26 kvSetInputSchema,
25 kvSetOutputSchema,27 kvSetOutputSchema,
28 sqliteCapabilitiesInputSchema,
29 sqliteCapabilitiesOutputSchema,
30 sqliteInsertRowsInputSchema,
31 sqliteInsertRowsOutputSchema,
26 sqliteQueryInputSchema,32 sqliteQueryInputSchema,
27 sqliteQueryOutputSchema,33 sqliteQueryOutputSchema,
28 storageCloseInputSchema,34 storageCloseInputSchema,
@@ -35,6 +41,14 @@ import { · 6 unmodified lines
35 storageOpenOutputSchema,41 storageOpenOutputSchema,
36} from "./storage_tool_contracts";42} from "./storage_tool_contracts";
37 43
44type StorageScopeType = "scratch" | "account" | "workspace" | "organization";
45
46type TaskStorageDefaults = {
47 currentInstanceId?: string;
48 currentScopeType?: StorageScopeType;
49 byScope: Partial<Record<StorageScopeType, string>>;
50};
51
38const STORAGE_SYSTEM_TOOLS = new Set([52const STORAGE_SYSTEM_TOOLS = new Set([
39 "storage.open",53 "storage.open",
40 "storage.list",54 "storage.list",
@@ -48,11 +62,27 @@ const STORAGE_SYSTEM_TOOLS = new Set([ · 7 unmodified lines
48 "fs.remove",62 "fs.remove",
49 "kv.get",63 "kv.get",
50 "kv.set",64 "kv.set",
65 "kv.put",
66 "kv.create",
67 "kv.update",
51 "kv.list",68 "kv.list",
69 "kv.keys",
52 "kv.delete",70 "kv.delete",
71 "kv.del",
72 "kv.has",
73 "kv.exists",
74 "kv.value",
75 "kv.incr",
76 "kv.decr",
53 "sqlite.query",77 "sqlite.query",
78 "sqlite.exec",
79 "sqlite.capabilities",
80 "sqlite.insert_rows",
81 "sqlite.bulk_insert",
54]);82]);
55 83
84const SQLITE_MAX_BIND_VARIABLES = 100;
85
56function toInputRecord(input: unknown): Record<string, unknown> {86function toInputRecord(input: unknown): Record<string, unknown> {
57 if (!input || typeof input !== "object" || Array.isArray(input)) {87 if (!input || typeof input !== "object" || Array.isArray(input)) {
58 return {};88 return {};
@@ -70,6 +100,107 @@ function normalizeScopeType(value: unknown): undefined | "scratch" | "account" | · 11 unmodified lines
70 return undefined;100 return undefined;
71}101}
72 102
103function 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
111function toErrorMessage(error: unknown): string {
112 if (error instanceof Error && typeof error.message === "string") {
113 return error.message;
114 }
115 return String(error);
116}
117
118function 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
141function 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
185function 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
193function quoteSqlIdentifier(identifier: string): string {
194 return `"${identifier.replaceAll('"', '""')}"`;
195}
196
197function 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
73function normalizeInstanceId(value: unknown): string | undefined {204function normalizeInstanceId(value: unknown): string | undefined {
74 if (typeof value !== "string") return undefined;205 if (typeof value !== "string") return undefined;
75 const normalized = value.trim();206 const normalized = value.trim();
@@ -85,6 +216,84 @@ function normalizeInputPayload(input: unknown): Record<string, unknown> { · 9 unmodified lines
85 };216 };
86}217}
87 218
219function asRecord(value: unknown): Record<string, unknown> {
220 return value && typeof value === "object" && !Array.isArray(value)
221 ? value as Record<string, unknown>
222 : {};
223}
224
225function 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
245async 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
257async 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
280async 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
88async function openStorageInstanceForTask(297async function openStorageInstanceForTask(
89 ctx: ActionCtx,298 ctx: ActionCtx,
90 task: TaskRecord,299 task: TaskRecord,
@@ -114,7 +323,7 @@ async function resolveStorageInstance( · 23 unmodified lines
114 payload: Record<string, unknown>,323 payload: Record<string, unknown>,
115): Promise<StorageInstanceRecord> {324): Promise<StorageInstanceRecord> {
116 const requestedInstanceId = normalizeInstanceId(payload.instanceId);325 const requestedInstanceId = normalizeInstanceId(payload.instanceId);
117 const scopeType = normalizeScopeType(payload.scopeType);326 const requestedScopeType = normalizeScopeType(payload.scopeType);
118 327
119 if (requestedInstanceId) {328 if (requestedInstanceId) {
120 const existing = await ctx.runQuery(internal.database.getStorageInstance, {329 const existing = await ctx.runQuery(internal.database.getStorageInstance, {
@@ -129,13 +338,55 @@ async function resolveStorageInstance( · 8 unmodified lines
129 const reopened = await openStorageInstanceForTask(ctx, task, {338 const reopened = await openStorageInstanceForTask(ctx, task, {
130 instanceId: requestedInstanceId,339 instanceId: requestedInstanceId,
131 });340 });
341 await saveTaskStorageDefault(ctx, task, reopened.scopeType, reopened.id, true, "provided");
132 return reopened;342 return reopened;
133 }343 }
134 344
135 return await openStorageInstanceForTask(ctx, task, {345 const defaults = await getTaskStorageDefaults(ctx, task);
136 scopeType,346
137 purpose: "auto",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,
138 });387 });
388 await saveTaskStorageDefault(ctx, task, created.scopeType, created.id, true, "opened");
389 return created;
139}390}
140 391
141async function touchInstance(392async function touchInstance(
@@ -167,14 +418,43 @@ export async function runStorageSystemTool( · 25 unmodified lines
167 input: unknown,418 input: unknown,
168): Promise<unknown> {419): Promise<unknown> {
169 const payload = normalizeInputPayload(input);420 const payload = normalizeInputPayload(input);
170 421 const normalizedToolPath = toolPath === "kv.put"
171 if (toolPath === "storage.open") {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") {
172 const parsed = storageOpenInputSchema.parse(payload);444 const parsed = storageOpenInputSchema.parse(payload);
173 const instance = await openStorageInstanceForTask(ctx, task, parsed);445 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 );
174 return storageOpenOutputSchema.parse({ instance });454 return storageOpenOutputSchema.parse({ instance });
175 }455 }
176 456
177 if (toolPath === "storage.list") {457 if (normalizedToolPath === "storage.list") {
178 const parsed = storageListInputSchema.parse(payload);458 const parsed = storageListInputSchema.parse(payload);
179 const instances = await ctx.runQuery(internal.database.listStorageInstances, {459 const instances = await ctx.runQuery(internal.database.listStorageInstances, {
180 workspaceId: task.workspaceId,460 workspaceId: task.workspaceId,
@@ -189,17 +469,24 @@ export async function runStorageSystemTool( · 8 unmodified lines
189 });469 });
190 }470 }
191 471
192 if (toolPath === "storage.close") {472 if (normalizedToolPath === "storage.close") {
193 const parsed = storageCloseInputSchema.parse(payload);473 const parsed = storageCloseInputSchema.parse(payload);
194 const instance = await ctx.runMutation(internal.database.closeStorageInstance, {474 const instance = await ctx.runMutation(internal.database.closeStorageInstance, {
195 workspaceId: task.workspaceId,475 workspaceId: task.workspaceId,
196 accountId: task.accountId,476 accountId: task.accountId,
197 instanceId: parsed.instanceId,477 instanceId: parsed.instanceId,
198 });478 });
479
480 await trackTaskStorageAccess(ctx, task, {
481 instanceId: parsed.instanceId,
482 scopeType: instance?.scopeType,
483 accessType: "provided",
484 });
485
199 return storageCloseOutputSchema.parse({ instance });486 return storageCloseOutputSchema.parse({ instance });
200 }487 }
201 488
202 if (toolPath === "storage.delete") {489 if (normalizedToolPath === "storage.delete") {
203 const parsed = storageDeleteInputSchema.parse(payload);490 const parsed = storageDeleteInputSchema.parse(payload);
204 const existing = await ctx.runQuery(internal.database.getStorageInstance, {491 const existing = await ctx.runQuery(internal.database.getStorageInstance, {
205 workspaceId: task.workspaceId,492 workspaceId: task.workspaceId,
@@ -221,10 +508,17 @@ export async function runStorageSystemTool( · 15 unmodified lines
221 accountId: task.accountId,508 accountId: task.accountId,
222 instanceId: parsed.instanceId,509 instanceId: parsed.instanceId,
223 });510 });
511
512 await trackTaskStorageAccess(ctx, task, {
513 instanceId: parsed.instanceId,
514 scopeType: existing?.scopeType,
515 accessType: "provided",
516 });
517
224 return storageDeleteOutputSchema.parse({ instance });518 return storageDeleteOutputSchema.parse({ instance });
225 }519 }
226 520
227 if (toolPath === "fs.read") {521 if (normalizedToolPath === "fs.read") {
228 const parsed = fsReadInputSchema.parse(payload);522 const parsed = fsReadInputSchema.parse(payload);
229 const instance = await resolveStorageInstance(ctx, task, payload);523 const instance = await resolveStorageInstance(ctx, task, payload);
230 const provider = getStorageProvider(instance.provider);524 const provider = getStorageProvider(instance.provider);
@@ -240,7 +534,7 @@ export async function runStorageSystemTool( · 9 unmodified lines
240 });534 });
241 }535 }
242 536
243 if (toolPath === "fs.write") {537 if (normalizedToolPath === "fs.write") {
244 const parsed = fsWriteInputSchema.parse(payload);538 const parsed = fsWriteInputSchema.parse(payload);
245 const instance = await resolveStorageInstance(ctx, task, payload);539 const instance = await resolveStorageInstance(ctx, task, payload);
246 const provider = getStorageProvider(instance.provider);540 const provider = getStorageProvider(instance.provider);
@@ -254,7 +548,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
254 });548 });
255 }549 }
256 550
257 if (toolPath === "fs.readdir") {551 if (normalizedToolPath === "fs.readdir") {
258 const parsed = fsReaddirInputSchema.parse(payload);552 const parsed = fsReaddirInputSchema.parse(payload);
259 const instance = await resolveStorageInstance(ctx, task, payload);553 const instance = await resolveStorageInstance(ctx, task, payload);
260 const provider = getStorageProvider(instance.provider);554 const provider = getStorageProvider(instance.provider);
@@ -268,7 +562,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
268 });562 });
269 }563 }
270 564
271 if (toolPath === "fs.stat") {565 if (normalizedToolPath === "fs.stat") {
272 const parsed = fsStatInputSchema.parse(payload);566 const parsed = fsStatInputSchema.parse(payload);
273 const instance = await resolveStorageInstance(ctx, task, payload);567 const instance = await resolveStorageInstance(ctx, task, payload);
274 const provider = getStorageProvider(instance.provider);568 const provider = getStorageProvider(instance.provider);
@@ -281,7 +575,7 @@ export async function runStorageSystemTool( · 6 unmodified lines
281 });575 });
282 }576 }
283 577
284 if (toolPath === "fs.mkdir") {578 if (normalizedToolPath === "fs.mkdir") {
285 const parsed = fsMkdirInputSchema.parse(payload);579 const parsed = fsMkdirInputSchema.parse(payload);
286 const instance = await resolveStorageInstance(ctx, task, payload);580 const instance = await resolveStorageInstance(ctx, task, payload);
287 const provider = getStorageProvider(instance.provider);581 const provider = getStorageProvider(instance.provider);
@@ -294,7 +588,7 @@ export async function runStorageSystemTool( · 6 unmodified lines
294 });588 });
295 }589 }
296 590
297 if (toolPath === "fs.remove") {591 if (normalizedToolPath === "fs.remove") {
298 const parsed = fsRemoveInputSchema.parse(payload);592 const parsed = fsRemoveInputSchema.parse(payload);
299 const instance = await resolveStorageInstance(ctx, task, payload);593 const instance = await resolveStorageInstance(ctx, task, payload);
300 const provider = getStorageProvider(instance.provider);594 const provider = getStorageProvider(instance.provider);
@@ -310,7 +604,7 @@ export async function runStorageSystemTool( · 9 unmodified lines
310 });604 });
311 }605 }
312 606
313 if (toolPath === "kv.get") {607 if (normalizedToolPath === "kv.get") {
314 const parsed = kvGetInputSchema.parse(payload);608 const parsed = kvGetInputSchema.parse(payload);
315 const instance = await resolveStorageInstance(ctx, task, payload);609 const instance = await resolveStorageInstance(ctx, task, payload);
316 const provider = getStorageProvider(instance.provider);610 const provider = getStorageProvider(instance.provider);
@@ -324,7 +618,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
324 });618 });
325 }619 }
326 620
327 if (toolPath === "kv.set") {621 if (normalizedToolPath === "kv.set") {
328 const parsed = kvSetInputSchema.parse(payload);622 const parsed = kvSetInputSchema.parse(payload);
329 const instance = await resolveStorageInstance(ctx, task, payload);623 const instance = await resolveStorageInstance(ctx, task, payload);
330 const provider = getStorageProvider(instance.provider);624 const provider = getStorageProvider(instance.provider);
@@ -337,7 +631,7 @@ export async function runStorageSystemTool( · 6 unmodified lines
337 });631 });
338 }632 }
339 633
340 if (toolPath === "kv.list") {634 if (normalizedToolPath === "kv.list") {
341 const parsed = kvListInputSchema.parse(payload);635 const parsed = kvListInputSchema.parse(payload);
342 const instance = await resolveStorageInstance(ctx, task, payload);636 const instance = await resolveStorageInstance(ctx, task, payload);
343 const provider = getStorageProvider(instance.provider);637 const provider = getStorageProvider(instance.provider);
@@ -351,7 +645,7 @@ export async function runStorageSystemTool( · 7 unmodified lines
351 });645 });
352 }646 }
353 647
354 if (toolPath === "kv.delete") {648 if (normalizedToolPath === "kv.delete") {
355 const parsed = kvDeleteInputSchema.parse(payload);649 const parsed = kvDeleteInputSchema.parse(payload);
356 const instance = await resolveStorageInstance(ctx, task, payload);650 const instance = await resolveStorageInstance(ctx, task, payload);
357 const provider = getStorageProvider(instance.provider);651 const provider = getStorageProvider(instance.provider);
@@ -364,18 +658,151 @@ export async function runStorageSystemTool( · 6 unmodified lines
364 });658 });
365 }659 }
366 660
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") {
368 const parsed = sqliteQueryInputSchema.parse(payload);785 const parsed = sqliteQueryInputSchema.parse(payload);
369 const instance = await resolveStorageInstance(ctx, task, payload);786 const instance = await resolveStorageInstance(ctx, task, payload);
370 const provider = getStorageProvider(instance.provider);787 const provider = getStorageProvider(instance.provider);
371 const mode = parsed.mode ?? "read";788 const mode = parsed.mode ?? (isReadOnlySql(parsed.sql) ? "read" : "write");
372 const maxRows = Math.max(1, Math.min(1_000, Math.floor(parsed.maxRows ?? 200)));789 const maxRows = Math.max(1, Math.min(1_000, Math.floor(parsed.maxRows ?? 200)));
373 const result = await provider.sqliteQuery(instance, {790 const params = parsed.params ?? [];
374 sql: parsed.sql,791 let result;
375 params: parsed.params ?? [],792 try {
376 mode,793 result = await provider.sqliteQuery(instance, {
377 maxRows,794 sql: parsed.sql,
378 });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 }
379 await touchInstance(ctx, task, instance, provider, mode === "write");806 await touchInstance(ctx, task, instance, provider, mode === "write");
380 return sqliteQueryOutputSchema.parse({807 return sqliteQueryOutputSchema.parse({
381 instanceId: instance.id,808 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
159 : "";159 : "";
160 const queryHint = toolPath.split(".").filter(Boolean).join(" ");160 const queryHint = toolPath.split(".").filter(Boolean).join(" ");
161 const discoverHint = `\nTry: const found = await tools.discover({ query: "${queryHint}", depth: 1, limit: 12 });`;161 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}`;
163}165}
164 166
165export async function resolveToolForCall(167export async function resolveToolForCall(
executor/packages/database/src/runtime/tool_invocation.ts+204−35
@@ -12,6 +12,10 @@ import type { · 11 unmodified lines
12 ToolRunContext,12 ToolRunContext,
13} from "../../../core/src/types";13} from "../../../core/src/types";
14import { executeSerializedTool, parseSerializedTool } from "../../../core/src/tool/source-serialization";14import { executeSerializedTool, parseSerializedTool } from "../../../core/src/tool/source-serialization";
15import {
16 compactArgTypeHintFromSchema,
17 compactReturnTypeHintFromSchema,
18} from "../../../core/src/type-hints";
15import { describeError } from "../../../core/src/utils";19import { describeError } from "../../../core/src/utils";
16import {20import {
17 decodeToolCallControlSignal,21 decodeToolCallControlSignal,
@@ -239,6 +243,53 @@ async function listRegistryNamespaces( · 221 unmodified lines
239 return parsed.success ? parsed.data : [];243 return parsed.success ? parsed.data : [];
240}244}
241 245
246function 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
253function 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
267function 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
284function 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
242async function searchRegistryTools(293async function searchRegistryTools(
243 ctx: ActionCtx,294 ctx: ActionCtx,
244 args: {295 args: {
@@ -403,13 +454,12 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 158 unmodified lines
403 accountId: task.accountId,454 accountId: task.accountId,
404 clientId: task.clientId,455 clientId: task.clientId,
405 });456 });
406 if (buildIdResult.isErr()) {457 const buildId = buildIdResult.isErr() ? undefined : buildIdResult.value;
407 throw buildIdResult.error;458 const state = buildId
408 }459 ? await ctx.runQuery(internal.toolRegistry.getState, {
409 const buildId = buildIdResult.value;460 workspaceId: task.workspaceId,
410 const state = await ctx.runQuery(internal.toolRegistry.getState, {461 }) as unknown as { openApiRefHintTables?: unknown } | null
411 workspaceId: task.workspaceId,462 : null;
412 }) as unknown as { openApiRefHintTables?: unknown } | null;
413 const refHintLookup = buildOpenApiRefHintLookup(state?.openApiRefHintTables);463 const refHintLookup = buildOpenApiRefHintLookup(state?.openApiRefHintTables);
414 464
415 const payload = typeof input === "string"465 const payload = typeof input === "string"
@@ -439,11 +489,45 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 23 unmodified lines
439 const parsedInput = catalogNamespacesInputSchema.safeParse(payload);489 const parsedInput = catalogNamespacesInputSchema.safeParse(payload);
440 const limitInput = parsedInput.success ? parsedInput.data.limit : undefined;490 const limitInput = parsedInput.success ? parsedInput.data.limit : undefined;
441 const limit = Math.max(1, Math.min(200, Number(limitInput ?? 200)));491 const limit = Math.max(1, Math.min(200, Number(limitInput ?? 200)));
442 const namespaces = await listRegistryNamespaces(ctx, {492 const registryNamespaces = buildId
443 workspaceId: task.workspaceId,493 ? await listRegistryNamespaces(ctx, {
444 buildId,494 workspaceId: task.workspaceId,
445 limit,495 buildId,
446 });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
447 const output = catalogNamespacesOutputSchema.parse({531 const output = catalogNamespacesOutputSchema.parse({
448 namespaces,532 namespaces,
449 total: namespaces.length,533 total: namespaces.length,
@@ -459,29 +543,47 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 9 unmodified lines
459 const limit = Math.max(1, Math.min(200, Number(limitInput ?? 50)));543 const limit = Math.max(1, Math.min(200, Number(limitInput ?? 50)));
460 const includeSchemas = parsedInput.success ? (parsedInput.data.includeSchemas ?? false) : false;544 const includeSchemas = parsedInput.success ? (parsedInput.data.includeSchemas ?? false) : false;
461 545
462 const raw = query546 const raw = !buildId
463 ? await searchRegistryTools(ctx, {547 ? []
464 workspaceId: task.workspaceId,548 : query
465 buildId,549 ? await searchRegistryTools(ctx, {
466 query,
467 limit,
468 includeSchemas,
469 })
470 : namespace
471 ? await listRegistryToolsByNamespace(ctx, {
472 workspaceId: task.workspaceId,550 workspaceId: task.workspaceId,
473 buildId,551 buildId,
474 namespace,552 query,
475 limit,553 limit,
476 includeSchemas,554 includeSchemas,
477 })555 })
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));
479 582
480 const refHintTable: Record<string, string> = {};583 const refHintTable: Record<string, string> = {};
481 const results = raw584 const registryResults = raw
482 .filter((entry) => !namespace || String(entry.preferredPath ?? entry.path ?? "").toLowerCase().startsWith(`${namespace}.`))585 .filter((entry) => !namespace || String(entry.preferredPath ?? entry.path ?? "").toLowerCase().startsWith(`${namespace}.`))
483 .filter((entry) => isAllowed(entry.path, entry.approval, entry.source))586 .filter((entry) => isAllowed(entry.path, entry.approval, entry.source))
484 .slice(0, limit)
485 .map((entry) => {587 .map((entry) => {
486 const preferredPath = entry.preferredPath ?? entry.path;588 const preferredPath = entry.preferredPath ?? entry.path;
487 const discoveryTyping = resolveEntryDiscoveryTyping(entry, refHintLookup);589 const discoveryTyping = resolveEntryDiscoveryTyping(entry, refHintLookup);
@@ -504,6 +606,34 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 16 unmodified lines
504 };606 };
505 });607 });
506 608
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
507 const output = catalogToolsOutputSchema.parse({637 const output = catalogToolsOutputSchema.parse({
508 ...(Object.keys(refHintTable).length > 0 ? { refHintTable } : {}),638 ...(Object.keys(refHintTable).length > 0 ? { refHintTable } : {}),
509 results,639 results,
@@ -519,20 +649,31 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 9 unmodified lines
519 const limit = Math.max(1, Math.min(50, Number(limitInput ?? 8)));649 const limit = Math.max(1, Math.min(50, Number(limitInput ?? 8)));
520 const compact = parsedInput.success ? (parsedInput.data.compact ?? true) : true;650 const compact = parsedInput.success ? (parsedInput.data.compact ?? true) : true;
521 const includeSchemas = parsedInput.success ? (parsedInput.data.includeSchemas ?? false) : false;651 const includeSchemas = parsedInput.success ? (parsedInput.data.includeSchemas ?? false) : false;
522 const hits = await searchRegistryTools(ctx, {652 const hits = buildId
523 workspaceId: task.workspaceId,653 ? await searchRegistryTools(ctx, {
524 buildId,654 workspaceId: task.workspaceId,
525 query,655 buildId,
526 limit: Math.max(limit * 2, limit),656 query,
527 includeSchemas,657 limit: Math.max(limit * 2, limit),
528 });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));
529 670
530 const filtered = hits671 const filtered = hits
531 .filter((entry) => isAllowed(entry.path, entry.approval, entry.source))672 .filter((entry) => isAllowed(entry.path, entry.approval, entry.source))
532 .slice(0, limit);673 .slice(0, Math.max(limit * 2, limit));
533 674
534 const refHintTable: Record<string, string> = {};675 const refHintTable: Record<string, string> = {};
535 const results = filtered.map((entry) => {676 const registryResults = filtered.map((entry) => {
536 const preferredPath = entry.preferredPath ?? entry.path;677 const preferredPath = entry.preferredPath ?? entry.path;
537 const description = compact ? String(entry.description ?? "").split("\n")[0] : entry.description;678 const description = compact ? String(entry.description ?? "").split("\n")[0] : entry.description;
538 const discoveryTyping = resolveEntryDiscoveryTyping(entry, refHintLookup);679 const discoveryTyping = resolveEntryDiscoveryTyping(entry, refHintLookup);
@@ -555,6 +696,34 @@ export async function invokeTool(ctx: ActionCtx, task: TaskRecord, call: ToolCal · 16 unmodified lines
555 };696 };
556 });697 });
557 698
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
558 const bestPath = results[0]?.path ?? null;727 const bestPath = results[0]?.path ?? null;
559 const output = discoverOutputSchema.parse({728 const output = discoverOutputSchema.parse({
560 bestPath,729 bestPath,
executor/packages/runner-sandbox-host/src/index.ts+5−2
@@ -320,10 +320,13 @@ export class AgentFSStorageObject extends DurableObject<Env> { · 319 unmodified lines
320 const limit = typeof payload.limit === "number" && Number.isFinite(payload.limit)320 const limit = typeof payload.limit === "number" && Number.isFinite(payload.limit)
321 ? Math.max(1, Math.min(500, Math.floor(payload.limit)))321 ? Math.max(1, Math.min(500, Math.floor(payload.limit)))
322 : 100;322 : 100;
323 const escaped = prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");323 const escaped = prefix
324 .replaceAll("^", "^^")
325 .replaceAll("%", "^%")
326 .replaceAll("_", "^_");
324 const rows = this.ctx.storage.sql327 const rows = this.ctx.storage.sql
325 .exec<{ key: string; value: string }>(328 .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 ?",
327 `${escaped}%`,330 `${escaped}%`,
328 limit,331 limit,
329 )332 )
executor/scripts/dev/storage/smoke.execute-flow.ts+15−0
@@ -81,6 +81,15 @@ function parseWorkersDevUrl(output: string): string | null { · 80 unmodified lines
81 return match ? match[0] : null;81 return match ? match[0] : null;
82}82}
83 83
84function 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
84async function deployCloudflareWorkerAndGetUrl(): Promise<string> {93async function deployCloudflareWorkerAndGetUrl(): Promise<string> {
85 console.log("[storage:smoke:execute-flow] deploying runner-sandbox-host...");94 console.log("[storage:smoke:execute-flow] deploying runner-sandbox-host...");
86 const output = await $`bun run --cwd packages/runner-sandbox-host deploy`95 const output = await $`bun run --cwd packages/runner-sandbox-host deploy`
@@ -172,6 +181,12 @@ async function run(): Promise<void> { · 85 unmodified lines
172 const requestedRuntime: RuntimeId = options.runtimeId181 const requestedRuntime: RuntimeId = options.runtimeId
173 ?? (process.env.CLOUDFLARE_SANDBOX_RUN_URL?.trim() ? "cloudflare-worker-loader" : "local-bun");182 ?? (process.env.CLOUDFLARE_SANDBOX_RUN_URL?.trim() ? "cloudflare-worker-loader" : "local-bun");
174 183
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
175 await maybeDeployConvex(options.deployConvex);190 await maybeDeployConvex(options.deployConvex);
176 191
177 if (requestedRuntime === "cloudflare-worker-loader") {192 if (requestedRuntime === "cloudflare-worker-loader") {