auditor-lambda 0.3.18 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,378 @@
1
+ import { posix } from "node:path";
2
+ import { isAuditExcludedStatus } from "./disposition.js";
3
+ import { graphEdge, normalizeGraphPath, resolveCandidate, } from "./graphPathUtils.js";
4
+ export const BROWSER_EXTENSION_HEURISTIC_NOTE = "Chrome extension manifest and HTML asset references were resolved deterministically from local paths; verify unusual dynamic registration manually.";
5
+ const CHROME_EXTENSION_BACKGROUND_EDGE = "chrome-extension-background-link";
6
+ const CHROME_EXTENSION_CONTENT_SCRIPT_EDGE = "chrome-extension-content-script-link";
7
+ const CHROME_EXTENSION_CONTENT_STYLE_EDGE = "chrome-extension-content-style-link";
8
+ const CHROME_EXTENSION_UI_PAGE_EDGE = "chrome-extension-ui-page-link";
9
+ const CHROME_EXTENSION_WEB_ACCESSIBLE_EDGE = "chrome-extension-web-accessible-resource-link";
10
+ const HTML_RESOURCE_EDGE = "html-resource-link";
11
+ const CHROME_EXTENSION_EDGE_CONFIDENCE = 0.94;
12
+ const HTML_RESOURCE_EDGE_CONFIDENCE = 0.86;
13
+ const EXTENSION_SURFACE_EDGE_KINDS = new Set([
14
+ CHROME_EXTENSION_BACKGROUND_EDGE,
15
+ CHROME_EXTENSION_CONTENT_SCRIPT_EDGE,
16
+ CHROME_EXTENSION_UI_PAGE_EDGE,
17
+ ]);
18
+ const HIGH_RISK_PERMISSION_TOKENS = [
19
+ "<all_urls>",
20
+ "activeTab",
21
+ "debugger",
22
+ "declarativeNetRequest",
23
+ "downloads",
24
+ "downloads.open",
25
+ "nativeMessaging",
26
+ "proxy",
27
+ "scripting",
28
+ "tabs",
29
+ "unlimitedStorage",
30
+ "webNavigation",
31
+ "webRequest",
32
+ ];
33
+ function isRecord(value) {
34
+ return value !== null && typeof value === "object" && !Array.isArray(value);
35
+ }
36
+ function asString(value) {
37
+ return typeof value === "string" && value.trim().length > 0
38
+ ? value.trim()
39
+ : undefined;
40
+ }
41
+ function asStringArray(value) {
42
+ return Array.isArray(value)
43
+ ? value
44
+ .map((item) => asString(item))
45
+ .filter((item) => item !== undefined)
46
+ : [];
47
+ }
48
+ function parseJsonObject(content) {
49
+ try {
50
+ const parsed = JSON.parse(content);
51
+ return isRecord(parsed) ? parsed : undefined;
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ export function isBrowserExtensionManifestPath(path) {
58
+ return posix.basename(normalizeGraphPath(path)).toLowerCase() === "manifest.json";
59
+ }
60
+ function isBrowserExtensionManifest(value) {
61
+ return (typeof value.manifest_version === "number" &&
62
+ (isRecord(value.background) ||
63
+ Array.isArray(value.content_scripts) ||
64
+ isRecord(value.action) ||
65
+ isRecord(value.browser_action) ||
66
+ isRecord(value.page_action) ||
67
+ isRecord(value.side_panel) ||
68
+ isRecord(value.options_ui) ||
69
+ typeof value.options_page === "string" ||
70
+ typeof value.devtools_page === "string" ||
71
+ isRecord(value.chrome_url_overrides) ||
72
+ Array.isArray(value.web_accessible_resources)));
73
+ }
74
+ function localPathCandidate(specifier) {
75
+ const withoutQuery = specifier.trim().split(/[?#]/, 1)[0]?.trim() ?? "";
76
+ if (withoutQuery.length === 0 ||
77
+ withoutQuery.startsWith("<") ||
78
+ withoutQuery.includes("*") ||
79
+ /^[a-z][a-z0-9+.-]*:/i.test(withoutQuery) ||
80
+ withoutQuery.startsWith("//")) {
81
+ return undefined;
82
+ }
83
+ return normalizeGraphPath(withoutQuery).replace(/^\/+/, "");
84
+ }
85
+ function resolveLocalReference(fromPath, specifier, pathLookup) {
86
+ const local = localPathCandidate(specifier);
87
+ if (!local) {
88
+ return undefined;
89
+ }
90
+ const isRootRelative = specifier.trim().startsWith("/");
91
+ const baseDir = posix.dirname(normalizeGraphPath(fromPath));
92
+ const candidate = isRootRelative || baseDir === "." ? local : posix.join(baseDir, local);
93
+ return resolveCandidate(candidate, pathLookup);
94
+ }
95
+ function addManifestReference(edges, params) {
96
+ const target = resolveLocalReference(params.fromPath, params.specifier, params.pathLookup);
97
+ if (!target) {
98
+ return;
99
+ }
100
+ edges.push(graphEdge({
101
+ from: params.fromPath,
102
+ to: target,
103
+ kind: params.kind,
104
+ confidence: CHROME_EXTENSION_EDGE_CONFIDENCE,
105
+ reason: `Chrome extension manifest field '${params.field}' references '${params.specifier}'.`,
106
+ }));
107
+ }
108
+ function collectExtensionUiPageReferences(manifest) {
109
+ const entries = [];
110
+ for (const objectField of ["action", "browser_action", "page_action"]) {
111
+ const value = manifest[objectField];
112
+ if (isRecord(value)) {
113
+ const popup = asString(value.default_popup);
114
+ if (popup)
115
+ entries.push({ field: `${objectField}.default_popup`, specifier: popup });
116
+ }
117
+ }
118
+ const sidePanel = manifest.side_panel;
119
+ if (isRecord(sidePanel)) {
120
+ const path = asString(sidePanel.default_path);
121
+ if (path)
122
+ entries.push({ field: "side_panel.default_path", specifier: path });
123
+ }
124
+ const optionsPage = asString(manifest.options_page);
125
+ if (optionsPage)
126
+ entries.push({ field: "options_page", specifier: optionsPage });
127
+ const optionsUi = manifest.options_ui;
128
+ if (isRecord(optionsUi)) {
129
+ const page = asString(optionsUi.page);
130
+ if (page)
131
+ entries.push({ field: "options_ui.page", specifier: page });
132
+ }
133
+ const devtoolsPage = asString(manifest.devtools_page);
134
+ if (devtoolsPage)
135
+ entries.push({ field: "devtools_page", specifier: devtoolsPage });
136
+ const overrides = manifest.chrome_url_overrides;
137
+ if (isRecord(overrides)) {
138
+ for (const [key, value] of Object.entries(overrides)) {
139
+ const page = asString(value);
140
+ if (page)
141
+ entries.push({ field: `chrome_url_overrides.${key}`, specifier: page });
142
+ }
143
+ }
144
+ const sandbox = manifest.sandbox;
145
+ if (isRecord(sandbox)) {
146
+ asStringArray(sandbox.pages).forEach((page, index) => entries.push({ field: `sandbox.pages.${index}`, specifier: page }));
147
+ }
148
+ return entries;
149
+ }
150
+ function collectWebAccessibleReferences(manifest) {
151
+ const entries = [];
152
+ const resources = manifest.web_accessible_resources;
153
+ if (!Array.isArray(resources)) {
154
+ return entries;
155
+ }
156
+ resources.forEach((item, index) => {
157
+ if (typeof item === "string") {
158
+ entries.push({
159
+ field: `web_accessible_resources.${index}`,
160
+ specifier: item,
161
+ });
162
+ return;
163
+ }
164
+ if (!isRecord(item)) {
165
+ return;
166
+ }
167
+ asStringArray(item.resources).forEach((resource, resourceIndex) => entries.push({
168
+ field: `web_accessible_resources.${index}.resources.${resourceIndex}`,
169
+ specifier: resource,
170
+ }));
171
+ });
172
+ return entries;
173
+ }
174
+ export function extractChromeExtensionManifestEdges(fromPath, content, pathLookup) {
175
+ if (!isBrowserExtensionManifestPath(fromPath)) {
176
+ return [];
177
+ }
178
+ const manifest = parseJsonObject(content);
179
+ if (!manifest || !isBrowserExtensionManifest(manifest)) {
180
+ return [];
181
+ }
182
+ const edges = [];
183
+ const background = manifest.background;
184
+ if (isRecord(background)) {
185
+ const serviceWorker = asString(background.service_worker);
186
+ if (serviceWorker) {
187
+ addManifestReference(edges, {
188
+ fromPath,
189
+ field: "background.service_worker",
190
+ kind: CHROME_EXTENSION_BACKGROUND_EDGE,
191
+ specifier: serviceWorker,
192
+ pathLookup,
193
+ });
194
+ }
195
+ asStringArray(background.scripts).forEach((script, index) => addManifestReference(edges, {
196
+ fromPath,
197
+ field: `background.scripts.${index}`,
198
+ kind: CHROME_EXTENSION_BACKGROUND_EDGE,
199
+ specifier: script,
200
+ pathLookup,
201
+ }));
202
+ }
203
+ const contentScripts = manifest.content_scripts;
204
+ if (Array.isArray(contentScripts)) {
205
+ contentScripts.forEach((item, index) => {
206
+ if (!isRecord(item)) {
207
+ return;
208
+ }
209
+ asStringArray(item.js).forEach((script, scriptIndex) => addManifestReference(edges, {
210
+ fromPath,
211
+ field: `content_scripts.${index}.js.${scriptIndex}`,
212
+ kind: CHROME_EXTENSION_CONTENT_SCRIPT_EDGE,
213
+ specifier: script,
214
+ pathLookup,
215
+ }));
216
+ asStringArray(item.css).forEach((style, styleIndex) => addManifestReference(edges, {
217
+ fromPath,
218
+ field: `content_scripts.${index}.css.${styleIndex}`,
219
+ kind: CHROME_EXTENSION_CONTENT_STYLE_EDGE,
220
+ specifier: style,
221
+ pathLookup,
222
+ }));
223
+ });
224
+ }
225
+ for (const { field, specifier } of collectExtensionUiPageReferences(manifest)) {
226
+ addManifestReference(edges, {
227
+ fromPath,
228
+ field,
229
+ kind: CHROME_EXTENSION_UI_PAGE_EDGE,
230
+ specifier,
231
+ pathLookup,
232
+ });
233
+ }
234
+ for (const { field, specifier } of collectWebAccessibleReferences(manifest)) {
235
+ addManifestReference(edges, {
236
+ fromPath,
237
+ field,
238
+ kind: CHROME_EXTENSION_WEB_ACCESSIBLE_EDGE,
239
+ specifier,
240
+ pathLookup,
241
+ });
242
+ }
243
+ return edges;
244
+ }
245
+ function extractHtmlAttributeReferences(content, elementName, attributeName) {
246
+ const unquotedAttributeValue = "[^\\s\"'<>`]+";
247
+ const pattern = new RegExp(`<${elementName}\\b[^>]*\\b${attributeName}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|(${unquotedAttributeValue}))`, "gi");
248
+ const values = [];
249
+ for (const match of content.matchAll(pattern)) {
250
+ const value = match[1] ?? match[2] ?? match[3];
251
+ if (value)
252
+ values.push(value);
253
+ }
254
+ return values;
255
+ }
256
+ export function extractHtmlResourceEdges(fromPath, content, pathLookup) {
257
+ const normalized = normalizeGraphPath(fromPath).toLowerCase();
258
+ if (!normalized.endsWith(".html") && !normalized.endsWith(".htm")) {
259
+ return [];
260
+ }
261
+ const references = [
262
+ ...extractHtmlAttributeReferences(content, "script", "src"),
263
+ ...extractHtmlAttributeReferences(content, "link", "href"),
264
+ ];
265
+ const edges = [];
266
+ for (const specifier of references) {
267
+ const target = resolveLocalReference(fromPath, specifier, pathLookup);
268
+ if (!target) {
269
+ continue;
270
+ }
271
+ edges.push(graphEdge({
272
+ from: fromPath,
273
+ to: target,
274
+ kind: HTML_RESOURCE_EDGE,
275
+ confidence: HTML_RESOURCE_EDGE_CONFIDENCE,
276
+ reason: `HTML resource attribute references '${specifier}'.`,
277
+ }));
278
+ }
279
+ return edges;
280
+ }
281
+ export function hasBrowserExtensionManifestFile(repoManifest) {
282
+ return repoManifest.files.some((file) => normalizeGraphPath(file.path).toLowerCase() === "manifest.json");
283
+ }
284
+ export function deriveBrowserExtensionLensesForPath(path) {
285
+ const normalized = normalizeGraphPath(path).toLowerCase();
286
+ if (normalized === "manifest.json") {
287
+ return ["security", "correctness", "config_deployment", "operability"];
288
+ }
289
+ if (normalized.startsWith("service/") ||
290
+ normalized.startsWith("background/") ||
291
+ normalized.includes("service-worker") ||
292
+ normalized.includes("background")) {
293
+ return ["security", "correctness", "reliability", "observability"];
294
+ }
295
+ if (normalized.startsWith("content/") || normalized.includes("content-script")) {
296
+ return ["security", "correctness", "reliability"];
297
+ }
298
+ if (normalized.includes("popup") ||
299
+ normalized.includes("sidebar") ||
300
+ normalized.includes("side-panel") ||
301
+ normalized.includes("panel") ||
302
+ normalized.endsWith(".html")) {
303
+ return ["security", "correctness", "maintainability"];
304
+ }
305
+ if (normalized.includes("worker")) {
306
+ return ["correctness", "reliability", "performance"];
307
+ }
308
+ return [];
309
+ }
310
+ export function inferBrowserExtensionUnitKind(path) {
311
+ const normalized = normalizeGraphPath(path).toLowerCase();
312
+ if (normalized === "manifest.json")
313
+ return "extension_config";
314
+ if (normalized.startsWith("service/") || normalized.startsWith("background/")) {
315
+ return "extension_background";
316
+ }
317
+ if (normalized.startsWith("content/"))
318
+ return "extension_content";
319
+ if (normalized.includes("worker"))
320
+ return "worker";
321
+ if (normalized.endsWith(".html"))
322
+ return "extension_ui";
323
+ return undefined;
324
+ }
325
+ function isExecutableExtensionSurfaceTarget(path) {
326
+ const normalized = normalizeGraphPath(path).toLowerCase();
327
+ return (normalized.endsWith(".js") ||
328
+ normalized.endsWith(".mjs") ||
329
+ normalized.endsWith(".cjs") ||
330
+ normalized.endsWith(".html") ||
331
+ normalized.endsWith(".htm"));
332
+ }
333
+ export function buildBrowserExtensionSurfacesFromGraph(graphBundle, disposition) {
334
+ const references = Array.isArray(graphBundle?.graphs.references)
335
+ ? graphBundle.graphs.references
336
+ : [];
337
+ const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
338
+ const surfaces = [];
339
+ const seen = new Set();
340
+ for (const edge of references) {
341
+ if (!edge.kind || !EXTENSION_SURFACE_EDGE_KINDS.has(edge.kind)) {
342
+ continue;
343
+ }
344
+ if (!isExecutableExtensionSurfaceTarget(edge.to)) {
345
+ continue;
346
+ }
347
+ const status = dispositionMap.get(edge.to);
348
+ if (status && isAuditExcludedStatus(status)) {
349
+ continue;
350
+ }
351
+ const kind = edge.kind === CHROME_EXTENSION_BACKGROUND_EDGE ? "background" : "interface";
352
+ const key = `${kind}:${edge.to}`;
353
+ if (seen.has(key)) {
354
+ continue;
355
+ }
356
+ seen.add(key);
357
+ surfaces.push({
358
+ id: `surface:${edge.to}`,
359
+ kind,
360
+ entrypoint: edge.to,
361
+ exposure: edge.kind === CHROME_EXTENSION_CONTENT_SCRIPT_EDGE ? "network" : "local",
362
+ notes: [BROWSER_EXTENSION_HEURISTIC_NOTE],
363
+ });
364
+ }
365
+ return surfaces.sort((a, b) => a.entrypoint.localeCompare(b.entrypoint) || a.kind.localeCompare(b.kind));
366
+ }
367
+ export function chromeExtensionRiskSignalsForManifest(content) {
368
+ const manifest = parseJsonObject(content);
369
+ if (!manifest || !isBrowserExtensionManifest(manifest)) {
370
+ return [];
371
+ }
372
+ const permissions = [
373
+ ...asStringArray(manifest.permissions),
374
+ ...asStringArray(manifest.optional_permissions),
375
+ ...asStringArray(manifest.host_permissions),
376
+ ];
377
+ return HIGH_RISK_PERMISSION_TOKENS.filter((token) => permissions.some((permission) => permission === token));
378
+ }
@@ -1,4 +1,4 @@
1
- import { isNodeModulesOrGit, isBuildOutput, isVendorPath, isBinaryArtifact, isLicensePath, isLockfilePath, isLogPath, isDocPath, isAuditArtifactPath, isGeneratedTestArtifactPath, isGeneratedInstallArtifactPath, isExamplesOrFixturesPath, normalizeExtractorPath, } from "./pathPatterns.js";
1
+ import { isNodeModulesOrGit, isBuildOutput, isVendorPath, isBinaryArtifact, isLicensePath, isLockfilePath, isLogPath, isDocPath, isGeneratedPath, isAuditArtifactPath, isGeneratedTestArtifactPath, isGeneratedInstallArtifactPath, isExamplesOrFixturesPath, normalizeExtractorPath, } from "./pathPatterns.js";
2
2
  function inferDisposition(path) {
3
3
  const normalized = normalizeExtractorPath(path);
4
4
  if (isNodeModulesOrGit(normalized)) {
@@ -33,6 +33,13 @@ function inferDisposition(path) {
33
33
  reason: "Generated audit artifact.",
34
34
  };
35
35
  }
36
+ if (isGeneratedPath(normalized)) {
37
+ return {
38
+ path,
39
+ status: "generated",
40
+ reason: "Generated artifact path.",
41
+ };
42
+ }
36
43
  if (isGeneratedTestArtifactPath(normalized)) {
37
44
  return {
38
45
  path,
@@ -11,6 +11,7 @@ const DEFAULT_IGNORES = [
11
11
  ".turbo",
12
12
  ".artifacts",
13
13
  ".audit-artifacts",
14
+ ".audit-code/install",
14
15
  ".agent",
15
16
  ".claude",
16
17
  "coverage",
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { posix } from "node:path";
4
4
  import { isAuditExcludedStatus } from "./disposition.js";
5
+ import { extractChromeExtensionManifestEdges, extractHtmlResourceEdges, } from "./browserExtension.js";
5
6
  import { extractCargoWorkspaceMemberEdges, extractGoWorkspaceModuleEdges, extractMavenModuleEdges, extractPackageEntrypointEdges, extractPackageScriptEdges, extractPyprojectTestpathLinks, extractTypescriptProjectReferenceEdges, extractWorkspacePackageEdges, extractYamlPathReferenceEdges, isCargoManifestPath, isGoWorkspaceManifestPath, isMavenPomPath, isPyprojectPath, } from "./graphManifestEdges.js";
6
7
  import { graphEdge, graphLookupKey, normalizeGraphPath, resolveCandidate, } from "./graphPathUtils.js";
7
8
  import { isTestPath, normalizeExtractorPath } from "./pathPatterns.js";
@@ -10,6 +11,7 @@ const SOURCE_LANGUAGES = new Set([
10
11
  "typescript",
11
12
  "javascript",
12
13
  "json",
14
+ "html",
13
15
  "yaml",
14
16
  "python",
15
17
  "go",
@@ -27,6 +29,8 @@ const SOURCE_EXTENSIONS = [
27
29
  ".mjs",
28
30
  ".cjs",
29
31
  ".json",
32
+ ".html",
33
+ ".htm",
30
34
  ".yml",
31
35
  ".yaml",
32
36
  ".py",
@@ -1309,6 +1313,8 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
1309
1313
  references.push(...extractReferenceEdges(file.path, content, pathLookup));
1310
1314
  references.push(...extractJsonSchemaReferenceEdges(file.path, content, pathLookup));
1311
1315
  references.push(...extractPackageEntrypointEdges(file.path, content, pathLookup));
1316
+ references.push(...extractChromeExtensionManifestEdges(file.path, content, pathLookup));
1317
+ references.push(...extractHtmlResourceEdges(file.path, content, pathLookup));
1312
1318
  references.push(...extractPackageScriptEdges(file.path, content, pathLookup));
1313
1319
  references.push(...extractWorkspacePackageEdges(file.path, content, pathLookup));
1314
1320
  references.push(...extractTypescriptProjectReferenceEdges(file.path, content, pathLookup));
@@ -10,6 +10,16 @@ const BINARY_EXTENSIONS = [
10
10
  ".jpg",
11
11
  ".jpeg",
12
12
  ".gif",
13
+ ".webp",
14
+ ".svg",
15
+ ".ico",
16
+ ".bmp",
17
+ ".avif",
18
+ ".wasm",
19
+ ".woff",
20
+ ".woff2",
21
+ ".ttf",
22
+ ".otf",
13
23
  ".pdf",
14
24
  ".zip",
15
25
  ];
@@ -216,7 +226,11 @@ export function isDeploymentConfigPath(normalized) {
216
226
  return hasToken(normalized, DEPLOYMENT_KEYWORDS) || endsWithAny(normalized, [".yml", ".yaml"]);
217
227
  }
218
228
  export function isGeneratedPath(normalized) {
219
- return isVendorPath(normalized) || hasToken(normalized, ["generated", "autogenerated"]);
229
+ return (isVendorPath(normalized) ||
230
+ normalized.endsWith(".map") ||
231
+ normalized.endsWith(".wasm.mjs") ||
232
+ normalized.endsWith(".wasm.js") ||
233
+ hasToken(normalized, ["generated", "autogenerated"]));
220
234
  }
221
235
  export function isSurfacePath(normalized) {
222
236
  return (hasSegment(normalized, "api") ||
@@ -1,8 +1,11 @@
1
1
  import type { RepoManifest } from "../types.js";
2
2
  import type { FileDisposition } from "../types/disposition.js";
3
+ import type { GraphBundle } from "../types/graph.js";
3
4
  import type { SurfaceManifest } from "../types/surfaces.js";
4
5
  /**
5
6
  * Detects likely execution surfaces from file paths using the shared extractor
6
7
  * heuristics, primarily to seed later audit planning.
7
8
  */
8
- export declare function buildSurfaceManifest(repoManifest: RepoManifest, disposition?: FileDisposition): SurfaceManifest;
9
+ export declare function buildSurfaceManifest(repoManifest: RepoManifest, disposition?: FileDisposition, options?: {
10
+ graphBundle?: GraphBundle;
11
+ }): SurfaceManifest;
@@ -1,3 +1,4 @@
1
+ import { buildBrowserExtensionSurfacesFromGraph } from "./browserExtension.js";
1
2
  import { isAuditExcludedStatus } from "./disposition.js";
2
3
  import { EXTRACTOR_HEURISTIC_NOTE, isBackgroundSurfacePath, isNetworkSurfacePath, isSurfacePath, normalizeExtractorPath, } from "./pathPatterns.js";
3
4
  function methodsForPath(path) {
@@ -11,9 +12,18 @@ function methodsForPath(path) {
11
12
  * Detects likely execution surfaces from file paths using the shared extractor
12
13
  * heuristics, primarily to seed later audit planning.
13
14
  */
14
- export function buildSurfaceManifest(repoManifest, disposition) {
15
+ export function buildSurfaceManifest(repoManifest, disposition, options = {}) {
15
16
  const surfaces = [];
17
+ const seen = new Set();
16
18
  const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
19
+ function addSurface(surface) {
20
+ const key = `${surface.kind}:${surface.entrypoint}`;
21
+ if (seen.has(key)) {
22
+ return;
23
+ }
24
+ seen.add(key);
25
+ surfaces.push(surface);
26
+ }
17
27
  for (const file of repoManifest.files) {
18
28
  const status = dispositionMap.get(file.path);
19
29
  if (status && isAuditExcludedStatus(status)) {
@@ -21,7 +31,7 @@ export function buildSurfaceManifest(repoManifest, disposition) {
21
31
  }
22
32
  const normalized = normalizeExtractorPath(file.path);
23
33
  if (isSurfacePath(normalized)) {
24
- surfaces.push({
34
+ addSurface({
25
35
  id: `surface:${file.path}`,
26
36
  kind: isBackgroundSurfacePath(normalized) ? "background" : "interface",
27
37
  entrypoint: file.path,
@@ -31,5 +41,8 @@ export function buildSurfaceManifest(repoManifest, disposition) {
31
41
  });
32
42
  }
33
43
  }
44
+ for (const surface of buildBrowserExtensionSurfacesFromGraph(options.graphBundle, disposition)) {
45
+ addSurface(surface);
46
+ }
34
47
  return { surfaces };
35
48
  }
@@ -10,6 +10,8 @@ const findingSchemaPath = join(packageRoot, "schemas", "finding.schema.json");
10
10
  const CURRENT_TASK_FILENAME = "current-task.json";
11
11
  const CURRENT_PROMPT_FILENAME = "current-prompt.md";
12
12
  const CURRENT_TASKS_FILENAME = "current-tasks.json";
13
+ const CURRENT_SINGLE_TASK_FILENAME = "current-single-task.json";
14
+ const CURRENT_SINGLE_TASK_PROMPT_FILENAME = "current-single-task-prompt.md";
13
15
  const CURRENT_SCHEMA_FILENAME = "audit-result.schema.json";
14
16
  const CURRENT_RESULTS_SCHEMA_FILENAME = "audit-results.schema.json";
15
17
  const CURRENT_FINDING_SCHEMA_FILENAME = "finding.schema.json";
@@ -63,6 +65,49 @@ async function writeDispatchSchemaFiles(artifactsDir) {
63
65
  await writeFile(join(dispatchDir, CURRENT_RESULTS_SCHEMA_FILENAME), await readFile(auditResultsSchemaPath, "utf8"), "utf8");
64
66
  await writeFile(join(dispatchDir, CURRENT_FINDING_SCHEMA_FILENAME), await readFile(findingSchemaPath, "utf8"), "utf8");
65
67
  }
68
+ function renderSingleTaskFallbackPrompt(task, auditTask) {
69
+ const commandArgv = JSON.stringify(task.worker_command);
70
+ const lineCounts = auditTask.file_paths
71
+ .map((path) => `- ${path}: ${auditTask.file_line_counts?.[path] ?? 0} lines`)
72
+ .join("\n");
73
+ return [
74
+ "# audit-code single-task fallback",
75
+ "",
76
+ "Use this file only when the conversation host cannot dispatch subagents.",
77
+ "This prompt is generated deterministically from the first pending task.",
78
+ "",
79
+ `run_id: ${task.run_id}`,
80
+ `task_id: ${auditTask.task_id}`,
81
+ `unit_id: ${auditTask.unit_id}`,
82
+ `pass_id: ${auditTask.pass_id}`,
83
+ `lens: ${auditTask.lens}`,
84
+ `rationale: ${auditTask.rationale}`,
85
+ "",
86
+ "Assigned files and line counts:",
87
+ lineCounts,
88
+ "",
89
+ "Instructions:",
90
+ "1. Read only the assigned files above.",
91
+ "2. Produce exactly one AuditResult object for task_id above, wrapped in a JSON array.",
92
+ "3. Write that JSON array to audit_results_path.",
93
+ "4. Run worker_command exactly, then stop without checking audit state or reading a report.",
94
+ "",
95
+ `audit_results_path: ${task.audit_results_path}`,
96
+ `worker_command: ${commandArgv}`,
97
+ "",
98
+ ].join("\n");
99
+ }
100
+ async function writeSingleTaskFallbackFiles(artifactsDir, task, currentTasks) {
101
+ if (task.preferred_executor !== "agent" ||
102
+ !task.audit_results_path ||
103
+ !currentTasks ||
104
+ currentTasks.length === 0) {
105
+ return;
106
+ }
107
+ const firstTask = currentTasks[0];
108
+ await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_SINGLE_TASK_FILENAME), firstTask);
109
+ await writeFile(join(artifactsDir, "dispatch", CURRENT_SINGLE_TASK_PROMPT_FILENAME), renderSingleTaskFallbackPrompt(task, firstTask), "utf8");
110
+ }
66
111
  export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, currentTasks, options = {}) {
67
112
  await mkdir(paths.runDir, { recursive: true });
68
113
  await writeJsonFile(paths.taskPath, task);
@@ -78,6 +123,7 @@ export async function writeWorkerTaskFiles(task, prompt, paths, artifactsDir, cu
78
123
  await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASK_FILENAME), task);
79
124
  await writeFile(join(artifactsDir, "dispatch", CURRENT_PROMPT_FILENAME), prompt, "utf8");
80
125
  await writeJsonFile(join(artifactsDir, "dispatch", CURRENT_TASKS_FILENAME), currentTasks ?? []);
126
+ await writeSingleTaskFallbackFiles(artifactsDir, task, currentTasks);
81
127
  await writeDispatchSchemaFiles(artifactsDir);
82
128
  }
83
129
  export async function writeDispatchBatchFiles(artifactsDir, runs, currentTasks) {
@@ -121,6 +167,8 @@ export async function clearDispatchFiles(artifactsDir) {
121
167
  CURRENT_TASK_FILENAME,
122
168
  CURRENT_PROMPT_FILENAME,
123
169
  CURRENT_TASKS_FILENAME,
170
+ CURRENT_SINGLE_TASK_FILENAME,
171
+ CURRENT_SINGLE_TASK_PROMPT_FILENAME,
124
172
  CURRENT_SCHEMA_FILENAME,
125
173
  CURRENT_RESULTS_SCHEMA_FILENAME,
126
174
  CURRENT_FINDING_SCHEMA_FILENAME,
@@ -1,3 +1,4 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import { join } from "node:path";
2
3
  import { isAuditExcludedStatus } from "../extractors/disposition.js";
3
4
  import { resolveNodeTool, runFirstAvailableCommand, } from "./localCommands.js";
@@ -5,6 +6,35 @@ function tryRunConfiguredFormatter(root, candidates) {
5
6
  const result = runFirstAvailableCommand(root, candidates);
6
7
  return result !== null && !result.error && result.exitCode === 0;
7
8
  }
9
+ const PRETTIER_CONFIG_FILES = [
10
+ ".prettierrc",
11
+ ".prettierrc.json",
12
+ ".prettierrc.yml",
13
+ ".prettierrc.yaml",
14
+ ".prettierrc.json5",
15
+ ".prettierrc.js",
16
+ ".prettierrc.cjs",
17
+ ".prettierrc.mjs",
18
+ "prettier.config.js",
19
+ "prettier.config.cjs",
20
+ "prettier.config.mjs",
21
+ ];
22
+ function hasPrettierConfig(root) {
23
+ if (PRETTIER_CONFIG_FILES.some((file) => existsSync(join(root, file)))) {
24
+ return true;
25
+ }
26
+ const packageJsonPath = join(root, "package.json");
27
+ if (!existsSync(packageJsonPath)) {
28
+ return false;
29
+ }
30
+ try {
31
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
32
+ return packageJson.prettier !== undefined;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
8
38
  export function runAutoFixExecutor(bundle, root) {
9
39
  if (!bundle.file_disposition) {
10
40
  throw new Error("Cannot run auto fix executor without file_disposition");
@@ -20,16 +50,17 @@ export function runAutoFixExecutor(bundle, root) {
20
50
  }
21
51
  const executedTools = [];
22
52
  // JS, TS, HTML, CSS, JSON, YAML, MD
23
- if (extensions.has("ts") ||
24
- extensions.has("js") ||
25
- extensions.has("tsx") ||
26
- extensions.has("jsx") ||
27
- extensions.has("html") ||
28
- extensions.has("css") ||
29
- extensions.has("json") ||
30
- extensions.has("yml") ||
31
- extensions.has("yaml") ||
32
- extensions.has("md")) {
53
+ if (hasPrettierConfig(root) &&
54
+ (extensions.has("ts") ||
55
+ extensions.has("js") ||
56
+ extensions.has("tsx") ||
57
+ extensions.has("jsx") ||
58
+ extensions.has("html") ||
59
+ extensions.has("css") ||
60
+ extensions.has("json") ||
61
+ extensions.has("yml") ||
62
+ extensions.has("yaml") ||
63
+ extensions.has("md"))) {
33
64
  if (tryRunConfiguredFormatter(root, [
34
65
  ...resolveNodeTool(root, join("node_modules", "prettier", "bin", "prettier.cjs"), ["--write", "."], "prettier --write ."),
35
66
  { command: "prettier", args: ["--write", "."], display: "prettier --write ." },