Compare commits

...
6 changed files with 480 additions and 142 deletions
@@ -16,6 +16,8 @@ from typing import Any, Mapping
CALLER = "chatgpt-governed-implementer-singlehost"
SCOPE = "release:candidate"
GOVERNED_TASK_SCOPE = "governed-task:project"
PROJECTED_SCOPES = frozenset({GOVERNED_TASK_SCOPE, SCOPE})
CREDENTIAL_HEADER = "X-AIOS-Caller-Projection-Credential"
@@ -66,7 +68,7 @@ class CallerProjectionClient:
) -> ProjectionResult:
if caller != CALLER:
raise CallerProjectionError("CALLER_MISMATCH", 403)
if scope != SCOPE:
if scope not in PROJECTED_SCOPES:
raise CallerProjectionError("SCOPE_MISMATCH", 403)
body = {
"caller": caller,
@@ -124,7 +126,14 @@ class CallerProjectionClient:
raise CallerProjectionError("CREDENTIAL_REHYDRATION_INVALID", 503)
if value.get("rehydration_state") not in {"GENERATED", "RECOVERED"}:
raise CallerProjectionError("REHYDRATION_STATE_INVALID", 503)
if value.get("effective_scopes") != [SCOPE]:
effective_scopes = value.get("effective_scopes")
if (
not isinstance(effective_scopes, list)
or len(effective_scopes) != len(set(effective_scopes))
or not all(isinstance(item, str) for item in effective_scopes)
or scope not in effective_scopes
or not set(effective_scopes).issubset(PROJECTED_SCOPES)
):
raise CallerProjectionError("PROJECTION_SCOPE_INVALID", 503)
if value.get("secret_exposed") is not False:
raise CallerProjectionError("PROJECTION_SECRET_EXPOSURE", 503)
@@ -40,6 +40,10 @@ PROJECTION_SCHEMA = "aios.external-capability-projection.request.v1"
PROJECTION_LIST_SCHEMA = "aios.external-capability-projection.list.v1"
CALLER_PROJECTION_SCHEMA = "aios.caller-admission-projection.v1"
CALLER_PROJECTION_TTL_DEFAULT = 300
CALLER_PROJECTION_SCOPES = frozenset({
"governed-task:project",
"release:candidate",
})
GATE_AMENDMENT_SCHEMA = "aios.release-preparation.gate-amendment.v1"
GATE_AMENDMENT_CLASSIFICATION = "SUPERVISOR_GLOBAL_EXTERNAL_ACK_GATE_REMEDIATED"
@@ -296,13 +300,13 @@ class DurablePolicy:
) -> dict[str, Any]:
if caller != self.account:
raise AuthorizationError("CALLER_MISMATCH", 403)
if scope != "release:candidate":
if scope not in CALLER_PROJECTION_SCOPES:
raise AuthorizationError("SCOPE_MISMATCH", 403)
if scope not in self.capabilities:
raise AuthorizationError("CAPABILITY_NOT_AUTHORIZED", 403)
if not isinstance(request, Mapping):
raise AuthorizationError("CALLER_PROJECTION_REQUEST_INVALID")
if request:
if scope == "release:candidate":
if set(request) != {"source_commit", "version"}:
raise AuthorizationError("CALLER_PROJECTION_FIELDS_INVALID")
source_commit = str(request.get("source_commit") or "")
@@ -313,6 +317,8 @@ class DurablePolicy:
self.constraints.release_version_prefix
):
raise AuthorizationError("RELEASE_VERSION_NOT_AUTHORIZED", 403)
elif request:
raise AuthorizationError("CALLER_PROJECTION_FIELDS_INVALID")
return {
"schema": CALLER_PROJECTION_SCHEMA,
"caller": caller,
@@ -566,98 +572,111 @@ class ReceiptStore:
grant_id: str,
policy_sha256: str,
ttl_seconds: int,
effective_scopes: Any,
) -> dict[str, Any]:
if not 30 <= ttl_seconds <= 900:
raise AuthorizationError("CALLER_PROJECTION_TTL_INVALID", 503)
projection_id = self.caller_projection_id(caller, scope)
normalized_scopes = sorted({str(item) for item in effective_scopes})
if (
not normalized_scopes
or scope not in normalized_scopes
or not set(normalized_scopes).issubset(CALLER_PROJECTION_SCOPES)
):
raise AuthorizationError("CALLER_PROJECTION_SCOPES_INVALID", 503)
requested_projection_id = self.caller_projection_id(caller, scope)
issued = datetime.now(timezone.utc)
issued_at = issued.isoformat()
expires_at = (issued + timedelta(seconds=ttl_seconds)).isoformat()
requested_recovery = False
with self._lock, sqlite3.connect(self.path) as con:
con.row_factory = sqlite3.Row
con.execute("BEGIN IMMEDIATE")
row = con.execute(
"SELECT * FROM caller_projections WHERE caller=? AND scope=?",
(caller, scope),
).fetchone()
if row is not None and row["projection_state"] not in {"ACKED", "PENDING"}:
con.rollback()
raise AuthorizationError("PROJECTION_STATE_INVALID", 503)
recovery = row is not None
if row is None:
con.execute(
"""
INSERT INTO caller_projections(
projection_id,caller,scope,grant_id,policy_sha256,
projection_state,credential_state,rehydration_state,
issued_at,expires_at,updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?)
""",
(
projection_id,
caller,
scope,
grant_id,
policy_sha256,
"PENDING",
"GENERATING",
"RECOVERING" if recovery else "GENERATING",
issued_at,
expires_at,
issued_at,
),
)
else:
for projected_scope in normalized_scopes:
projection_id = self.caller_projection_id(caller, projected_scope)
row = con.execute(
"SELECT * FROM caller_projections WHERE caller=? AND scope=?",
(caller, projected_scope),
).fetchone()
if row is not None and row["projection_state"] not in {"ACKED", "PENDING"}:
con.rollback()
raise AuthorizationError("PROJECTION_STATE_INVALID", 503)
recovery = row is not None
if projected_scope == scope:
requested_recovery = recovery
if row is None:
con.execute(
"""
INSERT INTO caller_projections(
projection_id,caller,scope,grant_id,policy_sha256,
projection_state,credential_state,rehydration_state,
issued_at,expires_at,updated_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?)
""",
(
projection_id,
caller,
projected_scope,
grant_id,
policy_sha256,
"PENDING",
"GENERATING",
"RECOVERING" if recovery else "GENERATING",
issued_at,
expires_at,
issued_at,
),
)
else:
con.execute(
"""
UPDATE caller_projections
SET projection_id=?,grant_id=?,policy_sha256=?,
projection_state='PENDING',
credential_state='GENERATING',
rehydration_state=?,
issued_at=?,expires_at=?,updated_at=?
WHERE caller=? AND scope=?
""",
(
projection_id,
grant_id,
policy_sha256,
"RECOVERING",
issued_at,
expires_at,
issued_at,
caller,
projected_scope,
),
)
con.execute(
"""
UPDATE caller_projections
SET projection_id=?,grant_id=?,policy_sha256=?,
projection_state='PENDING',
credential_state='GENERATING',
SET projection_state='ACKED',
credential_state='REHYDRATED',
rehydration_state=?,
issued_at=?,expires_at=?,updated_at=?
updated_at=?
WHERE caller=? AND scope=?
""",
(
projection_id,
grant_id,
policy_sha256,
"RECOVERING",
issued_at,
expires_at,
"RECOVERED" if recovery else "GENERATED",
issued_at,
caller,
scope,
projected_scope,
),
)
con.execute(
"""
UPDATE caller_projections
SET projection_state='ACKED',
credential_state='REHYDRATED',
rehydration_state=?,
updated_at=?
WHERE caller=? AND scope=?
""",
(
"RECOVERED" if recovery else "GENERATED",
issued_at,
caller,
scope,
),
)
con.commit()
return {
"schema": CALLER_PROJECTION_SCHEMA,
"projection_id": projection_id,
"projection_id": requested_projection_id,
"caller": caller,
"scope": scope,
"grant_id": grant_id,
"policy_sha256": policy_sha256,
"projection_state": "ACKED",
"credential_state": "REHYDRATED",
"rehydration_state": "RECOVERED" if recovery else "GENERATED",
"effective_scopes": [scope],
"rehydration_state": "RECOVERED" if requested_recovery else "GENERATED",
"effective_scopes": normalized_scopes,
"issued_at": issued_at,
"expires_at": expires_at,
"secret_exposed": False,
@@ -972,10 +991,17 @@ class Broker:
self.allowed_peer_users = allowed_peer_users or frozenset({expected_peer_user})
@staticmethod
def validate_caller_projection(value: Mapping[str, Any]) -> dict[str, Any]:
def validate_caller_projection(
value: Mapping[str, Any],
*,
expected_scopes: Any = None,
) -> dict[str, Any]:
expected = sorted(
set(expected_scopes) if expected_scopes is not None else CALLER_PROJECTION_SCOPES
)
if value.get("caller") != "chatgpt-governed-implementer-singlehost":
raise AuthorizationError("CALLER_MISMATCH", 503)
if value.get("scope") != "release:candidate":
if value.get("scope") not in expected:
raise AuthorizationError("SCOPE_MISMATCH", 503)
if value.get("projection_state") != "ACKED":
raise AuthorizationError("PROJECTION_STATE_INVALID", 503)
@@ -983,7 +1009,7 @@ class Broker:
raise AuthorizationError("CREDENTIAL_REHYDRATION_INVALID", 503)
if value.get("rehydration_state") not in {"GENERATED", "RECOVERED"}:
raise AuthorizationError("REHYDRATION_STATE_INVALID", 503)
if value.get("effective_scopes") != ["release:candidate"]:
if value.get("effective_scopes") != expected:
raise AuthorizationError("PROJECTION_SCOPE_INVALID", 503)
if value.get("secret_exposed") is not False:
raise AuthorizationError("PROJECTION_SECRET_EXPOSURE", 503)
@@ -1001,10 +1027,11 @@ class Broker:
raise AuthorizationError("IDEMPOTENCY_KEY_INVALID")
if caller != self.expected_account:
raise AuthorizationError("CALLER_MISMATCH", 403)
if scope != "release:candidate":
if scope not in CALLER_PROJECTION_SCOPES:
raise AuthorizationError("SCOPE_MISMATCH", 403)
policy = self.policy()
policy.validate_caller_scope(caller, scope, request)
effective_scopes = sorted(policy.capabilities & CALLER_PROJECTION_SCOPES)
try:
ttl_seconds = int(
os.environ.get(
@@ -1021,8 +1048,12 @@ class Broker:
grant_id=policy.grant_id,
policy_sha256=policy.policy_sha256,
ttl_seconds=ttl_seconds,
effective_scopes=effective_scopes,
)
value = self.validate_caller_projection(
value,
expected_scopes=effective_scopes,
)
value = self.validate_caller_projection(value)
value["_credential"] = credential
return value
@@ -1377,6 +1408,7 @@ class Handler(BaseHTTPRequestHandler):
routes = {
"/v1/project-governed-task": "governed-task:project",
"/v1/authorize-release-candidate": "release:candidate",
"/v1/project-caller": "caller:projection",
}
capability = routes.get(self.path)
if capability is None or self.broker is None:
+21 -6
View File
@@ -7,6 +7,8 @@ from pathlib import Path
from aios_caller_admission_projection import (
CALLER,
SCOPE,
GOVERNED_TASK_SCOPE,
PROJECTED_SCOPES,
CallerProjectionClient,
CallerProjectionError,
)
@@ -422,9 +424,21 @@ class H(BaseHTTPRequestHandler):
except Exception: return {'_raw':raw}
def actor(self):
auth=self.headers.get('Authorization',''); tok=auth[7:].strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
rec=authenticate(tok)
if rec:
scopes = set(rec.get('scopes') or [])
try:
rec=authenticate(tok)
except Exception:
rec=None
if not isinstance(rec, dict) or not rec:
return {
'ok': False,
'account': 'anonymous',
'role': 'anonymous',
'scopes': set(),
'static_scopes': set(),
'projection': None,
'credential_loaded': False,
}
scopes = set(rec.get('scopes') or [])
return {
'ok': True,
'account': safe(rec.get('account') or rec.get('name') or 'token-user'),
@@ -434,13 +448,12 @@ class H(BaseHTTPRequestHandler):
'projection': None,
'credential_loaded': False,
}
return {'ok':False,'account':'anonymous','role':'anonymous','scopes':set()}
def need(self, scope, *, projection_request=None):
a = self.actor()
if not a['ok']:
self.sendj({'error': 'unauthorized', 'hint': 'Bearer token required'}, 401)
return None
if a['account'] == CALLER and scope == SCOPE:
if a['account'] == CALLER and scope in PROJECTED_SCOPES:
try:
result = CALLER_PROJECTION_CLIENT.rehydrate(
a['account'],
@@ -490,7 +503,7 @@ class H(BaseHTTPRequestHandler):
try:
result = CALLER_PROJECTION_CLIENT.rehydrate(
actor['account'],
SCOPE,
GOVERNED_TASK_SCOPE,
{},
)
except CallerProjectionError as exc:
@@ -980,6 +993,8 @@ class H(BaseHTTPRequestHandler):
}
if path in governed_routes:
route, scope = governed_routes[path]
if path == '/createGovernedTask' and self.actor()['account'] == CALLER:
scope = GOVERNED_TASK_SCOPE
actor = self.need(scope)
if not actor:
return
+128 -62
View File
@@ -1,88 +1,154 @@
# Caller admission projection and rehydration
# Caller admission projection and Bridge execution rehydration
This runbook covers the exact durable Owner policy and the exact API caller:
This runbook covers the exact durable Owner policy and exact API caller:
- caller: \`chatgpt-governed-implementer-singlehost\`
- scope: \`release:candidate\`
- policy source: \`/etc/aios-gitops/owner-durable-authorization.json\`
- revocation source: \`/etc/aios-gitops/owner-durable-authorization-revocations.json\`
- caller: chatgpt-governed-implementer-singlehost
- durable policy: /etc/aios-gitops/owner-durable-authorization.json
- revocations: /etc/aios-gitops/owner-durable-authorization-revocations.json
- grant: owner-permanent-preparation-20260819
- durable capabilities: governed-task:project and release:candidate
## Admission source
The model API first authenticates the existing caller token from the static
registry. For this exact caller and exact release scope, static authentication
is not sufficient. The model API calls the existing Unix-socket Owner broker.
The broker reloads and validates the durable policy on every projection request,
including account, capability, optional expiry, and revocation.
The model API authenticates the caller with the existing static token registry
only to identify the caller and retain the existing static scopes. For the
exact caller, release:candidate and governed-task:project are not permanent
static-token grants. The model API asks the Unix-socket Owner broker to
rehydrate the requested durable capability.
Other callers retain their existing static-scope behavior. The target caller
does not receive a permanent \`release:candidate\` entry in the static registry,
and it is never treated as a release-worker account.
The broker reloads the durable policy and revocation file on every request,
checks the exact caller, requested capability, policy constraints, optional
expiry and revocation state, and rejects all capabilities outside the durable
policy. It never uses a release-worker identity or credential.
## Projection lifecycle and ACK semantics
The broker records only non-secret projection metadata in its existing durable
state database. The state sequence is:
The server-side flow is:
\`PENDING\` -> \`ACKED\`
durable Owner policy
-> exact caller and capability validation
-> caller projection rows for the policy capabilities
-> in-memory short-lived credential generation
-> projection ACKED / credential REHYDRATED
-> model API admission
-> Bridge governed-task admission and execution envelope
The ACK is produced by the broker's server-side projection consumer after the
exact policy and request binding have passed validation. The public response
contains projection state, effective scope, policy digest, expiry and
rehydration state. The short-lived credential material is returned only in a
private Unix-socket response header to the model API process. It is not written
to the response body, logs, diagnostics, Git, task metadata or terminal output.
The existing caller_projections table stores only projection metadata. A
rehydration request atomically projects the currently authorized caller
capabilities, so a valid current policy containing both capabilities returns
both
governed-task:project and release:candidate in effective_scopes. No raw
credential, bearer, token, or credential hash is stored.
ACKED means the broker completed the server-side projection transaction and
validated the returned public state. It is not an Owner Approval, release
approval, deployment authorization, or release-worker identity.
## Credential rehydration, expiry and revocation
The credential projection is generated in memory for a short TTL and is
discarded after the model API admission check. The durable row stores no raw
credential and no bearer token. After a broker or model API restart, the next
admission request creates a new short-lived credential from the durable
projection metadata; this is reported as \`RECOVERED\`.
Credential material is generated server-side, sent only in the private Unix
socket response header to the model API process, consumed for admission, and
discarded. The public response and introspection never contain it.
Every request reloads the policy. Missing, malformed, expired or revoked policy
fails closed. A caller mismatch, scope mismatch, invalid projection state,
broker outage, missing credential header or unexpected secret field also fails
closed. No static scope fallback is allowed for the target caller.
Each request reloads policy state. Missing, malformed, expired or revoked
policy, caller mismatch, capability mismatch, invalid projection state,
credential generation failure, or broker outage fails closed. Projection rows
are short-lived metadata and are rehydrated after process restart; the
rehydration state is GENERATED or RECOVERED.
## Diagnosis
The exact caller uses governed-task:project for createGovernedTask admission.
Other callers keep their existing branch:create path. The existing
agent-task:control, branch:create and file:write static behavior remains
available and is not expanded by this repair.
Use the read-only API with the caller's existing credential:
## Bridge execution projection
\`GET /api/caller-capabilities\`
A governed task created through the existing Owner broker -> Bridge path
receives metadata.execution with schema aios.online-agent.execution.v2 and an
execution_projection record. A source_capture placeholder with write
capabilities is intentionally not claimable. The existing deterministic plan
registry and execution-rebind CAS path must produce the deterministic plan
before a worker can claim it.
The legitimate execution chain is:
QUEUED -> PROJECTED -> deterministic-plan/rebind CAS -> CLAIMED
-> heartbeat -> Writer Lease -> CAS branch/file writes -> tests
-> official verifier -> immutable runtime evidence.
Never write metadata.execution, task events, plan records, ACKs, outbox rows or
broker database rows by hand. Never bypass the Bridge with SSH/root or a
release-worker credential.
## Restart recovery and failure states
After broker or model API restart, repeat the read-only capability check and
make one normal admission request. The broker must regenerate a short-lived
credential from the durable policy and report RECOVERED without manual token
operations.
Useful fail-closed states include:
- PROJECTION_BROKER_UNAVAILABLE
- POLICY_FILE_INVALID
- POLICY_EXPIRED
- OWNER_GRANT_REVOKED
- CALLER_MISMATCH
- SCOPE_MISMATCH
- CAPABILITY_NOT_AUTHORIZED
- PROJECTION_STATE_INVALID
- CREDENTIAL_REHYDRATION_MISSING
- TASK_EXECUTION_PROJECTION_MISSING
- TASK_PERSISTED_BINDING_CONFLICT
- EXECUTION_REBIND_CAS_MISMATCH
A rejected state is not repaired by editing SQLite, projection_outbox,
static token registry, task metadata or task events.
## Whoami and effective-scope diagnosis
Use the read-only model API with the caller's existing credential:
GET /api/caller-capabilities
The response is safe to display and contains:
- \`caller_identity\`
- \`admitted\`
- \`effective_scopes\`
- \`projection_state\`
- \`credential_state\`
- \`rehydration_state\`
- \`secret_exposed=false\`
- caller_identity
- admitted
- effective_scopes
- projection_state
- credential_state
- rehydration_state
- projection_error
- secret_exposed=false
It never contains a bearer, token, credential value or credential hash.
For the authorized current policy, effective_scopes must contain:
For a release candidate request, the model API also binds the exact
\`source_commit\` and \`version\` to the durable policy before invoking the
trusted release service. The caller cannot supply release evidence fields.
- agent-task:control
- branch:create
- file:write
- governed-task:project
- release:candidate
## Failure states and troubleshooting
No credential, token, bearer, password or secret value may appear in the
response, logs, diagnostics, Git diff or Task metadata.
- \`PROJECTION_BROKER_UNAVAILABLE\`: verify the broker unit, socket path and
the model API supplementary groups; do not add a static scope.
- \`OWNER_GRANT_REVOKED\` or \`POLICY_EXPIRED\`: restore a valid Owner policy
through governed policy management; do not edit broker state.
- \`CALLER_MISMATCH\` or \`SCOPE_MISMATCH\`: verify the authenticated account and
requested exact scope.
- \`PROJECTION_STATE_INVALID\`: inspect the durable projection through the
broker's read-only diagnostics and fail closed until it is reconciled.
- \`CREDENTIAL_REHYDRATION_MISSING\`: restart only the related control-plane
service after capturing pre-state and then repeat the read-only diagnosis.
- \`trusted candidate builder is not configured\`: Gate A admission is working,
but the separate existing TrustedCandidateBuilder runtime must be restored
before Gate B can create a candidate.
For broker-level diagnosis use the read-only /v1/capabilities endpoint through
the authorized Unix peer. It must identify the exact account and grant and
show both durable capabilities; it must not be used to create manual ACKs.
No Owner Approval, Deployment, Host Seed, Host Activation, Projector,
continuation task, Tag or Baseline action is part of this path.
## Release:candidate troubleshooting
For createReleaseCandidate, the caller must pass only source_commit and
version to the model API. The durable policy constraint requires an exact
40-hex source commit and version beginning with V2R2C. The model API first
rehydrates release:candidate and then invokes the existing release service.
If release admission fails, check caller-capabilities, policy validity,
revocation state, projection_state, credential_state and rehydration_state.
Do not add release:candidate to static tokens and do not use
online-release-builder-cn-network-mkt-20260819.
Creating and reading a Release Candidate is the end of this repair. Owner
Approval, Deployment, Host Seed, Host Activation, Projector, continuation
Task, Tag and Baseline actions are outside this scope.
+25 -2
View File
@@ -30,6 +30,7 @@ Upstream = BROKER_MODULE.Upstream
CALLER = "chatgpt-governed-implementer-singlehost"
SCOPE = "release:candidate"
GOVERNED_TASK_SCOPE = "governed-task:project"
SOURCE = "257c7a8df433f75b364bcaf69cbbc32e8c2e5d38"
@@ -38,7 +39,9 @@ def policy(*, expires_at=None, capabilities=None):
grant_id="grant-caller-projection-v1",
owner_identity="owner-durable-policy",
account=CALLER,
capabilities=frozenset(capabilities or {SCOPE}),
capabilities=frozenset(
capabilities or {GOVERNED_TASK_SCOPE, SCOPE}
),
issued_at=datetime.now(timezone.utc) - timedelta(minutes=1),
not_before=datetime.now(timezone.utc) - timedelta(minutes=1),
persistent_until_revoked=True,
@@ -83,7 +86,27 @@ class CallerProjectionTests(unittest.TestCase):
self.assertGreaterEqual(len(credential), 32)
self.assertEqual("ACKED", value["projection_state"])
self.assertEqual("REHYDRATED", value["credential_state"])
self.assertEqual([SCOPE], value["effective_scopes"])
self.assertEqual(
[GOVERNED_TASK_SCOPE, SCOPE],
value["effective_scopes"],
)
self.assertFalse(value["secret_exposed"])
def test_governed_task_projection_uses_durable_capability(self):
broker = make_broker(Path(tempfile.mkdtemp()))
with patch.object(broker, "policy", return_value=policy()):
value = broker.project_caller(
CALLER,
GOVERNED_TASK_SCOPE,
{},
idempotency_key="caller-projection-governed-task-v1",
)
value.pop("_credential")
self.assertEqual(GOVERNED_TASK_SCOPE, value["scope"])
self.assertEqual(
[GOVERNED_TASK_SCOPE, SCOPE],
value["effective_scopes"],
)
self.assertFalse(value["secret_exposed"])
def test_secret_material_is_not_durable(self):
@@ -0,0 +1,193 @@
from __future__ import annotations
import json
import sys
import types
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
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))
try:
import orchestrator.action_monitor
except ModuleNotFoundError:
_orchestrator = types.ModuleType("orchestrator")
_action_monitor = types.ModuleType("orchestrator.action_monitor")
_action_monitor.ActionMonitor = type("ActionMonitor", (), {})
_orchestrator.__path__ = []
sys.modules["orchestrator"] = _orchestrator
sys.modules["orchestrator.action_monitor"] = _action_monitor
import aios_durable_owner_authorization as broker_api
import aios_model_api_staging as model_api
CALLER = "chatgpt-governed-implementer-singlehost"
SCOPE = "release:candidate"
GOVERNED_TASK_SCOPE = "governed-task:project"
def _invoke_capabilities(
headers,
*,
auth_result=None,
auth_side_effect=None,
projection_client=None,
):
handler = object.__new__(model_api.H)
handler.headers = headers
responses = []
handler.sendj = lambda value, code=200: responses.append((value, code))
if auth_side_effect is not None:
auth_patch = patch.object(
model_api, "authenticate", side_effect=auth_side_effect
)
else:
auth_patch = patch.object(
model_api, "authenticate", return_value=auth_result
)
client = (
projection_client
if projection_client is not None
else model_api.CALLER_PROJECTION_CLIENT
)
with auth_patch, patch.object(model_api, "CALLER_PROJECTION_CLIENT", client):
model_api.H.caller_capabilities(handler)
assert len(responses) == 1
return responses[0]
@pytest.mark.parametrize(
"headers",
[
{},
{"Authorization": "Bearer invalid"},
{"Authorization": "Bearer unknown"},
{"X-AIOS-Token": "rec-none"},
],
)
def test_anonymous_invalid_unknown_and_rec_none_are_structured_401(headers):
payload, status = _invoke_capabilities(headers, auth_result=None)
assert status == 401
assert payload == {"error": "unauthorized", "hint": "Bearer token required"}
assert "Traceback" not in json.dumps(payload)
def test_authenticate_exception_is_fail_closed_without_socket_abort():
payload, status = _invoke_capabilities(
{"Authorization": "Bearer malformed"},
auth_side_effect=RuntimeError("malformed"),
)
assert status == 401
assert payload["error"] == "unauthorized"
assert "Traceback" not in json.dumps(payload)
def test_authorized_capability_introspection_remains_secret_free():
row = {
"account": CALLER,
"role": "user",
"scopes": ["agent-task:control", "branch:create", "file:write"],
}
result = SimpleNamespace(
public={
"projection_state": "ACKED",
"credential_state": "ACTIVE",
"rehydration_state": "REHYDRATED",
"effective_scopes": [GOVERNED_TASK_SCOPE, SCOPE],
},
credential_loaded=True,
)
class _ProjectionClient:
def rehydrate(self, caller, scope, request):
assert caller == CALLER
assert scope == GOVERNED_TASK_SCOPE
assert request == {}
return result
payload, status = _invoke_capabilities(
{"Authorization": "Bearer valid"},
auth_result=row,
projection_client=_ProjectionClient(),
)
assert status == 200
assert payload["caller_identity"] == CALLER
assert payload["admitted"] is True
assert SCOPE in payload["effective_scopes"]
assert GOVERNED_TASK_SCOPE in payload["effective_scopes"]
assert payload["projection_state"] == "ACKED"
assert payload["credential_state"] == "ACTIVE"
assert payload["rehydration_state"] == "REHYDRATED"
assert payload["secret_exposed"] is False
assert "_credential" not in json.dumps(payload)
class _FakeBroker:
def __init__(self):
self.calls = []
def project_caller(self, caller, scope, request, *, idempotency_key):
self.calls.append((caller, scope, request, idempotency_key))
return {
"caller": caller,
"scope": scope,
"projection_state": "ACKED",
"credential_state": "ACTIVE",
"rehydration_state": "REHYDRATED",
"effective_scopes": [SCOPE],
"_credential": "x" * 40,
}
def _dispatch_broker(path="/v1/project-caller", *, peer=True, account=True):
broker = _FakeBroker()
handler = object.__new__(broker_api.Handler)
handler.path = path
handler.broker = broker
handler.peer_authorized = lambda: peer
handler.account_authorized = lambda: account
handler.body = lambda: {
"caller": CALLER,
"scope": SCOPE,
"idempotency_key": "runtime-regression-test-20260823",
"request": {
"repository": "aios/aios",
"target_branch": "agent/runtime-regression-test",
"release_version": "V2R2C2",
},
}
responses = []
handler.send_json = lambda value, status, internal_credential=None: responses.append(
(value, status, internal_credential)
)
broker_api.Handler.do_POST(handler)
return responses, broker
def test_existing_project_caller_route_reaches_handler_without_public_secret():
responses, broker = _dispatch_broker()
assert len(responses) == 1
value, status, internal_credential = responses[0]
assert status == 200
assert broker.calls
assert broker.calls[0][0:2] == (CALLER, SCOPE)
assert internal_credential == "x" * 40
assert "_credential" not in value
assert "x" * 40 not in json.dumps(value)
def test_unauthorized_peer_is_denied_and_unregistered_route_is_not_added():
responses, broker = _dispatch_broker(peer=False)
assert responses[0][1] == 403
assert broker.calls == []
responses, broker = _dispatch_broker(path="/v1/not-registered")
assert responses[0][1] == 404
assert broker.calls == []