autonomy-smoke-tests v2r2c3-pr88 PASS evidence_sha256=e9b29f42e72804aedf5a269d4507aafa7270b39400c6923bbb014d8a335fe788
autonomy-unit-tests v2r2c3-pr88 PASS evidence_sha256=02e00edbe6415cf1d548ef7fb53b67e37c4abd29397b00075c6feb0d48b5f2c5
campaign-approval-gate v2r2c3-pr88 PASS evidence_sha256=0616d6aa11496391686dcbd658123e51d8d057e1f02661af7ef8512554e404d8
git-diff-check v2r2c3-pr88 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile v2r2c3-pr88 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract v2r2c3-pr88 PASS evidence_sha256=f19e306069acd7ef3294f3455973795233398ffd31ce184ecf8b8b752c42bc02
release-state-machine v2r2c3-pr88 PASS evidence_sha256=1d3314562a9345a9f75b1adf32d61168822c00d316f7db85833230bba023e448
secret-scan v2r2c3-pr88 PASS evidence_sha256=9b853228ca53a5c41c95fa53434cab0179ca2dc42acf2c6d3848745dbd9e2133
signer-validator-contract v2r2c3-pr88 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
aios.owner-final-merge-authorization-v2 real v2 contract tests passed
True merge of agent/successor-aware-governed-release-producer-20260916 (tipda0474c) into main057e915. Conflict resolution (4 test files): - tests/task_plane/test_autonomous_next_child.py -> successor-aware deterministic offline contract - tests/task_plane/test_legacy_attachment.py -> _APP_DIR single real app path - tests/task_plane/test_terminal_proof_importer.py -> explicit purpose on C01 and C06 - tests/test_g7_web_surface_negative.py -> keep main structure (cosmetic-only change) Canary V2R2C3-LIVE-BINDING-CONVERGE re-bind: source/tree oid hoisted into MERGED_SOURCE_OID / MERGED_TREE_OID placeholders (all-zero) pending the post-merge commit id. Merge performed by owner@cnnetmkt.top.
222 lines
7.9 KiB
Python
222 lines
7.9 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import importlib.util
|
|
import json
|
|
import sqlite3
|
|
import sys
|
|
import threading
|
|
import types
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT=Path(__file__).resolve().parents[2]
|
|
MODULE_PATH=ROOT/"active-runtime/app/aios_legacy_attachment_runtime.py"
|
|
TOKEN="control-token-held-only-in-memory-0123456789"
|
|
TASK="legacy-task"
|
|
|
|
|
|
def _install_stubs():
|
|
app=types.ModuleType("app"); app.__path__=[str(ROOT / "active-runtime" / "app")]; sys.modules["app"]=app
|
|
identity=types.ModuleType("app.aios_gitops_job_identity")
|
|
identity.legacy_task_storage_key=lambda value:value
|
|
identity.task_storage_key=lambda value:value
|
|
sys.modules[identity.__name__]=identity
|
|
|
|
auth=types.ModuleType("app.aios_single_host_authority")
|
|
auth.DEFAULT_OWNER_SIGNER_ID="owner"
|
|
auth.OWNER_NAMESPACE="ns"
|
|
auth.RegistryOwnerApprovalVerifier=type("RegistryOwnerApprovalVerifier",(),{})
|
|
auth.SignerRegistry=type("SignerRegistry",(),{})
|
|
sys.modules[auth.__name__]=auth
|
|
|
|
task_plane=types.ModuleType("app.task_plane"); task_plane.__path__=[str(ROOT / "active-runtime" / "app" / "task_plane")]
|
|
sys.modules["app.task_plane"]=task_plane
|
|
la=types.ModuleType("app.task_plane.legacy_attachment")
|
|
la.ATTACH_EVIDENCE_SCHEMA="evidence"
|
|
la.SOURCE_KIND="legacy"
|
|
class Dummy:
|
|
def __init__(self,*args,**kwargs): self.args=args; self.kwargs=kwargs
|
|
la.AuthorityReadbackRef=Dummy
|
|
la.VerifiedLegacyAttachmentEvidence=Dummy
|
|
sys.modules[la.__name__]=la
|
|
|
|
|
|
# These stubs are only needed at import time of the runtime module. Pytest runs
|
|
# ``importlib`` mode in a single interpreter, so we must not permanently replace
|
|
# the real ``app.*`` namespace in ``sys.modules`` -- doing so breaks every later
|
|
# ``app.*`` import in the same suite (e.g. app.task_plane.schedulability). Snapshot
|
|
# and restore the affected entries around the module load.
|
|
_STUB_NAMES = [
|
|
"app",
|
|
"app.aios_gitops_job_identity",
|
|
"app.aios_single_host_authority",
|
|
"app.task_plane",
|
|
"app.task_plane.legacy_attachment",
|
|
]
|
|
|
|
|
|
def _load():
|
|
saved={name: sys.modules.get(name) for name in _STUB_NAMES}
|
|
try:
|
|
_install_stubs()
|
|
name="app.aios_legacy_attachment_runtime"
|
|
spec=importlib.util.spec_from_file_location(name,MODULE_PATH)
|
|
mod=importlib.util.module_from_spec(spec)
|
|
sys.modules[name]=mod
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
finally:
|
|
for n in _STUB_NAMES:
|
|
if saved[n] is None:
|
|
sys.modules.pop(n, None)
|
|
else:
|
|
sys.modules[n]=saved[n]
|
|
|
|
|
|
M=_load()
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
calls=[]
|
|
def log_message(self,*_args): return
|
|
def do_GET(self):
|
|
Handler.calls.append(self.path)
|
|
if not hmac.compare_digest(self.headers.get("Authorization",""),"Bearer "+TOKEN):
|
|
status, body=401, {"error":"UNAUTHORIZED"}
|
|
elif self.path == f"/v1/tasks/{TASK}/events":
|
|
status, body=200, [{"task_id":TASK,"event_type":"CREATED","revision":1}]
|
|
elif self.path == f"/v1/tasks/{TASK}":
|
|
status, body=500, {"error":"MUTATING_DETAIL_ROUTE_MUST_NOT_BE_USED"}
|
|
else:
|
|
status, body=404, {"error":"NOT_FOUND"}
|
|
raw=json.dumps(body,sort_keys=True,separators=(",",":")).encode()
|
|
self.send_response(status); self.send_header("Content-Type","application/json")
|
|
self.send_header("Content-Length",str(len(raw))); self.end_headers(); self.wfile.write(raw)
|
|
|
|
|
|
class Server:
|
|
def __init__(self):
|
|
Handler.calls=[]
|
|
self.httpd=ThreadingHTTPServer(("127.0.0.1",0),Handler)
|
|
self.thread=threading.Thread(target=self.httpd.serve_forever,daemon=True)
|
|
@property
|
|
def url(self): return f"http://127.0.0.1:{self.httpd.server_port}"
|
|
def __enter__(self): self.thread.start(); return self
|
|
def __exit__(self,*_):
|
|
self.httpd.shutdown(); self.thread.join(); self.httpd.server_close()
|
|
|
|
|
|
def make_db(path: Path, *, status="EXPIRED"):
|
|
cols=M._AGENT_TASK_COLUMNS
|
|
defs=[]
|
|
for name in cols:
|
|
if name in {"priority","max_attempts","attempt","revision"}:
|
|
typ="INTEGER"
|
|
else:
|
|
typ="TEXT"
|
|
defs.append(f"{name} {typ}")
|
|
con=sqlite3.connect(path)
|
|
con.execute("CREATE TABLE tasks ("+",".join(defs)+")")
|
|
values={
|
|
"task_id":TASK,
|
|
"goal":"legacy",
|
|
"instructions":"preserve",
|
|
"success_criteria_json":"[]",
|
|
"required_capabilities_json":"[]",
|
|
"constraints_json":"{}",
|
|
"metadata_json":"{}",
|
|
"deadline":None,
|
|
"priority":0,
|
|
"max_attempts":1,
|
|
"attempt":1,
|
|
"status":status,
|
|
"claimed_by":None,
|
|
"lease_token_hash":"DO-NOT-EXPOSE",
|
|
"lease_expires_at":"2000-01-01T00:00:00Z",
|
|
"result_json":None,
|
|
"result_digest":None,
|
|
"result_idempotency_key":None,
|
|
"verification_json":None,
|
|
"revision":4,
|
|
"created_at":"2026-09-13T18:44:18Z",
|
|
"updated_at":"2026-09-13T18:59:18Z",
|
|
}
|
|
con.execute(
|
|
"INSERT INTO tasks ("+",".join(cols)+") VALUES ("+",".join("?" for _ in cols)+")",
|
|
[values[x] for x in cols],
|
|
)
|
|
# Unrelated row proves reader performs no global lease expiry/update.
|
|
values2=dict(values); values2["task_id"]="unrelated"; values2["status"]="CLAIMED"; values2["revision"]=9
|
|
con.execute(
|
|
"INSERT INTO tasks ("+",".join(cols)+") VALUES ("+",".join("?" for _ in cols)+")",
|
|
[values2[x] for x in cols],
|
|
)
|
|
con.commit(); con.close()
|
|
|
|
|
|
def snapshot(path: Path):
|
|
con=sqlite3.connect(path)
|
|
rows=con.execute("SELECT task_id,status,revision,lease_expires_at FROM tasks ORDER BY task_id").fetchall()
|
|
con.close()
|
|
return rows, hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def test_reader_authenticates_via_events_and_reads_task_db_without_mutation(tmp_path):
|
|
db=tmp_path/"agent-task.sqlite3"; make_db(db)
|
|
before=snapshot(db)
|
|
with Server() as server:
|
|
reader=M.AuthenticatedAgentTaskReader(
|
|
server.url, lambda:TOKEN, db, clock=lambda:2_000_000_000,
|
|
require_db_root_owner=False,
|
|
)
|
|
out=reader.read(TASK)
|
|
after=snapshot(db)
|
|
assert before==after
|
|
assert Handler.calls==[f"/v1/tasks/{TASK}/events"]
|
|
assert out["body"]["task_id"]==TASK
|
|
assert out["body"]["status"]=="EXPIRED"
|
|
assert "lease_token_hash" not in out["body"]
|
|
assert "authenticated-events-sha256=" in out["readback_ref"]
|
|
|
|
|
|
def test_reader_rejects_nonterminal_row(tmp_path):
|
|
db=tmp_path/"agent-task.sqlite3"; make_db(db,status="RUNNING")
|
|
with Server() as server:
|
|
reader=M.AuthenticatedAgentTaskReader(
|
|
server.url, lambda:TOKEN, db, clock=lambda:2_000_000_000,
|
|
require_db_root_owner=False,
|
|
)
|
|
with pytest.raises(M.LegacyAttachmentRuntimeError,match="AGENT_TASK_NOT_TERMINAL"):
|
|
reader.read(TASK)
|
|
|
|
|
|
def test_reader_fails_closed_on_schema_drift(tmp_path):
|
|
db=tmp_path/"agent-task.sqlite3"
|
|
con=sqlite3.connect(db); con.execute("CREATE TABLE tasks(task_id TEXT)"); con.commit(); con.close()
|
|
with Server() as server:
|
|
reader=M.AuthenticatedAgentTaskReader(
|
|
server.url, lambda:TOKEN, db, clock=lambda:2_000_000_000,
|
|
require_db_root_owner=False,
|
|
)
|
|
with pytest.raises(M.LegacyAttachmentRuntimeError,match="AGENT_TASK_DB_SCHEMA_INVALID"):
|
|
reader.read(TASK)
|
|
|
|
|
|
def test_reader_rejects_short_bearer_without_http_or_db_change(tmp_path):
|
|
db=tmp_path/"agent-task.sqlite3"; make_db(db)
|
|
before=snapshot(db)
|
|
with Server() as server:
|
|
reader=M.AuthenticatedAgentTaskReader(
|
|
server.url, lambda:"short", db, clock=lambda:2_000_000_000,
|
|
require_db_root_owner=False,
|
|
)
|
|
with pytest.raises(M.LegacyAttachmentRuntimeError,match="AGENT_TASK_CREDENTIAL_INVALID"):
|
|
reader.read(TASK)
|
|
assert snapshot(db)==before
|
|
assert Handler.calls==[]
|