import { getQuery, getRouterParam, setResponseHeader, setResponseStatus, readMultipartFormData, } from "h3"; import { createError } from "h3"; import { uploadFile } from "../file-upload/index.js"; import { getOrgContext } from "../org/context.js"; import { getSession } from "../server/auth.js"; import { readBody, DEFAULT_UPLOAD_MAX_FILE_BYTES, isAllowedUploadMimeType, } from "../server/h3-helpers.js"; import { runWithRequestContext } from "../server/request-context.js"; import { getResourceKind, isRemoteAgentPath, parseCustomAgentProfile, parseRemoteAgentManifest, parseSkillMetadata, type CustomAgentProfile, type RemoteAgentManifest, type SkillMetadata, } from "./metadata.js"; import { resourceGet, resourceGetByPath, resourcePut, resourceDelete, resourceList, resourceListAccessible, resourceMove, resourceEffectiveContext, ensurePersonalDefaults, canWriteLocalWorkspaceResourcePath, isLocalWorkspaceResourceId, organizationIdFromResourceOwner, sharedResourceOwner, SHARED_OWNER, WORKSPACE_OWNER, type ResourceMeta, } from "./store.js"; // --------------------------------------------------------------------------- // Owner resolution // --------------------------------------------------------------------------- async function resolveOwner(event: any, shared?: boolean): Promise { if (shared) return sharedResourceOwner(await resolveOrgId(event)); const session = await getSession(event); if (!session?.email) { throw createError({ statusCode: 401, statusMessage: "Unauthenticated" }); } return session.email; } function canReadOwner( owner: string, email: string, orgId?: string | null, ): boolean { const ownerOrgId = organizationIdFromResourceOwner(owner); return ( owner === email || owner === SHARED_OWNER || owner === WORKSPACE_OWNER || (!!ownerOrgId && ownerOrgId === orgId) ); } function mergeScopedResources( primary: ResourceMeta[], inherited: ResourceMeta[], ): ResourceMeta[] { const seen = new Set(primary.map((resource) => resource.path)); return [ ...primary, ...inherited.filter((resource) => !seen.has(resource.path)), ]; } async function listSharedResources( orgId: string | null, prefix?: string, options?: Parameters[2], ): Promise { const organizationOwner = sharedResourceOwner(orgId); if (organizationOwner === SHARED_OWNER) { return options ? resourceList(SHARED_OWNER, prefix, options) : resourceList(SHARED_OWNER, prefix); } const [organization, legacyAppDefaults] = await Promise.all([ options ? resourceList(organizationOwner, prefix, options) : resourceList(organizationOwner, prefix), options ? resourceList(SHARED_OWNER, prefix, options) : resourceList(SHARED_OWNER, prefix), ]); return mergeScopedResources(organization, legacyAppDefaults); } async function resolveEmail(event: any): Promise { const session = await getSession(event); if (!session?.email) { throw createError({ statusCode: 401, statusMessage: "Unauthenticated" }); } return session.email; } async function resolveOrgId(event: any): Promise { const ctx = await getOrgContext(event); return ctx.orgId ?? null; } /** * Reject writes to organization-wide resources unless the user is the * organization owner/admin (or the deployment is solo — no org membership). * Read access remains open to every org member. */ async function assertCanEditShared(event: any): Promise { const session = await getSession(event); if (!session?.email) { throw createError({ statusCode: 401, statusMessage: "Unauthenticated" }); } const ctx = await getOrgContext(event); if (!ctx.orgId) return; // solo / dev mode — no org, treat as owner if (ctx.role === "owner" || ctx.role === "admin") return; throw createError({ statusCode: 403, message: "Only organization admins can edit organization files", }); } function shouldIncludeAgentScratch(query: Record): boolean { return ( query.includeAgentScratch === "true" || query.includeScratch === "true" || query.includeAgentScratch === true || query.includeScratch === true ); } // --------------------------------------------------------------------------- // Tree building // --------------------------------------------------------------------------- interface JobMetadata { schedule?: string; scheduleDescription?: string; enabled?: boolean; lastStatus?: string; lastRun?: string; nextRun?: string; } interface TreeNode { name: string; path: string; type: "file" | "folder"; kind?: "file" | "skill" | "job" | "agent" | "remote-agent"; children?: TreeNode[]; resource?: ResourceMeta; jobMeta?: JobMetadata; skillMeta?: SkillMetadata; agentMeta?: CustomAgentProfile; remoteAgentMeta?: RemoteAgentManifest; } function buildTree(resources: ResourceMeta[]): TreeNode[] { const root: TreeNode[] = []; for (const res of resources) { const parts = res.path.split("/").filter(Boolean); let current = root; for (let i = 0; i < parts.length; i++) { const part = parts[i]; const isLast = i === parts.length - 1; const currentPath = "/" + parts.slice(0, i + 1).join("/"); if (isLast) { current.push({ name: part, path: currentPath, type: "file", kind: getResourceKind(res.path), resource: res, }); } else { let folder = current.find( (n) => n.name === part && n.type === "folder", ); if (!folder) { folder = { name: part, path: currentPath, type: "folder", children: [], }; current.push(folder); } current = folder.children!; } } } sortTree(root); return root; } /** Sort tree nodes: folders first, then files, alphabetically within each group */ function sortTree(nodes: TreeNode[]): void { nodes.sort((a, b) => { if (a.type !== b.type) return a.type === "folder" ? -1 : 1; return a.name.localeCompare(b.name, undefined, { sensitivity: "base" }); }); for (const node of nodes) { if (node.children) sortTree(node.children); } } // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- /** GET /_agent-native/resources — list resources */ export async function handleListResources(event: any) { const query = getQuery(event); const prefix = (query.prefix as string) || undefined; const scope = (query.scope as string) || "all"; const email = await resolveEmail(event); const orgId = await resolveOrgId(event); const includeAgentScratch = shouldIncludeAgentScratch(query); const localListOptions = includeAgentScratch ? { includeAgentScratch: true } : undefined; const scopedListOptions = includeAgentScratch ? { includeAgentScratch: true, userEmail: email, orgId } : { userEmail: email, orgId }; // Seed personal AGENTS.md + LEARNINGS.md on first access await ensurePersonalDefaults(email); let resources: ResourceMeta[]; if (scope === "personal") { resources = localListOptions ? await resourceList(email, prefix, localListOptions) : await resourceList(email, prefix); } else if (scope === "workspace") { resources = await resourceList(WORKSPACE_OWNER, prefix, scopedListOptions); } else if (scope === "shared") { resources = await listSharedResources(orgId, prefix, localListOptions); } else { // "all" — personal + organization/shared + inherited workspace resources = await resourceListAccessible(email, prefix, scopedListOptions); } return { resources }; } /** GET /_agent-native/resources/tree — build nested tree */ export async function handleGetResourceTree(event: any) { const query = getQuery(event); const scope = (query.scope as string) || "all"; const email = await resolveEmail(event); const orgId = await resolveOrgId(event); const includeAgentScratch = shouldIncludeAgentScratch(query); const localListOptions = includeAgentScratch ? { includeAgentScratch: true } : undefined; const scopedListOptions = includeAgentScratch ? { includeAgentScratch: true, userEmail: email, orgId } : { userEmail: email, orgId }; // Seed personal AGENTS.md + LEARNINGS.md on first access await ensurePersonalDefaults(email); let resources: ResourceMeta[]; if (scope === "personal") { resources = localListOptions ? await resourceList(email, undefined, localListOptions) : await resourceList(email); } else if (scope === "workspace") { resources = await resourceList( WORKSPACE_OWNER, undefined, scopedListOptions, ); } else if (scope === "shared") { resources = await listSharedResources(orgId, undefined, localListOptions); } else { resources = await resourceListAccessible( email, undefined, scopedListOptions, ); } const tree = buildTree(resources); // Enrich typed resources with parsed metadata for richer UI await enrichTreeNodes(tree); return { tree }; } /** GET /_agent-native/resources/effective?path=... — show inheritance stack */ export async function handleGetEffectiveResourceContext(event: any) { const query = getQuery(event); const path = query.path; if (typeof path !== "string" || path.trim().length === 0) { setResponseStatus(event, 400); return { error: "path is required" }; } const email = await resolveEmail(event); const orgId = await resolveOrgId(event); await ensurePersonalDefaults(email); return resourceEffectiveContext(email, path, { userEmail: email, orgId }); } /** * Walk the tree and add typed metadata for jobs, skills, and agents. */ async function enrichTreeNodes(nodes: TreeNode[]): Promise { let parseFn: typeof import("../jobs/scheduler.js").parseJobFrontmatter; let describeFn: typeof import("../jobs/cron.js").describeCron; try { const scheduler = await import("../jobs/scheduler.js"); const cron = await import("../jobs/cron.js"); parseFn = scheduler.parseJobFrontmatter; describeFn = cron.describeCron; } catch { return; // Jobs module not available } for (const node of nodes) { if (node.type === "folder" && node.children) { await enrichTreeNodes(node.children); } if (node.type === "file" && node.resource) { try { const full = await resourceGet(node.resource.id); if (!full?.content) continue; if ( node.resource.path.startsWith("jobs/") && node.resource.path.endsWith(".md") ) { const { meta } = parseFn(full.content); node.jobMeta = { schedule: meta.schedule, scheduleDescription: meta.schedule ? describeFn(meta.schedule) : undefined, enabled: meta.enabled, lastStatus: meta.lastStatus, lastRun: meta.lastRun, nextRun: meta.nextRun, }; } if ( node.resource.path.startsWith("skills/") && node.resource.path.endsWith(".md") ) { node.skillMeta = parseSkillMetadata(full.content, node.resource.path) ?? undefined; } if ( node.resource.path.startsWith("agents/") && node.resource.path.endsWith(".md") ) { node.agentMeta = parseCustomAgentProfile(full.content, node.resource.path) ?? undefined; } if (isRemoteAgentPath(node.resource.path)) { node.remoteAgentMeta = parseRemoteAgentManifest(full.content, node.resource.path) ?? undefined; } } catch { // Skip individual file errors } } } } /** GET /_agent-native/resources/:id — get single resource with content. * If the request comes from an /