Route the sender lane's cluster writes through AUDIT_KUBECONFIG too.
Round-10 review (railiance-platform) found that ContainedOperator inherited an ssh-only kubectl, so an E2 would write OpenBao and then fail its sync, restart and rollback. Also: a bad AUDIT_KUBECONFIG now leaves window-error evidence, and the receipt script validates bodies and lets a later request-changes withdraw an approval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 236506@bnt-lap001 Assistant-Session: 09087018-91e1-48c5-ab2f-acfbb62c92e4
This commit is contained in:
parent
8d574be1bf
commit
dad91e8c44
6 changed files with 119 additions and 12 deletions
|
|
@ -31,6 +31,7 @@ import os
|
|||
import re
|
||||
import secrets
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
|
@ -280,6 +281,17 @@ class ContainedOperator(CREDS.Operator):
|
|||
def __init__(self, remote: str) -> None: # noqa: D107 - deliberately no super().__init__
|
||||
self.remote = remote
|
||||
self.bao_env = LEASE.contained_bao_env()
|
||||
# Same rule as PREFLIGHT.Remote: with AUDIT_KUBECONFIG set, cluster calls
|
||||
# (including the projection/cleanup sync and restart writes) run locally
|
||||
# against the attended admin kubeconfig; unset keeps ssh <remote> kubectl.
|
||||
self.kubeconfig = PREFLIGHT.attended_kubeconfig()
|
||||
|
||||
def kubectl(self, args: list[str], *, label: str, input_text: str | None = None,
|
||||
allow_missing: bool = False) -> subprocess.CompletedProcess[str]:
|
||||
if self.kubeconfig is None:
|
||||
return super().kubectl(args, label=label, input_text=input_text, allow_missing=allow_missing)
|
||||
return CREDS.safe_run(["kubectl", "--kubeconfig", self.kubeconfig, *args], label=label,
|
||||
input_text=input_text, allow_missing=allow_missing)
|
||||
|
||||
|
||||
def contained_operator(args: argparse.Namespace) -> Any:
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ The window is 15 minutes and starts now, or N minutes from now. Writing the
|
|||
receipt and running the window is the founder GO for that window; D5
|
||||
(2fd5849c) authorizes the exercise class only.
|
||||
|
||||
The owner acknowledgements are the owners' latest State Hub `approve` receipts
|
||||
for the *current* review contract
|
||||
The owner acknowledgements are the owners' State Hub receipts for the
|
||||
*current* review contract
|
||||
(interfaces/RAILIANCE-WP-0024-owner-reviews.json). They are found by subject
|
||||
`WP0024-OWNER-RECEIPT/v1/<owner>/approve/<contract digest>`. The script
|
||||
refuses when any pinned artifact differs from the contract, or when a
|
||||
required owner has no approval for this digest. The receipt holds no
|
||||
`WP0024-OWNER-RECEIPT/v1/<owner>/<decision>/<contract digest>`, each body
|
||||
validated by the review module. An owner's latest receipt must be `approve`.
|
||||
The script refuses when any pinned artifact differs from the contract, or when
|
||||
a required owner has no current approval for this digest. The receipt holds no
|
||||
credential values, and the script never overwrites an existing file.
|
||||
"""
|
||||
import importlib.util
|
||||
|
|
@ -42,12 +43,17 @@ def approvals(review, contract, api_base):
|
|||
messages = json.load(response)
|
||||
found = {}
|
||||
for owner in OWNERS:
|
||||
subject = f"{review.RECEIPT_PREFIX}/v1/{owner}/approve/{digest}"
|
||||
hits = sorted((m for m in messages if m.get("subject") == subject and m.get("from_agent") == owner),
|
||||
key=lambda m: m["created_at"])
|
||||
if not hits:
|
||||
sys.exit(f"no {owner} approval for contract {digest[:8]}; run the owner round first")
|
||||
found[owner] = {"acknowledged": True, "message_id": hits[-1]["id"]}
|
||||
# Validate each body with the review module's own parser (subject, sender,
|
||||
# schema). The latest receipt for this digest decides: a request-changes
|
||||
# after an approval withdraws it.
|
||||
receipts = []
|
||||
for message in messages:
|
||||
receipt = review.parse_receipt_message(message)
|
||||
if receipt and receipt["owner"] == owner and receipt["contract_digest"] == digest:
|
||||
receipts.append((receipt["created_at"], receipt["decision"], message["id"]))
|
||||
if not receipts or max(receipts)[1] != "approve":
|
||||
sys.exit(f"no current {owner} approval for contract {digest[:8]}; run the owner round first")
|
||||
found[owner] = {"acknowledged": True, "message_id": max(receipts)[2]}
|
||||
return digest, found
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -271,7 +271,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# so this script never prints; its outcome lives only in the evidence dir.
|
||||
try:
|
||||
window(args)
|
||||
except (OSError, KeyError, WindowError, LANE.LaneError, LANE.LEASE.ProcedureError) as exc:
|
||||
except (OSError, KeyError, WindowError, LANE.LaneError, LANE.LEASE.ProcedureError,
|
||||
LANE.PREFLIGHT.PreflightError) as exc:
|
||||
try:
|
||||
LANE.emit_result(
|
||||
{"status": "failed", "error": str(exc), "secret_values_observed": False},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue