Files
oh-my-pi/packages/coding-agent/src/internal-urls/artifact-protocol.ts
T
can1357 c3054d5cea fix(coding-agent): capped read-stack resource use and fixed selector routing
tar/tgz stat-gated at 256MB, zip entries reject oversized declared sizes; raw ?q= sqlite capped at 1000 rows; giant-file reads stop scanning to EOF; multi-range reads slice one pass; malformed URL selectors error instead of dumping; archive-root selectors, member tag immutability, case-insensitive selector tokens, session-pinned artifact lookups, shared+escaped suffix globs; archive dir listings honor offsets; binary files get a NUL-sniff notice.
2026-06-10 01:27:17 +02:00

108 lines
3.1 KiB
TypeScript

/**
* Protocol handler for artifact:// URLs.
*
* Resolves artifact IDs against the artifacts directories of every active
* session. Unlike agent://, artifacts are raw text with no JSON extraction.
*
* URL form:
* - artifact://<id> - Full artifact content
*
* Pagination is handled by the read tool via offset/limit parameters.
*/
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent } from "@oh-my-pi/pi-utils";
import { artifactsDirsFromRegistry } from "./registry-helpers";
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
export class ArtifactProtocolHandler implements ProtocolHandler {
readonly scheme = "artifact";
readonly immutable = true;
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
const id = url.rawHost || url.hostname;
if (!id) {
throw new Error("artifact:// URL requires a numeric ID: artifact://0");
}
if (!/^\d+$/.test(id)) {
throw new Error(`artifact:// ID must be numeric, got: ${id}`);
}
// Artifact ids are per-session counters; in multi-session hosts the same
// id exists in several dirs. Pin resolution to the calling session's
// artifacts dir first so `artifact://3` means *this* session's #3.
const dirs = artifactsDirsFromRegistry();
const pinnedDir = context?.localProtocolOptions?.getArtifactsDir?.() ?? null;
if (pinnedDir) {
const pinnedIndex = dirs.indexOf(pinnedDir);
if (pinnedIndex >= 0) dirs.splice(pinnedIndex, 1);
dirs.unshift(pinnedDir);
}
if (dirs.length === 0) {
throw new Error("No session - artifacts unavailable");
}
let foundPath: string | undefined;
let anyDirExists = false;
const availableIds = new Set<string>();
for (const dir of dirs) {
let files: string[];
try {
files = await fs.readdir(dir);
anyDirExists = true;
} catch (err) {
if (isEnoent(err)) continue;
throw err;
}
const match = files.find(f => f.startsWith(`${id}.`));
if (match) {
foundPath = path.join(dir, match);
break;
}
for (const f of files) {
const m = f.match(/^(\d+)\./);
if (m) availableIds.add(m[1]);
}
}
if (!anyDirExists) {
throw new Error("No artifacts directory found");
}
if (!foundPath) {
const sorted = [...availableIds].sort((a, b) => Number(a) - Number(b));
const availableStr = sorted.length > 0 ? sorted.join(", ") : "none";
throw new Error(`Artifact ${id} not found. Available: ${availableStr}`);
}
const content = await Bun.file(foundPath).text();
return {
url: url.href,
content,
contentType: "text/plain",
size: Buffer.byteLength(content, "utf-8"),
sourcePath: foundPath,
};
}
async complete(): Promise<UrlCompletion[]> {
const ids = new Set<string>();
for (const dir of artifactsDirsFromRegistry()) {
let files: string[];
try {
files = await fs.readdir(dir);
} catch (err) {
if (isEnoent(err)) continue;
throw err;
}
for (const f of files) {
const m = f.match(/^(\d+)\./);
if (m) ids.add(m[1]!);
}
}
return [...ids].sort((a, b) => Number(a) - Number(b)).map(value => ({ value }));
}
}