Files
codex-app-server/cloud/audit_loop_wiring.py
T
host 3eac2a702e audit: wire loop driver to server endpoints and release gate (track 2)
Server-side execution for the track-1 pure driver (#260), no driver
semantics changed: audit verdict / fix completion / correction /
rebuild confirmation endpoints persist LoopState in audit_loop_state,
write every step through the #241/#247 ledger path (ledger first,
state second, fail-closed), derive fix/rebuild subtasks, and block
merge/release via the existing #247 chain reader. POST
/tasks/{id}/release follows the #259 contract (global-admin write,
fresh hard-gate evidence, merged status, multi-source 409 with no
audit write on block); deploy.py stays outside task gates.
2026-11-03 02:49:25 +00:00

955 lines
43 KiB
Python

"""Server-side wiring for the project-level audit loop (track 2).
This module is the ONLY place that connects the track-1 pure driver
(``cloud/audit_loop_driver.py``, PR #260) to durable server state.
It changes no driver semantics and it does not re-implement the
driver, the #247 gate reader, or the #241 hash chain:
- Decisions come from the driver, verbatim:
``decide_audit`` / ``after_fix`` / ``apply_correction`` /
``confirm_rebuild``. This module never recomputes a pushback
round, a failure count, or a rebuild budget; every number it
persists comes from ``Action.next_state``.
- Merge blocking stays exactly the #247 reader
(``audit_wiring.pushback_block_for_task``) ANDed with the five
hard-gate checks in ``server._merge_task``. This module does NOT
add a second gate reader for merge: the loop-state table below
is driver state, not a gate. If chain and loop state ever
diverged (only possible if a caller wrote state without going
through this module), merge follows the chain, because the
chain is the auditable record. The normal flow cannot diverge:
``process_*`` writes the ledger FIRST and only then saves the
state, so a ledger failure leaves the old state untouched.
- Ledger writes go through the #241/#247 layer only
(``audit_wiring.record_*`` over a ``LedgerStore``-shaped store).
No second write path is invented here.
- Quota and cost are caller-injected, as the driver requires.
The quota adapter below follows the existing server quota
model (``server._quota_status``: a USD-micros budget per user
per month, ``used >= quota`` blocks, ``None`` means uncapped);
it does not invent a second quota table. #212's design口径 is
the server's existing ``user_quotas`` table, reused as-is.
- ``evaluate_feedback`` / the #238 metrics loop is untouched:
it stays ``auto_apply: False``. Nothing in this module feeds
loop outcomes back into the standard documents, and no
threshold in this module loosens or tightens a standard.
- ``deploy.py`` is NOT in any task gate here. It deploys the
platform itself, has no task id, and does not go through
server HTTP auth; binding it to a task chain would lock
deployment exactly when a fix or rollback needs deploying.
The release endpoint below is a gate + authorization for a
release, not a deployment runner.
Module mapping (task -> loop key), stated once and tested:
- The loop key is ``(module_id, contract_version)``, exactly as
the driver counts it.
- ``module_id``: a task's explicit ``module_id`` field when it
is a non-empty string, else its ``module`` field, else the
task id itself. Trade-off, stated not hidden: an explicit
module field lets several tasks share one loop (one module,
one counter), which is what a module-level loop means; the
task-id fallback isolates a task that names no module, so two
unrelated tasks can never silently share a counter. Callers
must not mix the two for the same module: once a module is
named, every verdict/fix for it must carry that name.
- ``contract_version``: the verdict's (or re-audit verdict's)
``contract_version`` when given, else the task's
``contract_version`` field. When neither exists the call
fails closed (ValueError) instead of inventing a version:
an invented version would merge counters across versions.
- Loop state is persisted in table ``audit_loop_state`` keyed
by ``(module_id, contract_version)`` with the owning
``task_id`` recorded for traceability. It is NOT memory-only:
a server restart reloads the same counters, rounds, rebuild
budget, targeted budget, and pending confirmation from this
table. The table is created idempotently by
``ensure_loop_state_table``.
Automatic chain (what "automatic" means here, precisely):
- "Task completed -> independent audit -> fail auto-derives a
fix subtask and blocks merge/release (original task stays
``completed``, it is NOT rolled back to a re-run)" is
``process_audit_verdict``: the driver decides, the ledger is
written, the state is saved, and a fix/rebuild subtask
descriptor is returned. The server handler turns the
descriptor into a real queued task row; this module itself
creates no task rows (it has no HTTP/task-row layer).
- "Fix completed -> automatic full re-audit" is
``process_fix_completion``: the fix-completion event itself
advances the loop through the driver's ``after_fix``, which
requires a FULL re-audit verdict. The automated checks run
here (hard gate five + directory/contract + demo/runnable,
regression, security, signer-separation, QA-link re-runs)
are EVIDENCE for that re-audit, never the verdict: if any
automated check fails, a claimed pass is forced to fail
before it reaches the driver; if they all pass, the loop
still waits for the auditor-submitted verdict, and a
missing verdict or ``diff_only`` is refused with ValueError
by the driver. Automated-checks-passing is NEVER reported
as audit-passing.
- Subjective audit conclusions still come from the auditor:
this module accepts verdicts, it does not invent them.
No verdict is fabricated anywhere in this file.
Release gate (``evaluate_release_gate``) follows the #259
contract: re-read the SAME #247 reader at release time, re-run
the hard gate on caller-supplied FRESH evidence (a merge-time
200 is never an input), and require task status ``merged``.
All three are ANDed. ``blocked_sources`` lists every blocking
source when several block at once, except a chain-read failure,
which short-circuits per #259 (no other reading is trustworthy
then). A 409 from the endpoint writes NO audit record, because
a check writing to the chain would move the chain head and
invalidate an anchor taken before the check.
Confirmation rule (acceptance-triggered rebuild):
- ``process_rebuild_confirmation`` never auto-confirms. It
requires an explicit ``confirmed_by`` that is a real user id:
the reserved markers ``"system"`` and ``"coordinator"`` are
refused with ValueError, and the server endpoint additionally
requires the request body to carry ``user_confirmed: true``
AND ``confirmed_by`` equal to the authenticated caller.
No code path in this module or in the server handlers calls
confirmation on behalf of a user. A rejection escalates to
the coordinator and consumes no rebuild budget (driver).
Pending and confirmed are two chain rows, never an edit.
Confirmation is NOT G6 acceptance: the user still holds
final acceptance afterwards, and there is no
``tick_acceptance`` anywhere in this wiring.
"""
import json
import os
import time
from cloud import audit_loop_driver as _driver
from cloud import audit_wiring as _wiring
from cloud.merge_gate_hard import evaluate_hard_merge_gate as _hard_gate
# ------------------------------------------------------------------
# Mapping: task / verdict -> loop key
# ------------------------------------------------------------------
_RESERVED_CONFIRMERS = ("system", "coordinator", "")
# Automated re-audit evidence: extra checks beyond the hard five.
# Each must be exactly True; missing or non-True is a failure,
# never a skip. These names are the driver's FULL_REAUDIT_CRITERIA
# minus the five hard-gate items, plus the QA re-run check.
_AUTOMATED_EXTRA_CHECKS = (
"security_unconditional",
"regression_exact_artifact",
"demo_runnable",
"known_deviation_due_todo",
"audit_signer_separation",
"qa_failed_links_rerun",
)
def resolve_module_id(task, explicit=None):
"""Task -> module_id. Explicit argument wins, then the task's
own module fields, then the task id (isolated fallback).
See the module docstring for the trade-off."""
if isinstance(explicit, str) and explicit.strip():
return explicit.strip()
if isinstance(task, dict):
for key in ("module_id", "module"):
value = task.get(key)
if isinstance(value, str) and value.strip():
return value.strip()
task_id = task.get("id")
if isinstance(task_id, str) and task_id.strip():
return task_id.strip()
raise ValueError("cannot resolve module_id: no module field "
"and no task id")
def resolve_contract_version(task, verdict=None):
"""Task/verdict -> contract_version. Verdict wins when given
(a version change is how the driver resets counters), else
the task's field. Neither -> ValueError, never an invented
version."""
if isinstance(verdict, dict):
version = verdict.get("contract_version")
if isinstance(version, str) and version.strip():
return version.strip()
if isinstance(task, dict):
version = task.get("contract_version")
if isinstance(version, str) and version.strip():
return version.strip()
raise ValueError("cannot resolve contract_version: neither the "
"verdict nor the task carries one")
def task_project_id(task):
"""Project id for ledger rows (empty string when unknown;
the ledger layer, not this module, decides whether that is
acceptable for a given row type)."""
if isinstance(task, dict):
project_id = task.get("project_id")
if isinstance(project_id, str):
return project_id
return ""
def task_id_of(task):
if isinstance(task, dict):
return task.get("id")
return None
# ------------------------------------------------------------------
# Loop-state persistence (table audit_loop_state)
# ------------------------------------------------------------------
def ensure_loop_state_table(conn):
conn.execute(
"CREATE TABLE IF NOT EXISTS audit_loop_state("
"module_id TEXT NOT NULL, "
"contract_version TEXT NOT NULL, "
"task_id TEXT, "
"state_json TEXT NOT NULL, "
"updated_at REAL, "
"PRIMARY KEY(module_id, contract_version))")
try:
conn.commit()
except Exception:
pass
def state_to_dict(state):
return {
"module_id": state.module_id,
"contract_version": state.contract_version,
"pushback_round": state.pushback_round,
"failure_count": state.failure_count,
"audit_fail_count": state.audit_fail_count,
"acceptance_fail_count": state.acceptance_fail_count,
"rebuild_no": state.rebuild_no,
"targeted_used": state.targeted_used,
"status": state.status,
"pending_confirmation": state.pending_confirmation,
"retained_tests": list(state.retained_tests or []),
"retained_passed_items": list(state.retained_passed_items or []),
"previous_artifacts": list(state.previous_artifacts or []),
"history": list(state.history or []),
}
def state_from_dict(data):
if not isinstance(data, dict):
raise ValueError("loop state must be a dict")
return _driver.LoopState(
module_id=data["module_id"],
contract_version=data["contract_version"],
pushback_round=int(data.get("pushback_round") or 0),
failure_count=int(data.get("failure_count") or 0),
audit_fail_count=int(data.get("audit_fail_count") or 0),
acceptance_fail_count=int(data.get("acceptance_fail_count") or 0),
rebuild_no=int(data.get("rebuild_no") or 0),
targeted_used=bool(data.get("targeted_used")),
status=data.get("status") or "auditing",
pending_confirmation=bool(data.get("pending_confirmation")),
retained_tests=list(data.get("retained_tests") or []),
retained_passed_items=list(
data.get("retained_passed_items") or []),
previous_artifacts=list(data.get("previous_artifacts") or []),
history=list(data.get("history") or []),
)
def load_loop_state(conn, module_id, contract_version):
"""Load persisted state for (module_id, contract_version),
or None when the loop has never started. Numbers are never
taken from a caller: they come from this row, or from a
fresh driver state."""
ensure_loop_state_table(conn)
row = conn.execute(
"SELECT state_json FROM audit_loop_state"
" WHERE module_id = ? AND contract_version = ?",
(module_id, contract_version)).fetchone()
if row is None:
return None
raw = row[0]
return state_from_dict(json.loads(raw))
def save_loop_state(conn, task_id, state):
"""Upsert by (module_id, contract_version) using a portable
select-then-write (no dialect-specific upsert syntax, so the
same code works on the SQLite and Postgres adapters)."""
ensure_loop_state_table(conn)
payload = json.dumps(state_to_dict(state), sort_keys=True,
ensure_ascii=False)
existing = conn.execute(
"SELECT module_id FROM audit_loop_state"
" WHERE module_id = ? AND contract_version = ?",
(state.module_id, state.contract_version)).fetchone()
if existing is None:
conn.execute(
"INSERT INTO audit_loop_state(module_id,"
" contract_version, task_id, state_json, updated_at)"
" VALUES(?, ?, ?, ?, ?)",
(state.module_id, state.contract_version, task_id,
payload, time.time()))
else:
conn.execute(
"UPDATE audit_loop_state SET task_id = ?,"
" state_json = ?, updated_at = ?"
" WHERE module_id = ? AND contract_version = ?",
(task_id, payload, time.time(),
state.module_id, state.contract_version))
conn.commit()
# ------------------------------------------------------------------
# Quota / cost injection (server quota model, no new table)
# ------------------------------------------------------------------
def build_quota_check(quota_usd_micros, used_usd_micros):
"""Server quota -> driver quota_check callable.
``quota_usd_micros`` is None for uncapped users (always
allowed). Otherwise an estimate (USD float, as the driver
carries it) blocks when used + estimate would exceed the
budget; with no estimate the existing server rule applies
(used >= quota blocks). Any lookup problem must surface as
a raising callable: the driver fail-closes a raising check
to quota_blocked, it never treats it as allowed.
"""
def _check(estimate):
if quota_usd_micros is None:
return True
if estimate is None:
return used_usd_micros < quota_usd_micros
try:
estimate_micros = int(float(estimate) * 1000000)
except (TypeError, ValueError):
return False
return used_usd_micros + estimate_micros <= quota_usd_micros
return _check
def cost_threshold_from_env(env=None):
"""User-set rebuild cost-escalation threshold in USD, from
CLOUD_AUDIT_LOOP_COST_THRESHOLD_USD. Unset / unparsable
means None: no cost escalation, only quota checks. The
threshold is never hard-coded here; an operator sets it."""
raw = (env if env is not None else os.environ).get(
"CLOUD_AUDIT_LOOP_COST_THRESHOLD_USD", "")
raw = (raw or "").strip()
if not raw:
return None
try:
value = float(raw)
except ValueError:
return None
return value if value >= 0 else None
# ------------------------------------------------------------------
# Automated re-audit evidence (never a verdict by itself)
# ------------------------------------------------------------------
def evaluate_automated_reaudit(hard_gate_kwargs, extra_checks):
"""Run the automatable part of a full re-audit.
``hard_gate_kwargs`` are the kwargs for
``evaluate_hard_merge_gate`` (fresh evidence, supplied by
the caller at fix-completion time). ``extra_checks`` maps
each _AUTOMATED_EXTRA_CHECKS name to a boolean supplied by
the caller (demo run result, regression artifact comparison,
and so on). Missing evidence fails, it never passes by
default. Returns {automated_checks_pass, failed_checks,
hard_gate}. This result is evidence only: see
process_fix_completion for how a failed automated check
forces a claimed pass to fail, and how passing automated
checks still require an auditor verdict.
"""
failed = []
gate = None
if not isinstance(hard_gate_kwargs, dict):
failed.append("hard_gate_evidence_missing")
else:
# Vocabulary translation at the boundary: merge bodies
# carry "branch_protection", the hard-gate function
# takes "protection". Anything else is passed through
# untouched (unknown kwargs still fail loudly, which
# is the honest outcome for malformed evidence).
kwargs = dict(hard_gate_kwargs)
if "branch_protection" in kwargs:
kwargs.setdefault("protection", kwargs.pop("branch_protection"))
gate = _hard_gate(**kwargs)
if not gate.get("allowed"):
failed.extend("hard_gate:%s" % code
for code in (gate.get("blocks") or []))
checks = extra_checks if isinstance(extra_checks, dict) else {}
for name in _AUTOMATED_EXTRA_CHECKS:
if checks.get(name) is not True:
failed.append(name)
return {"automated_checks_pass": not failed,
"failed_checks": failed, "hard_gate": gate}
# ------------------------------------------------------------------
# Ledger sink: driver events -> #241/#247 write path, fail-closed
# ------------------------------------------------------------------
def _prevalidate_ledger_events(action, old_impl_ref):
"""Refuse, BEFORE writing anything, the two driver-event
shapes the #241 layer cannot store: a rebuild event with
no old-implementation reference, and a meta-review event
with no chain entry id. Writing the verdict row first and
then failing on the rebuild row would leave a half-written
round on the chain; fail-closed means refusing the whole
step up front. The old_impl_ref itself is caller-supplied
(a real commit/tag when known, else a task-scoped archive
reference); this module never invents a commit hash."""
for event in (action.ledger_events or []):
payload = event.get("payload") or {}
if event.get("event_type") == "audit_rebuild":
ref = payload.get("old_impl_ref") or old_impl_ref
if not isinstance(ref, dict) or not any(
isinstance(ref.get(key), str) and ref[key]
for key in ("commit", "tag", "archive_ref")):
raise ValueError(
"rebuild ledger event needs old_impl_ref "
"(commit/tag/archive_ref) from the caller; "
"refusing to write a partial round")
if event.get("event_type") == "audit_meta_review":
entry_id = payload.get("original_entry_id")
if (isinstance(entry_id, bool)
or not isinstance(entry_id, int)
or entry_id < 1):
raise ValueError(
"meta-review ledger event needs "
"original_entry_id (chain id >= 1)")
def write_loop_ledger_events(ledger_store, task_id, project_id,
action, old_impl_ref=None,
user_id="system"):
"""Write one Action's ledger events + compat hint through
the #247 record_* functions, in driver order. Any failure
propagates (fail-closed): the caller must NOT save the new
loop state after this raises. Unknown event types raise
instead of being silently skipped."""
_prevalidate_ledger_events(action, old_impl_ref)
written = []
for event in (action.ledger_events or []):
event_type = event.get("event_type")
payload = event.get("payload") or {}
# The driver's meta-review event carries no module key
# (its field table does not require one); the loop key
# for the chain row then comes from next_state, which
# is the same loop the correction acted on. Other
# events carry their own key and are left untouched.
module_id = payload.get("module_id") \
or action.next_state.module_id
contract_version = payload.get("contract_version") \
or action.next_state.contract_version
if event_type == "audit_verdict":
written.append(_wiring.record_audit_verdict(
ledger_store, task_id, project_id, module_id,
contract_version, payload.get("verdict"),
payload.get("source"),
failures=payload.get("failures"),
user_id=user_id, extra=payload.get("extra")))
elif event_type == "audit_pushback_round":
trigger = _wiring._trigger_check_names(
payload.get("trigger_items"))
if not trigger:
# A fail verdict with no itemised failures still
# happened; the generic code names that fact
# instead of inventing a specific check.
trigger = ["audit_verdict_not_pass"]
if payload.get("round_status") == "re_reviewed":
written.append(_wiring.record_reaudit(
ledger_store, task_id, project_id, module_id,
contract_version, payload.get("round_no"),
payload.get("re_review_result"),
package=payload.get("pushback_package"),
failure_source=payload.get("failure_source"),
failure_counts=payload.get("failure_counts"),
user_id=user_id))
else:
written.append(_wiring.record_pushback(
ledger_store, task_id, project_id, module_id,
contract_version, payload.get("round_no"),
trigger, payload.get("pushback_package"),
failure_source=payload.get("failure_source"),
failure_counts=payload.get("failure_counts"),
user_id=user_id))
elif event_type == "audit_rebuild":
root_cause = payload.get("root_cause")
# Driver emits "implementation"; the #247 layer maps
# contract/design to contract_design, so a
# contract_design payload is handed over as
# "contract" (the layer's own vocabulary).
if root_cause == "contract_design":
root_cause = "contract"
written.append(_wiring.record_rebuild(
ledger_store, task_id, project_id, module_id,
contract_version, payload.get("rebuild_no"),
payload.get("old_impl_ref") or old_impl_ref,
root_cause=root_cause or "implementation",
status=payload.get("status") or "decided",
retained_assets=payload.get("retained_assets"),
trigger_source=payload.get("trigger_source"),
user_id=user_id))
elif event_type == "audit_third_failure_assessment":
# Translation at the boundary (driver field vs #247
# API): the driver emits the #241 determination
# ("continuation_round" / "early_rebuild"), while
# #247's record_* takes the triage verdict
# ("targeted_fix" / "early_rebuild") and maps it to
# the same determination on the chain. Translating
# here is this layer's job; the driver is untouched.
triage_verdict = {
"continuation_round": "targeted_fix",
"early_rebuild": "early_rebuild",
}.get(payload.get("determination"))
if triage_verdict is None:
raise ValueError(
"unknown third-failure determination: %r"
% (payload.get("determination"),))
written.append(_wiring.record_third_failure_assessment(
ledger_store, task_id, project_id, module_id,
contract_version, triage_verdict,
payload.get("basis"),
failure_total=payload.get("failure_total") or 3,
max_extra_rounds=payload.get("max_extra_rounds"),
user_id=user_id))
elif event_type == "audit_test_change":
changes = payload.get("test_changes") or {}
written.append(_wiring.record_test_change(
ledger_store, task_id, project_id, module_id,
contract_version, changes,
payload.get("auditor") or user_id,
bool(payload.get("contract_refrozen")),
refreeze_ref=payload.get("refreeze_ref"),
reason=payload.get("reason") or "",
user_id=user_id))
elif event_type == "audit_meta_review":
written.append(_wiring.record_meta_review(
ledger_store, task_id, project_id, module_id,
contract_version, payload.get("original_entry_id"),
payload.get("original_verdict"),
payload.get("corrected_verdict"),
bool(payload.get("misjudgment_found")),
payload.get("reviewer"),
findings=payload.get("findings"),
triggered_process=payload.get("triggered_process"),
user_id=user_id))
elif event_type == "audit_rebuild_notice":
written.append(_wiring.record_rebuild_notice(
ledger_store, task_id, project_id, module_id,
contract_version, payload.get("rebuild_no"),
payload.get("notification_content"),
payload.get("notified_party"),
payload.get("confirmation_status"),
confirmed_by=payload.get("confirmed_by"),
user_id=user_id))
else:
raise ValueError(
"unknown loop ledger event type: %r" % (event_type,))
hint = getattr(action, "compat_hint", None)
if hint == "audit_wiring_event:blocked_quota":
written.append(_wiring.record_escalation(
ledger_store, task_id, project_id,
action.next_state.module_id,
action.next_state.contract_version,
"blocked_quota", reason=action.reason,
user_id=user_id))
elif hint == "audit_wiring_event:escalate":
written.append(_wiring.record_escalation(
ledger_store, task_id, project_id,
action.next_state.module_id,
action.next_state.contract_version,
"escalate", target=action.escalation_target,
reason=action.reason, user_id=user_id))
elif hint == "audit_wiring_event:escalate_refreeze":
written.append(_wiring.record_escalation(
ledger_store, task_id, project_id,
action.next_state.module_id,
action.next_state.contract_version,
"escalate_refreeze",
target=action.escalation_target,
reason=action.reason, user_id=user_id))
return written
def derive_subtask(action, parent_task):
"""Driver Action -> fix/rebuild subtask descriptor, or None.
This is a descriptor only: the server handler turns it into
a queued task row (same project, created_by = the parent
task's creator for cost attribution). No subtask is derived
for pass / escalate / refreeze / pending confirmation /
quota_blocked: a pending acceptance rebuild must not create
a rebuild task before the user confirms, and an escalation
goes to a human, not to an auto-created task.
A correction to pass is the one exception inside kind
"pushback": it schedules a re-audit only, so its
descriptor is kind "reaudit", never a fix task.
"""
if getattr(action, "correction_action", None) == "schedule_reaudit":
return {
"kind": "reaudit",
"parent_task_id": task_id_of(parent_task),
"module_id": action.next_state.module_id,
"contract_version": action.next_state.contract_version,
"scope": "reaudit_only",
"pushback_package": None,
"requires_reaudit": True,
}
if action.kind == "pushback":
return {
"kind": "fix",
"parent_task_id": task_id_of(parent_task),
"module_id": action.next_state.module_id,
"contract_version": action.next_state.contract_version,
"scope": action.scope,
"targeted_items": action.targeted_items,
"pushback_package": action.pushback_package,
"requires_reaudit": True,
}
if action.kind == "rebuild":
return {
"kind": "rebuild",
"parent_task_id": task_id_of(parent_task),
"module_id": action.next_state.module_id,
"contract_version": action.next_state.contract_version,
"scope": "rebuild",
"pushback_package": action.pushback_package,
"requires_reaudit": True,
}
return None
def derive_notification(action):
"""Notification/escalation descriptor for an Action, or
None. Escalations carry the FULL round history and the
caller-supplied cost estimate (never just the last round);
a pending acceptance rebuild carries the driver's notice
content (failure reasons, rebuild scope, retained assets,
first gate)."""
if action.kind == "pending_user_confirmation":
content = None
for event in (action.ledger_events or []):
if event.get("event_type") == "audit_rebuild_notice":
content = event.get("payload", {}).get(
"notification_content")
return {"party": "user", "content": content,
"rebuild_no": action.rebuild_no,
"confirmation_status": "pending"}
if action.kind == "escalate":
return {"party": action.escalation_target,
"reason": action.reason,
"escalation_history": action.escalation_history,
"cost_estimate": action.cost_estimate,
"grants_user_acceptance": False}
return None
def _finish_process(conn, ledger_store, task, action,
old_impl_ref=None, user_id="system",
ledger_task_id=None):
"""Ledger rows are attributed to ``ledger_task_id`` (the
loop's root task) when given, else to the task passed in.
This matters for fix completions: the #247 reader replays
ONE task's chain, so a passing re-audit written under a
transient fix-subtask id would never unblock the parent
task's merge. The server handler therefore passes the root
task id (following forked_from) for every loop call; the
loop-state row itself still records the calling task id
for traceability.
Shared tail of the process_* functions: ledger first
(fail-closed, raises before any state save), then persist,
then derive the subtask/notification descriptors. A
quota_blocked action is the exception that proves the
ledger-first rule: its state is returned unchanged, so it
is deliberately NOT saved (saving an identical state would
still be harmless, but not saving is the auditable proof
that no round and no rebuild was consumed)."""
ledger_id = ledger_task_id or task_id_of(task)
if action.kind == "quota_blocked":
# Only the compat wiring row is written; the loop state
# is returned exactly as the driver returned it.
write_loop_ledger_events(ledger_store, ledger_id,
task_project_id(task), action,
old_impl_ref=old_impl_ref,
user_id=user_id)
return {
"action_kind": action.kind,
"reason": action.reason,
"next_state": action.next_state,
"merge_blocked": action.merge_blocked,
"release_blocked": action.release_blocked,
"subtask": None,
"notification": None,
"requires_reaudit": action.requires_reaudit,
"escalation_grants_user_acceptance": False,
"state_saved": False,
}
write_loop_ledger_events(ledger_store, ledger_id,
task_project_id(task), action,
old_impl_ref=old_impl_ref,
user_id=user_id)
save_loop_state(conn, task_id_of(task), action.next_state)
return {
"action_kind": action.kind,
"reason": action.reason,
"next_state": action.next_state,
"merge_blocked": action.merge_blocked,
"release_blocked": action.release_blocked,
"subtask": derive_subtask(action, task),
"notification": derive_notification(action),
"requires_reaudit": action.requires_reaudit,
"escalation_grants_user_acceptance": False,
"state_saved": True,
}
def process_audit_verdict(conn, ledger_store, task, verdict, *,
quota_check=None, cost_threshold=None,
old_impl_ref=None, user_id="system",
ledger_task_id=None):
"""One independent audit/acceptance verdict -> loop step.
The original task's status is never touched here (the
server handler likewise does not change it): a fail blocks
merge/release via the chain and derives a fix subtask, it
does not roll the completed task back into a re-run."""
if not isinstance(verdict, dict):
raise TypeError("verdict must be a dict")
module_id = resolve_module_id(task)
contract_version = resolve_contract_version(task, verdict)
state = load_loop_state(conn, module_id, contract_version)
if state is None:
state = _driver.new_loop_state(module_id, contract_version)
action = _driver.decide_audit(
state, verdict, quota_check=quota_check,
cost_threshold=cost_threshold)
return _finish_process(conn, ledger_store, task, action,
old_impl_ref=old_impl_ref,
user_id=user_id,
ledger_task_id=ledger_task_id)
def process_fix_completion(conn, ledger_store, task, fix_result, *,
automated=None, quota_check=None,
cost_threshold=None, old_impl_ref=None,
user_id="system", ledger_task_id=None):
"""A fix landing -> automatic FULL re-audit.
``automated`` is {"hard_gate": {...}, "extra_checks": {...}}
or None (no automated evidence supplied: the re-audit then
rests entirely on the auditor verdict, and the caller must
say so in its own records -- this function never claims an
automated pass it did not run). When automated evidence IS
supplied and any check fails, a claimed pass verdict is
forced to fail with the automated failures appended, before
the driver ever sees it. The driver then enforces the rest:
a missing reaudit verdict or diff_only raises ValueError,
and only the re-audit verdict counts (the fix itself never
counts). The loop state must already exist: a fix with no
prior loop is refused rather than inventing a fresh loop
that a pass could silently clear.
"""
if not isinstance(fix_result, dict):
raise TypeError("fix_result must be a dict")
reaudit = (fix_result.get("reaudit_verdict")
or fix_result.get("reaudit")
or fix_result.get("verdict"))
module_id = resolve_module_id(task)
# Load by the task's own contract version first: that is
# the loop's current version, and the driver itself applies
# a version change carried by the re-audit verdict (reset).
# Loading by the verdict version first would mistake a
# version-change re-audit for a loop that never started.
contract_version = None
state = None
task_version = None
if isinstance(task, dict):
task_version = task.get("contract_version")
if isinstance(task_version, str) and task_version.strip():
state = load_loop_state(conn, module_id,
task_version.strip())
contract_version = task_version.strip()
if state is None:
contract_version = resolve_contract_version(task, reaudit)
state = load_loop_state(conn, module_id, contract_version)
if state is None:
raise ValueError(
"no audit loop state for module %s@%s: a fix cannot "
"re-audit a loop that never started" % (
module_id, contract_version))
automated_result = None
if automated is not None:
automated_result = evaluate_automated_reaudit(
automated.get("hard_gate"),
automated.get("extra_checks"))
if not automated_result["automated_checks_pass"]:
forced = dict(fix_result)
verdict = dict(reaudit) if isinstance(reaudit, dict) \
else {}
verdict["verdict"] = "fail"
failures = list(verdict.get("failures") or [])
failures.extend(
{"check": check,
"detail": "automated re-audit check failed"}
for check in automated_result["failed_checks"])
verdict["failures"] = failures
forced["reaudit_verdict"] = verdict
fix_result = forced
action = _driver.after_fix(
state, fix_result, quota_check=quota_check,
cost_threshold=cost_threshold)
result = _finish_process(conn, ledger_store, task, action,
old_impl_ref=old_impl_ref,
user_id=user_id,
ledger_task_id=ledger_task_id)
result["automated_checks"] = automated_result
return result
def process_correction(conn, ledger_store, task, correction, *,
user_id="system", ledger_task_id=None):
"""An audit-of-audit correction. Semantics are the driver's
(#247, unchanged): corrected-to-fail re-blocks at once and
counts a failure; corrected-to-pass does NOT unblock (it
schedules a re-audit, keeps merge/release blocked, zeroes
the effective failure count). The original chain row is
never edited: only the new meta-review row is written."""
if not isinstance(correction, dict):
raise TypeError("correction must be a dict")
module_id = resolve_module_id(task)
contract_version = resolve_contract_version(
task, {"contract_version": correction.get("contract_version")}
if correction.get("contract_version") else None)
state = load_loop_state(conn, module_id, contract_version)
if state is None:
state = _driver.new_loop_state(module_id, contract_version)
action = _driver.apply_correction(state, correction)
result = _finish_process(conn, ledger_store, task, action,
user_id=user_id,
ledger_task_id=ledger_task_id)
result["correction_action"] = action.correction_action
return result
def process_rebuild_confirmation(conn, ledger_store, task, *,
user_confirmed, confirmed_by,
quota_check=None,
cost_threshold=None,
cost_estimate=None,
old_impl_ref=None,
user_id="system",
ledger_task_id=None):
"""Resolve a pending acceptance-triggered rebuild.
``confirmed_by`` must be a real user id: "system" and
"coordinator" are refused here, and the server endpoint
additionally requires it to equal the authenticated caller
with an explicit user_confirmed body flag. Nothing in this
module calls this on a user's behalf."""
if (not isinstance(confirmed_by, str)
or confirmed_by.strip() in _RESERVED_CONFIRMERS):
raise ValueError(
"rebuild confirmation requires a real user id as "
"confirmed_by, not a system/coordinator marker")
module_id = resolve_module_id(task)
contract_version = resolve_contract_version(task)
state = load_loop_state(conn, module_id, contract_version)
if state is None:
raise ValueError(
"no audit loop state for module %s@%s" % (
module_id, contract_version))
action = _driver.confirm_rebuild(
state, user_confirmed=bool(user_confirmed),
quota_check=quota_check, cost_threshold=cost_threshold,
cost_estimate=cost_estimate)
return _finish_process(conn, ledger_store, task, action,
old_impl_ref=old_impl_ref,
user_id=user_id,
ledger_task_id=ledger_task_id)
# ------------------------------------------------------------------
# Release gate (#259 contract, pure evaluator)
# ------------------------------------------------------------------
def evaluate_release_gate(task_id, conn, *, task_status=None,
hard_gate=None):
"""Pure release decision. Inputs, in #259 evaluation order:
1. Chain read via the SAME #247 reader merge uses. Any
read failure (other than a never-written chain, which
the reader reports as unblocked) short-circuits to
release_blocked_state_unavailable.
2. Task status: must be "merged" (release_requires_merged).
3. Fresh hard gate: None means evidence missing
(release_hard_gate_evidence_missing); allowed=False
means release_blocked_hard_gate with blocks verbatim.
4. Audit pushback: reader blocked means
release_blocked_audit_pushback with failed_checks /
round / modules verbatim.
Returns {allowed, http_status, code, blocked_by,
blocked_sources, task_status, hard_gate, audit_pushback}.
``blocked_sources`` lists every blocking source when
several block (chain failure excepted, see above). This
evaluator never writes anything: no audit, no chain row,
no status change. The endpoint's 200/409 handling must
preserve that read-only property for the check itself;
any success record is an explicit post-check action, not
part of this evaluation.
"""
try:
pushback = _wiring.pushback_block_for_task(conn, task_id)
except Exception:
return {
"allowed": False, "http_status": 409,
"code": "release_blocked_state_unavailable",
"blocked_by": "audit_pushback_state_unavailable",
"blocked_sources": ["audit_pushback_state_unavailable"],
"task_status": task_status, "hard_gate": hard_gate,
"audit_pushback": None,
}
sources = []
first_code = None
first_by = None
def _add(code, by):
nonlocal first_code, first_by
sources.append(by)
if first_code is None:
first_code, first_by = code, by
if task_status != "merged":
_add("release_requires_merged", "task_status")
if hard_gate is None:
_add("release_hard_gate_evidence_missing", "hard_gate")
elif not hard_gate.get("allowed"):
_add("release_blocked_hard_gate", "hard_gate")
if pushback.get("merge_blocked") or pushback.get("release_blocked"):
_add("release_blocked_audit_pushback", "audit_pushback")
if not sources:
return {
"allowed": True, "http_status": 200,
"code": "release_allowed", "blocked_by": None,
"blocked_sources": [], "task_status": task_status,
"hard_gate": hard_gate, "audit_pushback": pushback,
}
return {
"allowed": False, "http_status": 409,
"code": first_code, "blocked_by": first_by,
"blocked_sources": sources, "task_status": task_status,
"hard_gate": hard_gate, "audit_pushback": pushback,
}