feat(agent99): add digest-bound truth-chain index executor
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m54s
CD Pipeline / build-and-deploy (push) Successful in 15m28s
CD Pipeline / post-deploy-checks (push) Successful in 4m16s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m54s
CD Pipeline / build-and-deploy (push) Successful in 15m28s
CD Pipeline / post-deploy-checks (push) Successful in 4m16s
This commit is contained in:
425
ops/agent99-truth-chain-index-executor-overlay.py
Normal file
425
ops/agent99-truth-chain-index-executor-overlay.py
Normal file
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply the fixed truth-chain index set without accepting raw SQL."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.db.base import get_engine
|
||||
|
||||
|
||||
_SCHEMA_VERSION = "agent99_truth_chain_index_executor_v1"
|
||||
_COMMAND_ID = "truth_chain_structured_indexes_v1"
|
||||
_CANONICAL_ASSET = "database:awoooi:prod/truth-chain-structured-indexes"
|
||||
_MIGRATION_SHA256 = (
|
||||
"657089552ceed8de916d87135164b255c85edc154bf437f870b8c2bbdf14c509"
|
||||
)
|
||||
_SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")
|
||||
|
||||
_EXTENSION_OPERATIONS = (
|
||||
("pg_trgm", "CREATE EXTENSION IF NOT EXISTS pg_trgm"),
|
||||
("btree_gin", "CREATE EXTENSION IF NOT EXISTS btree_gin"),
|
||||
)
|
||||
|
||||
_INDEX_OPERATIONS = (
|
||||
(
|
||||
"idx_aol_incident_text_created",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_aol_incident_text_created
|
||||
ON automation_operation_log ((incident_id::text), created_at DESC)
|
||||
WHERE incident_id IS NOT NULL
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_aol_input_incident_created",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_aol_input_incident_created
|
||||
ON automation_operation_log ((input ->> 'incident_id'), created_at DESC)
|
||||
WHERE input ? 'incident_id'
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_aol_output_incident_created",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_aol_output_incident_created
|
||||
ON automation_operation_log ((output ->> 'incident_id'), created_at DESC)
|
||||
WHERE output ? 'incident_id'
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_aol_input_source_incidents_gin",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_aol_input_source_incidents_gin
|
||||
ON automation_operation_log
|
||||
USING GIN ((COALESCE(input #> '{source_refs,incident_ids}', '[]'::jsonb)))
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_aol_output_source_incidents_gin",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_aol_output_source_incidents_gin
|
||||
ON automation_operation_log
|
||||
USING GIN ((COALESCE(output #> '{source_refs,incident_ids}', '[]'::jsonb)))
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_outbound_source_incident_ids_gin",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_outbound_source_incident_ids_gin
|
||||
ON awooop_outbound_message
|
||||
USING GIN ((COALESCE(source_envelope #> '{source_refs,incident_ids}', '[]'::jsonb)))
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_outbound_source_code_refs_gin",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_outbound_source_code_refs_gin
|
||||
ON awooop_outbound_message
|
||||
USING GIN ((COALESCE(source_envelope #> '{source_refs,code_refs}', '[]'::jsonb)))
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_outbound_callback_incident_recent",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_outbound_callback_incident_recent
|
||||
ON awooop_outbound_message (
|
||||
project_id,
|
||||
(source_envelope #>> '{callback_reply,incident_id}'),
|
||||
queued_at DESC
|
||||
)
|
||||
WHERE source_envelope #>> '{callback_reply,incident_id}' IS NOT NULL
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_outbound_alert_incident_recent",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_outbound_alert_incident_recent
|
||||
ON awooop_outbound_message (
|
||||
project_id,
|
||||
(source_envelope #>> '{alert_notification,incident_id}'),
|
||||
queued_at DESC
|
||||
)
|
||||
WHERE source_envelope #>> '{alert_notification,incident_id}' IS NOT NULL
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_outbound_source_incident_recent",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_outbound_source_incident_recent
|
||||
ON awooop_outbound_message (
|
||||
project_id,
|
||||
(source_envelope #>> '{source_refs,incident_id}'),
|
||||
queued_at DESC
|
||||
)
|
||||
WHERE source_envelope #>> '{source_refs,incident_id}' IS NOT NULL
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_outbound_top_incident_recent",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_outbound_top_incident_recent
|
||||
ON awooop_outbound_message (
|
||||
project_id,
|
||||
(source_envelope ->> 'incident_id'),
|
||||
queued_at DESC
|
||||
)
|
||||
WHERE source_envelope ->> 'incident_id' IS NOT NULL
|
||||
""",
|
||||
),
|
||||
(
|
||||
"idx_outbound_project_content_preview_trgm",
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_outbound_project_content_preview_trgm
|
||||
ON awooop_outbound_message
|
||||
USING GIN (project_id, content_preview gin_trgm_ops)
|
||||
WHERE content_preview IS NOT NULL
|
||||
""",
|
||||
),
|
||||
)
|
||||
|
||||
_EXPECTED_EXTENSIONS = tuple(name for name, _ in _EXTENSION_OPERATIONS)
|
||||
_EXPECTED_INDEXES = tuple(name for name, _ in _INDEX_OPERATIONS)
|
||||
|
||||
_CATALOG_SQL = text(
|
||||
"""
|
||||
SELECT index_class.relname AS index_name,
|
||||
index_state.indisvalid,
|
||||
index_state.indisready
|
||||
FROM pg_catalog.pg_class AS index_class
|
||||
JOIN pg_catalog.pg_index AS index_state
|
||||
ON index_state.indexrelid = index_class.oid
|
||||
WHERE index_class.relname IN (
|
||||
'idx_aol_incident_text_created',
|
||||
'idx_aol_input_incident_created',
|
||||
'idx_aol_output_incident_created',
|
||||
'idx_aol_input_source_incidents_gin',
|
||||
'idx_aol_output_source_incidents_gin',
|
||||
'idx_outbound_source_incident_ids_gin',
|
||||
'idx_outbound_source_code_refs_gin',
|
||||
'idx_outbound_callback_incident_recent',
|
||||
'idx_outbound_alert_incident_recent',
|
||||
'idx_outbound_source_incident_recent',
|
||||
'idx_outbound_top_incident_recent',
|
||||
'idx_outbound_project_content_preview_trgm'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _normalized_sql(value: str) -> str:
|
||||
return re.sub(r"\s+", " ", value.strip()).lower()
|
||||
|
||||
|
||||
def _sha256(value: str) -> str:
|
||||
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _operation_digest() -> str:
|
||||
statements = [sql for _, sql in (*_EXTENSION_OPERATIONS, *_INDEX_OPERATIONS)]
|
||||
return _sha256("\n".join(_normalized_sql(sql) for sql in statements))
|
||||
|
||||
|
||||
def _safe_error(exc: BaseException) -> dict[str, Any]:
|
||||
database_error = getattr(exc, "orig", None)
|
||||
sqlstate = str(
|
||||
getattr(database_error, "sqlstate", None)
|
||||
or getattr(database_error, "pgcode", None)
|
||||
or ""
|
||||
)
|
||||
return {
|
||||
"error_class": exc.__class__.__name__,
|
||||
"database_error_class": (
|
||||
database_error.__class__.__name__ if database_error else None
|
||||
),
|
||||
"sqlstate": sqlstate or None,
|
||||
}
|
||||
|
||||
|
||||
async def _catalog(connection: Any) -> dict[str, Any]:
|
||||
extension_result = await connection.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT extname
|
||||
FROM pg_catalog.pg_extension
|
||||
WHERE extname IN ('pg_trgm', 'btree_gin')
|
||||
ORDER BY extname
|
||||
"""
|
||||
)
|
||||
)
|
||||
extensions = {
|
||||
str(row.get("extname") or "")
|
||||
for row in extension_result.mappings().all()
|
||||
}
|
||||
index_result = await connection.execute(_CATALOG_SQL)
|
||||
index_rows = list(index_result.mappings().all())
|
||||
present = {str(row.get("index_name") or "") for row in index_rows}
|
||||
valid_ready = {
|
||||
str(row.get("index_name") or "")
|
||||
for row in index_rows
|
||||
if row.get("indisvalid") and row.get("indisready")
|
||||
}
|
||||
missing_extensions = sorted(set(_EXPECTED_EXTENSIONS) - extensions)
|
||||
missing_indexes = sorted(set(_EXPECTED_INDEXES) - present)
|
||||
invalid_or_unready = sorted(present - valid_ready)
|
||||
return {
|
||||
"expected_extension_count": len(_EXPECTED_EXTENSIONS),
|
||||
"extension_count": len(extensions),
|
||||
"missing_extensions": missing_extensions,
|
||||
"expected_index_count": len(_EXPECTED_INDEXES),
|
||||
"present_index_count": len(present),
|
||||
"valid_ready_index_count": len(valid_ready),
|
||||
"missing_indexes": missing_indexes,
|
||||
"invalid_or_unready_indexes": invalid_or_unready,
|
||||
"passed": not missing_extensions
|
||||
and not missing_indexes
|
||||
and not invalid_or_unready,
|
||||
}
|
||||
|
||||
|
||||
async def _execute_fixed_operation(
|
||||
connection: Any,
|
||||
*,
|
||||
operation_type: str,
|
||||
name: str,
|
||||
sql: str,
|
||||
) -> dict[str, Any]:
|
||||
started = time.perf_counter()
|
||||
try:
|
||||
await connection.execute(text(sql))
|
||||
except Exception as exc:
|
||||
return {
|
||||
"type": operation_type,
|
||||
"name": name,
|
||||
"statement_sha256": _sha256(_normalized_sql(sql)),
|
||||
"duration_ms": round((time.perf_counter() - started) * 1000),
|
||||
"success": False,
|
||||
**_safe_error(exc),
|
||||
}
|
||||
return {
|
||||
"type": operation_type,
|
||||
"name": name,
|
||||
"statement_sha256": _sha256(_normalized_sql(sql)),
|
||||
"duration_ms": round((time.perf_counter() - started) * 1000),
|
||||
"success": True,
|
||||
"error_class": None,
|
||||
"database_error_class": None,
|
||||
"sqlstate": None,
|
||||
}
|
||||
|
||||
|
||||
async def _run(args: argparse.Namespace) -> dict[str, Any]:
|
||||
for identity in (args.trace_id, args.run_id, args.work_item_id):
|
||||
if not _SAFE_ID.fullmatch(identity):
|
||||
raise ValueError("identity_invalid")
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", args.source_revision):
|
||||
raise ValueError("source_revision_invalid")
|
||||
if args.migration_sha256 != _MIGRATION_SHA256:
|
||||
raise ValueError("migration_sha256_mismatch")
|
||||
|
||||
started = time.perf_counter()
|
||||
operations: list[dict[str, Any]] = []
|
||||
engine = get_engine()
|
||||
async with engine.connect() as raw_connection:
|
||||
connection = await raw_connection.execution_options(
|
||||
isolation_level="AUTOCOMMIT"
|
||||
)
|
||||
before = await _catalog(connection)
|
||||
if args.mode == "apply" and before["invalid_or_unready_indexes"]:
|
||||
after = before
|
||||
terminal = "blocked_invalid_or_unready_index"
|
||||
else:
|
||||
if args.mode == "apply":
|
||||
await connection.execute(text("SET lock_timeout = '15s'"))
|
||||
await connection.execute(text("SET statement_timeout = '10min'"))
|
||||
for name, sql in _EXTENSION_OPERATIONS:
|
||||
if name not in before["missing_extensions"]:
|
||||
continue
|
||||
operation = await _execute_fixed_operation(
|
||||
connection,
|
||||
operation_type="create_extension",
|
||||
name=name,
|
||||
sql=sql,
|
||||
)
|
||||
operations.append(operation)
|
||||
if not operation["success"]:
|
||||
break
|
||||
if not operations or operations[-1]["success"]:
|
||||
for name, sql in _INDEX_OPERATIONS:
|
||||
if name not in before["missing_indexes"]:
|
||||
continue
|
||||
operation = await _execute_fixed_operation(
|
||||
connection,
|
||||
operation_type="create_index_concurrently",
|
||||
name=name,
|
||||
sql=sql,
|
||||
)
|
||||
operations.append(operation)
|
||||
if not operation["success"]:
|
||||
break
|
||||
after = await _catalog(connection)
|
||||
failed_operation = any(not operation["success"] for operation in operations)
|
||||
if args.mode == "check":
|
||||
terminal = "check_ready"
|
||||
elif args.mode == "verify":
|
||||
terminal = "verified_healthy" if after["passed"] else "verification_failed"
|
||||
elif failed_operation:
|
||||
terminal = "apply_failed"
|
||||
else:
|
||||
terminal = (
|
||||
"apply_verified_local"
|
||||
if after["passed"]
|
||||
else "verification_failed"
|
||||
)
|
||||
|
||||
process_identity = ":".join(
|
||||
[socket.gethostname(), str(os.getpid()), args.run_id, args.mode]
|
||||
)
|
||||
write_attempted = bool(operations)
|
||||
writes_performed = any(operation["success"] for operation in operations)
|
||||
return {
|
||||
"schema_version": _SCHEMA_VERSION,
|
||||
"command_id": _COMMAND_ID,
|
||||
"canonical_asset": _CANONICAL_ASSET,
|
||||
"mode": args.mode,
|
||||
"trace_id": args.trace_id,
|
||||
"run_id": args.run_id,
|
||||
"work_item_id": args.work_item_id,
|
||||
"source_revision": args.source_revision,
|
||||
"migration_sha256": args.migration_sha256,
|
||||
"operation_digest_sha256": _operation_digest(),
|
||||
"pod_name": socket.gethostname(),
|
||||
"process_identity_sha256": _sha256(process_identity),
|
||||
"elapsed_ms": round((time.perf_counter() - started) * 1000),
|
||||
"catalog_before": before,
|
||||
"catalog_after": after,
|
||||
"operations": operations,
|
||||
"write_attempted": write_attempted,
|
||||
"writes_performed": writes_performed,
|
||||
"raw_sql_accepted": False,
|
||||
"raw_sql_emitted": False,
|
||||
"query_parameters_emitted": False,
|
||||
"secret_value_read": False,
|
||||
"arbitrary_command_allowed": False,
|
||||
"cross_domain_fallback_allowed": False,
|
||||
"verifier": {
|
||||
"passed": args.mode == "check" or bool(after["passed"]),
|
||||
"target_ready": bool(after["passed"]),
|
||||
},
|
||||
"terminal": terminal,
|
||||
}
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--mode", choices=("check", "apply", "verify"), required=True)
|
||||
parser.add_argument("--trace-id", required=True)
|
||||
parser.add_argument("--run-id", required=True)
|
||||
parser.add_argument("--work-item-id", required=True)
|
||||
parser.add_argument("--source-revision", required=True)
|
||||
parser.add_argument("--migration-sha256", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
application_output = io.StringIO()
|
||||
with redirect_stdout(application_output), redirect_stderr(application_output):
|
||||
receipt = asyncio.run(_run(_parse_args()))
|
||||
except Exception as exc:
|
||||
receipt = {
|
||||
"schema_version": _SCHEMA_VERSION,
|
||||
"command_id": _COMMAND_ID,
|
||||
"terminal": "failed",
|
||||
**_safe_error(exc),
|
||||
"write_attempted": False,
|
||||
"writes_performed": False,
|
||||
"raw_sql_accepted": False,
|
||||
"raw_sql_emitted": False,
|
||||
"query_parameters_emitted": False,
|
||||
"secret_value_read": False,
|
||||
"arbitrary_command_allowed": False,
|
||||
"cross_domain_fallback_allowed": False,
|
||||
}
|
||||
print(json.dumps(receipt, separators=(",", ":"), sort_keys=True))
|
||||
return 0 if receipt.get("terminal") in {
|
||||
"check_ready",
|
||||
"apply_verified_local",
|
||||
"verified_healthy",
|
||||
} else 3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
315
ops/agent99-truth-chain-index-manager.ps1
Normal file
315
ops/agent99-truth-chain-index-manager.ps1
Normal file
@@ -0,0 +1,315 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet("Check", "Apply")][string]$Mode = "Check",
|
||||
[Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$TraceId,
|
||||
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f-]{36}$")][string]$RunId,
|
||||
[Parameter(Mandatory = $true)][ValidatePattern("^[A-Za-z0-9][A-Za-z0-9._:@+-]{0,127}$")][string]$WorkItemId,
|
||||
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{40}$")][string]$SourceRevision,
|
||||
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{64}$")][string]$MigrationSha256,
|
||||
[Parameter(Mandatory = $true)][ValidatePattern("^[0-9a-f]{64}$")][string]$ExpectedOverlaySha256,
|
||||
[string]$StagedOverlayPath = "C:\Windows\Temp\Agent99\truth-chain-index-executor-overlay.py",
|
||||
[Parameter(Mandatory = $true)][string]$ResultPath,
|
||||
[string]$AgentRoot = "C:\Wooo\Agent99"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ProgressPreference = "SilentlyContinue"
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
|
||||
$CommandId = "truth_chain_structured_indexes_v1"
|
||||
$CanonicalAsset = "database:awoooi:prod/truth-chain-structured-indexes"
|
||||
$JumpHost = "192.168.0.110"
|
||||
$ControlPlaneHost = "192.168.0.120"
|
||||
$RemoteUser = "wooo"
|
||||
$Kubectl = "/usr/local/bin/kubectl"
|
||||
$Namespace = "awoooi-prod"
|
||||
$PodSelector = "app=awoooi-api,environment=prod,system=awoooi"
|
||||
$Container = "api"
|
||||
$IdentityFile = Join-Path $AgentRoot "keys\agent99_ed25519"
|
||||
$ApiPodPattern = "^awoooi-api-[a-z0-9][a-z0-9-]{0,126}$"
|
||||
$EvidenceRoot = [System.IO.Path]::GetFullPath((Join-Path $AgentRoot "evidence\"))
|
||||
$StagingRoot = [System.IO.Path]::GetFullPath("C:\Windows\Temp\Agent99\")
|
||||
$ResultFullPath = [System.IO.Path]::GetFullPath($ResultPath)
|
||||
$OverlayFullPath = [System.IO.Path]::GetFullPath($StagedOverlayPath)
|
||||
$startedAt = Get-Date
|
||||
$checkReceipt = $null
|
||||
$applyReceipt = $null
|
||||
$verifierReceipt = $null
|
||||
$executorPod = $null
|
||||
$verifierPod = $null
|
||||
$errorCode = $null
|
||||
$transportAnomalies = @()
|
||||
$transientCleanupPassed = $true
|
||||
$stagedOverlayDeleted = $false
|
||||
|
||||
if (-not $ResultFullPath.StartsWith($EvidenceRoot, [System.StringComparison]::OrdinalIgnoreCase)) { throw "result_path_outside_evidence" }
|
||||
if (-not $OverlayFullPath.StartsWith($StagingRoot, [System.StringComparison]::OrdinalIgnoreCase)) { throw "overlay_path_outside_staging" }
|
||||
|
||||
function Get-Sha256 {
|
||||
param([string]$Path)
|
||||
(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Write-AtomicJson {
|
||||
param([string]$Path, [object]$Value)
|
||||
[System.IO.Directory]::CreateDirectory((Split-Path -Parent $Path)) | Out-Null
|
||||
$temporaryPath = "$Path.tmp"
|
||||
[System.IO.File]::WriteAllText($temporaryPath, ($Value | ConvertTo-Json -Depth 20), [System.Text.UTF8Encoding]::new($false))
|
||||
Move-Item -LiteralPath $temporaryPath -Destination $Path -Force
|
||||
}
|
||||
|
||||
function Get-ReadyApiPods {
|
||||
$remoteCommand = (
|
||||
"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes " +
|
||||
"-o NumberOfPasswordPrompts=0 -o ConnectTimeout=8 -l $RemoteUser " +
|
||||
"$ControlPlaneHost sudo -n $Kubectl get pods " +
|
||||
"--namespace $Namespace --selector '$PodSelector' " +
|
||||
"--field-selector 'status.phase=Running' --output name"
|
||||
)
|
||||
$arguments = @(
|
||||
"-i", $IdentityFile,
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
"-o", "NumberOfPasswordPrompts=0",
|
||||
"-o", "ConnectTimeout=8",
|
||||
"-o", "ServerAliveInterval=20",
|
||||
"-o", "ServerAliveCountMax=3",
|
||||
"-l", $RemoteUser,
|
||||
$JumpHost,
|
||||
$remoteCommand
|
||||
)
|
||||
$token = [Guid]::NewGuid().ToString("N")
|
||||
$stdoutPath = Join-Path $env:TEMP "agent99-index-pods-$token.out"
|
||||
$stderrPath = Join-Path $env:TEMP "agent99-index-pods-$token.err"
|
||||
try {
|
||||
$process = Start-Process -FilePath "ssh.exe" -ArgumentList $arguments -NoNewWindow `
|
||||
-RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -PassThru
|
||||
if (-not $process.WaitForExit(120 * 1000)) {
|
||||
try { & taskkill.exe /PID $process.Id /T /F 2>&1 | Out-Null } catch {}
|
||||
throw "pod_inventory_timeout"
|
||||
}
|
||||
$process.WaitForExit() | Out-Null
|
||||
$process.Refresh()
|
||||
$exitCode = if ($null -ne $process.ExitCode) { [int]$process.ExitCode } else { 255 }
|
||||
$stdout = if (Test-Path -LiteralPath $stdoutPath -PathType Leaf) { [string](Get-Content -LiteralPath $stdoutPath -Raw) } else { "" }
|
||||
$pods = @()
|
||||
foreach ($line in @($stdout -split "`r?`n" | Where-Object { $_.Trim() })) {
|
||||
if ($line.Trim() -notmatch "^pod/(.+)$") { throw "pod_inventory_shape_invalid" }
|
||||
$podName = [string]$Matches[1]
|
||||
if ($podName -notmatch $ApiPodPattern) { throw "pod_inventory_shape_invalid" }
|
||||
$pods += $podName
|
||||
}
|
||||
$pods = @($pods | Sort-Object -Unique)
|
||||
if ($pods.Count -lt 2) { throw "two_ready_api_pods_required" }
|
||||
if ($exitCode -notin @(0, 255)) { throw "pod_inventory_transport_failed" }
|
||||
if ($exitCode -eq 255) {
|
||||
$script:transportAnomalies += [ordered]@{ stage = "pod_inventory"; exitCode = 255; acceptedBecause = "strict_pod_inventory_contract" }
|
||||
}
|
||||
return $pods
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
||||
if ((Test-Path -LiteralPath $stdoutPath) -or (Test-Path -LiteralPath $stderrPath)) { $script:transientCleanupPassed = $false }
|
||||
}
|
||||
}
|
||||
|
||||
function Test-StageReceipt {
|
||||
param([object]$Receipt, [string]$ExpectedMode, [string]$ExpectedPod)
|
||||
if (-not $Receipt) { return $false }
|
||||
$allowedTerminal = switch ($ExpectedMode) {
|
||||
"check" { [string]$Receipt.terminal -eq "check_ready" }
|
||||
"apply" { [string]$Receipt.terminal -eq "apply_verified_local" }
|
||||
"verify" { [string]$Receipt.terminal -eq "verified_healthy" }
|
||||
default { $false }
|
||||
}
|
||||
return [bool](
|
||||
[string]$Receipt.schema_version -eq "agent99_truth_chain_index_executor_v1" -and
|
||||
[string]$Receipt.command_id -eq $CommandId -and
|
||||
[string]$Receipt.canonical_asset -eq $CanonicalAsset -and
|
||||
[string]$Receipt.mode -eq $ExpectedMode -and
|
||||
[string]$Receipt.trace_id -eq $TraceId -and
|
||||
[string]$Receipt.run_id -eq $RunId -and
|
||||
[string]$Receipt.work_item_id -eq $WorkItemId -and
|
||||
[string]$Receipt.source_revision -eq $SourceRevision -and
|
||||
[string]$Receipt.migration_sha256 -eq $MigrationSha256 -and
|
||||
[string]$Receipt.pod_name -eq $ExpectedPod -and
|
||||
[string]$Receipt.process_identity_sha256 -match "^[0-9a-f]{64}$" -and
|
||||
[string]$Receipt.operation_digest_sha256 -match "^[0-9a-f]{64}$" -and
|
||||
$Receipt.raw_sql_accepted -eq $false -and
|
||||
$Receipt.raw_sql_emitted -eq $false -and
|
||||
$Receipt.query_parameters_emitted -eq $false -and
|
||||
$Receipt.secret_value_read -eq $false -and
|
||||
$Receipt.arbitrary_command_allowed -eq $false -and
|
||||
$Receipt.cross_domain_fallback_allowed -eq $false -and
|
||||
$Receipt.verifier.passed -eq $true -and
|
||||
$allowedTerminal
|
||||
)
|
||||
}
|
||||
|
||||
function Get-CatalogSignature {
|
||||
param([object]$Catalog)
|
||||
if (-not $Catalog) { return "" }
|
||||
[ordered]@{
|
||||
expectedExtensionCount = [int]$Catalog.expected_extension_count
|
||||
extensionCount = [int]$Catalog.extension_count
|
||||
missingExtensions = @($Catalog.missing_extensions)
|
||||
expectedIndexCount = [int]$Catalog.expected_index_count
|
||||
presentIndexCount = [int]$Catalog.present_index_count
|
||||
validReadyIndexCount = [int]$Catalog.valid_ready_index_count
|
||||
missingIndexes = @($Catalog.missing_indexes)
|
||||
invalidOrUnreadyIndexes = @($Catalog.invalid_or_unready_indexes)
|
||||
passed = [bool]$Catalog.passed
|
||||
} | ConvertTo-Json -Depth 6 -Compress
|
||||
}
|
||||
|
||||
function Invoke-FixedStage {
|
||||
param([ValidateSet("check", "apply", "verify")][string]$Stage, [string]$PodName)
|
||||
if ($PodName -notmatch $ApiPodPattern) { throw "target_pod_contract_failed" }
|
||||
$remoteCommand = (
|
||||
"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes " +
|
||||
"-o NumberOfPasswordPrompts=0 -o ConnectTimeout=8 -l $RemoteUser " +
|
||||
"$ControlPlaneHost sudo -n $Kubectl exec --stdin " +
|
||||
"--namespace $Namespace --container $Container $PodName -- " +
|
||||
"python -B - --mode $Stage --trace-id $TraceId --run-id $RunId " +
|
||||
"--work-item-id $WorkItemId --source-revision $SourceRevision " +
|
||||
"--migration-sha256 $MigrationSha256"
|
||||
)
|
||||
$arguments = @(
|
||||
"-i", $IdentityFile,
|
||||
"-o", "BatchMode=yes",
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "StrictHostKeyChecking=yes",
|
||||
"-o", "NumberOfPasswordPrompts=0",
|
||||
"-o", "ConnectTimeout=8",
|
||||
"-o", "ServerAliveInterval=20",
|
||||
"-o", "ServerAliveCountMax=3",
|
||||
"-l", $RemoteUser,
|
||||
$JumpHost,
|
||||
$remoteCommand
|
||||
)
|
||||
$token = [Guid]::NewGuid().ToString("N")
|
||||
$stdoutPath = Join-Path $env:TEMP "agent99-index-$Stage-$token.out"
|
||||
$stderrPath = Join-Path $env:TEMP "agent99-index-$Stage-$token.err"
|
||||
try {
|
||||
$process = Start-Process -FilePath "ssh.exe" -ArgumentList $arguments -NoNewWindow `
|
||||
-RedirectStandardInput $OverlayFullPath -RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath -PassThru
|
||||
$timeoutSeconds = if ($Stage -eq "apply") { 1800 } else { 180 }
|
||||
if (-not $process.WaitForExit($timeoutSeconds * 1000)) {
|
||||
try { & taskkill.exe /PID $process.Id /T /F 2>&1 | Out-Null } catch {}
|
||||
throw "stage_timeout:$Stage"
|
||||
}
|
||||
$process.WaitForExit() | Out-Null
|
||||
$process.Refresh()
|
||||
$exitCode = if ($null -ne $process.ExitCode) { [int]$process.ExitCode } else { 255 }
|
||||
try {
|
||||
$receipt = Get-Content -LiteralPath $stdoutPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
throw "stage_receipt_parse_failed:$Stage"
|
||||
}
|
||||
if (-not (Test-StageReceipt -Receipt $receipt -ExpectedMode $Stage -ExpectedPod $PodName)) { throw "stage_receipt_contract_failed:$Stage" }
|
||||
if ($exitCode -notin @(0, 255)) { throw "stage_transport_failed:$Stage" }
|
||||
if ($exitCode -eq 255) {
|
||||
$script:transportAnomalies += [ordered]@{ stage = $Stage; pod = $PodName; exitCode = 255; acceptedBecause = "strict_stage_receipt_contract" }
|
||||
}
|
||||
return $receipt
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
|
||||
if ((Test-Path -LiteralPath $stdoutPath) -or (Test-Path -LiteralPath $stderrPath)) { $script:transientCleanupPassed = $false }
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ($path in @($OverlayFullPath, $IdentityFile)) {
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "required_asset_missing:$([System.IO.Path]::GetFileName($path))" }
|
||||
}
|
||||
if ((Get-Sha256 $OverlayFullPath) -ne $ExpectedOverlaySha256) { throw "overlay_hash_mismatch" }
|
||||
$pods = Get-ReadyApiPods
|
||||
$executorPod = [string]$pods[0]
|
||||
$verifierPod = [string]$pods[1]
|
||||
$checkReceipt = Invoke-FixedStage -Stage "check" -PodName $executorPod
|
||||
if ($Mode -eq "Apply" -and -not $checkReceipt.catalog_after.passed) {
|
||||
$applyReceipt = Invoke-FixedStage -Stage "apply" -PodName $executorPod
|
||||
}
|
||||
$verifierStage = if ($Mode -eq "Apply") { "verify" } else { "check" }
|
||||
$verifierReceipt = Invoke-FixedStage -Stage $verifierStage -PodName $verifierPod
|
||||
} catch {
|
||||
$errorCode = [string]$_.Exception.Message
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $OverlayFullPath -Force -ErrorAction SilentlyContinue
|
||||
$stagedOverlayDeleted = -not (Test-Path -LiteralPath $OverlayFullPath)
|
||||
}
|
||||
|
||||
$independentVerifier = [bool](
|
||||
$checkReceipt -and $verifierReceipt -and
|
||||
[string]$checkReceipt.pod_name -ne [string]$verifierReceipt.pod_name -and
|
||||
[string]$checkReceipt.process_identity_sha256 -ne [string]$verifierReceipt.process_identity_sha256
|
||||
)
|
||||
$writesExpected = [bool]($Mode -eq "Apply" -and $checkReceipt -and -not $checkReceipt.catalog_after.passed)
|
||||
$applyContractPassed = [bool](
|
||||
-not $writesExpected -or
|
||||
($applyReceipt -and $applyReceipt.write_attempted -eq $true -and $applyReceipt.verifier.passed -eq $true)
|
||||
)
|
||||
$catalogAgreement = [bool](
|
||||
$checkReceipt -and $verifierReceipt -and
|
||||
(Get-CatalogSignature $checkReceipt.catalog_after) -eq
|
||||
(Get-CatalogSignature $verifierReceipt.catalog_after)
|
||||
)
|
||||
$targetReady = [bool]($verifierReceipt -and $verifierReceipt.catalog_after.passed -eq $true)
|
||||
$modeContractPassed = [bool](
|
||||
($Mode -eq "Check" -and $catalogAgreement) -or
|
||||
($Mode -eq "Apply" -and $targetReady)
|
||||
)
|
||||
$passed = [bool](
|
||||
-not $errorCode -and $checkReceipt -and $verifierReceipt -and
|
||||
$independentVerifier -and $applyContractPassed -and $modeContractPassed -and
|
||||
$transientCleanupPassed -and $stagedOverlayDeleted
|
||||
)
|
||||
$finishedAt = Get-Date
|
||||
$terminal = if ($passed) {
|
||||
if ($Mode -eq "Check") { "verified_check_no_write" }
|
||||
elseif ($writesExpected) { "runtime_verified_controlled_apply" }
|
||||
else { "runtime_verified_healthy_no_write" }
|
||||
} else {
|
||||
"degraded_with_safe_next_action"
|
||||
}
|
||||
$receipt = [ordered]@{
|
||||
schemaVersion = "agent99_truth_chain_index_manager_v1"
|
||||
traceId = $TraceId
|
||||
runId = $RunId
|
||||
workItemId = $WorkItemId
|
||||
generatedAt = $finishedAt.ToString("o")
|
||||
executor = "host:192.168.0.99"
|
||||
target = $CanonicalAsset
|
||||
sourceRevision = $SourceRevision
|
||||
migrationSha256 = $MigrationSha256
|
||||
dispatchPath = "windows99_digest_bound_stdin_to_host110_to_host120_to_two_api_pods"
|
||||
risk = [ordered]@{ level = if ($Mode -eq "Apply") { "medium" } else { "low" }; rawSqlAccepted = $false; arbitraryCommandAllowed = $false; secretValueRead = $false }
|
||||
check = [ordered]@{ overlaySha256 = $ExpectedOverlaySha256; receipt = $checkReceipt }
|
||||
execution = [ordered]@{ mode = $Mode; performed = $writesExpected; receipt = $applyReceipt; errorCode = $errorCode; elapsedMs = [math]::Round(($finishedAt - $startedAt).TotalMilliseconds) }
|
||||
verifier = [ordered]@{ receipt = $verifierReceipt; independent = $independentVerifier; catalogAgreement = $catalogAgreement; targetReady = $targetReady; passed = $passed }
|
||||
cleanup = [ordered]@{ stagedOverlayDeleted = $stagedOverlayDeleted; transientOutputDeleted = $transientCleanupPassed }
|
||||
rollback = [ordered]@{ automatic = $false; sourceAsset = "awooop_truth_chain_structured_reference_indexes_2026-07-17_down.sql" }
|
||||
transportAnomalies = @($transportAnomalies)
|
||||
telegramNotificationSent = $false
|
||||
kmRagWritebackPerformed = $false
|
||||
terminal = $terminal
|
||||
}
|
||||
Write-AtomicJson -Path $ResultFullPath -Value $receipt
|
||||
|
||||
[ordered]@{
|
||||
resultPath = $ResultFullPath
|
||||
resultSha256 = Get-Sha256 $ResultFullPath
|
||||
terminal = $terminal
|
||||
writesExpected = $writesExpected
|
||||
writeCount = if ($applyReceipt) { @($applyReceipt.operations).Count } else { 0 }
|
||||
validReadyIndexCount = if ($verifierReceipt) { [int]$verifierReceipt.catalog_after.valid_ready_index_count } else { 0 }
|
||||
targetReady = $targetReady
|
||||
independentVerifier = $independentVerifier
|
||||
stagedOverlayDeleted = $stagedOverlayDeleted
|
||||
errorCode = $errorCode
|
||||
} | ConvertTo-Json -Depth 8 -Compress
|
||||
|
||||
if (-not $passed) { exit 3 }
|
||||
exit 0
|
||||
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import hashlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
OVERLAY = ROOT / "ops/agent99-truth-chain-index-executor-overlay.py"
|
||||
MANAGER = ROOT / "ops/agent99-truth-chain-index-manager.ps1"
|
||||
MIGRATION = (
|
||||
ROOT
|
||||
/ "apps/api/migrations/awooop_truth_chain_structured_reference_indexes_2026-07-17.sql"
|
||||
)
|
||||
EXPECTED_MIGRATION_SHA256 = (
|
||||
"657089552ceed8de916d87135164b255c85edc154bf437f870b8c2bbdf14c509"
|
||||
)
|
||||
|
||||
|
||||
def _assignment(module: ast.Module, name: str):
|
||||
for node in module.body:
|
||||
if (
|
||||
isinstance(node, ast.Assign)
|
||||
and len(node.targets) == 1
|
||||
and isinstance(node.targets[0], ast.Name)
|
||||
and node.targets[0].id == name
|
||||
):
|
||||
return ast.literal_eval(node.value)
|
||||
raise AssertionError(f"assignment not found: {name}")
|
||||
|
||||
|
||||
def _normalize_sql(value: str) -> str:
|
||||
return re.sub(r"\s+", " ", value.strip()).lower()
|
||||
|
||||
|
||||
def _migration_statements() -> list[str]:
|
||||
source = MIGRATION.read_text(encoding="utf-8")
|
||||
source = "\n".join(
|
||||
line for line in source.splitlines() if not line.lstrip().startswith("--")
|
||||
)
|
||||
return [part.strip() for part in source.split(";") if part.strip()]
|
||||
|
||||
|
||||
def test_fixed_executor_sql_matches_the_digest_bound_migration() -> None:
|
||||
module = ast.parse(OVERLAY.read_text(encoding="utf-8"))
|
||||
extensions = _assignment(module, "_EXTENSION_OPERATIONS")
|
||||
indexes = _assignment(module, "_INDEX_OPERATIONS")
|
||||
embedded = [sql for _, sql in (*extensions, *indexes)]
|
||||
|
||||
assert len(extensions) == 2
|
||||
assert len(indexes) == 12
|
||||
assert [_normalize_sql(sql) for sql in embedded] == [
|
||||
_normalize_sql(sql) for sql in _migration_statements()
|
||||
]
|
||||
assert hashlib.sha256(MIGRATION.read_bytes()).hexdigest() == (
|
||||
EXPECTED_MIGRATION_SHA256
|
||||
)
|
||||
|
||||
|
||||
def test_executor_has_no_raw_sql_or_arbitrary_command_input() -> None:
|
||||
source = OVERLAY.read_text(encoding="utf-8")
|
||||
|
||||
assert 'choices=("check", "apply", "verify")' in source
|
||||
assert "--trace-id" in source
|
||||
assert "--run-id" in source
|
||||
assert "--work-item-id" in source
|
||||
assert "--source-revision" in source
|
||||
assert "--migration-sha256" in source
|
||||
assert "--sql" not in source
|
||||
assert "--command" not in source
|
||||
assert '"raw_sql_accepted": False' in source
|
||||
assert '"raw_sql_emitted": False' in source
|
||||
assert '"query_parameters_emitted": False' in source
|
||||
assert '"secret_value_read": False' in source
|
||||
assert '"arbitrary_command_allowed": False' in source
|
||||
assert '"cross_domain_fallback_allowed": False' in source
|
||||
assert 'isolation_level="AUTOCOMMIT"' in source
|
||||
assert "CREATE INDEX CONCURRENTLY IF NOT EXISTS" in source
|
||||
assert 'text("SET lock_timeout = \'15s\'")' in source
|
||||
assert 'text("SET statement_timeout = \'10min\'")' in source
|
||||
|
||||
|
||||
def test_check_success_is_separate_from_target_readiness() -> None:
|
||||
source = OVERLAY.read_text(encoding="utf-8")
|
||||
|
||||
assert '"passed": args.mode == "check" or bool(after["passed"])' in source
|
||||
assert '"target_ready": bool(after["passed"])' in source
|
||||
assert 'terminal = "check_ready"' in source
|
||||
assert 'terminal = "verified_healthy" if after["passed"]' in source
|
||||
|
||||
|
||||
def test_manager_is_windows99_bound_and_uses_independent_api_pods() -> None:
|
||||
source = MANAGER.read_text(encoding="utf-8")
|
||||
|
||||
assert 'executor = "host:192.168.0.99"' in source
|
||||
assert '$JumpHost = "192.168.0.110"' in source
|
||||
assert '$ControlPlaneHost = "192.168.0.120"' in source
|
||||
assert '$PodSelector = "app=awoooi-api,environment=prod,system=awoooi"' in source
|
||||
assert "two_ready_api_pods_required" in source
|
||||
assert '$executorPod = [string]$pods[0]' in source
|
||||
assert '$verifierPod = [string]$pods[1]' in source
|
||||
assert 'Invoke-FixedStage -Stage "check" -PodName $executorPod' in source
|
||||
assert 'Invoke-FixedStage -Stage "apply" -PodName $executorPod' in source
|
||||
assert '$verifierStage = if ($Mode -eq "Apply") { "verify" } else { "check" }' in source
|
||||
assert "Get-CatalogSignature $checkReceipt.catalog_after" in source
|
||||
assert "Get-CatalogSignature $verifierReceipt.catalog_after" in source
|
||||
assert "$independentVerifier" in source
|
||||
|
||||
|
||||
def test_manager_is_digest_bound_fail_closed_and_cleans_staging() -> None:
|
||||
source = MANAGER.read_text(encoding="utf-8")
|
||||
|
||||
assert "overlay_hash_mismatch" in source
|
||||
assert "stage_receipt_contract_failed" in source
|
||||
assert "target_pod_contract_failed" in source
|
||||
assert "StrictHostKeyChecking=yes" in source
|
||||
assert "StrictHostKeyChecking=no" not in source
|
||||
assert "NumberOfPasswordPrompts=0" in source
|
||||
assert "-RedirectStandardInput $OverlayFullPath" in source
|
||||
assert 'Remove-Item -LiteralPath $OverlayFullPath -Force' in source
|
||||
assert "$stagedOverlayDeleted" in source
|
||||
assert 'rawSqlAccepted = $false' in source
|
||||
assert 'arbitraryCommandAllowed = $false' in source
|
||||
assert 'secretValueRead = $false' in source
|
||||
assert 'telegramNotificationSent = $false' in source
|
||||
assert 'kmRagWritebackPerformed = $false' in source
|
||||
assert '"runtime_verified_controlled_apply"' in source
|
||||
|
||||
Reference in New Issue
Block a user