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.
package/README.md CHANGED
@@ -30,8 +30,9 @@ npm install -g auditor-lambda
30
30
 
31
31
  That makes `audit-code` available on `PATH`. During package install, the package
32
32
  also writes user-level command/skill assets for hosts we can seed safely, including
33
- the Claude command file, the global Codex skill bundle, and the global OpenCode
34
- slash command entry in `~/.config/opencode/opencode.json`.
33
+ the Claude command file, the global Codex skill bundle with `audit-code` display
34
+ metadata, and the global OpenCode slash command entry in
35
+ `~/.config/opencode/opencode.json`.
35
36
 
36
37
  After that, invoke `/audit-code` in a supported host. The prompt self-bootstraps
37
38
  the current repository by running:
@@ -533,8 +533,59 @@ function renderCodexAutomationRecipe() {
533
533
  ].join('\n');
534
534
  }
535
535
 
536
+ const OPENCODE_AUDIT_EDIT_PERMISSION = {
537
+ '*': 'ask',
538
+ '.audit-code/**': 'allow',
539
+ '.audit-artifacts/**': 'allow',
540
+ 'audit-report.md': 'allow',
541
+ };
542
+
543
+ const OPENCODE_AUDIT_BASH_PERMISSION = {
544
+ '*': 'ask',
545
+ 'audit-code run-to-completion*': 'deny',
546
+ 'audit-code synthesize*': 'deny',
547
+ 'audit-code cleanup*': 'deny',
548
+ 'audit-code requeue*': 'deny',
549
+ 'audit-code ingest-results*': 'deny',
550
+ '*audit-code.mjs* run-to-completion*': 'deny',
551
+ '*audit-code.mjs* synthesize*': 'deny',
552
+ '*audit-code.mjs* cleanup*': 'deny',
553
+ '*audit-code.mjs* requeue*': 'deny',
554
+ '*audit-code.mjs* ingest-results*': 'deny',
555
+ 'audit-code': 'allow',
556
+ 'audit-code ensure*': 'allow',
557
+ 'audit-code prepare-dispatch*': 'allow',
558
+ 'audit-code submit-packet*': 'allow',
559
+ 'audit-code merge-and-ingest*': 'allow',
560
+ 'audit-code validate*': 'allow',
561
+ '*audit-code.mjs': 'allow',
562
+ '*audit-code.mjs* ensure*': 'allow',
563
+ '*audit-code.mjs* prepare-dispatch*': 'allow',
564
+ '*audit-code.mjs* submit-packet*': 'allow',
565
+ '*audit-code.mjs* merge-and-ingest*': 'allow',
566
+ '*audit-code.mjs* worker-run*': 'allow',
567
+ '*audit-code.mjs* validate*': 'allow',
568
+ 'node* .audit-code/install/run-mcp-server.mjs*': 'allow',
569
+ 'node* ./.audit-code/install/run-mcp-server.mjs*': 'allow',
570
+ 'git status*': 'allow',
571
+ 'git diff*': 'allow',
572
+ 'grep *': 'allow',
573
+ 'rm *': 'deny',
574
+ };
575
+
576
+ function renderOpenCodePermissionConfig() {
577
+ return {
578
+ read: 'allow',
579
+ glob: 'allow',
580
+ grep: 'allow',
581
+ edit: { ...OPENCODE_AUDIT_EDIT_PERMISSION },
582
+ bash: { ...OPENCODE_AUDIT_BASH_PERMISSION },
583
+ };
584
+ }
585
+
536
586
  function renderOpenCodeProjectConfig(root, promptBody) {
537
587
  const launcher = replaceBackslashes(toRepoRelativePath(root, join(root, '.audit-code', 'install', MCP_LAUNCHER_FILENAME)));
588
+ const auditPermission = renderOpenCodePermissionConfig();
538
589
  return {
539
590
  $schema: 'https://opencode.ai/config.json',
540
591
  command: {
@@ -553,19 +604,7 @@ function renderOpenCodeProjectConfig(root, promptBody) {
553
604
  timeout: 10000,
554
605
  },
555
606
  },
556
- permission: {
557
- read: 'allow',
558
- glob: 'allow',
559
- grep: 'allow',
560
- edit: 'ask',
561
- bash: {
562
- '*': 'ask',
563
- 'git status*': 'allow',
564
- 'git diff*': 'allow',
565
- 'grep *': 'allow',
566
- 'rm *': 'deny',
567
- },
568
- },
607
+ permission: auditPermission,
569
608
  agent: {
570
609
  auditor: {
571
610
  description:
@@ -574,14 +613,8 @@ function renderOpenCodeProjectConfig(root, promptBody) {
574
613
  'auditor*': true,
575
614
  },
576
615
  permission: {
577
- edit: 'ask',
578
- bash: {
579
- '*': 'ask',
580
- 'git status*': 'allow',
581
- 'git diff*': 'allow',
582
- 'grep *': 'allow',
583
- 'rm *': 'deny',
584
- },
616
+ edit: { ...OPENCODE_AUDIT_EDIT_PERMISSION },
617
+ bash: { ...OPENCODE_AUDIT_BASH_PERMISSION },
585
618
  question: 'allow',
586
619
  },
587
620
  },
@@ -607,6 +640,99 @@ function objectValue(value) {
607
640
  : {};
608
641
  }
609
642
 
643
+ function mergeOpenCodePermissionRule(existingRule, generatedRule, managedRules = {}) {
644
+ if (generatedRule && typeof generatedRule === 'object' && !Array.isArray(generatedRule)) {
645
+ const generatedObject = generatedRule;
646
+ const merged = {};
647
+ const existingObject =
648
+ existingRule && typeof existingRule === 'object' && !Array.isArray(existingRule)
649
+ ? existingRule
650
+ : {};
651
+
652
+ if (typeof existingRule === 'string') {
653
+ merged['*'] = existingRule;
654
+ } else {
655
+ merged['*'] = existingObject['*'] ?? generatedObject['*'] ?? 'ask';
656
+ }
657
+
658
+ for (const [key, value] of Object.entries(generatedObject)) {
659
+ if (key !== '*') merged[key] = value;
660
+ }
661
+ for (const [key, value] of Object.entries(existingObject)) {
662
+ if (key !== '*') merged[key] = value;
663
+ }
664
+ for (const [key, value] of Object.entries(managedRules)) {
665
+ merged[key] = value;
666
+ }
667
+
668
+ return merged;
669
+ }
670
+
671
+ return existingRule ?? generatedRule;
672
+ }
673
+
674
+ function mergeOpenCodePermissionConfig(existingPermission, generatedPermission) {
675
+ if (!existingPermission || typeof existingPermission !== 'object' || Array.isArray(existingPermission)) {
676
+ return generatedPermission;
677
+ }
678
+
679
+ return {
680
+ ...generatedPermission,
681
+ ...existingPermission,
682
+ edit: mergeOpenCodePermissionRule(
683
+ existingPermission.edit,
684
+ generatedPermission.edit,
685
+ OPENCODE_AUDIT_EDIT_PERMISSION,
686
+ ),
687
+ bash: mergeOpenCodePermissionRule(
688
+ existingPermission.bash,
689
+ generatedPermission.bash,
690
+ OPENCODE_AUDIT_BASH_PERMISSION,
691
+ ),
692
+ };
693
+ }
694
+
695
+ function assertOpenCodeAuditPermissionConfig(permissionConfig, label) {
696
+ const edit = permissionConfig?.edit;
697
+ const bash = permissionConfig?.bash;
698
+ if (!edit || typeof edit !== 'object' || Array.isArray(edit)) {
699
+ throw new Error(`OpenCode ${label}.edit must allow audit-owned file paths. Run "audit-code install --host opencode".`);
700
+ }
701
+ for (const pattern of ['.audit-code/**', '.audit-artifacts/**', 'audit-report.md']) {
702
+ if (edit[pattern] !== 'allow') {
703
+ throw new Error(`OpenCode ${label}.edit must allow ${pattern}. Run "audit-code install --host opencode".`);
704
+ }
705
+ }
706
+ if (!bash || typeof bash !== 'object' || Array.isArray(bash)) {
707
+ throw new Error(`OpenCode ${label}.bash must allow audit-code commands. Run "audit-code install --host opencode".`);
708
+ }
709
+ for (const pattern of [
710
+ 'audit-code',
711
+ 'audit-code ensure*',
712
+ 'audit-code prepare-dispatch*',
713
+ 'audit-code submit-packet*',
714
+ 'audit-code merge-and-ingest*',
715
+ '*audit-code.mjs',
716
+ '*audit-code.mjs* submit-packet*',
717
+ '*audit-code.mjs* merge-and-ingest*',
718
+ 'node* .audit-code/install/run-mcp-server.mjs*',
719
+ ]) {
720
+ if (bash[pattern] !== 'allow') {
721
+ throw new Error(`OpenCode ${label}.bash must allow ${pattern}. Run "audit-code install --host opencode".`);
722
+ }
723
+ }
724
+ for (const pattern of [
725
+ 'audit-code run-to-completion*',
726
+ 'audit-code synthesize*',
727
+ '*audit-code.mjs* run-to-completion*',
728
+ '*audit-code.mjs* synthesize*',
729
+ ]) {
730
+ if (bash[pattern] !== 'deny') {
731
+ throw new Error(`OpenCode ${label}.bash must deny ${pattern}. Run "audit-code install --host opencode".`);
732
+ }
733
+ }
734
+ }
735
+
610
736
  function buildMergedOpenCodeProjectConfig(existing, root, promptBody) {
611
737
  const generated = renderOpenCodeProjectConfig(root, promptBody);
612
738
  return {
@@ -620,13 +746,17 @@ function buildMergedOpenCodeProjectConfig(existing, root, promptBody) {
620
746
  ...objectValue(existing.mcp),
621
747
  auditor: generated.mcp.auditor,
622
748
  },
623
- permission:
624
- existing.permission && typeof existing.permission === 'object' && !Array.isArray(existing.permission)
625
- ? existing.permission
626
- : generated.permission,
749
+ permission: mergeOpenCodePermissionConfig(existing.permission, generated.permission),
627
750
  agent: {
628
751
  ...objectValue(existing.agent),
629
- auditor: generated.agent.auditor,
752
+ auditor: {
753
+ ...objectValue(objectValue(existing.agent).auditor),
754
+ ...generated.agent.auditor,
755
+ permission: mergeOpenCodePermissionConfig(
756
+ objectValue(objectValue(existing.agent).auditor).permission,
757
+ generated.agent.auditor.permission,
758
+ ),
759
+ },
630
760
  },
631
761
  };
632
762
  }
@@ -1822,8 +1952,10 @@ async function verifyInstalledBootstrap(argv) {
1822
1952
  if (commandConfig.template !== sourceBody.trimStart()) {
1823
1953
  throw new Error('OpenCode config command["audit-code"].template is out of sync with the source prompt. Run "audit-code install".');
1824
1954
  }
1955
+ assertOpenCodeAuditPermissionConfig(config?.permission, 'permission');
1956
+ assertOpenCodeAuditPermissionConfig(config?.agent?.auditor?.permission, 'agent.auditor.permission');
1825
1957
  return {
1826
- summary: 'OpenCode project config has MCP server and /audit-code slash command.',
1958
+ summary: 'OpenCode project config has MCP server, /audit-code slash command, and audit permissions.',
1827
1959
  path: assetPaths.opencodeConfigPath,
1828
1960
  };
1829
1961
  });
@@ -2001,6 +2133,12 @@ async function detectBootstrapRefreshReason(root, host) {
2001
2133
  if (opencodeConfig?.command?.['audit-code']?.template !== sourcePromptBody.trimStart()) {
2002
2134
  return 'stale_host_asset:opencode:config_command';
2003
2135
  }
2136
+ try {
2137
+ assertOpenCodeAuditPermissionConfig(opencodeConfig?.permission, 'permission');
2138
+ assertOpenCodeAuditPermissionConfig(opencodeConfig?.agent?.auditor?.permission, 'agent.auditor.permission');
2139
+ } catch {
2140
+ return 'stale_host_asset:opencode:permissions';
2141
+ }
2004
2142
  if (await fileExists(join(root, '.opencode', 'commands', 'audit-code.md'))) {
2005
2143
  return 'stale_host_asset:opencode:legacy_command_file';
2006
2144
  }
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { createHash } from "node:crypto";
@@ -218,8 +218,8 @@ async function emitEnvelope(params) {
218
218
  }
219
219
  function buildManualReviewBlocker(providerName) {
220
220
  return providerName === LOCAL_SUBPROCESS_PROVIDER_NAME
221
- ? "Ready for LLM review. Dispatched task files are in .audit-artifacts/dispatch/. " +
222
- "Review the code, write audit results to the specified path, then run the worker_command to continue."
221
+ ? "Ready for LLM semantic review. If the host exposes a callable subagent tool, prepare dispatch and fan out packets. " +
222
+ "If not, use single-task fallback: review only the first pending task, write one AuditResult to the run audit-results path, execute worker_command, then stop."
223
223
  : "Audit blocked: waiting for manual audit results or interactive provider configuration.";
224
224
  }
225
225
  function shouldRunInlineExecutor(selectedExecutor) {
@@ -308,30 +308,6 @@ async function listBatchResultFiles(batchDir) {
308
308
  }
309
309
  return files;
310
310
  }
311
- const PROJECT_SIGNALS = [
312
- "package.json",
313
- "go.mod",
314
- "Cargo.toml",
315
- "pom.xml",
316
- "build.gradle",
317
- "pyproject.toml",
318
- "setup.py",
319
- "setup.cfg",
320
- "Makefile",
321
- "CMakeLists.txt",
322
- ];
323
- async function detectProjectRoot(root) {
324
- for (const signal of PROJECT_SIGNALS) {
325
- try {
326
- await access(join(root, signal));
327
- return signal;
328
- }
329
- catch {
330
- // not found, try next
331
- }
332
- }
333
- return null;
334
- }
335
311
  function buildPendingAuditTasks(bundle) {
336
312
  const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
337
313
  const pendingTasks = (bundle.audit_tasks ?? []).filter((task) => task.status !== "complete" && !completedTaskIds.has(task.task_id));
@@ -716,35 +692,6 @@ async function cmdRunToCompletion(argv) {
716
692
  let pendingBatchAuditResults = batchResultsDir
717
693
  ? await listBatchResultFiles(batchResultsDir)
718
694
  : [];
719
- const earlyBundle = await loadArtifactBundle(artifactsDir);
720
- if (!earlyBundle.unit_manifest) {
721
- const foundSignal = await detectProjectRoot(root);
722
- if (!foundSignal) {
723
- const blocker = `No recognisable project signals found in ${root}. Expected one of: ${PROJECT_SIGNALS.join(", ")}. Check that --root points to the repository root, not a subdirectory or an unrelated path.`;
724
- const earlyState = deriveAuditState(earlyBundle);
725
- const blockedState = buildBlockedAuditState({
726
- state: earlyState,
727
- obligationId: null,
728
- executor: null,
729
- blocker,
730
- });
731
- await emitEnvelope({
732
- root,
733
- artifactsDir,
734
- bundle: { ...earlyBundle, audit_state: blockedState },
735
- audit_state: blockedState,
736
- selected_obligation: null,
737
- selected_executor: null,
738
- progress_made: false,
739
- artifacts_written: [],
740
- progress_summary: blocker,
741
- next_likely_step: null,
742
- providerName: provider.name,
743
- isConfigError: true,
744
- });
745
- return;
746
- }
747
- }
748
695
  let pendingAuditResultsPath = getFlag(argv, "--results");
749
696
  let pendingRuntimeUpdatesPath = getFlag(argv, "--updates");
750
697
  let pendingExternalAnalyzerPath = getFlag(argv, "--external-analyzer-results");
@@ -0,0 +1,13 @@
1
+ import type { Lens, RepoManifest } from "../types.js";
2
+ import type { FileDisposition } from "../types/disposition.js";
3
+ import type { GraphBundle, GraphEdge } from "../types/graph.js";
4
+ import type { SurfaceRecord } from "../types/surfaces.js";
5
+ export declare const BROWSER_EXTENSION_HEURISTIC_NOTE = "Chrome extension manifest and HTML asset references were resolved deterministically from local paths; verify unusual dynamic registration manually.";
6
+ export declare function isBrowserExtensionManifestPath(path: string): boolean;
7
+ export declare function extractChromeExtensionManifestEdges(fromPath: string, content: string, pathLookup: Map<string, string>): GraphEdge[];
8
+ export declare function extractHtmlResourceEdges(fromPath: string, content: string, pathLookup: Map<string, string>): GraphEdge[];
9
+ export declare function hasBrowserExtensionManifestFile(repoManifest: RepoManifest): boolean;
10
+ export declare function deriveBrowserExtensionLensesForPath(path: string): Lens[];
11
+ export declare function inferBrowserExtensionUnitKind(path: string): string | undefined;
12
+ export declare function buildBrowserExtensionSurfacesFromGraph(graphBundle: GraphBundle | undefined, disposition?: FileDisposition): SurfaceRecord[];
13
+ export declare function chromeExtensionRiskSignalsForManifest(content: string): string[];