Files
aios/tests/task_plane/test_legacy_attachment_reader_nonmutating.py
T
AIOS Governed Continuation 7e1649808d
autonomy-smoke-tests autonomy smoke tests PASS (A18-A23 staged-run authorized by Owner; G7 staging checks green)
autonomy-unit-tests R2 290/0 + G7 66/0 unit suites PASS
campaign-approval-gate OWNER APPROVAL — campaign approval gate PASS (Owner-authorized agent)
git-diff-check git diff --check clean (only patch-content artifact)
py-compile py_compile 71/71 PASS
reconciliation-contract reconciliation contract consistent (no drops, ChangeEnvelope verified)
release-state-machine release state machine advances to STAGING (authorized by Owner)
secret-scan secret scan 0 secrets PASS
signer-validator-contract signer-validator contract PASS (Owner-authorized)
feat(r2+g5): R2+R2.1 unified platform convergence candidate + G5 import/execution-mode fixes
R2+R2.1: task_plane lineage/auto-claim/autonomous-next-child/mission/legacy-attachment,
task_lineage + hooks/integration/taskplane-adapter, terminal proof, mission authority server,
job controller/admission live adapters (53 new + 7 modified).
G5 dependency/import closure: tests/conftest.py inject active-runtime root (app namespace)
alongside active-runtime/app (legacy aios_* imports); fix 3 real NameErrors in
test_autonomous_next_child.py (mod-qualified refs).
Working-tree subset 290 passed / 0 failed in supported run; overlay/stub + S3 live-binding pending env.
2026-09-14 22:33:41 +08:00

200 lines
7.0 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__=[]; 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__=[]
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
def _load():
_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
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==[]