a229a22d54
Implements the bot-bottle side of the forge-native PRD that is self-contained in this repo (the forge sidecar and orchestrate command belong to the separate bot-bottle-orchestrator, a PRD non-goal): - contrib/forge/base.py: Forge ABC + ScopedForge enforcing the read-anywhere / write-scoped model (writes rejected outside the assigned issue/PRs via ForgeScopeError). - contrib/gitea/client.py: GiteaClient (stdlib-only HTTP, mirrors the deploy-key provisioner) + GiteaForge. Token held by the caller (the sidecar), not injected by cred-proxy. - contrib/gitea/forge_state.py: ForgeState dataclass + atomic read/write/delete/all under ~/.bot-bottle/forge/<owner>/<repo>/. - contrib/gitea/provenance.py: build_provenance_footer — collapsed markdown audit footer; watchdog/gitleaks/egress rendering. - cli/resume.py: `resume --headless --prompt` reusing the shipped assume_yes + headless_prompt launch core (the new half of chunk 1). 47 new unit tests; pylint 9.98/10, pyright clean. Forge sidecar (chunk 4), orchestrate command (chunk 6), and forge_env plumbing are deferred: their only consumer is the separate orchestrator and they are untestable in isolation here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WL77TgFxKbs3cidGMG9dz7
104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
"""Provenance footer (PRD forge-native-integration, chunk 5).
|
|
|
|
Every orchestrator-posted comment ends with this footer — non-optional
|
|
and not configurable off. It renders the run's audit trail (agent,
|
|
bottle, timing, exit, gitleaks, done-signal source, egress) as a
|
|
collapsed markdown block the reviewer sees at the moment of the merge
|
|
decision.
|
|
|
|
The function is pure: the orchestrator, which holds the run context,
|
|
supplies the values. In particular `egress_routes` is the pre-rendered
|
|
list of allowed-route lines the orchestrator computed from the run's
|
|
resolved egress policy — this module does not parse backend-specific
|
|
egress state. (The PRD sketch named an `egress_log_path`; passing the
|
|
already-rendered lines keeps the footer builder pure and fully testable
|
|
and leaves egress-state parsing where the data lives.)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
def _parse(ts: str) -> datetime | None:
|
|
try:
|
|
return datetime.fromisoformat(ts)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
|
|
def _format_duration(started_at: str, finished_at: str) -> str:
|
|
start = _parse(started_at)
|
|
end = _parse(finished_at)
|
|
if start is None or end is None:
|
|
return "unknown"
|
|
secs = int((end - start).total_seconds())
|
|
if secs < 0:
|
|
return "unknown"
|
|
if secs < 60:
|
|
return f"{secs}s"
|
|
return f"{secs // 60}m {secs % 60}s"
|
|
|
|
|
|
def build_provenance_footer(
|
|
slug: str,
|
|
*,
|
|
agent_name: str,
|
|
bottle_names: tuple[str, ...],
|
|
started_at: str,
|
|
finished_at: str,
|
|
exit_code: int,
|
|
watchdog_fired: bool = False,
|
|
gitleaks_clean: bool | None = None,
|
|
egress_routes: list[str] | None = None,
|
|
) -> str:
|
|
"""Return a markdown string for appending to a Gitea comment body.
|
|
|
|
`watchdog_fired=True` marks runs where the agent did not signal
|
|
completion, so reviewers know the audit trail may be incomplete.
|
|
`gitleaks_clean=None` renders the gitleaks row as "not run".
|
|
`egress_routes` is omitted entirely when None/empty.
|
|
"""
|
|
bottle_label = ", ".join(f"`{b}`" for b in bottle_names) if bottle_names else "—"
|
|
exit_cell = f"{exit_code} {'✓' if exit_code == 0 else '✗'}"
|
|
|
|
if gitleaks_clean is None:
|
|
gitleaks_cell = "— not run"
|
|
elif gitleaks_clean:
|
|
gitleaks_cell = "✓ no secrets detected"
|
|
else:
|
|
gitleaks_cell = "✗ secrets detected"
|
|
|
|
if watchdog_fired:
|
|
done_cell = "watchdog — agent did not signal"
|
|
else:
|
|
done_cell = "sidecar `signal_done`"
|
|
|
|
lines = [
|
|
"<details><summary>🔬 Run provenance</summary>",
|
|
"",
|
|
"| Field | Value |",
|
|
"|---|---|",
|
|
f"| agent | `{agent_name}` |",
|
|
f"| bottle | {bottle_label} |",
|
|
f"| slug | `{slug}` |",
|
|
f"| started | {started_at} |",
|
|
f"| duration | {_format_duration(started_at, finished_at)} |",
|
|
f"| exit | {exit_cell} |",
|
|
f"| gitleaks | {gitleaks_cell} |",
|
|
f"| done signal | {done_cell} |",
|
|
]
|
|
|
|
if egress_routes:
|
|
lines.append("")
|
|
lines.append(
|
|
f"**Egress** (deny-by-default; {len(egress_routes)} "
|
|
f"route{'s' if len(egress_routes) != 1 else ''} allowed)"
|
|
)
|
|
for route in egress_routes:
|
|
lines.append(f"- {route}")
|
|
|
|
lines.append("")
|
|
lines.append("</details>")
|
|
return "\n".join(lines)
|