583 lines
21 KiB
Python
583 lines
21 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import hashlib
|
|
from pathlib import Path
|
|
import sys
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP = ROOT / "active-runtime" / "app"
|
|
if str(APP) not in sys.path:
|
|
sys.path.insert(0, str(APP))
|
|
|
|
from aios_task_admission_bootstrap import ( # noqa: E402
|
|
OwnerDelegationEpoch,
|
|
TaskAdmissionBootstrapService,
|
|
TaskAdmissionError,
|
|
TaskAdmissionGrant,
|
|
TaskAdmissionPolicy,
|
|
TaskAdmissionStore,
|
|
canonical_json_bytes,
|
|
)
|
|
from aios_task_admission_live_adapters import ( # noqa: E402
|
|
AgentTaskAdmissionPort,
|
|
BoundOwnerAuthorizationBackend,
|
|
)
|
|
|
|
|
|
BASE_OID = "9" * 40
|
|
EXECUTION_DIGEST = "a" * 64
|
|
TASK_ID = "repository-discovery-p0-20260903"
|
|
BRANCH = "agent/repository-discovery-p0-20260902"
|
|
IDEMPOTENCY = "repository-discovery-p0-admission-20260903"
|
|
FILES = [
|
|
"active-runtime/app/aios_repository_discovery.py",
|
|
"active-runtime/app/aios_chatgpt_bridge_v2.py",
|
|
"docs/gitops/openapi-chatgpt-actions.yaml",
|
|
"tests/test_repository_discovery.py",
|
|
]
|
|
CAPS = [
|
|
"governed.task.create",
|
|
"agent-task:control",
|
|
"task.principal.issue",
|
|
"writer.lease",
|
|
"governed.write.files",
|
|
"git.read",
|
|
"testing",
|
|
"verification",
|
|
]
|
|
|
|
|
|
class FakeAuthorization:
|
|
def __init__(self, *, policy_digest: str, now: int = 1_800_000_000):
|
|
self.now = now
|
|
self.signature_calls = 0
|
|
self.epoch_calls = 0
|
|
self.signature_ok = True
|
|
self.epoch = OwnerDelegationEpoch(
|
|
epoch_id="epoch-repository-discovery-20260903",
|
|
owner_identity="owner",
|
|
trust_domain="aios.owner",
|
|
repository="aios/aios",
|
|
allowed_branch_prefixes=("agent/repository-discovery-",),
|
|
allowed_capabilities=frozenset(CAPS),
|
|
allowed_file_prefixes=("active-runtime", "docs", "tests"),
|
|
risk_ceiling="medium",
|
|
issued_at=now - 60,
|
|
not_before=now - 60,
|
|
expires_at=now + 3600,
|
|
revocation_epoch=0,
|
|
policy_digest=policy_digest,
|
|
)
|
|
|
|
def verify_delegation_epoch(self, epoch_id: str) -> OwnerDelegationEpoch:
|
|
self.epoch_calls += 1
|
|
if epoch_id != self.epoch.epoch_id:
|
|
raise TaskAdmissionError("UNKNOWN_EPOCH", 403)
|
|
return self.epoch
|
|
|
|
def verify_owner_signature(
|
|
self,
|
|
*,
|
|
key_id: str,
|
|
canonical_payload: bytes,
|
|
signature_b64: str,
|
|
) -> str:
|
|
self.signature_calls += 1
|
|
if key_id != "owner-key-1" or not self.signature_ok:
|
|
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403)
|
|
expected = base64.b64encode(
|
|
hashlib.sha256(canonical_payload + b"\0owner-key-1").digest()
|
|
).decode("ascii")
|
|
if signature_b64 != expected:
|
|
raise TaskAdmissionError("TASK_ADMISSION_OWNER_SIGNATURE_INVALID", 403)
|
|
return "owner"
|
|
|
|
|
|
class FakeTaskPort:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
self.grants = []
|
|
self._lock = threading.Lock()
|
|
|
|
def admit(self, grant):
|
|
with self._lock:
|
|
self.calls += 1
|
|
self.grants.append(grant)
|
|
return {"task_id": grant.task_id, "state": "QUEUED"}
|
|
|
|
|
|
@pytest.fixture
|
|
def policy():
|
|
return TaskAdmissionPolicy(
|
|
allowed_capabilities=frozenset(CAPS),
|
|
max_file_count=16,
|
|
max_task_ttl_seconds=3600,
|
|
max_envelope_ttl_seconds=900,
|
|
max_risk="medium",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def service(tmp_path: Path, policy: TaskAdmissionPolicy):
|
|
now = 1_800_000_000
|
|
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest(), now=now)
|
|
port = FakeTaskPort()
|
|
svc = TaskAdmissionBootstrapService(
|
|
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
|
|
authorization=auth,
|
|
task_port=port,
|
|
policy=policy,
|
|
clock=lambda: now,
|
|
)
|
|
return svc, auth, port
|
|
|
|
|
|
def request(**overrides):
|
|
value = {
|
|
"idempotency_key": IDEMPOTENCY,
|
|
"task_id": TASK_ID,
|
|
"repository": "aios/aios",
|
|
"authority_base_oid": BASE_OID,
|
|
"target_branch": BRANCH,
|
|
"generation": 1,
|
|
"execution_digest": EXECUTION_DIGEST,
|
|
"requested_capabilities": list(CAPS),
|
|
"file_allowlist": list(FILES),
|
|
"risk": "medium",
|
|
"expires_in_seconds": 600,
|
|
}
|
|
value.update(overrides)
|
|
return value
|
|
|
|
|
|
def test_envelope_exact_replay_returns_same_nonce_and_payload(service):
|
|
svc, _, _ = service
|
|
first = svc.create_envelope(request())
|
|
second = svc.create_envelope(request())
|
|
assert first["idempotent"] is False
|
|
assert second["idempotent"] is True
|
|
assert first["envelope"] == second["envelope"]
|
|
assert first["canonical_payload_base64"] == second["canonical_payload_base64"]
|
|
|
|
|
|
def test_envelope_idempotency_conflict_fails_closed(service):
|
|
svc, _, _ = service
|
|
svc.create_envelope(request())
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_IDEMPOTENCY_CONFLICT"):
|
|
svc.create_envelope(request(risk="low"))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"field,value,code",
|
|
[
|
|
("repository", "other/repo", "TASK_ADMISSION_REPOSITORY_REJECTED"),
|
|
("target_branch", "main", "TASK_ADMISSION_BRANCH_REJECTED"),
|
|
("authority_base_oid", "HEAD", "TASK_ADMISSION_BASE_OID_INVALID"),
|
|
("requested_capabilities", ["production.write"], "TASK_ADMISSION_PROHIBITED_CAPABILITY"),
|
|
("requested_capabilities", ["git:*"], "TASK_ADMISSION_CAPABILITY_INVALID"),
|
|
("file_allowlist", ["../secrets"], "TASK_ADMISSION_FILE_PATH_INVALID"),
|
|
("file_allowlist", ["/etc/passwd"], "TASK_ADMISSION_FILE_PATH_INVALID"),
|
|
("risk", "high", "TASK_ADMISSION_RISK_OUT_OF_POLICY"),
|
|
],
|
|
)
|
|
def test_envelope_policy_rejects_scope_widening(service, field, value, code):
|
|
svc, _, _ = service
|
|
with pytest.raises(TaskAdmissionError, match=code):
|
|
svc.create_envelope(request(**{field: value}))
|
|
|
|
|
|
def test_valid_delegation_epoch_admits_exact_task_once(service):
|
|
svc, auth, port = service
|
|
envelope = svc.create_envelope(request())["envelope"]
|
|
result = svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope["envelope_id"],
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
)
|
|
assert result["schema"] == "aios.task-admission-receipt.v1"
|
|
assert result["task_id"] == TASK_ID
|
|
assert result["task_state"] == "QUEUED"
|
|
assert result["parent_epoch_id"] == auth.epoch.epoch_id
|
|
assert port.calls == 1
|
|
|
|
replay = svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope["envelope_id"],
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
)
|
|
assert replay["task_id"] == TASK_ID
|
|
assert replay["idempotent"] is True
|
|
assert port.calls == 1
|
|
|
|
|
|
def test_post_admission_projection_runs_after_exact_task_readback_once(
|
|
tmp_path: Path, policy: TaskAdmissionPolicy
|
|
):
|
|
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest())
|
|
port = FakeTaskPort()
|
|
observations = []
|
|
|
|
def project(grant, task):
|
|
observations.append((grant.task_id, task["task_id"], task["state"]))
|
|
return {"job_id": "projected-job"}
|
|
|
|
svc = TaskAdmissionBootstrapService(
|
|
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
|
|
authorization=auth,
|
|
task_port=port,
|
|
policy=policy,
|
|
clock=lambda: 1_800_000_000,
|
|
post_admission=project,
|
|
)
|
|
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
|
|
proof = {
|
|
"envelope_id": envelope_id,
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
|
|
first = svc.authorize_and_admit(proof)
|
|
replay = svc.authorize_and_admit(proof)
|
|
|
|
assert first["task_id"] == TASK_ID
|
|
assert replay["idempotent"] is True
|
|
assert observations == [(TASK_ID, TASK_ID, "QUEUED")]
|
|
|
|
|
|
def test_epoch_capability_widening_rejected_before_task_port(service):
|
|
svc, auth, port = service
|
|
# Envelope policy allows the capability, parent epoch deliberately does not.
|
|
auth.epoch = OwnerDelegationEpoch(
|
|
**{
|
|
**auth.epoch.as_public_dict(),
|
|
"allowed_capabilities": frozenset(
|
|
item for item in CAPS if item != "writer.lease"
|
|
),
|
|
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
|
|
"allowed_file_prefixes": auth.epoch.allowed_file_prefixes,
|
|
}
|
|
)
|
|
envelope = svc.create_envelope(request())["envelope"]
|
|
with pytest.raises(
|
|
TaskAdmissionError, match="TASK_ADMISSION_EPOCH_CAPABILITY_WIDENING"
|
|
):
|
|
svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope["envelope_id"],
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
)
|
|
assert port.calls == 0
|
|
|
|
|
|
def test_epoch_file_scope_widening_rejected(service):
|
|
svc, auth, port = service
|
|
auth.epoch = OwnerDelegationEpoch(
|
|
**{
|
|
**auth.epoch.as_public_dict(),
|
|
"allowed_capabilities": auth.epoch.allowed_capabilities,
|
|
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
|
|
"allowed_file_prefixes": ("docs",),
|
|
}
|
|
)
|
|
envelope = svc.create_envelope(request())["envelope"]
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_EPOCH_FILE_WIDENING"):
|
|
svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope["envelope_id"],
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
)
|
|
assert port.calls == 0
|
|
|
|
|
|
def test_revoked_or_expired_epoch_rejected(service):
|
|
svc, auth, port = service
|
|
auth.epoch = OwnerDelegationEpoch(
|
|
**{
|
|
**auth.epoch.as_public_dict(),
|
|
"allowed_capabilities": auth.epoch.allowed_capabilities,
|
|
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
|
|
"allowed_file_prefixes": auth.epoch.allowed_file_prefixes,
|
|
"revocation_epoch": 1,
|
|
}
|
|
)
|
|
envelope = svc.create_envelope(request())["envelope"]
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_EPOCH_REVOKED"):
|
|
svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope["envelope_id"],
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
)
|
|
assert port.calls == 0
|
|
|
|
|
|
def test_owner_signature_path_verifies_canonical_payload(service):
|
|
svc, auth, port = service
|
|
created = svc.create_envelope(request())
|
|
payload = canonical_json_bytes(created["envelope"])
|
|
signature = base64.b64encode(
|
|
hashlib.sha256(payload + b"\0owner-key-1").digest()
|
|
).decode("ascii")
|
|
result = svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": created["envelope"]["envelope_id"],
|
|
"authorization_kind": "owner_signature",
|
|
"owner_key_id": "owner-key-1",
|
|
"signature_base64": signature,
|
|
}
|
|
)
|
|
assert result["authorization_kind"] == "owner_signature"
|
|
assert result["owner_identity"] == "owner"
|
|
assert auth.signature_calls == 1
|
|
assert port.calls == 1
|
|
|
|
|
|
def test_bad_owner_signature_is_rejected(service):
|
|
svc, _, port = service
|
|
envelope = svc.create_envelope(request())["envelope"]
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_OWNER_SIGNATURE_INVALID"):
|
|
svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope["envelope_id"],
|
|
"authorization_kind": "owner_signature",
|
|
"owner_key_id": "owner-key-1",
|
|
"signature_base64": base64.b64encode(b"bad").decode("ascii"),
|
|
}
|
|
)
|
|
assert port.calls == 0
|
|
|
|
|
|
def test_expired_envelope_cannot_admit(tmp_path: Path, policy: TaskAdmissionPolicy):
|
|
now = [1_800_000_000]
|
|
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest(), now=now[0])
|
|
port = FakeTaskPort()
|
|
svc = TaskAdmissionBootstrapService(
|
|
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
|
|
authorization=auth,
|
|
task_port=port,
|
|
policy=policy,
|
|
clock=lambda: now[0],
|
|
)
|
|
envelope = svc.create_envelope(request(expires_in_seconds=60))["envelope"]
|
|
now[0] += 61
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_ENVELOPE_EXPIRED"):
|
|
svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope["envelope_id"],
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
)
|
|
assert port.calls == 0
|
|
|
|
|
|
def test_concurrent_exact_authorization_calls_admit_once(service):
|
|
svc, auth, port = service
|
|
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
|
|
body = {
|
|
"envelope_id": envelope_id,
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
with ThreadPoolExecutor(max_workers=16) as pool:
|
|
results = list(pool.map(lambda _: svc.authorize_and_admit(body), range(16)))
|
|
assert {item["task_id"] for item in results} == {TASK_ID}
|
|
assert port.calls == 1
|
|
|
|
|
|
def test_cross_instance_envelope_winner_is_the_durable_value(tmp_path: Path, policy: TaskAdmissionPolicy):
|
|
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest())
|
|
port = FakeTaskPort()
|
|
stores = [TaskAdmissionStore(tmp_path / "shared.sqlite3") for _ in range(16)]
|
|
services = [
|
|
TaskAdmissionBootstrapService(
|
|
store=store, authorization=auth, task_port=port, policy=policy,
|
|
clock=lambda: 1_800_000_000,
|
|
)
|
|
for store in stores
|
|
]
|
|
with ThreadPoolExecutor(max_workers=16) as pool:
|
|
values = list(pool.map(lambda svc: svc.create_envelope(request()), services))
|
|
assert len({item["envelope"]["envelope_id"] for item in values}) == 1
|
|
assert sum(not item["idempotent"] for item in values) == 1
|
|
|
|
|
|
def test_cross_instance_authorization_calls_create_one_task(tmp_path: Path, policy: TaskAdmissionPolicy):
|
|
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest())
|
|
port = FakeTaskPort()
|
|
database = tmp_path / "shared.sqlite3"
|
|
first = TaskAdmissionBootstrapService(
|
|
store=TaskAdmissionStore(database), authorization=auth, task_port=port,
|
|
policy=policy, clock=lambda: 1_800_000_000,
|
|
)
|
|
envelope_id = first.create_envelope(request())["envelope"]["envelope_id"]
|
|
services = [
|
|
TaskAdmissionBootstrapService(
|
|
store=TaskAdmissionStore(database), authorization=auth, task_port=port,
|
|
policy=policy, clock=lambda: 1_800_000_000,
|
|
)
|
|
for _ in range(16)
|
|
]
|
|
body = {
|
|
"envelope_id": envelope_id,
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
with ThreadPoolExecutor(max_workers=16) as pool:
|
|
values = list(pool.map(lambda svc: svc.authorize_and_admit(body), services))
|
|
assert {item["task_id"] for item in values} == {TASK_ID}
|
|
assert port.calls == 1
|
|
|
|
|
|
@pytest.mark.parametrize("file_name", ["docs//gitops/a.yaml", "docs/./gitops/a.yaml", "docs/a\x00.yaml"])
|
|
def test_file_paths_must_already_be_canonical(service, file_name):
|
|
svc, _, _ = service
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_FILE_PATH_INVALID"):
|
|
svc.create_envelope(request(file_allowlist=[file_name]))
|
|
|
|
|
|
@pytest.mark.parametrize("generation", [True, "1", 1.0, 0])
|
|
def test_generation_is_a_strict_positive_integer(service, generation):
|
|
svc, _, _ = service
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_GENERATION_INVALID"):
|
|
svc.create_envelope(request(generation=generation))
|
|
|
|
|
|
def test_policy_digest_drift_and_changed_proof_cannot_replay(service):
|
|
svc, auth, _ = service
|
|
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
|
|
body = {
|
|
"envelope_id": envelope_id,
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
svc.authorize_and_admit(body)
|
|
svc.policy = TaskAdmissionPolicy(
|
|
allowed_capabilities=frozenset(CAPS), policy_digest="policy-has-changed"
|
|
)
|
|
with pytest.raises(TaskAdmissionError, match="TASK_ADMISSION_POLICY_DIGEST_DRIFT"):
|
|
svc.authorize_and_admit(body)
|
|
|
|
|
|
def test_grant_expiry_is_clipped_to_parent_epoch(tmp_path: Path, policy: TaskAdmissionPolicy):
|
|
now = 1_800_000_000
|
|
auth = FakeAuthorization(policy_digest=policy.normalized_policy_digest(), now=now)
|
|
auth.epoch = OwnerDelegationEpoch(
|
|
**{**auth.epoch.as_public_dict(), "allowed_capabilities": auth.epoch.allowed_capabilities,
|
|
"allowed_branch_prefixes": auth.epoch.allowed_branch_prefixes,
|
|
"allowed_file_prefixes": auth.epoch.allowed_file_prefixes, "expires_at": now + 120}
|
|
)
|
|
port = FakeTaskPort()
|
|
svc = TaskAdmissionBootstrapService(
|
|
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"), authorization=auth,
|
|
task_port=port, policy=policy, clock=lambda: now,
|
|
)
|
|
result = svc.authorize_and_admit({
|
|
"envelope_id": svc.create_envelope(request())["envelope"]["envelope_id"],
|
|
"authorization_kind": "delegation_epoch", "delegation_epoch_id": auth.epoch.epoch_id,
|
|
})
|
|
assert port.grants[0].expires_at == now + 120
|
|
assert result["task_id"] == TASK_ID
|
|
|
|
|
|
def test_owner_backend_uses_bound_caller_and_decoded_armored_signature(policy):
|
|
captured = {}
|
|
|
|
class Verifier:
|
|
def verify(self, **kwargs):
|
|
captured.update(kwargs)
|
|
return True
|
|
|
|
epoch = FakeAuthorization(policy_digest=policy.normalized_policy_digest()).epoch
|
|
backend = BoundOwnerAuthorizationBackend(
|
|
authenticated_caller={"uid": "bridge-service"},
|
|
verified_epoch_provider=lambda **kwargs: captured.update(epoch_lookup=kwargs) or epoch,
|
|
signature_verifier=Verifier(),
|
|
)
|
|
assert backend.verify_delegation_epoch(epoch.epoch_id) == epoch
|
|
assert captured["epoch_lookup"]["caller"] == {"uid": "bridge-service"}
|
|
assert backend.verify_owner_signature(
|
|
key_id="owner-key-1", canonical_payload=b"exact", signature_b64=base64.b64encode(b"-----BEGIN SSH SIGNATURE-----").decode(),
|
|
) == "aios-task-grant-signer"
|
|
assert captured["namespace"] == "aios-owner-approval-v1"
|
|
assert captured["payload"] == b"exact"
|
|
|
|
|
|
def test_official_task_port_reconciles_ambiguous_create(tmp_path: Path, policy):
|
|
grant = TaskAdmissionBootstrapService(
|
|
store=TaskAdmissionStore(tmp_path / "admission.sqlite3"),
|
|
authorization=FakeAuthorization(policy_digest=policy.normalized_policy_digest()),
|
|
task_port=FakeTaskPort(), policy=policy, clock=lambda: 1_800_000_000,
|
|
)
|
|
# Construct via the public flow so the port sees a production-shaped grant.
|
|
created = grant.create_envelope(request())["envelope"]
|
|
envelope = grant._load_envelope(created["envelope_id"])
|
|
task_grant = TaskAdmissionGrant(
|
|
grant_id="task-grant-adapter-test", envelope_id=envelope.envelope_id,
|
|
task_id=envelope.task_id, repository=envelope.repository,
|
|
authority_base_oid=envelope.authority_base_oid, target_branch=envelope.target_branch,
|
|
generation=envelope.generation, execution_digest=envelope.execution_digest,
|
|
capabilities=envelope.requested_capabilities, file_allowlist=envelope.file_allowlist,
|
|
risk=envelope.risk, trust_domain=envelope.trust_domain,
|
|
policy_digest=envelope.policy_digest, authorization_kind="owner_signature",
|
|
owner_identity="aios-task-grant-signer", parent_epoch_id=None,
|
|
issued_at=1_800_000_000, expires_at=1_800_000_600,
|
|
)
|
|
|
|
class NotFound(Exception):
|
|
status = 404
|
|
|
|
class Transport:
|
|
def __init__(self): self.calls = []; self.task = None
|
|
def request(self, method, path, body=None):
|
|
self.calls.append((method, path, body))
|
|
if method == "GET" and self.task is None: raise NotFound()
|
|
if method == "POST":
|
|
self.task = {key: value for key, value in body.items() if key != "idempotency_key"}
|
|
self.task["status"] = "QUEUED"
|
|
raise TimeoutError("response lost after side effect")
|
|
return self.task
|
|
|
|
transport = Transport()
|
|
plan = {
|
|
"external_job_id": "fixture-job-adapter-01", "repository": task_grant.repository,
|
|
"target_branch": task_grant.target_branch, "generation": task_grant.generation,
|
|
"execution_digest": task_grant.execution_digest,
|
|
}
|
|
assert AgentTaskAdmissionPort(transport, execution_plan_provider=lambda _: plan).admit(task_grant) == {"task_id": TASK_ID, "state": "QUEUED"}
|
|
assert [call[:2] for call in transport.calls] == [
|
|
("GET", "/v1/tasks/" + TASK_ID), ("POST", "/v1/tasks"), ("GET", "/v1/tasks/" + TASK_ID),
|
|
]
|
|
|
|
|
|
def test_status_is_read_only_and_reports_admitted_task(service):
|
|
svc, auth, _ = service
|
|
envelope_id = svc.create_envelope(request())["envelope"]["envelope_id"]
|
|
before = svc.status(envelope_id)
|
|
assert before["status"] == "PENDING_AUTHORIZATION"
|
|
assert before["task"] is None
|
|
svc.authorize_and_admit(
|
|
{
|
|
"envelope_id": envelope_id,
|
|
"authorization_kind": "delegation_epoch",
|
|
"delegation_epoch_id": auth.epoch.epoch_id,
|
|
}
|
|
)
|
|
after = svc.status(envelope_id)
|
|
assert after["status"] == "ADMITTED"
|
|
assert after["task"]["task_id"] == TASK_ID
|