Serve every media file privately at /media/<id>
There is no public media any more. `/media/public/<id>` served a media row with no session at all, so anyone with the id could read a file. The public/private split is gone: the path is `/media/<id>` (and `/media/<id>/thumbnail`), every request is signed in first, and `grabUserMedia` decides whether the user may see it: their own upload, a bot's or workspace's picture or file they reach, or a file on a task they reach. Anything else is a 404 before a path is built. The `media.is_private` column and the two per-user directories went with it: uploads live in `media/<user_id>/` and there is nothing to pick between. The files browser's media volume is now the viewer's own folder whatever their role. Bots also say which of the two things they mean. A `[file:ID]` ref is a saved file and its chip opens the bytes; a folder is a new `[folder:volume:owner:path]` ref (`src/utils/folder-ref.ts`) whose chip opens the files browser there and downloads nothing. The old `[path:...]` ref and the extension sniffing that guessed file from folder are gone, so `import_file` names the folder holding a file it copied rather than the file itself. The prompts say what the Assistant got wrong: list before importing or saving, one copy per artifact, never split a file into parts unless asked, never claim the store is empty without a fresh list, and never invent a size cap. A re-import now says the path already existed instead of silently overwriting and reporting a fresh copy.
This commit is contained in:
@@ -4,10 +4,10 @@ import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import enqueueFileIngest from "@/src/file-ingest/enqueue-file-ingest";
|
||||
import recordBotFile from "@/src/functions/backend/db/bot-files/record-bot-file";
|
||||
import recordBotFile, { grabBotFileRow } from "@/src/functions/backend/db/bot-files/record-bot-file";
|
||||
import grabComputerMcpUrl from "@/src/functions/backend/db/computers/grab-computer-mcp-url";
|
||||
import computerNotReadyMessage from "@/src/utils/computer-not-ready-message";
|
||||
import pathRef from "@/src/utils/path-ref";
|
||||
import folderRef from "@/src/utils/folder-ref";
|
||||
import type { StorageScope } from "@/src/utils/storage-access";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import downloadComputerFile, { shellQuote } from "./download-computer-file";
|
||||
@@ -39,10 +39,12 @@ type Args = {
|
||||
* a whole folder (taken as a gzipped tar and unpacked into one row per
|
||||
* file). The copies are kept with the app, stay after the computer is
|
||||
* rebuilt, and are searchable at once; the originals stay on the computer,
|
||||
* where the message's path ref points at them.
|
||||
* where the message's folder ref points at the folder holding them.
|
||||
*
|
||||
* The bytes come back through the sandbox's own shell
|
||||
* (`runComputerCommand`), base64 encoded, which every sandbox has.
|
||||
* The bytes come back over the sandbox's streaming download endpoint
|
||||
* (`downloadComputerFile`), never through its shell, whose output is
|
||||
* capped: a `base64` round trip through `/v1/shell/exec` silently cut every
|
||||
* import at 30,047 characters.
|
||||
*/
|
||||
export default function importFileTool({
|
||||
computer_id,
|
||||
@@ -56,7 +58,7 @@ export default function importFileTool({
|
||||
|
||||
return {
|
||||
name: "import_file",
|
||||
description: `Copy a file or folder from this computer into your own files, so it is kept with the app and stays after the computer is rebuilt. Give its path on the computer, e.g. "seo/coderank/raw/crawl.json" or "seo/coderank/raw"; a folder comes in as it is, in one go. Up to ${max_mb} MB.${
|
||||
description: `Copy a file or folder from this computer into your own files, so it is kept with the app and stays after the computer is rebuilt. Give its path on the computer, e.g. "seo/coderank/raw/crawl.json" or "seo/coderank/raw"; a folder comes in as it is, in one go. Up to ${max_mb} MB. Check with list_files and the computer's own tools what you already imported: importing the same thing twice just writes over the copy, so do it once.${
|
||||
own?.scope == "workspace" ? " The copies land in the workspace's shared files." : ""
|
||||
} Imports are searchable at once. Name a copy with its [file:ID] ref. Import the report you wrote before the last long steps of a run, so it is never lost to the step limit.`,
|
||||
schema: z.object({
|
||||
@@ -153,6 +155,12 @@ async function importOneFile({
|
||||
|
||||
if (!download.ok) return importFailure({ reason: download.reason, size: download.size, path: target.source });
|
||||
|
||||
const previous = await grabBotFileRow({
|
||||
scope: target.scope.scope,
|
||||
owner_id: target.scope.owner_id,
|
||||
path: name,
|
||||
});
|
||||
|
||||
const saved = await recordBotFile({
|
||||
bot_id: target.bot_id,
|
||||
scope: target.scope.scope,
|
||||
@@ -171,11 +179,13 @@ async function importOneFile({
|
||||
}
|
||||
|
||||
return [
|
||||
`Copied ~/${target.source} into your files as ${saved.path} [file:${saved.id}], ${formatSize({ bytes: download.size })} byte for byte.`,
|
||||
`The original is still on the computer: ${pathRef({
|
||||
previous?.id
|
||||
? `You already had ${saved.path} (same ref, no second copy): its bytes are now this file, ${formatSize({ bytes: download.size })} byte for byte.`
|
||||
: `Copied ~/${target.source} into your files as ${saved.path} [file:${saved.id}], ${formatSize({ bytes: download.size })} byte for byte.`,
|
||||
`The original is still on the computer, in ${folderRef({
|
||||
volume: "computer",
|
||||
owner_id: target.computer_id,
|
||||
path: target.source,
|
||||
path: sourceFolder({ path: target.source }),
|
||||
})}`,
|
||||
"Name the copy with its [file:ID] ref when you tell the user about it.",
|
||||
].join("\n");
|
||||
@@ -215,7 +225,7 @@ async function importFolder({
|
||||
`Copied ${saved.length} file(s) from ~/${target.source} into your files under ${name}/:`,
|
||||
...saved.slice(0, 20).map((file) => `- ${file.path} [file:${file.id}]`),
|
||||
saved.length > 20 ? `(and ${saved.length - 20} more)` : "",
|
||||
`The originals are still on the computer: ${pathRef({
|
||||
`The originals are still on the computer, in ${folderRef({
|
||||
volume: "computer",
|
||||
owner_id: computer_id,
|
||||
path: target.source,
|
||||
@@ -226,6 +236,16 @@ async function importFolder({
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* The folder holding a path, for a folder ref: a ref is always a folder, so
|
||||
* a file's ref points at the directory the user can open to see it.
|
||||
*/
|
||||
function sourceFolder({ path: source }: { path: string }) {
|
||||
const segments = source.split("/").filter(Boolean);
|
||||
|
||||
return segments.slice(0, -1).join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the path is a file, a folder, or not there at all.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import listBotFiles from "@/src/functions/backend/db/bot-files/list-bot-files";
|
||||
import folderRef from "@/src/utils/folder-ref";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import { fileRef, formatBytes, type StorageToolContext } from "./storage-tool-context";
|
||||
|
||||
@@ -13,12 +14,15 @@ type Args = {
|
||||
|
||||
/**
|
||||
* Lists a bot's files: one folder's, or everything under it, or whatever
|
||||
* matches a name. Each file comes with its ref so the bot can read it.
|
||||
* matches a name. Each file comes with its `[file:ID]` ref so the bot can
|
||||
* read it, and the answer names the folder it is in with a folder ref, so a
|
||||
* bot that points at "everything in reports/" writes something the user can
|
||||
* actually open.
|
||||
*/
|
||||
export default function listFilesTool(context: StorageToolContext): HarnessTool<Args> {
|
||||
return {
|
||||
name: "list_files",
|
||||
description: `List the files you have saved, newest first: one folder's with "folder", everything with "recursive", or the ones whose name or path has "search". Each line has its path, size and ref (e.g. [file:12]) to read it.`,
|
||||
description: `List the files you have saved, newest first: one folder's with "folder", everything with "recursive", or the ones whose name or path has "search". Each line has its path, size and [file:ID] ref (e.g. [file:12]) to read it. The answer also names the folder as a folder ref, which opens it in Files without downloading anything: a file is a [file:ID] ref, a folder is a [folder:...] ref.`,
|
||||
schema: z.object({
|
||||
folder: z.string().max(AppData.BotFilePathMaxChars).optional(),
|
||||
search: z.string().max(120).optional(),
|
||||
@@ -56,7 +60,31 @@ export default function listFilesTool(context: StorageToolContext): HarnessTool<
|
||||
? `\n(Showing the first ${AppData.BotFileListMaxItems}. Narrow it with "folder" or "search".)`
|
||||
: "";
|
||||
|
||||
return `${files.length} file(s):\n${lines.join("\n")}${more}`;
|
||||
return `${files.length} file(s):\n${lines.join("\n")}${more}\n\nThe folder: ${folderRefFor({ files, folder, scopes: context.scopes })}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The folder the listing is about, as a folder ref the bot can put in a
|
||||
* message: the one it asked for, else the one the first file is in. Its
|
||||
* volume is the scope the file lives in.
|
||||
*/
|
||||
function folderRefFor({
|
||||
files,
|
||||
folder,
|
||||
scopes,
|
||||
}: {
|
||||
files: { scope?: string | null; owner_id?: number | string | null; folder?: string | null }[];
|
||||
folder: string;
|
||||
scopes: StorageToolContext["scopes"];
|
||||
}) {
|
||||
const first = files[0];
|
||||
const scope = first
|
||||
? scopes.find((it) => it.scope == first.scope && Number(it.owner_id) == Number(first.owner_id))
|
||||
: scopes[0];
|
||||
const path = folder || first?.folder || "";
|
||||
const volume = scope?.scope == "workspace" ? "workspace" : "bot";
|
||||
|
||||
return folderRef({ volume, owner_id: scope?.owner_id || 0, path });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user