@vigolium/piolium 0.0.1
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/LICENSE +21 -0
- package/README.md +117 -0
- package/agents/access-auditor.md +300 -0
- package/agents/assumption-breaker.md +154 -0
- package/agents/attack-designer.md +116 -0
- package/agents/code-scanner.md +139 -0
- package/agents/concurrency-auditor.md +238 -0
- package/agents/confirm-writer.md +257 -0
- package/agents/context-reviewer.md +274 -0
- package/agents/cross-verifier.md +165 -0
- package/agents/cve-scout.md +381 -0
- package/agents/env-builder.md +282 -0
- package/agents/env-profiler.md +205 -0
- package/agents/evidence-collector.md +140 -0
- package/agents/finding-grader.md +142 -0
- package/agents/finding-writer.md +148 -0
- package/agents/flow-tracer.md +106 -0
- package/agents/goal-backtracer.md +146 -0
- package/agents/history-miner.md +467 -0
- package/agents/independent-verifier.md +118 -0
- package/agents/intent-mapper.md +183 -0
- package/agents/longshot-collector.md +128 -0
- package/agents/longshot-prober.md +126 -0
- package/agents/patch-auditor.md +73 -0
- package/agents/poc-author.md +124 -0
- package/agents/poc-runner.md +194 -0
- package/agents/probe-lead.md +269 -0
- package/agents/red-challenger.md +101 -0
- package/agents/report-composer.md +208 -0
- package/agents/review-adjudicator.md +216 -0
- package/agents/spec-auditor.md +155 -0
- package/agents/taint-tracer.md +265 -0
- package/agents/test-locator.md +209 -0
- package/agents/threat-modeler.md +132 -0
- package/agents/variant-scanner.md +108 -0
- package/agents/variant-spotter.md +110 -0
- package/bin/piolium.mjs +376 -0
- package/extensions/piolium/_vendor/yaml.bundle.d.mts +6 -0
- package/extensions/piolium/_vendor/yaml.bundle.mjs +139 -0
- package/extensions/piolium/agent-runner.ts +322 -0
- package/extensions/piolium/agents.ts +266 -0
- package/extensions/piolium/audit-state.ts +522 -0
- package/extensions/piolium/bundled-resources.ts +97 -0
- package/extensions/piolium/candidate-scan.ts +966 -0
- package/extensions/piolium/command-target.ts +177 -0
- package/extensions/piolium/console-stream.ts +57 -0
- package/extensions/piolium/export-results.ts +380 -0
- package/extensions/piolium/findings.ts +448 -0
- package/extensions/piolium/heartbeat.ts +182 -0
- package/extensions/piolium/help.ts +234 -0
- package/extensions/piolium/index.ts +1865 -0
- package/extensions/piolium/longshot.ts +530 -0
- package/extensions/piolium/matcher-suggestions.ts +196 -0
- package/extensions/piolium/matcher-utils.ts +83 -0
- package/extensions/piolium/modes/balanced.ts +750 -0
- package/extensions/piolium/modes/confirm-bootstrap.ts +186 -0
- package/extensions/piolium/modes/confirm.ts +697 -0
- package/extensions/piolium/modes/deep.ts +917 -0
- package/extensions/piolium/modes/diff.ts +177 -0
- package/extensions/piolium/modes/lite.ts +540 -0
- package/extensions/piolium/modes/longshot.ts +595 -0
- package/extensions/piolium/modes/merge.ts +204 -0
- package/extensions/piolium/modes/phase-runner.ts +267 -0
- package/extensions/piolium/modes/reinvest.ts +546 -0
- package/extensions/piolium/modes/revisit.ts +279 -0
- package/extensions/piolium/modes.ts +48 -0
- package/extensions/piolium/phase-labels.ts +123 -0
- package/extensions/piolium/phase-status-strip.ts +92 -0
- package/extensions/piolium/prompt-prefix-editor.ts +39 -0
- package/extensions/piolium/providers/anthropic-vertex.ts +836 -0
- package/extensions/piolium/recon.ts +409 -0
- package/extensions/piolium/result-stats.ts +105 -0
- package/extensions/piolium/retry.ts +120 -0
- package/extensions/piolium/scheduler.ts +212 -0
- package/extensions/piolium/secrets.ts +368 -0
- package/extensions/piolium/tools/web-tools.ts +148 -0
- package/package.json +77 -0
- package/skills/agentic-actions-auditor/SKILL.md +327 -0
- package/skills/agentic-actions-auditor/references/action-profiles.md +186 -0
- package/skills/agentic-actions-auditor/references/cross-file-resolution.md +209 -0
- package/skills/agentic-actions-auditor/references/foundations.md +94 -0
- package/skills/agentic-actions-auditor/references/vector-a-env-var-intermediary.md +77 -0
- package/skills/agentic-actions-auditor/references/vector-b-direct-expression-injection.md +83 -0
- package/skills/agentic-actions-auditor/references/vector-c-cli-data-fetch.md +83 -0
- package/skills/agentic-actions-auditor/references/vector-d-pr-target-checkout.md +88 -0
- package/skills/agentic-actions-auditor/references/vector-e-error-log-injection.md +88 -0
- package/skills/agentic-actions-auditor/references/vector-f-subshell-expansion.md +82 -0
- package/skills/agentic-actions-auditor/references/vector-g-eval-of-ai-output.md +91 -0
- package/skills/agentic-actions-auditor/references/vector-h-dangerous-sandbox-configs.md +102 -0
- package/skills/agentic-actions-auditor/references/vector-i-wildcard-allowlists.md +88 -0
- package/skills/audit/SKILL.md +562 -0
- package/skills/audit/assets/icon.svg +7 -0
- package/skills/audit/hooks/scripts/validate_phase_output.py +550 -0
- package/skills/audit/references/adversarial-review.md +148 -0
- package/skills/audit/references/architecture-aware-sast.md +306 -0
- package/skills/audit/references/audit-workflow.md +737 -0
- package/skills/audit/references/chamber-protocol.md +384 -0
- package/skills/audit/references/creative-attack-modes.md +221 -0
- package/skills/audit/references/deep-analysis.md +273 -0
- package/skills/audit/references/domain-attack-playbooks.md +1129 -0
- package/skills/audit/references/knowledge-base-template.md +513 -0
- package/skills/audit/references/real-env-validation.md +191 -0
- package/skills/audit/references/report-templates.md +417 -0
- package/skills/audit/references/triage-and-prereqs.md +134 -0
- package/skills/audit/scripts/consolidate_drafts.py +554 -0
- package/skills/audit/scripts/partition_findings.py +152 -0
- package/skills/audit/scripts/rg-hotspots.sh +121 -0
- package/skills/audit/scripts/stamp_file_state.py +349 -0
- package/skills/code-reviewer/SKILL.md +65 -0
- package/skills/codeql/SKILL.md +281 -0
- package/skills/codeql/references/build-fixes.md +90 -0
- package/skills/codeql/references/diagnostic-query-templates.md +339 -0
- package/skills/codeql/references/extension-yaml-format.md +209 -0
- package/skills/codeql/references/important-only-suite.md +153 -0
- package/skills/codeql/references/language-details.md +207 -0
- package/skills/codeql/references/macos-arm64e-workaround.md +179 -0
- package/skills/codeql/references/performance-tuning.md +111 -0
- package/skills/codeql/references/quality-assessment.md +172 -0
- package/skills/codeql/references/ruleset-catalog.md +63 -0
- package/skills/codeql/references/run-all-suite.md +92 -0
- package/skills/codeql/references/sarif-processing.md +79 -0
- package/skills/codeql/references/threat-models.md +51 -0
- package/skills/codeql/workflows/build-database.md +280 -0
- package/skills/codeql/workflows/create-data-extensions.md +261 -0
- package/skills/codeql/workflows/run-analysis.md +301 -0
- package/skills/differential-review/SKILL.md +220 -0
- package/skills/differential-review/adversarial.md +203 -0
- package/skills/differential-review/methodology.md +234 -0
- package/skills/differential-review/patterns.md +300 -0
- package/skills/differential-review/reporting.md +369 -0
- package/skills/fp-check/SKILL.md +125 -0
- package/skills/fp-check/references/bug-class-verification.md +114 -0
- package/skills/fp-check/references/deep-verification.md +143 -0
- package/skills/fp-check/references/evidence-templates.md +91 -0
- package/skills/fp-check/references/false-positive-patterns.md +115 -0
- package/skills/fp-check/references/gate-reviews.md +27 -0
- package/skills/fp-check/references/standard-verification.md +78 -0
- package/skills/insecure-defaults/SKILL.md +117 -0
- package/skills/insecure-defaults/references/examples.md +409 -0
- package/skills/last30days/SKILL.md +444 -0
- package/skills/sarif-parsing/SKILL.md +483 -0
- package/skills/sarif-parsing/resources/jq-queries.md +162 -0
- package/skills/sarif-parsing/resources/sarif_helpers.py +331 -0
- package/skills/security-threat-model/LICENSE.txt +201 -0
- package/skills/security-threat-model/SKILL.md +81 -0
- package/skills/security-threat-model/agents/openai.yaml +4 -0
- package/skills/security-threat-model/references/prompt-template.md +255 -0
- package/skills/security-threat-model/references/security-controls-and-assets.md +32 -0
- package/skills/semgrep/SKILL.md +212 -0
- package/skills/semgrep/references/rulesets.md +162 -0
- package/skills/semgrep/references/scan-modes.md +110 -0
- package/skills/semgrep/references/scanner-task-prompt.md +140 -0
- package/skills/semgrep/scripts/merge_sarif.py +203 -0
- package/skills/semgrep/workflows/scan-workflow.md +311 -0
- package/skills/semgrep-rule-creator/SKILL.md +168 -0
- package/skills/semgrep-rule-creator/references/quick-reference.md +202 -0
- package/skills/semgrep-rule-creator/references/workflow.md +240 -0
- package/skills/semgrep-rule-variant-creator/SKILL.md +205 -0
- package/skills/semgrep-rule-variant-creator/references/applicability-analysis.md +250 -0
- package/skills/semgrep-rule-variant-creator/references/language-syntax-guide.md +324 -0
- package/skills/semgrep-rule-variant-creator/references/workflow.md +518 -0
- package/skills/sharp-edges/SKILL.md +292 -0
- package/skills/sharp-edges/references/auth-patterns.md +252 -0
- package/skills/sharp-edges/references/case-studies.md +274 -0
- package/skills/sharp-edges/references/config-patterns.md +333 -0
- package/skills/sharp-edges/references/crypto-apis.md +190 -0
- package/skills/sharp-edges/references/lang-c.md +205 -0
- package/skills/sharp-edges/references/lang-csharp.md +285 -0
- package/skills/sharp-edges/references/lang-go.md +270 -0
- package/skills/sharp-edges/references/lang-java.md +263 -0
- package/skills/sharp-edges/references/lang-javascript.md +269 -0
- package/skills/sharp-edges/references/lang-kotlin.md +265 -0
- package/skills/sharp-edges/references/lang-php.md +245 -0
- package/skills/sharp-edges/references/lang-python.md +274 -0
- package/skills/sharp-edges/references/lang-ruby.md +273 -0
- package/skills/sharp-edges/references/lang-rust.md +272 -0
- package/skills/sharp-edges/references/lang-swift.md +287 -0
- package/skills/sharp-edges/references/language-specific.md +588 -0
- package/skills/spec-to-code-compliance/SKILL.md +357 -0
- package/skills/spec-to-code-compliance/resources/COMPLETENESS_CHECKLIST.md +69 -0
- package/skills/spec-to-code-compliance/resources/IR_EXAMPLES.md +417 -0
- package/skills/spec-to-code-compliance/resources/OUTPUT_REQUIREMENTS.md +105 -0
- package/skills/supply-chain-risk-auditor/SKILL.md +67 -0
- package/skills/supply-chain-risk-auditor/resources/results-template.md +41 -0
- package/skills/variant-analysis/METHODOLOGY.md +327 -0
- package/skills/variant-analysis/SKILL.md +142 -0
- package/skills/variant-analysis/resources/codeql/cpp.ql +119 -0
- package/skills/variant-analysis/resources/codeql/go.ql +69 -0
- package/skills/variant-analysis/resources/codeql/java.ql +71 -0
- package/skills/variant-analysis/resources/codeql/javascript.ql +63 -0
- package/skills/variant-analysis/resources/codeql/python.ql +80 -0
- package/skills/variant-analysis/resources/semgrep/cpp.yaml +98 -0
- package/skills/variant-analysis/resources/semgrep/go.yaml +63 -0
- package/skills/variant-analysis/resources/semgrep/java.yaml +61 -0
- package/skills/variant-analysis/resources/semgrep/javascript.yaml +60 -0
- package/skills/variant-analysis/resources/semgrep/python.yaml +72 -0
- package/skills/variant-analysis/resources/variant-report-template.md +75 -0
- package/skills/vuln-report/SKILL.md +137 -0
- package/skills/vuln-report/agents/openai.yaml +4 -0
- package/skills/vuln-report/references/report-template.md +135 -0
- package/skills/wooyun-legacy/SKILL.md +367 -0
- package/skills/wooyun-legacy/references/bank-penetration.md +222 -0
- package/skills/wooyun-legacy/references/checklists/command-execution-checklist.md +119 -0
- package/skills/wooyun-legacy/references/checklists/csrf-checklist.md +74 -0
- package/skills/wooyun-legacy/references/checklists/file-upload-checklist.md +108 -0
- package/skills/wooyun-legacy/references/checklists/info-disclosure-checklist.md +114 -0
- package/skills/wooyun-legacy/references/checklists/logic-flaws-checklist.md +95 -0
- package/skills/wooyun-legacy/references/checklists/misconfig-checklist.md +124 -0
- package/skills/wooyun-legacy/references/checklists/path-traversal-checklist.md +87 -0
- package/skills/wooyun-legacy/references/checklists/rce-checklist.md +93 -0
- package/skills/wooyun-legacy/references/checklists/sql-injection-checklist.md +97 -0
- package/skills/wooyun-legacy/references/checklists/ssrf-checklist.md +99 -0
- package/skills/wooyun-legacy/references/checklists/unauthorized-access-checklist.md +89 -0
- package/skills/wooyun-legacy/references/checklists/weak-password-checklist.md +115 -0
- package/skills/wooyun-legacy/references/checklists/xss-checklist.md +103 -0
- package/skills/wooyun-legacy/references/checklists/xxe-checklist.md +130 -0
- package/skills/wooyun-legacy/references/info-disclosure.md +975 -0
- package/skills/wooyun-legacy/references/logic-flaws.md +721 -0
- package/skills/wooyun-legacy/references/path-traversal.md +1191 -0
- package/skills/wooyun-legacy/references/telecom-penetration.md +156 -0
- package/skills/wooyun-legacy/references/unauthorized-access.md +980 -0
- package/skills/wooyun-legacy/references/xss.md +746 -0
- package/skills/zeroize-audit/SKILL.md +371 -0
- package/skills/zeroize-audit/configs/c.yaml +21 -0
- package/skills/zeroize-audit/configs/default.yaml +128 -0
- package/skills/zeroize-audit/configs/rust.yaml +83 -0
- package/skills/zeroize-audit/prompts/report_template.md +238 -0
- package/skills/zeroize-audit/prompts/system.md +163 -0
- package/skills/zeroize-audit/prompts/task.md +97 -0
- package/skills/zeroize-audit/references/compile-commands.md +231 -0
- package/skills/zeroize-audit/references/detection-strategy.md +191 -0
- package/skills/zeroize-audit/references/ir-analysis.md +252 -0
- package/skills/zeroize-audit/references/mcp-analysis.md +221 -0
- package/skills/zeroize-audit/references/poc-generation.md +470 -0
- package/skills/zeroize-audit/references/rust-zeroization-patterns.md +867 -0
- package/skills/zeroize-audit/schemas/input.json +83 -0
- package/skills/zeroize-audit/schemas/output.json +140 -0
- package/skills/zeroize-audit/tools/analyze_asm.sh +202 -0
- package/skills/zeroize-audit/tools/analyze_cfg.py +381 -0
- package/skills/zeroize-audit/tools/analyze_heap.sh +211 -0
- package/skills/zeroize-audit/tools/analyze_ir_semantic.py +429 -0
- package/skills/zeroize-audit/tools/diff_ir.sh +135 -0
- package/skills/zeroize-audit/tools/diff_rust_mir.sh +189 -0
- package/skills/zeroize-audit/tools/emit_asm.sh +67 -0
- package/skills/zeroize-audit/tools/emit_ir.sh +77 -0
- package/skills/zeroize-audit/tools/emit_rust_asm.sh +178 -0
- package/skills/zeroize-audit/tools/emit_rust_ir.sh +150 -0
- package/skills/zeroize-audit/tools/emit_rust_mir.sh +158 -0
- package/skills/zeroize-audit/tools/extract_compile_flags.py +284 -0
- package/skills/zeroize-audit/tools/generate_poc.py +1329 -0
- package/skills/zeroize-audit/tools/mcp/apply_confidence_gates.py +113 -0
- package/skills/zeroize-audit/tools/mcp/check_mcp.sh +68 -0
- package/skills/zeroize-audit/tools/mcp/normalize_mcp_evidence.py +125 -0
- package/skills/zeroize-audit/tools/scripts/check_llvm_patterns.py +481 -0
- package/skills/zeroize-audit/tools/scripts/check_mir_patterns.py +554 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm.py +424 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm_aarch64.py +300 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm_x86.py +283 -0
- package/skills/zeroize-audit/tools/scripts/find_dangerous_apis.py +375 -0
- package/skills/zeroize-audit/tools/scripts/semantic_audit.py +923 -0
- package/skills/zeroize-audit/tools/track_dataflow.sh +196 -0
- package/skills/zeroize-audit/tools/validate_rust_toolchain.sh +298 -0
- package/skills/zeroize-audit/workflows/phase-0-preflight.md +150 -0
- package/skills/zeroize-audit/workflows/phase-1-source-analysis.md +144 -0
- package/skills/zeroize-audit/workflows/phase-2-compiler-analysis.md +139 -0
- package/skills/zeroize-audit/workflows/phase-3-interim-report.md +46 -0
- package/skills/zeroize-audit/workflows/phase-4-poc-generation.md +46 -0
- package/skills/zeroize-audit/workflows/phase-5-poc-validation.md +136 -0
- package/skills/zeroize-audit/workflows/phase-6-final-report.md +44 -0
- package/skills/zeroize-audit/workflows/phase-7-test-generation.md +42 -0
- package/themes/piolium-srcery.json +94 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge mode (`/piolium-merge`).
|
|
3
|
+
*
|
|
4
|
+
* Combines multiple `piolium/` result trees into one. Two stages:
|
|
5
|
+
*
|
|
6
|
+
* M1-M3 (deterministic): copy + collision-rename + per-finding validation.
|
|
7
|
+
* M4-M7 (agent-driven): semantic dedup + quarantine + renumber + final report.
|
|
8
|
+
*
|
|
9
|
+
* MVP scope:
|
|
10
|
+
* - The deterministic stage is fully implemented in TypeScript here.
|
|
11
|
+
* - The agent-driven stage runs as a single report-composer pass with
|
|
12
|
+
* a long task prompt covering all of M2-M7.
|
|
13
|
+
*
|
|
14
|
+
* Each input directory is identified by an alias (`a`, `b`, ...) that gets
|
|
15
|
+
* prepended to colliding finding ids so the dedup agent has clean inputs.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
cpSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
renameSync,
|
|
24
|
+
statSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { basename, join } from "node:path";
|
|
28
|
+
import type { AgentRuntimeModel } from "../agent-runner.ts";
|
|
29
|
+
import { loadAgents } from "../agents.ts";
|
|
30
|
+
import { applyPhaseStatus, initAudit, markAuditStatus } from "../audit-state.ts";
|
|
31
|
+
import { runReconAsync } from "../recon.ts";
|
|
32
|
+
import { type PhaseUiHooks, runAgentPhase } from "./phase-runner.ts";
|
|
33
|
+
|
|
34
|
+
export interface RunMergeOptions {
|
|
35
|
+
cwd: string;
|
|
36
|
+
/** Source `piolium/` trees to merge. Must be ≥2. */
|
|
37
|
+
sources: string[];
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
ui?: PhaseUiHooks;
|
|
40
|
+
agentRuntime?: AgentRuntimeModel;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RunMergeResult {
|
|
44
|
+
auditId: string;
|
|
45
|
+
status: "complete" | "failed";
|
|
46
|
+
mergedFindings: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ATTACK_SURFACE_DIR = "piolium/attack-surface";
|
|
50
|
+
export const MERGE_ATTACK_SURFACE_SUMMARY = `${ATTACK_SURFACE_DIR}/merge-summary.md`;
|
|
51
|
+
|
|
52
|
+
function aliasFor(index: number): string {
|
|
53
|
+
return String.fromCharCode("a".charCodeAt(0) + index);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function copyTree(src: string, dest: string): void {
|
|
57
|
+
cpSync(src, dest, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function copyFindings(src: string, dest: string, alias: string): string[] {
|
|
61
|
+
const srcFindings = join(src, "findings");
|
|
62
|
+
if (!existsSync(srcFindings)) return [];
|
|
63
|
+
const destFindings = join(dest, "findings");
|
|
64
|
+
mkdirSync(destFindings, { recursive: true });
|
|
65
|
+
const written: string[] = [];
|
|
66
|
+
for (const entry of readdirSync(srcFindings)) {
|
|
67
|
+
const srcDir = join(srcFindings, entry);
|
|
68
|
+
if (!statSync(srcDir).isDirectory()) continue;
|
|
69
|
+
const renamed = `${alias}-${entry}`;
|
|
70
|
+
const destDir = join(destFindings, renamed);
|
|
71
|
+
if (existsSync(destDir)) continue; // shouldn't happen with alias prefix
|
|
72
|
+
mkdirSync(destDir, { recursive: true });
|
|
73
|
+
copyTree(srcDir, destDir);
|
|
74
|
+
written.push(destDir);
|
|
75
|
+
}
|
|
76
|
+
return written;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function copyAttackSurface(src: string, dest: string, alias: string): string | undefined {
|
|
80
|
+
const srcAttackSurface = join(src, "attack-surface");
|
|
81
|
+
if (!existsSync(srcAttackSurface)) return undefined;
|
|
82
|
+
const destRoot = join(dest, "attack-surface");
|
|
83
|
+
mkdirSync(destRoot, { recursive: true });
|
|
84
|
+
const destAttackSurface = join(destRoot, alias);
|
|
85
|
+
if (existsSync(destAttackSurface)) return destAttackSurface;
|
|
86
|
+
copyTree(srcAttackSurface, destAttackSurface);
|
|
87
|
+
return destAttackSurface;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function runMergeAudit(opts: RunMergeOptions): Promise<RunMergeResult> {
|
|
91
|
+
const { cwd, signal, ui } = opts;
|
|
92
|
+
if (opts.sources.length < 2) {
|
|
93
|
+
throw new Error("Merge requires at least two source piolium/ trees.");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ui?.setStatus?.("piolium-merge", "● preparing recon");
|
|
97
|
+
const recon = await runReconAsync(cwd, { signal });
|
|
98
|
+
const audit = await initAudit(cwd, {
|
|
99
|
+
mode: "merge",
|
|
100
|
+
...(recon.commit ? { commit: recon.commit } : { commit: null }),
|
|
101
|
+
...(recon.branch ? { branch: recon.branch } : { branch: "nogit" }),
|
|
102
|
+
...(recon.repository ? { repository: recon.repository } : {}),
|
|
103
|
+
history_available: recon.historyAvailable,
|
|
104
|
+
agent_sdk: "pi",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const workspace = join(cwd, "piolium", "merge-workspace");
|
|
108
|
+
mkdirSync(workspace, { recursive: true });
|
|
109
|
+
mkdirSync(join(cwd, ATTACK_SURFACE_DIR), { recursive: true });
|
|
110
|
+
|
|
111
|
+
// M1: copy each source's findings/ and attack-surface/ into the workspace under aliases.
|
|
112
|
+
const merged: string[] = [];
|
|
113
|
+
const attackSurfaceSnapshots: Record<string, string> = {};
|
|
114
|
+
const aliasMap: Record<string, string> = {};
|
|
115
|
+
for (let i = 0; i < opts.sources.length; i++) {
|
|
116
|
+
const src = opts.sources[i];
|
|
117
|
+
if (!src) continue;
|
|
118
|
+
const alias = aliasFor(i);
|
|
119
|
+
aliasMap[alias] = src;
|
|
120
|
+
const written = copyFindings(src, workspace, alias);
|
|
121
|
+
merged.push(...written);
|
|
122
|
+
const attackSurface = copyAttackSurface(src, workspace, alias);
|
|
123
|
+
if (attackSurface) attackSurfaceSnapshots[alias] = attackSurface;
|
|
124
|
+
}
|
|
125
|
+
writeFileSync(
|
|
126
|
+
join(workspace, "findings-index.json"),
|
|
127
|
+
`${JSON.stringify({ aliasMap, merged: merged.map((p) => basename(p)) }, null, "\t")}\n`,
|
|
128
|
+
);
|
|
129
|
+
writeFileSync(
|
|
130
|
+
join(workspace, "attack-surface-index.json"),
|
|
131
|
+
`${JSON.stringify(
|
|
132
|
+
{
|
|
133
|
+
aliasMap,
|
|
134
|
+
attackSurfaceSnapshots: Object.fromEntries(
|
|
135
|
+
Object.entries(attackSurfaceSnapshots).map(([alias, path]) => [alias, basename(path)]),
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
null,
|
|
139
|
+
"\t",
|
|
140
|
+
)}\n`,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// M2-M7: agent-driven semantic dedup + renumber + final report.
|
|
144
|
+
// Upstream assigns finding-writer to M3 (auto-fix) and report-composer to
|
|
145
|
+
// M6 (regenerate summaries); the rest are deterministic. piolium runs the
|
|
146
|
+
// whole M2-M7 tail as one report-composer pass.
|
|
147
|
+
const { agents } = loadAgents({ cwd });
|
|
148
|
+
const synth = agents.get("report-composer");
|
|
149
|
+
|
|
150
|
+
const task = [
|
|
151
|
+
"You are running /piolium-merge: combining multiple archon-audit result trees into one canonical piolium/ output.",
|
|
152
|
+
"",
|
|
153
|
+
`Workspace: ${workspace}`,
|
|
154
|
+
"Each finding directory there is named `<alias>-<original-id>-<slug>` (alias = a/b/c... per source).",
|
|
155
|
+
"Each source attack-surface corpus, when present, is copied under `merge-workspace/attack-surface/<alias>/`.",
|
|
156
|
+
`Source map (alias → path):\n${Object.entries(aliasMap)
|
|
157
|
+
.map(([k, v]) => ` ${k}: ${v}`)
|
|
158
|
+
.join("\n")}`,
|
|
159
|
+
"",
|
|
160
|
+
"Steps:",
|
|
161
|
+
" M2 — semantic dedup: identify findings that describe the same root cause across aliases. Decide canonical winner. Record decisions in `merge-workspace/dedup-decisions.json`.",
|
|
162
|
+
" M3 — auto-fix: repair frontmatter, malformed PoC JSON, naming violations.",
|
|
163
|
+
" M4 — quarantine: move unfixable findings to `piolium/quarantine/<orig-id>-<slug>/QUARANTINE.md` with reason.",
|
|
164
|
+
" M5 — renumber: assign deterministic IDs by severity (M-001 critical … M-NNN info). Write `merge-workspace/rename-map.json`.",
|
|
165
|
+
" M6 — apply rename: rename surviving finding directories under `piolium/findings/` and rewrite per-report internal links.",
|
|
166
|
+
` M7 — merge durable source context into \`${MERGE_ATTACK_SURFACE_SUMMARY}\`, then regenerate \`piolium/final-audit-report.md\` from the merged findings with an Attack Surface Summary linking \`${ATTACK_SURFACE_DIR}/\`.`,
|
|
167
|
+
"",
|
|
168
|
+
"Cap surviving findings at 60. Quality > quantity.",
|
|
169
|
+
].join("\n");
|
|
170
|
+
|
|
171
|
+
let failed = false;
|
|
172
|
+
try {
|
|
173
|
+
await runAgentPhase({
|
|
174
|
+
cwd,
|
|
175
|
+
audit,
|
|
176
|
+
phaseName: "M1",
|
|
177
|
+
statusKey: "piolium-merge",
|
|
178
|
+
statusLabel: "● merge dedup",
|
|
179
|
+
agent: synth,
|
|
180
|
+
missingAgentMessage: "report-composer missing",
|
|
181
|
+
task,
|
|
182
|
+
gate: () =>
|
|
183
|
+
existsSync(join(cwd, "piolium/final-audit-report.md")) &&
|
|
184
|
+
existsSync(join(cwd, MERGE_ATTACK_SURFACE_SUMMARY)),
|
|
185
|
+
mode: "merge",
|
|
186
|
+
ui,
|
|
187
|
+
agentRuntime: opts.agentRuntime,
|
|
188
|
+
...(signal ? { signal } : {}),
|
|
189
|
+
});
|
|
190
|
+
} catch {
|
|
191
|
+
failed = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The single agent pass covers M1-M7; reflect that across the tracked
|
|
195
|
+
// phase set so the status strip isn't stuck showing M2-M7 pending.
|
|
196
|
+
for (const p of ["M1", "M2", "M3", "M4", "M5", "M6", "M7"]) {
|
|
197
|
+
await applyPhaseStatus(cwd, audit, p, { status: failed ? "failed" : "complete" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await markAuditStatus(cwd, audit.audit_id, failed ? "failed" : "complete");
|
|
201
|
+
void renameSync; // placeholder for future deterministic rename pass
|
|
202
|
+
ui?.notify?.(failed ? "Merge failed." : "Merge complete.", failed ? "error" : "info");
|
|
203
|
+
return { auditId: audit.audit_id, status: failed ? "failed" : "complete", mergedFindings: merged };
|
|
204
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for orchestrators (lite/balanced/deep/etc.) — wraps a single
|
|
3
|
+
* phase invocation with audit-state transitions, gate verification, and
|
|
4
|
+
* structured logging via UI hooks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { type AgentRunError, type AgentRuntimeModel, runAgent } from "../agent-runner.ts";
|
|
10
|
+
import type { RuntimeContext } from "../agent-runner.ts";
|
|
11
|
+
import type { AgentDefinition } from "../agents.ts";
|
|
12
|
+
import { type AuditRunState, applyPhaseStatus } from "../audit-state.ts";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
15
|
+
type PhaseHeartbeat,
|
|
16
|
+
createPhaseHeartbeatTracker,
|
|
17
|
+
heartbeatStateFields,
|
|
18
|
+
} from "../heartbeat.ts";
|
|
19
|
+
import {
|
|
20
|
+
errorMessage,
|
|
21
|
+
readNonNegativeIntEnv,
|
|
22
|
+
readPositiveIntEnv,
|
|
23
|
+
retryBackoffMs,
|
|
24
|
+
sleep,
|
|
25
|
+
} from "../retry.ts";
|
|
26
|
+
|
|
27
|
+
export interface PhaseUiHooks {
|
|
28
|
+
notify?: (text: string, level: "info" | "warning" | "error") => void;
|
|
29
|
+
setStatus?: (key: string, text?: string) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Forwarded copy of every child agent event. Wire this from the command
|
|
32
|
+
* handler to surface tool calls + assistant text in the parent chat;
|
|
33
|
+
* otherwise the subagent runs silently and the user only sees a footer
|
|
34
|
+
* status indicator.
|
|
35
|
+
*/
|
|
36
|
+
onAgentEvent?: (phase: string, event: AgentSessionEvent) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Periodic parent-side health signal while a child agent is running.
|
|
39
|
+
* This fires even when the child model is quiet, so the UI can prove the
|
|
40
|
+
* phase has not been forgotten.
|
|
41
|
+
*/
|
|
42
|
+
onPhaseHeartbeat?: (phase: string, heartbeat?: PhaseHeartbeat) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RunAgentPhaseOptions {
|
|
46
|
+
cwd: string;
|
|
47
|
+
audit: AuditRunState;
|
|
48
|
+
phaseName: string;
|
|
49
|
+
statusKey: string;
|
|
50
|
+
statusLabel: string;
|
|
51
|
+
agent: AgentDefinition | undefined;
|
|
52
|
+
missingAgentMessage: string;
|
|
53
|
+
task: string;
|
|
54
|
+
runtimeExtras?: Partial<Omit<RuntimeContext, "cwd" | "mode">>;
|
|
55
|
+
gate: () => boolean;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
ui?: PhaseUiHooks;
|
|
58
|
+
mode: AuditRunState["mode"];
|
|
59
|
+
agentRuntime?: AgentRuntimeModel;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
/** Number of retries after the first attempt. Defaults to PIOLIUM_PHASE_MAX_RETRIES or 5. */
|
|
62
|
+
maxRetries?: number;
|
|
63
|
+
retryBackoffBaseMs?: number;
|
|
64
|
+
retryBackoffMaxMs?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const HEARTBEAT_INTERVAL_MS = readPositiveIntEnv(
|
|
68
|
+
"PIOLIUM_HEARTBEAT_INTERVAL_MS",
|
|
69
|
+
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
function defaultPhaseMaxRetries(): number {
|
|
73
|
+
return readNonNegativeIntEnv("PIOLIUM_PHASE_MAX_RETRIES", 5);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function defaultPhaseBackoffBaseMs(): number {
|
|
77
|
+
return readPositiveIntEnv("PIOLIUM_PHASE_BACKOFF_BASE_MS", 5000);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function defaultPhaseBackoffMaxMs(): number {
|
|
81
|
+
return readPositiveIntEnv("PIOLIUM_PHASE_BACKOFF_MAX_MS", 120_000);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makePhaseSignal(
|
|
85
|
+
parent: AbortSignal | undefined,
|
|
86
|
+
timeoutMs: number | undefined,
|
|
87
|
+
phaseName: string,
|
|
88
|
+
): { signal?: AbortSignal; cleanup: () => void } {
|
|
89
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
90
|
+
return { ...(parent ? { signal: parent } : {}), cleanup: () => {} };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const ctrl = new AbortController();
|
|
94
|
+
let timeout: ReturnType<typeof setTimeout> | undefined = setTimeout(() => {
|
|
95
|
+
ctrl.abort(new Error(`Phase ${phaseName} timed out after ${timeoutMs}ms`));
|
|
96
|
+
}, timeoutMs);
|
|
97
|
+
const onParentAbort = () => {
|
|
98
|
+
ctrl.abort(parent?.reason ?? new Error(`Phase ${phaseName} aborted`));
|
|
99
|
+
};
|
|
100
|
+
if (parent) {
|
|
101
|
+
if (parent.aborted) onParentAbort();
|
|
102
|
+
else parent.addEventListener("abort", onParentAbort, { once: true });
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
signal: ctrl.signal,
|
|
106
|
+
cleanup: () => {
|
|
107
|
+
if (timeout) clearTimeout(timeout);
|
|
108
|
+
timeout = undefined;
|
|
109
|
+
if (parent) parent.removeEventListener("abort", onParentAbort);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function runAgentPhase(opts: RunAgentPhaseOptions): Promise<void> {
|
|
115
|
+
const { cwd, audit, phaseName, statusKey, statusLabel, agent, ui, gate, mode } = opts;
|
|
116
|
+
if (audit.phases[phaseName]?.status === "complete" && gate()) return;
|
|
117
|
+
|
|
118
|
+
const maxRetries = Math.max(0, opts.maxRetries ?? defaultPhaseMaxRetries());
|
|
119
|
+
const maxAttempts = maxRetries + 1;
|
|
120
|
+
const backoffBaseMs = opts.retryBackoffBaseMs ?? defaultPhaseBackoffBaseMs();
|
|
121
|
+
const backoffMaxMs = opts.retryBackoffMaxMs ?? defaultPhaseBackoffMaxMs();
|
|
122
|
+
const onAgentEvent = ui?.onAgentEvent;
|
|
123
|
+
const onPhaseHeartbeat = ui?.onPhaseHeartbeat;
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (!agent) {
|
|
127
|
+
await applyPhaseStatus(cwd, audit, phaseName, {
|
|
128
|
+
status: "failed",
|
|
129
|
+
error: opts.missingAgentMessage,
|
|
130
|
+
});
|
|
131
|
+
throw new Error(opts.missingAgentMessage);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
135
|
+
const attemptLabel =
|
|
136
|
+
maxAttempts > 1 ? `${statusLabel} (${attempt}/${maxAttempts})` : statusLabel;
|
|
137
|
+
ui?.setStatus?.(statusKey, attemptLabel);
|
|
138
|
+
await applyPhaseStatus(cwd, audit, phaseName, {
|
|
139
|
+
status: "in_progress",
|
|
140
|
+
attempt,
|
|
141
|
+
max_attempts: maxAttempts,
|
|
142
|
+
retry_backoff_ms: null,
|
|
143
|
+
next_retry_at: null,
|
|
144
|
+
...(attempt > 1 ? { error: `Retry attempt ${attempt}/${maxAttempts} running.` } : {}),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const runId = `${phaseName.toLowerCase()}-${audit.audit_id.replace(/[:.]/g, "-")}-a${attempt}-${randomUUID().slice(0, 8)}`;
|
|
148
|
+
const phaseSignal = makePhaseSignal(opts.signal, opts.timeoutMs, phaseName);
|
|
149
|
+
const heartbeat = createPhaseHeartbeatTracker({
|
|
150
|
+
phase: phaseName,
|
|
151
|
+
label: attemptLabel,
|
|
152
|
+
runId,
|
|
153
|
+
});
|
|
154
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | undefined;
|
|
155
|
+
const emitHeartbeat = () => {
|
|
156
|
+
const snapshot = heartbeat.snapshot();
|
|
157
|
+
onPhaseHeartbeat?.(phaseName, snapshot);
|
|
158
|
+
void applyPhaseStatus(cwd, audit, phaseName, {
|
|
159
|
+
status: "in_progress",
|
|
160
|
+
...heartbeatStateFields(snapshot),
|
|
161
|
+
}).catch(() => {});
|
|
162
|
+
};
|
|
163
|
+
try {
|
|
164
|
+
emitHeartbeat();
|
|
165
|
+
heartbeatTimer = setInterval(emitHeartbeat, HEARTBEAT_INTERVAL_MS);
|
|
166
|
+
await runAgent({
|
|
167
|
+
agent,
|
|
168
|
+
task: opts.task,
|
|
169
|
+
runId,
|
|
170
|
+
runtime: { cwd, mode, phase: phaseName, ...opts.runtimeExtras },
|
|
171
|
+
...(opts.agentRuntime ? opts.agentRuntime : {}),
|
|
172
|
+
...(phaseSignal.signal ? { signal: phaseSignal.signal } : {}),
|
|
173
|
+
onEvent: (event) => {
|
|
174
|
+
heartbeat.recordEvent(event);
|
|
175
|
+
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
176
|
+
onPhaseHeartbeat?.(phaseName, heartbeat.snapshot());
|
|
177
|
+
}
|
|
178
|
+
onAgentEvent?.(phaseName, event);
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!gate()) {
|
|
183
|
+
throw new Error(`Phase ${phaseName} gate failed — expected artifact missing.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await applyPhaseStatus(cwd, audit, phaseName, {
|
|
187
|
+
status: "complete",
|
|
188
|
+
attempt,
|
|
189
|
+
max_attempts: maxAttempts,
|
|
190
|
+
retry_backoff_ms: null,
|
|
191
|
+
next_retry_at: null,
|
|
192
|
+
last_error: null,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const message = errorMessage(err);
|
|
197
|
+
const failure = err as Partial<AgentRunError>;
|
|
198
|
+
const artifacts = failure.result?.transcriptPath ? [failure.result.transcriptPath] : undefined;
|
|
199
|
+
|
|
200
|
+
if (gate()) {
|
|
201
|
+
await applyPhaseStatus(cwd, audit, phaseName, {
|
|
202
|
+
status: "complete",
|
|
203
|
+
attempt,
|
|
204
|
+
max_attempts: maxAttempts,
|
|
205
|
+
retry_backoff_ms: null,
|
|
206
|
+
next_retry_at: null,
|
|
207
|
+
last_error: null,
|
|
208
|
+
...(artifacts ? { artifacts } : {}),
|
|
209
|
+
});
|
|
210
|
+
ui?.notify?.(
|
|
211
|
+
`Phase ${phaseName} errored but its required artifact exists; treating it as complete.`,
|
|
212
|
+
"warning",
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (opts.signal?.aborted || attempt >= maxAttempts) {
|
|
218
|
+
await applyPhaseStatus(cwd, audit, phaseName, {
|
|
219
|
+
status: "failed",
|
|
220
|
+
error:
|
|
221
|
+
attempt >= maxAttempts && maxRetries > 0
|
|
222
|
+
? `Failed after ${maxRetries} retries: ${message}`
|
|
223
|
+
: message,
|
|
224
|
+
attempt,
|
|
225
|
+
max_attempts: maxAttempts,
|
|
226
|
+
retry_backoff_ms: null,
|
|
227
|
+
next_retry_at: null,
|
|
228
|
+
last_error: message,
|
|
229
|
+
...(artifacts ? { artifacts } : {}),
|
|
230
|
+
});
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const backoffMs = retryBackoffMs(attempt, backoffBaseMs, backoffMaxMs);
|
|
235
|
+
const nextRetryAt = new Date(Date.now() + backoffMs).toISOString();
|
|
236
|
+
await applyPhaseStatus(cwd, audit, phaseName, {
|
|
237
|
+
status: "in_progress",
|
|
238
|
+
error: `Attempt ${attempt}/${maxAttempts} failed: ${message}. Retrying at ${nextRetryAt}.`,
|
|
239
|
+
attempt,
|
|
240
|
+
max_attempts: maxAttempts,
|
|
241
|
+
retry_backoff_ms: backoffMs,
|
|
242
|
+
next_retry_at: nextRetryAt,
|
|
243
|
+
last_error: message,
|
|
244
|
+
...(artifacts ? { artifacts } : {}),
|
|
245
|
+
});
|
|
246
|
+
ui?.notify?.(
|
|
247
|
+
`Phase ${phaseName} attempt ${attempt}/${maxAttempts} failed; retrying in ${Math.ceil(backoffMs / 1000)}s.`,
|
|
248
|
+
"warning",
|
|
249
|
+
);
|
|
250
|
+
ui?.setStatus?.(statusKey, `${statusLabel} retrying in ${Math.ceil(backoffMs / 1000)}s`);
|
|
251
|
+
await sleep(backoffMs, opts.signal);
|
|
252
|
+
} finally {
|
|
253
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
254
|
+
onPhaseHeartbeat?.(phaseName, undefined);
|
|
255
|
+
phaseSignal.cleanup();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
ui?.setStatus?.(statusKey, undefined);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await applyPhaseStatus(cwd, audit, phaseName, {
|
|
263
|
+
status: "failed",
|
|
264
|
+
error: `Phase ${phaseName} failed unexpectedly without throwing.`,
|
|
265
|
+
});
|
|
266
|
+
throw new Error(`Phase ${phaseName} failed`);
|
|
267
|
+
}
|