feat(agent): 完善权限与结果引用安全
This commit is contained in:
+42
-4
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user