feat(agent): 完善权限与结果引用安全

This commit is contained in:
2026-08-06 18:41:52 +08:00
parent a5e91ac2b8
commit b19af8846a
22 changed files with 621 additions and 47 deletions
+42 -4
View File
@@ -1,4 +1,7 @@
import { readJsonFile } from "../utils/fileStore.js";
import { realpath, stat } from "node:fs/promises";
import { isAbsolute, relative } from "node:path";
import { readJsonFile, removeFileIfExists } from "../utils/fileStore.js";
import {
type ResultReferenceKind,
type ResultReferenceRecord,
@@ -33,7 +36,11 @@ export type RenderJunctionPayload = {
};
export class ResultReferenceResolver {
constructor(private readonly store: ResultReferenceStore) {}
constructor(
private readonly store: ResultReferenceStore,
private readonly importRoot: string,
private readonly importMaxBytes: number,
) {}
// Resolver 负责按结果类型做结构校验,Store 只关心授权和落盘。
async register(input: RegisterResultReferenceInput) {
@@ -63,7 +70,17 @@ export class ResultReferenceResolver {
filePath: string,
input: Omit<RegisterResultReferenceInput, "data" | "kind" | "schemaVersion">,
) {
const raw = await readJsonFile<unknown>(filePath);
const resolvedFilePath = await resolvePathInsideRoot(filePath, this.importRoot);
const fileStat = await stat(resolvedFilePath);
if (!fileStat.isFile()) {
throw new Error("render payload path must point to a regular file");
}
if (fileStat.size > this.importMaxBytes) {
throw new Error(
`render payload file exceeds RESULT_REF_IMPORT_MAX_BYTES (${this.importMaxBytes})`,
);
}
const raw = await readJsonFile<unknown>(resolvedFilePath);
if (raw === null) {
throw new Error(`render payload file not found: ${filePath}`);
}
@@ -78,13 +95,15 @@ export class ResultReferenceResolver {
throw new Error("render payload file does not contain a valid junction render payload");
}
return this.register({
const record = await this.register({
...input,
data: payload,
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
schemaVersion: 1,
source: RESULT_REFERENCE_SOURCE.agentGenerated,
});
await removeFileIfExists(resolvedFilePath);
return record;
}
async getFullAuthorized(
@@ -167,6 +186,25 @@ export const extractRenderJunctionPayload = (
};
};
const resolvePathInsideRoot = async (filePath: string, rootPath: string) => {
if (!isAbsolute(filePath)) {
throw new Error("render payload file_path must be absolute");
}
const [resolvedFilePath, resolvedRootPath] = await Promise.all([
realpath(filePath),
realpath(rootPath),
]);
const relativePath = relative(resolvedRootPath, resolvedFilePath);
if (
relativePath === ".." ||
relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) ||
isAbsolute(relativePath)
) {
throw new Error("render payload file must be inside RESULT_REF_IMPORT_DIR");
}
return resolvedFilePath;
};
const normalizeDataForKind = (
kind: ResultReferenceKind,
data: unknown,