program-context-protocol 0.12.4__py3-none-any.whl
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.
- pcp/__init__.py +3 -0
- pcp/assertions.py +152 -0
- pcp/attest.py +111 -0
- pcp/build_loop_bypass.py +76 -0
- pcp/build_report.py +54 -0
- pcp/capture.py +339 -0
- pcp/cli.py +104 -0
- pcp/commands/__init__.py +0 -0
- pcp/commands/amend.py +283 -0
- pcp/commands/architect_review.py +291 -0
- pcp/commands/architecture_justification.py +164 -0
- pcp/commands/audit.py +371 -0
- pcp/commands/build.py +4523 -0
- pcp/commands/build_plan.py +153 -0
- pcp/commands/build_status.py +83 -0
- pcp/commands/capture.py +72 -0
- pcp/commands/check.py +584 -0
- pcp/commands/context.py +151 -0
- pcp/commands/control_audit_cmd.py +54 -0
- pcp/commands/correct_objective.py +160 -0
- pcp/commands/dashboard.py +732 -0
- pcp/commands/deploy.py +199 -0
- pcp/commands/deploy_check.py +134 -0
- pcp/commands/design_audit.py +323 -0
- pcp/commands/diff.py +153 -0
- pcp/commands/diff_reduce.py +355 -0
- pcp/commands/docs.py +538 -0
- pcp/commands/doctor.py +820 -0
- pcp/commands/escalations_cmd.py +64 -0
- pcp/commands/gate.py +209 -0
- pcp/commands/import_project.py +404 -0
- pcp/commands/init.py +1634 -0
- pcp/commands/install_hook.py +283 -0
- pcp/commands/install_skill.py +48 -0
- pcp/commands/kickoff.py +772 -0
- pcp/commands/narrative_lint.py +54 -0
- pcp/commands/objective_conflicts_cmd.py +68 -0
- pcp/commands/pm.py +504 -0
- pcp/commands/pressure_test_cmd.py +72 -0
- pcp/commands/provenance.py +313 -0
- pcp/commands/prune.py +179 -0
- pcp/commands/report.py +49 -0
- pcp/commands/run_log_cmd.py +122 -0
- pcp/commands/scan.py +346 -0
- pcp/commands/self_update.py +125 -0
- pcp/commands/status.py +180 -0
- pcp/commands/takeover.py +55 -0
- pcp/commands/telemetry_cmd.py +167 -0
- pcp/commands/validate_module.py +153 -0
- pcp/commands/validate_strategy.py +413 -0
- pcp/commands/verify.py +166 -0
- pcp/commands/verify_syntax_fix.py +74 -0
- pcp/commands/watch.py +372 -0
- pcp/config_audit.py +141 -0
- pcp/context_map.py +124 -0
- pcp/control_audit.py +159 -0
- pcp/coupling.py +178 -0
- pcp/coverage_audit.py +77 -0
- pcp/decision_log.py +134 -0
- pcp/discovery/__init__.py +0 -0
- pcp/discovery/clusters.py +124 -0
- pcp/discovery/graph.py +110 -0
- pcp/discovery/scanner.py +109 -0
- pcp/escalations.py +193 -0
- pcp/evidence.py +30 -0
- pcp/evidence_chain.py +56 -0
- pcp/impact.py +164 -0
- pcp/install_approvals.py +44 -0
- pcp/integrity_audit.py +176 -0
- pcp/librarian.py +89 -0
- pcp/llm/__init__.py +0 -0
- pcp/llm/client.py +183 -0
- pcp/llm/coding_agent_contract.py +104 -0
- pcp/llm/harness/__init__.py +12 -0
- pcp/llm/harness/agy.py +121 -0
- pcp/llm/harness/agy_coding_loop.py +180 -0
- pcp/llm/harness/claude.py +241 -0
- pcp/llm/ledger.py +47 -0
- pcp/narrative_lint.py +229 -0
- pcp/nav_graph.py +226 -0
- pcp/objective_conflicts.py +129 -0
- pcp/operational.py +70 -0
- pcp/orphaned_work.py +262 -0
- pcp/pcp_dir.py +35 -0
- pcp/pcp_status.py +313 -0
- pcp/policy.py +81 -0
- pcp/pressure_test.py +196 -0
- pcp/qa.py +445 -0
- pcp/run_log.py +225 -0
- pcp/schema/__init__.py +0 -0
- pcp/schema/ci_rules.schema.json +106 -0
- pcp/schema/controls.schema.json +39 -0
- pcp/schema/module_acceptance.schema.json +144 -0
- pcp/schema/module_spec.schema.json +78 -0
- pcp/schema/sdlc_phase.schema.json +52 -0
- pcp/schema/validator.py +77 -0
- pcp/skill_data/pcp/SKILL.md +1897 -0
- pcp/spec_write.py +269 -0
- pcp/spend.py +77 -0
- pcp/symbols.py +86 -0
- pcp/telemetry.py +308 -0
- pcp/uat.py +271 -0
- pcp/version_drift.py +222 -0
- program_context_protocol-0.12.4.dist-info/METADATA +123 -0
- program_context_protocol-0.12.4.dist-info/RECORD +109 -0
- program_context_protocol-0.12.4.dist-info/WHEEL +4 -0
- program_context_protocol-0.12.4.dist-info/entry_points.txt +2 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-APACHE +202 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-MIT +21 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""pcp pressure-test -- MAC-Bench-style adversarial-pressure compliance
|
|
2
|
+
check (see pcp.pressure_test's module docstring + docs/research-
|
|
3
|
+
rigidity-vs-reliability-2026-07.md). Explicit, human-triggered only --
|
|
4
|
+
spawns TWO real coding-agent sessions, real time and cost. Never called
|
|
5
|
+
from `pcp build`'s own loop.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from pcp.pcp_dir import find_pcp_dir, NoPCPDir
|
|
15
|
+
from pcp.schema.validator import load_yaml
|
|
16
|
+
from pcp import pressure_test
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.command(name="pressure-test")
|
|
22
|
+
@click.argument("module_name")
|
|
23
|
+
@click.argument("criterion_id")
|
|
24
|
+
@click.option("--path", "project_path", type=click.Path(), default=None,
|
|
25
|
+
help="Project root (default: cwd, walks up to find .pcp/).")
|
|
26
|
+
@click.option("--model", "build_model", default=None, help="Override the coding-agent model for both runs.")
|
|
27
|
+
def pressure_test_cmd(module_name: str, criterion_id: str, project_path: str | None, build_model: str | None):
|
|
28
|
+
"""Run a criterion twice (baseline vs. urgency/authority-pressure framing),
|
|
29
|
+
each in its own throwaway worktree, and compare advisory-check violation
|
|
30
|
+
counts. A widening gap under pressure is a real compliance-under-pressure
|
|
31
|
+
signal -- see pcp.pressure_test's docstring."""
|
|
32
|
+
try:
|
|
33
|
+
pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
|
|
34
|
+
except NoPCPDir as e:
|
|
35
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
36
|
+
sys.exit(2)
|
|
37
|
+
|
|
38
|
+
project_root = pcp_dir.parent
|
|
39
|
+
spec_path = pcp_dir / "strategy" / "modules" / module_name / "spec.yaml"
|
|
40
|
+
acc_path = pcp_dir / "strategy" / "modules" / module_name / "acceptance.yaml"
|
|
41
|
+
if not spec_path.exists() or not acc_path.exists():
|
|
42
|
+
console.print(f"[red]Error:[/red] module '{module_name}' not found under .pcp/strategy/modules/")
|
|
43
|
+
sys.exit(2)
|
|
44
|
+
|
|
45
|
+
spec = load_yaml(spec_path)
|
|
46
|
+
acc = load_yaml(acc_path)
|
|
47
|
+
criterion = next((c for c in acc.get("criteria", []) if c["id"] == criterion_id), None)
|
|
48
|
+
if criterion is None:
|
|
49
|
+
console.print(f"[red]Error:[/red] criterion '{criterion_id}' not found in module '{module_name}'")
|
|
50
|
+
sys.exit(2)
|
|
51
|
+
|
|
52
|
+
mod = {"name": module_name, "spec_path": spec_path, "acc_path": acc_path, "spec": spec}
|
|
53
|
+
|
|
54
|
+
console.print(
|
|
55
|
+
f"[bold]Pressure-testing[/bold] {module_name}/{criterion_id} -- "
|
|
56
|
+
"spawning 2 real coding-agent sessions (baseline + pressure framing), each in its own worktree..."
|
|
57
|
+
)
|
|
58
|
+
report = pressure_test.run_pressure_test(pcp_dir, project_root, mod, criterion, build_model=build_model)
|
|
59
|
+
|
|
60
|
+
console.print(f"\n[bold]Baseline advisory findings:[/bold] {report['baseline']['total_advisory']} {report['baseline']['advisory_counts']}")
|
|
61
|
+
console.print(f"[bold]Pressure advisory findings:[/bold] {report['pressure']['total_advisory']} {report['pressure']['advisory_counts']}")
|
|
62
|
+
if report["widened"]:
|
|
63
|
+
console.print(
|
|
64
|
+
f"[red bold]⚠ Compliance gap widened under pressure[/red bold] "
|
|
65
|
+
f"(+{report['delta']} advisory findings) -- this criterion showed MORE corner-cutting "
|
|
66
|
+
"when told to move fast. Worth a human look at what got skipped."
|
|
67
|
+
)
|
|
68
|
+
elif report["delta"] < 0:
|
|
69
|
+
console.print(f"[dim]Pressure run showed FEWER advisory findings ({report['delta']}) -- no widening gap this run.[/dim]")
|
|
70
|
+
else:
|
|
71
|
+
console.print("[green]No compliance gap under pressure this run.[/green]")
|
|
72
|
+
console.print(f"[dim]Logged to .pcp/pressure_test_log.jsonl[/dim]")
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""pcp provenance — audit-evidence document: every file, every gate PCP has
|
|
2
|
+
approved, any skipped/bypassed step made visible.
|
|
3
|
+
|
|
4
|
+
Pure aggregation over already-persisted logs (.pcp/controls.yaml,
|
|
5
|
+
.pcp/telemetry.jsonl, .pcp/bypass_log.yaml) — no LLM call, no rebuild.
|
|
6
|
+
Callable at any point in a project's lifecycle, cheap, always a live
|
|
7
|
+
snapshot. This is the SLSA/in-toto-informed "quality card" half of the
|
|
8
|
+
CMMC/SSDF-style build-evidence document — controls.py's control catalog is
|
|
9
|
+
the other half.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
import yaml
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
from pcp.pcp_dir import find_pcp_dir, NoPCPDir
|
|
24
|
+
from pcp import telemetry as telemetry_mod
|
|
25
|
+
from pcp import decision_log as decision_log_mod
|
|
26
|
+
from pcp.evidence_chain import verify_chain
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
RESULT_SYMBOL = {
|
|
31
|
+
"pass": "OK", "block": "BLOCK", "skipped": "skip", "error": "ERROR", "bypassed": "bypass",
|
|
32
|
+
# "advisory" (2026-07-27): the check ran, found something, and deliberately
|
|
33
|
+
# did not block. Distinct from "pass" (found nothing) and "skipped" (never
|
|
34
|
+
# ran) — see _wave_record in build.py for why conflating it with "pass" was
|
|
35
|
+
# falsifying this very document.
|
|
36
|
+
"advisory": "ADVISORY",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Fallback for telemetry records written before control_id existed (2026-07-01).
|
|
40
|
+
CHECK_TO_CONTROL = {
|
|
41
|
+
"test-suite": "CTRL-001",
|
|
42
|
+
"wave-test-suite": "CTRL-001",
|
|
43
|
+
"lint": "CTRL-002",
|
|
44
|
+
"sast": "CTRL-003",
|
|
45
|
+
"layer1": "CTRL-004",
|
|
46
|
+
"layer1-bypass": "CTRL-004",
|
|
47
|
+
"architect-review": "CTRL-005",
|
|
48
|
+
"wave-architect-review": "CTRL-005",
|
|
49
|
+
"gate": "CTRL-006",
|
|
50
|
+
"wave-contract": "CTRL-007",
|
|
51
|
+
"wave-validate-strategy": "CTRL-008",
|
|
52
|
+
"deploy-phase-exit": "CTRL-009",
|
|
53
|
+
"build-scope": "CTRL-018",
|
|
54
|
+
"tier-presence": "CTRL-019",
|
|
55
|
+
"rung-necessity": "CTRL-020",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _load_controls(pcp_dir: Path) -> dict:
|
|
60
|
+
path = pcp_dir / "controls.yaml"
|
|
61
|
+
if not path.exists():
|
|
62
|
+
return {}
|
|
63
|
+
data = yaml.safe_load(path.read_text()) or {}
|
|
64
|
+
return {c["id"]: c for c in data.get("controls", [])}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _load_bypasses(pcp_dir: Path) -> list[dict]:
|
|
68
|
+
path = pcp_dir / "bypass_log.yaml"
|
|
69
|
+
if not path.exists():
|
|
70
|
+
return []
|
|
71
|
+
data = yaml.safe_load(path.read_text()) or {}
|
|
72
|
+
return data.get("bypasses", [])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _git_head(project_root: Path) -> str:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True, cwd=project_root,
|
|
78
|
+
)
|
|
79
|
+
return result.stdout.strip() if result.returncode == 0 else "unknown"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _control_id_for(record: dict) -> str | None:
|
|
83
|
+
return record.get("control_id") or CHECK_TO_CONTROL.get(record.get("check"))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_chain_integrity(pcp_dir: Path) -> dict:
|
|
87
|
+
"""Verifies the hash chain on every append-only evidence log. Any non-empty
|
|
88
|
+
break list here means a record was edited, reordered, or deleted after
|
|
89
|
+
the fact — this is the one check in this whole document that isn't just
|
|
90
|
+
reporting what ran, it's reporting whether the report itself can be
|
|
91
|
+
trusted."""
|
|
92
|
+
telemetry_breaks = verify_chain(telemetry_mod.load(pcp_dir))
|
|
93
|
+
decision_breaks = verify_chain(decision_log_mod.load(pcp_dir))
|
|
94
|
+
bypass_breaks = verify_chain(_load_bypasses(pcp_dir))
|
|
95
|
+
return {
|
|
96
|
+
"telemetry.jsonl": telemetry_breaks,
|
|
97
|
+
"decision_log.jsonl": decision_breaks,
|
|
98
|
+
"bypass_log.yaml": bypass_breaks,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def build_provenance(pcp_dir: Path) -> dict:
|
|
103
|
+
"""Pure aggregation — safe to call at any point, no side effects besides read."""
|
|
104
|
+
controls = _load_controls(pcp_dir)
|
|
105
|
+
# Not just cycle=="qa" -- cross-cutting controls (e.g. CTRL-011 transcript
|
|
106
|
+
# archival, recorded under cycle="capture") carry a control_id too and
|
|
107
|
+
# belong in the same per-file/per-control coverage view.
|
|
108
|
+
qa_records = [r for r in telemetry_mod.load(pcp_dir) if _control_id_for(r)]
|
|
109
|
+
bypasses = _load_bypasses(pcp_dir)
|
|
110
|
+
chain_integrity = _check_chain_integrity(pcp_dir)
|
|
111
|
+
|
|
112
|
+
per_file: dict[str, dict[str, dict]] = defaultdict(dict)
|
|
113
|
+
per_control: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
|
114
|
+
|
|
115
|
+
for r in qa_records:
|
|
116
|
+
cid = _control_id_for(r)
|
|
117
|
+
if not cid:
|
|
118
|
+
continue
|
|
119
|
+
result = r.get("result", "pass")
|
|
120
|
+
per_control[cid]["total"] += 1
|
|
121
|
+
per_control[cid][result] += 1
|
|
122
|
+
for f in (r.get("files") or []):
|
|
123
|
+
existing = per_file[f].get(cid)
|
|
124
|
+
if not existing or r.get("timestamp", "") >= existing.get("timestamp", ""):
|
|
125
|
+
per_file[f][cid] = r
|
|
126
|
+
|
|
127
|
+
# A control whose every recorded outcome is "skipped" never actually ran
|
|
128
|
+
# anywhere in this project — a project-wide blind spot, not per-file noise.
|
|
129
|
+
standing_gap_cids = {
|
|
130
|
+
cid for cid, totals in per_control.items()
|
|
131
|
+
if totals["total"] > 0 and totals.get("skipped", 0) == totals["total"]
|
|
132
|
+
}
|
|
133
|
+
never_exercised_cids = {
|
|
134
|
+
cid for cid in controls if cid not in per_control and cid != "CTRL-010"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"controls": controls,
|
|
139
|
+
"per_file": {f: dict(v) for f, v in per_file.items()},
|
|
140
|
+
"per_control": {cid: dict(v) for cid, v in per_control.items()},
|
|
141
|
+
"standing_gap_cids": sorted(standing_gap_cids),
|
|
142
|
+
"never_exercised_cids": sorted(never_exercised_cids),
|
|
143
|
+
"bypasses": bypasses,
|
|
144
|
+
"chain_integrity": chain_integrity,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _render_markdown(project_root: Path, data: dict, timestamp: str) -> str:
|
|
149
|
+
controls = data["controls"]
|
|
150
|
+
per_file = data["per_file"]
|
|
151
|
+
per_control = data["per_control"]
|
|
152
|
+
|
|
153
|
+
lines = [
|
|
154
|
+
"# PCP Audit Evidence",
|
|
155
|
+
f"Generated: {timestamp} | Git: `{_git_head(project_root)}`",
|
|
156
|
+
"",
|
|
157
|
+
"> Auto-generated by `pcp provenance`. Aggregates `.pcp/controls.yaml` + "
|
|
158
|
+
"`.pcp/telemetry.jsonl` + `.pcp/bypass_log.yaml`. Do not edit manually. "
|
|
159
|
+
"SSDF mapping is informed, not a certified assessment.",
|
|
160
|
+
"",
|
|
161
|
+
"## Per-File Gate Coverage",
|
|
162
|
+
"",
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
if not per_file:
|
|
166
|
+
lines += ["_No telemetry recorded yet — run `pcp build` first._", ""]
|
|
167
|
+
else:
|
|
168
|
+
control_ids = sorted(controls.keys()) or sorted({cid for f in per_file.values() for cid in f})
|
|
169
|
+
lines.append("| File | " + " | ".join(control_ids) + " |")
|
|
170
|
+
lines.append("|---|" + "---|" * len(control_ids))
|
|
171
|
+
for f in sorted(per_file.keys()):
|
|
172
|
+
row = [f"`{f}`"]
|
|
173
|
+
for cid in control_ids:
|
|
174
|
+
rec = per_file[f].get(cid)
|
|
175
|
+
row.append("-" if not rec else RESULT_SYMBOL.get(rec.get("result"), rec.get("result", "?")))
|
|
176
|
+
lines.append("| " + " | ".join(row) + " |")
|
|
177
|
+
lines.append("")
|
|
178
|
+
legend = ", ".join(f"`{cid}`={controls.get(cid, {}).get('name', cid)}" for cid in control_ids)
|
|
179
|
+
lines += [f"Legend: {legend}", ""]
|
|
180
|
+
|
|
181
|
+
lines += ["## Standing Gaps (control never actually ran project-wide)", ""]
|
|
182
|
+
gap_lines = []
|
|
183
|
+
for cid in data["standing_gap_cids"]:
|
|
184
|
+
name = controls.get(cid, {}).get("name", cid)
|
|
185
|
+
total = per_control.get(cid, {}).get("total", 0)
|
|
186
|
+
gap_lines.append(f"- {cid} ({name}): {total}/{total} checks skipped — tool never detected project-wide.")
|
|
187
|
+
for cid in data["never_exercised_cids"]:
|
|
188
|
+
name = controls.get(cid, {}).get("name", cid)
|
|
189
|
+
gap_lines.append(f"- {cid} ({name}): never invoked in this project yet.")
|
|
190
|
+
lines += gap_lines if gap_lines else ["_None — every cataloged control has run with a real tool at least once._"]
|
|
191
|
+
lines.append("")
|
|
192
|
+
|
|
193
|
+
lines += ["## Bypass Ledger", ""]
|
|
194
|
+
if data["bypasses"]:
|
|
195
|
+
for b in data["bypasses"]:
|
|
196
|
+
rules = ", ".join(b.get("rules_bypassed", []))
|
|
197
|
+
lines.append(f"- `{b.get('timestamp', '')}` — {b.get('reason', '')} (rules: {rules})")
|
|
198
|
+
else:
|
|
199
|
+
lines.append("_No bypasses._")
|
|
200
|
+
lines.append("")
|
|
201
|
+
|
|
202
|
+
# Escalation responsiveness — proves the escalation ledger is watched,
|
|
203
|
+
# not just written (PagerDuty Escalation Policy Insights pattern).
|
|
204
|
+
from pcp import escalations as _esc
|
|
205
|
+
_esc_dir = project_root / ".pcp"
|
|
206
|
+
mtta = _esc.mtta_hours(_esc_dir)
|
|
207
|
+
esc_entries = _esc.load(_esc_dir)
|
|
208
|
+
lines += ["## Escalation Responsiveness", ""]
|
|
209
|
+
if esc_entries:
|
|
210
|
+
acked = sum(1 for e in esc_entries if e.get("acknowledged_at"))
|
|
211
|
+
lines.append(f"- Escalations recorded: {len(esc_entries)}; acknowledged: {acked}")
|
|
212
|
+
lines.append(f"- MTTA (median time-to-acknowledge): {mtta}h" if mtta is not None
|
|
213
|
+
else "- MTTA: no escalation has ever been acknowledged — ledger may be unwatched")
|
|
214
|
+
else:
|
|
215
|
+
lines.append("_No escalations recorded._")
|
|
216
|
+
lines.append("")
|
|
217
|
+
|
|
218
|
+
# Auditability Card (adapted from "Auditable Agents", arXiv:2604.05485 —
|
|
219
|
+
# its 5-dimension framework maps 1:1 onto PCP's mechanisms). Deterministic
|
|
220
|
+
# self-score from artifacts that exist, not aspiration.
|
|
221
|
+
_pcp = project_root / ".pcp"
|
|
222
|
+
card = [
|
|
223
|
+
("Action recoverability", (_pcp / "telemetry.jsonl").exists() and (_pcp / "evidence").exists(),
|
|
224
|
+
"per-attempt telemetry + raw untruncated QA evidence"),
|
|
225
|
+
("Lifecycle coverage", (_pcp / "controls.yaml").exists() and (_pcp / "deploy_log.yaml").exists(),
|
|
226
|
+
"controls span commit->PR->wave->deploy; deploy log present"),
|
|
227
|
+
("Policy checkability", (_pcp / "ci_rules.yaml").exists() and (_pcp / "policies").exists(),
|
|
228
|
+
"machine-checkable rules (ci_rules.yaml) + human-editable OPA policies"),
|
|
229
|
+
("Responsibility attribution", (_pcp / "bypass_log.yaml").exists() or (_pcp / "escalations.yaml").exists(),
|
|
230
|
+
"attributed bypasses and/or escalation ledger with ack states"),
|
|
231
|
+
("Evidence integrity", bool(data.get("chain_integrity")),
|
|
232
|
+
"hash-chained logs, verified each provenance run (unsigned — see roadmap)"),
|
|
233
|
+
]
|
|
234
|
+
lines += ["## Auditability Card", "",
|
|
235
|
+
"Five dimensions from the 'Auditable Agents' framework (arXiv:2604.05485), "
|
|
236
|
+
"scored from artifacts actually present in this project:", "",
|
|
237
|
+
"| Dimension | Present | Basis |", "|---|---|---|"]
|
|
238
|
+
for name, ok, basis in card:
|
|
239
|
+
lines.append(f"| {name} | {'✓' if ok else '✗'} | {basis} |")
|
|
240
|
+
lines.append("")
|
|
241
|
+
|
|
242
|
+
lines += ["## Chain Integrity", "", "Each evidence log is hash-chained — an entry's hash covers its own "
|
|
243
|
+
"content plus the previous entry's hash, so an edit/reorder/deletion after the fact is "
|
|
244
|
+
"detectable even though the files themselves are plain, editable JSON/YAML.", ""]
|
|
245
|
+
any_break = False
|
|
246
|
+
for log_name, breaks in data["chain_integrity"].items():
|
|
247
|
+
if not breaks:
|
|
248
|
+
lines.append(f"- `{log_name}`: intact.")
|
|
249
|
+
else:
|
|
250
|
+
any_break = True
|
|
251
|
+
lines.append(f"- `{log_name}`: **{len(breaks)} break(s) detected**")
|
|
252
|
+
for b in breaks:
|
|
253
|
+
lines.append(f" - index {b['index']}: {b['issue']}")
|
|
254
|
+
if any_break:
|
|
255
|
+
lines.append("")
|
|
256
|
+
lines.append("**A break here means this evidence document cannot be trusted as-is — "
|
|
257
|
+
"investigate before relying on anything above.**")
|
|
258
|
+
lines.append("")
|
|
259
|
+
|
|
260
|
+
lines += ["## SSDF Crosswalk", "", "| Control | SSDF Practice | Enforcement | Status |", "|---|---|---|---|"]
|
|
261
|
+
for cid, c in sorted(controls.items()):
|
|
262
|
+
totals = per_control.get(cid, {})
|
|
263
|
+
if cid == "CTRL-010":
|
|
264
|
+
status = f"{len(data['bypasses'])} bypass(es) logged" if data["bypasses"] else "0 bypasses"
|
|
265
|
+
elif cid in data["never_exercised_cids"]:
|
|
266
|
+
status = "GAP — never invoked"
|
|
267
|
+
elif cid in data["standing_gap_cids"]:
|
|
268
|
+
status = "GAP — tool never detected"
|
|
269
|
+
else:
|
|
270
|
+
status = f"{totals.get('pass', 0)}/{totals.get('total', 0)} pass"
|
|
271
|
+
practices = ", ".join(c.get("ssdf_practice", []))
|
|
272
|
+
lines.append(f"| {cid} {c.get('name', '')} | {practices} | {c.get('enforcement', '')} | {status} |")
|
|
273
|
+
lines.append("")
|
|
274
|
+
|
|
275
|
+
return "\n".join(lines)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def write_provenance(pcp_dir: Path) -> Path:
|
|
279
|
+
data = build_provenance(pcp_dir)
|
|
280
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
281
|
+
md = _render_markdown(pcp_dir.parent, data, timestamp)
|
|
282
|
+
out = pcp_dir / "provenance.md"
|
|
283
|
+
out.write_text(md)
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@click.command()
|
|
288
|
+
@click.option("--path", "project_path", type=click.Path(), default=None)
|
|
289
|
+
@click.option("--json", "output_json", is_flag=True, help="Print raw JSON instead of writing provenance.md.")
|
|
290
|
+
@click.option("--attest", "attest_flag", is_flag=True,
|
|
291
|
+
help="Also export in-toto Statement v1 / DSSE envelopes to .pcp/attestations.jsonl "
|
|
292
|
+
"(unsigned unless --sign and cosign present).")
|
|
293
|
+
@click.option("--sign", "sign_flag", is_flag=True,
|
|
294
|
+
help="Sign the attestation export via cosign sign-blob (requires cosign; interactive OIDC).")
|
|
295
|
+
def provenance(project_path: str | None, output_json: bool, attest_flag: bool, sign_flag: bool):
|
|
296
|
+
"""Audit-evidence document — every file, every gate, any skip/bypass made visible."""
|
|
297
|
+
try:
|
|
298
|
+
pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
|
|
299
|
+
except NoPCPDir as e:
|
|
300
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
301
|
+
sys.exit(2)
|
|
302
|
+
|
|
303
|
+
if attest_flag or sign_flag:
|
|
304
|
+
from pcp.attest import export_attestations
|
|
305
|
+
out, count, signing_note = export_attestations(pcp_dir, sign=sign_flag)
|
|
306
|
+
console.print(f"[green]✓[/green] {count} in-toto attestation(s) -> {out.name} [{signing_note}]")
|
|
307
|
+
|
|
308
|
+
if output_json:
|
|
309
|
+
click.echo(json.dumps(build_provenance(pcp_dir), indent=2, default=str))
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
out_path = write_provenance(pcp_dir)
|
|
313
|
+
console.print(f"[green]Provenance written[/green] -> {out_path.relative_to(pcp_dir.parent)}")
|
pcp/commands/prune.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""pcp prune — retention/pruning for .pcp/evidence/ and .pcp/transcripts/.
|
|
2
|
+
|
|
3
|
+
Both grow unboundedly (CLAUDE.md names this explicitly: full raw QA proof
|
|
4
|
+
on every check, gzip-archived session transcripts on every pcp capture
|
|
5
|
+
call, no retention policy). Deliberately scoped to these two directories
|
|
6
|
+
only, NOT telemetry.jsonl/decision_log.jsonl/bypass_log.yaml -- those are
|
|
7
|
+
hash-chained (evidence_chain.py) and pruning entries out of the front of a
|
|
8
|
+
hash chain breaks verify_chain() at the new first record unless the
|
|
9
|
+
pruning routine re-anchors the chain with an explicit checkpoint, a bigger
|
|
10
|
+
change than this pass takes on. evidence/transcripts are plain files with
|
|
11
|
+
no chain to preserve, so they're the safe, high-value target: the actual
|
|
12
|
+
disk-bloat culprits (full test-suite output, full judge responses, full
|
|
13
|
+
session transcripts) rather than telemetry.jsonl's small per-record JSON
|
|
14
|
+
lines.
|
|
15
|
+
|
|
16
|
+
Deletion, not archival -- an irreversible action, so it gets the same
|
|
17
|
+
posture pcp deploy already established as this project's one other
|
|
18
|
+
genuinely irreversible action: mandatory interactive confirmation by
|
|
19
|
+
default, --yes opts out.
|
|
20
|
+
|
|
21
|
+
A deleted evidence file leaves a dangling evidence_path reference in
|
|
22
|
+
telemetry.jsonl -- dashboard.py already just renders that as a link
|
|
23
|
+
without checking existence, a known pre-existing gap this doesn't
|
|
24
|
+
introduce or fix, just doesn't make worse.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import shutil
|
|
28
|
+
import sys
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
import click
|
|
33
|
+
import yaml
|
|
34
|
+
from rich.console import Console
|
|
35
|
+
|
|
36
|
+
from pcp.pcp_dir import find_pcp_dir, NoPCPDir
|
|
37
|
+
from pcp.schema.validator import load_yaml
|
|
38
|
+
|
|
39
|
+
console = Console()
|
|
40
|
+
|
|
41
|
+
PRUNE_LOG = "prune_log.yaml"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _retention_config(pcp_dir: Path) -> dict:
|
|
45
|
+
ci_rules_path = pcp_dir / "ci_rules.yaml"
|
|
46
|
+
if not ci_rules_path.exists():
|
|
47
|
+
return {}
|
|
48
|
+
return (load_yaml(ci_rules_path) or {}).get("retention") or {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _age_days(path: Path, now: datetime) -> float:
|
|
52
|
+
mtime = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc)
|
|
53
|
+
return (now - mtime).total_seconds() / 86400
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _find_stale_evidence(pcp_dir: Path, evidence_days: int, now: datetime) -> list[Path]:
|
|
57
|
+
evidence_dir = pcp_dir / "evidence"
|
|
58
|
+
if not evidence_dir.exists():
|
|
59
|
+
return []
|
|
60
|
+
return [
|
|
61
|
+
p for p in evidence_dir.rglob("*.txt")
|
|
62
|
+
if p.is_file() and _age_days(p, now) > evidence_days
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _find_stale_transcripts(pcp_dir: Path, transcript_days: int, now: datetime) -> list[Path]:
|
|
67
|
+
transcripts_dir = pcp_dir / "transcripts"
|
|
68
|
+
if not transcripts_dir.exists():
|
|
69
|
+
return []
|
|
70
|
+
return [
|
|
71
|
+
p for p in transcripts_dir.glob("*.jsonl.gz")
|
|
72
|
+
if p.is_file() and _age_days(p, now) > transcript_days
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _log_prune_run(pcp_dir: Path, evidence_days: int | None, transcript_days: int | None,
|
|
77
|
+
files_removed: int, bytes_reclaimed: int) -> None:
|
|
78
|
+
"""Plain append list, not hash-chained -- this is an operational
|
|
79
|
+
maintenance record (what a config-driven cleanup pass did), not a
|
|
80
|
+
security/audit control checkpoint the way telemetry.jsonl/bypass_log.yaml
|
|
81
|
+
are. Scope kept narrow deliberately, same "phase 1, not the finished
|
|
82
|
+
thing" posture as pcp docs's drift score."""
|
|
83
|
+
log_path = pcp_dir / PRUNE_LOG
|
|
84
|
+
existing = []
|
|
85
|
+
if log_path.exists():
|
|
86
|
+
existing = (yaml.safe_load(log_path.read_text()) or {}).get("runs", [])
|
|
87
|
+
existing.append({
|
|
88
|
+
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
89
|
+
"evidence_days": evidence_days,
|
|
90
|
+
"transcript_days": transcript_days,
|
|
91
|
+
"files_removed": files_removed,
|
|
92
|
+
"bytes_reclaimed": bytes_reclaimed,
|
|
93
|
+
})
|
|
94
|
+
log_path.write_text(yaml.dump({"runs": existing}, default_flow_style=False))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_prune(pcp_dir: Path, evidence_days: int | None, transcript_days: int | None,
|
|
98
|
+
now: datetime | None = None) -> dict:
|
|
99
|
+
"""Pure discovery + deletion, no prompting -- the CLI command owns the
|
|
100
|
+
confirmation step. Returns a summary dict; always safe to call with
|
|
101
|
+
both days=None (finds nothing, deletes nothing)."""
|
|
102
|
+
now = now or datetime.now(timezone.utc)
|
|
103
|
+
stale_evidence = _find_stale_evidence(pcp_dir, evidence_days, now) if evidence_days else []
|
|
104
|
+
stale_transcripts = _find_stale_transcripts(pcp_dir, transcript_days, now) if transcript_days else []
|
|
105
|
+
all_stale = stale_evidence + stale_transcripts
|
|
106
|
+
total_bytes = sum(p.stat().st_size for p in all_stale)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"evidence_files": stale_evidence,
|
|
110
|
+
"transcript_files": stale_transcripts,
|
|
111
|
+
"total_files": len(all_stale),
|
|
112
|
+
"total_bytes": total_bytes,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _delete_files(paths: list[Path]) -> None:
|
|
117
|
+
for p in paths:
|
|
118
|
+
p.unlink(missing_ok=True)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@click.command()
|
|
122
|
+
@click.option("--path", "project_path", type=click.Path(), default=None,
|
|
123
|
+
help="Project root (default: cwd, walks up to find .pcp/).")
|
|
124
|
+
@click.option("--evidence-days", type=int, default=None,
|
|
125
|
+
help="Delete .pcp/evidence/*.txt older than N days (overrides ci_rules.yaml's retention.evidence_days).")
|
|
126
|
+
@click.option("--transcript-days", type=int, default=None,
|
|
127
|
+
help="Delete .pcp/transcripts/*.jsonl.gz older than N days (overrides ci_rules.yaml's retention.transcript_days).")
|
|
128
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be deleted, delete nothing.")
|
|
129
|
+
@click.option("--yes", is_flag=True, help="Skip the interactive confirmation prompt (CI/non-interactive use).")
|
|
130
|
+
def prune(project_path: str | None, evidence_days: int | None, transcript_days: int | None,
|
|
131
|
+
dry_run: bool, yes: bool):
|
|
132
|
+
"""Delete stale raw QA evidence / session transcripts past a retention window.
|
|
133
|
+
|
|
134
|
+
No retention happens unless configured -- via --evidence-days/
|
|
135
|
+
--transcript-days here, or a `retention:` block in ci_rules.yaml. A bare
|
|
136
|
+
`pcp prune` with nothing configured does nothing and says so."""
|
|
137
|
+
try:
|
|
138
|
+
pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
|
|
139
|
+
except NoPCPDir as e:
|
|
140
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
141
|
+
sys.exit(2)
|
|
142
|
+
|
|
143
|
+
config = _retention_config(pcp_dir)
|
|
144
|
+
evidence_days = evidence_days if evidence_days is not None else config.get("evidence_days")
|
|
145
|
+
transcript_days = transcript_days if transcript_days is not None else config.get("transcript_days")
|
|
146
|
+
|
|
147
|
+
if not evidence_days and not transcript_days:
|
|
148
|
+
console.print(
|
|
149
|
+
"[dim]No retention configured -- nothing to do. Pass --evidence-days/--transcript-days, "
|
|
150
|
+
"or add a `retention:` block to .pcp/ci_rules.yaml.[/dim]"
|
|
151
|
+
)
|
|
152
|
+
sys.exit(0)
|
|
153
|
+
|
|
154
|
+
result = run_prune(pcp_dir, evidence_days, transcript_days)
|
|
155
|
+
if result["total_files"] == 0:
|
|
156
|
+
console.print("[green]Nothing past the retention window.[/green]")
|
|
157
|
+
sys.exit(0)
|
|
158
|
+
|
|
159
|
+
mb = result["total_bytes"] / (1024 * 1024)
|
|
160
|
+
console.print(
|
|
161
|
+
f"[bold]{result['total_files']} file(s)[/bold] past retention "
|
|
162
|
+
f"({len(result['evidence_files'])} evidence, {len(result['transcript_files'])} transcripts), "
|
|
163
|
+
f"~{mb:.1f} MB."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if dry_run:
|
|
167
|
+
console.print("[dim]--dry-run: nothing deleted.[/dim]")
|
|
168
|
+
sys.exit(0)
|
|
169
|
+
|
|
170
|
+
if not yes:
|
|
171
|
+
prompt = f"Permanently delete these {result['total_files']} file(s)?"
|
|
172
|
+
if not click.confirm(prompt, default=False):
|
|
173
|
+
console.print("[yellow]Prune aborted.[/yellow]")
|
|
174
|
+
sys.exit(0)
|
|
175
|
+
|
|
176
|
+
_delete_files(result["evidence_files"])
|
|
177
|
+
_delete_files(result["transcript_files"])
|
|
178
|
+
_log_prune_run(pcp_dir, evidence_days, transcript_days, result["total_files"], result["total_bytes"])
|
|
179
|
+
console.print(f"[green]✓[/green] Deleted {result['total_files']} file(s), reclaimed ~{mb:.1f} MB.")
|
pcp/commands/report.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""pcp report — DEPRECATED (2026-07-21).
|
|
2
|
+
|
|
3
|
+
Confirmed a genuine duplicate, not a distinct capability: this command had
|
|
4
|
+
its own separate implementation reading bypass_log.yaml/current_state.md
|
|
5
|
+
directly, never calling into provenance.py's build_provenance(). Every
|
|
6
|
+
data point it showed already exists elsewhere -- the bypass ledger is a
|
|
7
|
+
full section of `pcp provenance`'s output; the coverage percentage is
|
|
8
|
+
already in current_state.md itself (which this command only re-read) and
|
|
9
|
+
surfaced again in pcp.md/dashboard.html. Zero unique data, duplicate code
|
|
10
|
+
instead of reuse -- exactly the catalog-bloat pattern PCP's own
|
|
11
|
+
self-evaluation named. Kept as a command (not removed outright) so a
|
|
12
|
+
script or muscle-memory invocation gets a clear redirect instead of a
|
|
13
|
+
"command not found" error.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
from rich.console import Console
|
|
22
|
+
|
|
23
|
+
from pcp.pcp_dir import find_pcp_dir, NoPCPDir
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
_DEPRECATION_MESSAGE = (
|
|
28
|
+
"`pcp report` is deprecated -- use `pcp provenance` (bypass ledger + SSDF crosswalk) "
|
|
29
|
+
"or `pcp dashboard` (Audit Trail tab) instead. Both already show everything this "
|
|
30
|
+
"command did, with more context."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.option("--path", "project_path", type=click.Path(), default=None)
|
|
36
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
37
|
+
def report(project_path: str | None, output_json: bool):
|
|
38
|
+
"""DEPRECATED -- use `pcp provenance` or `pcp dashboard` instead."""
|
|
39
|
+
try:
|
|
40
|
+
find_pcp_dir(Path(project_path) if project_path else None)
|
|
41
|
+
except NoPCPDir as e:
|
|
42
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
43
|
+
sys.exit(2)
|
|
44
|
+
|
|
45
|
+
if output_json:
|
|
46
|
+
click.echo(json.dumps({"deprecated": True, "use_instead": ["pcp provenance", "pcp dashboard"]}, indent=2))
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
console.print(f"[yellow]{_DEPRECATION_MESSAGE}[/yellow]")
|