-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathprepare-agent-task-upload.mjs
More file actions
185 lines (169 loc) · 9.86 KB
/
Copy pathprepare-agent-task-upload.mjs
File metadata and controls
185 lines (169 loc) · 9.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import { constants } from "node:fs"
import { lstat, mkdir, open, readdir, readFile, rm, writeFile } from "node:fs/promises"
import { isUtf8 } from "node:buffer"
import { isAbsolute, join, relative, resolve } from "node:path"
import { assertNoRuntimeSourcePaths, sanitizeRuntimeSourceJson } from "./runtime-source-sanitizer.mjs"
const MAX_UPLOAD_FILE_BYTES = 4 * 1024 * 1024
const workspace = resolve(process.env.AGENT_TASK_WORKSPACE || process.cwd())
const uploadPath = resolve(process.env.AGENT_TASK_UPLOAD_PATH || join(workspace, ".codebox", "agent-task-upload"))
const requestPath = resolve(process.env.AGENT_TASK_REQUEST_PATH || join(workspace, ".codebox", "agent-task-request.json"))
const artifactsPath = join(workspace, ".codebox", "agent-task-artifacts")
const secretValues = ["OPENAI_API_KEY", "MODEL_PROVIDER_SECRET_1", "MODEL_PROVIDER_SECRET_2", "MODEL_PROVIDER_SECRET_3", "MODEL_PROVIDER_SECRET_4", "MODEL_PROVIDER_SECRET_5", "GITHUB_TOKEN", "GH_TOKEN", "ACCESS_TOKEN", "EXTERNAL_PACKAGE_SOURCE_POLICY"].map((name) => process.env[name]).filter(Boolean)
const runtimeSourceRoot = process.env.WP_CODEBOX_RUNTIME_SOURCE_ROOT ? resolve(process.env.WP_CODEBOX_RUNTIME_SOURCE_ROOT) : ""
const runtimeSourcePrefix = process.env.WP_CODEBOX_RUNTIME_SOURCE_PREFIX ? resolve(process.env.WP_CODEBOX_RUNTIME_SOURCE_PREFIX) : ""
const runtimeSourceRoots = [runtimeSourceRoot, runtimeSourcePrefix].filter(Boolean)
const SOURCE_TREE = /(^|\/)(prepared-plugins|prepared-source-packages|source-package[^/]*)(\/|$)/i
const SOURCE_FILE = /\.(?:php|phtml|js|mjs|cjs|jsx|ts|tsx)$/i
const RUNTIME_SOURCE_CONTENT = /(?:Plugin Name:|WP_Agents_Registry|OpenAiProvider)/
function redact(value) {
return secretValues.reduce((output, secret) => output.split(secret).join("[REDACTED]"), value)
}
function sanitizeText(text) {
return sanitizeRuntimeSourceJson(text, runtimeSourceRoots)
}
function compactNativeInput(text) {
const privateFields = new Set(["source_package_root", "component_contracts", "extra_plugins", "provider_plugins", "runtime_overlays", "prepared_sources"])
const compact = (value) => {
if (Array.isArray(value)) return value.map(compact)
const entry = record(value)
if (!Object.keys(entry).length) return value
return Object.fromEntries(Object.entries(entry).flatMap(([key, item]) => privateFields.has(key) ? [] : [[key, compact(item)]]))
}
try {
return `${JSON.stringify(compact(JSON.parse(sanitizeText(text))), null, 2)}\n`
} catch {
return sanitizeText(text)
}
}
function isPrivateRuntimePath(value) {
if (!runtimeSourceRoots.length || typeof value !== "string") return false
const path = resolve(value)
return runtimeSourceRoots.some((root) => {
const contained = relative(root, path)
return path === root || (contained !== ".." && !contained.startsWith(`..${String.fromCharCode(47)}`) && !isAbsolute(contained))
})
}
function safeRelativeArtifactPath(value) {
if (typeof value !== "string" || !value.trim() || isAbsolute(value)) return ""
const path = value.replace(/\\/g, "/").replace(/^\.\//, "")
if (path.split("/").some((part) => !part || part === "." || part === "..")) return ""
return path
}
function sourceCategory(path, absolutePath) {
if (isPrivateRuntimePath(absolutePath)) return "private-runtime"
if (SOURCE_TREE.test(path)) return "source-tree"
if (SOURCE_FILE.test(path)) return "source-file"
return ""
}
async function stageTextFile(source, destination, options = {}) {
const metadata = await lstat(source).catch(() => null)
if (!metadata?.isFile() || metadata.size > MAX_UPLOAD_FILE_BYTES) return false
const handle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW).catch(() => null)
if (!handle) return false
const openedMetadata = await handle.stat()
const contents = openedMetadata.isFile() && openedMetadata.size <= MAX_UPLOAD_FILE_BYTES ? await handle.readFile() : null
await handle.close()
if (!contents || contents.includes(0) || !isUtf8(contents)) return false
const text = redact(options.compactNativeInput ? compactNativeInput(contents.toString("utf8")) : sanitizeText(contents.toString("utf8")))
assertNoRuntimeSourcePaths(text, runtimeSourceRoots, "Runtime source paths must never be persisted in artifact uploads.")
if (RUNTIME_SOURCE_CONTENT.test(text)) throw new Error("Prepared runtime plugin source contents must never be staged for artifact upload.")
await mkdir(resolve(destination, ".."), { recursive: true })
await writeFile(destination, text)
return true
}
function record(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {}
}
function declarations(request) {
return (Array.isArray(record(request).artifacts?.declarations) ? record(request).artifacts.declarations : [])
.flatMap((declaration) => {
const entry = record(declaration)
return typeof entry.name === "string" && entry.name.trim()
? [{ name: entry.name.trim(), type: typeof entry.type === "string" ? entry.type.trim() : "" }]
: []
})
}
function declaredArtifactPaths(result, allowed) {
const paths = new Set()
const visit = (value) => {
if (Array.isArray(value)) return value.forEach(visit)
const entry = record(value)
if (!Object.keys(entry).length) return
const artifact = record(entry.artifact)
const path = safeRelativeArtifactPath(artifact.path)
const declared = allowed.some((candidate) => candidate.name === entry.name && (!candidate.type || candidate.type === entry.type))
if (path && declared) paths.add(path)
Object.values(entry).forEach(visit)
}
visit(result)
return [...paths].sort()
}
async function exclusions(root, declaredPaths) {
const counts = new Map()
const count = (category) => counts.set(category, (counts.get(category) || 0) + 1)
const visit = async (directory) => {
const entries = await readdir(directory, { withFileTypes: true }).catch(() => [])
for (const entry of entries) {
const source = join(directory, entry.name)
const path = relative(root, source).replaceAll("\\", "/")
if (entry.isDirectory()) await visit(source)
else if (entry.isFile()) {
const category = sourceCategory(path, source)
if (category) count(category)
else if (!declaredPaths.has(path)) count("undeclared-artifact")
} else count("special-file")
}
}
await visit(root)
return [...counts.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([category, count]) => ({ category, count }))
}
function runtimeProvenance(request) {
const sources = Array.isArray(record(request).runtime_sources) ? record(request).runtime_sources : []
return sources.flatMap((source) => {
const entry = record(source)
if (typeof entry.role !== "string") return []
const provenance = { role: entry.role }
if (record(entry.source).type === "https_zip") {
const sourceInfo = record(entry.source)
provenance.source = Object.fromEntries(["type", "url", "sha256", "archive_root"].flatMap((key) => typeof sourceInfo[key] === "string" ? [[key, sourceInfo[key]]] : []))
} else Object.assign(provenance, ...["repository", "revision", "digest"].flatMap((key) => typeof entry[key] === "string" ? [{ [key]: entry[key] }] : []))
return [provenance]
})
}
async function finalScan(directory) {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const path = join(directory, entry.name)
const relativePath = relative(uploadPath, path).replaceAll("\\", "/")
if (sourceCategory(relativePath, path)) throw new Error("Prepared runtime plugin sources must never be persisted in artifact uploads.")
if (entry.isDirectory()) await finalScan(path)
else if (entry.isFile()) {
const bytes = await readFile(path)
const text = isUtf8(bytes) ? bytes.toString("utf8") : ""
assertNoRuntimeSourcePaths(text, runtimeSourceRoots, "Runtime source paths must never be persisted in artifact uploads.")
if (RUNTIME_SOURCE_CONTENT.test(text)) throw new Error("Prepared runtime plugin source contents must never be persisted in artifact uploads.")
} else throw new Error("Only regular files may be persisted in artifact uploads.")
}
}
const parseJsonOrEmpty = (text) => {
try { return JSON.parse(text) } catch { return {} }
}
const request = parseJsonOrEmpty(await readFile(requestPath, "utf8").catch(() => "{}"))
const resultSource = join(workspace, ".codebox", "agent-task-workflow-result.json")
const result = parseJsonOrEmpty(await readFile(resultSource, "utf8").catch(() => "{}"))
const declaredPaths = new Set(declaredArtifactPaths(result, declarations(request)))
await rm(uploadPath, { recursive: true, force: true })
await mkdir(uploadPath, { recursive: true })
await stageTextFile(requestPath, join(uploadPath, ".codebox", "agent-task-request.json"))
await stageTextFile(resultSource, join(uploadPath, ".codebox", "agent-task-workflow-result.json"))
await stageTextFile(join(workspace, ".codebox", "native-agent-task-input.json"), join(uploadPath, ".codebox", "native-agent-task-input.json"), { compactNativeInput: true })
for (const path of declaredPaths) {
const source = resolve(artifactsPath, path)
if (relative(artifactsPath, source).startsWith("..") || sourceCategory(path, source)) {
throw new Error("Declared reviewer artifacts must not reference source files or private runtime internals.")
}
await stageTextFile(source, join(uploadPath, ".codebox", "agent-task-artifacts", path))
}
await mkdir(join(uploadPath, ".codebox", "agent-task-artifacts"), { recursive: true })
await writeFile(join(uploadPath, ".codebox", "agent-task-artifacts", "runtime-provenance.json"), `${JSON.stringify({ schema: "wp-codebox/agent-task-runtime-provenance/v1", sources: runtimeProvenance(request) }, null, 2)}\n`)
await writeFile(join(uploadPath, ".codebox", "agent-task-artifacts", "exclusions.json"), `${JSON.stringify({ schema: "wp-codebox/agent-task-upload-exclusions/v1", exclusions: await exclusions(artifactsPath, declaredPaths) }, null, 2)}\n`)
await finalScan(uploadPath)