Compare commits

...
Author SHA1 Message Date
owner 63ab41a09b chore(code-review-graph): add CRG knowledge-graph MCP + CodeBuddy hooks for aios
git-diff-check git_diff_check_clean
secret-scan secret_scan_clean
py-compile no_python_changed_vacuous_pass
- .mcp.json: CodeBuddy stdio MCP -> code-review-graph.exe serve (venv, PYTHONUTF8=1)
- .codebuddy/settings.json: PostToolUse incremental update + SessionStart status hooks
- .codebuddy/skills/*: platform-native graph/explore/review skills
- CODEBUDDY.md: graph usage instructions
- .gitignore: ignore .code-review-graph/ data

Embedding uses siliconflow Qwen/Qwen3-VL-Embedding-8B via user-level CRG_OPENAI_* env
(API key kept out of repo).
2026-09-15 09:26:59 +08:00
owner 5cf20128d8 Merge pull request 'GOVERNED: Final integration R2+R2.1+G5 & G7 security closure (G1-G7 APPROVED)' (#84) from agent/final-integration-g1-g7-20260914-1409utc into main
Reviewed-on: #84
2026-09-14 23:00:55 +08:00
AIOS Governed Continuation 7e1649808d feat(r2+g5): R2+R2.1 unified platform convergence candidate + G5 import/execution-mode fixes
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)
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
AIOS Governed Continuation abe81d7f64 fix(sec): port G7 security-critical closure (S1-S5 + negative tests) onto final integration tree
Ported from authority branch agent/g7-security-critical-closure-20260914 (d5f5c9b, base=beabd053).
S1 auth: role/user from server token->identity mapping, ignore client X-AIOS-* headers.
S2 bootstrap credential: PBKDF2 random-salt, fail-closed SECRET, no plaintext fallback, session HMAC+expiry.
S3 path containment + control-plane file-read hardening.
S4 admin UI XSS + real governed approval + dashboard CSRF/branch validation.
S5 runtime delivery: registry.yaml conflict, openapi securityScheme, install.sh fail propagation, do_POST shadowing.
Tests: 5 negative suites (66 tests). Evidence pack in deliverables/g7-evidence/.
2026-09-14 22:11:51 +08:00
owner beabd05396 test(governed): align reopen fake CAS with expected_revision contract (V2R2C2 hot-fix)
autonomy-smoke-tests pr82 PASS evidence_sha256=c16c68f86ee0c4e8cfd0af3f76078be45441ee151395fbde5800145c08e189b5
autonomy-unit-tests pr82 PASS evidence_sha256=168bb88573b44876966c64ec2dc2b9e030fc7635ca0b18538678f1c492c97ea2
campaign-approval-gate pr82 PASS evidence_sha256=0616d6aa11496391686dcbd658123e51d8d057e1f02661af7ef8512554e404d8
git-diff-check pr82 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile pr82 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract pr82 PASS evidence_sha256=8250e3e736db14432c421c46f995c201540e2b9c50958bdeb3ef7b057c4fd577
release-state-machine pr82 PASS evidence_sha256=b6c51b401d65a9c69435f8b86cecab752d65ded7188228aab3de3eb010990b60
secret-scan pr82 PASS evidence_sha256=f8ed9052d5ba0fbe8e52f171ea180fa353a89b4049cd3e1ed881736d0622c752
signer-validator-contract pr82 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
aios.owner-final-merge-authorization-v2 real v2 contract tests passed
2026-09-12 15:29:18 +08:00
owner 5a4e952b39 fix(governed): pin saga CAS to current revision in approval reopen (V2R2C2)
autonomy-smoke-tests pr82 PASS evidence_sha256=c16c68f86ee0c4e8cfd0af3f76078be45441ee151395fbde5800145c08e189b5
autonomy-unit-tests pr82 FAIL evidence_sha256=44994ff4cfe08df298fe59f790f3f98df7aa29e4d239ecd185b3143818c540cd
campaign-approval-gate pr82 PASS evidence_sha256=0616d6aa11496391686dcbd658123e51d8d057e1f02661af7ef8512554e404d8
git-diff-check pr82 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile pr82 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract pr82 PASS evidence_sha256=a0d5c1849c776f73fa64b02300e10c193241c0f9460e6ad4e69d73ae720de90b
release-state-machine pr82 PASS evidence_sha256=1d3314562a9345a9f75b1adf32d61168822c00d316f7db85833230bba023e448
secret-scan pr82 PASS evidence_sha256=9ca1f810bf76589206704716c4f3701a6b6aca1066b1d0c24b91d5be008f52a2
signer-validator-contract pr82 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
2026-09-12 15:16:32 +08:00
owner b70c89217b feat(governed): add /reopenOwnerApproval to silent-release allow-list (V2R2C2 v1.2)
autonomy-smoke-tests pr81 PASS evidence_sha256=e9b29f42e72804aedf5a269d4507aafa7270b39400c6923bbb014d8a335fe788
autonomy-unit-tests pr81 PASS evidence_sha256=068610c15e2716ad9dbda4df158f891693e53cb7da2c57aef3783e722fb6f354
campaign-approval-gate pr81 PASS evidence_sha256=0616d6aa11496391686dcbd658123e51d8d057e1f02661af7ef8512554e404d8
git-diff-check pr81 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile pr81 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract pr81 PASS evidence_sha256=f96a26b816cb58aec4639ca90e87da07ed8cf7d041d82fb67d755bbd5085dfca
release-state-machine pr81 PASS evidence_sha256=6cea88752b3e58ae5cee56ff3b1b13eab34117918d6bad9661194c031e2dc157
secret-scan pr81 PASS evidence_sha256=e85624b4ea35c4f7019bc9d9a3d7d44124afca891c3487fe9f550faf6c743245
signer-validator-contract pr81 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
aios.owner-final-merge-authorization-v2 real v2 contract tests passed
Narrow v1.2 delta: add the Owner approval-reopen POST path to
SILENT_RELEASE_POST_PATHS so reopen can proceed in Shadow without enabling
AIOS_GITOPS_WRITES_ENABLED. Route handler, release:approve guard, global
write check and all other allow-list paths are unchanged.

Adds tests/test_reopen_shadow_gate_contract.py covering:
- reopen is a narrow silent-release POST (route requires release:approve)
- global write gate is not enabled by this source patch
- all existing silent-release POST paths remain present
2026-09-12 14:29:33 +08:00
AIOS Governed Continuation dd2bd8fa08 feat(governed): add Owner approval reopen control-plane capability (V2R2C2)
autonomy-smoke-tests pr80 PASS evidence_sha256=c16c68f86ee0c4e8cfd0af3f76078be45441ee151395fbde5800145c08e189b5
autonomy-unit-tests pr80 PASS evidence_sha256=02e00edbe6415cf1d548ef7fb53b67e37c4abd29397b00075c6feb0d48b5f2c5
campaign-approval-gate pr80 PASS evidence_sha256=0616d6aa11496391686dcbd658123e51d8d057e1f02661af7ef8512554e404d8
git-diff-check pr80 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
py-compile pr80 PASS evidence_sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
reconciliation-contract pr80 PASS evidence_sha256=a98ae8e4b231cd5a106781a85b051ad0543e0481d604919bab8fc1047de342eb
release-state-machine pr80 PASS evidence_sha256=b6c51b401d65a9c69435f8b86cecab752d65ded7188228aab3de3eb010990b60
secret-scan pr80 PASS evidence_sha256=fb45191fc28616fa47b5c8ac1f5d6541f3e8d3163eb6653c9678ab84dff44e61
signer-validator-contract pr80 PASS evidence_sha256=775b7aa27d0c17f48b3feff20c836f14b6ed8ebcb90ec7e0b76d2a13c579268a
aios.owner-final-merge-authorization-v2 real v2 contract tests passed
Add a fail-closed control-plane operation POST /reopenOwnerApproval (scope
release:approve) that reopens one expired Owner approval cycle only after
signed_owner_approval_accepted is the final Saga event and every release byte
(candidate/source/tree/artifact/manifest/release-plan/policy) matches exactly.

- New module release_approval_reopen.py: archives the old approval byte-for-byte
  under approval-history/<candidate>/<fingerprint>.json, then drives
  ReleaseSagaStore.compare_and_set(APPROVED -> READY_FOR_APPROVAL) and removes
  only the old active approval slot (post-CAS). Idempotent per idempotency key.
- ReleaseCandidate v1 durable records do not persist side_effects_executed; this
  accepts an absent marker only when the accepted-approval event is the final
  Saga event, and rejects any explicit non-false value.
- Does not modify ReleaseSagaStore, never calls force_state_for_recovery_test,
  never performs direct DB or legacy-global writes. Publishes control-plane
  capability only; keeps 4642b74 on the mainline ancestry.
2026-09-12 12:13:33 +08:00
110 changed files with 24370 additions and 268 deletions
+28
View File
@@ -0,0 +1,28 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|Bash",
"hooks": [
{
"type": "command",
"command": "cat >/dev/null || true; CRG=\"C:/Users/ImRuY/.workbuddy-ai/binaries/python/envs/default/Scripts/code-review-graph.exe\"; [ -x \"$CRG\" ] || exit 0; git rev-parse --git-dir >/dev/null 2>&1 && PYTHONUTF8=1 \"$CRG\" update --skip-flows --repo \"$(git rev-parse --show-toplevel 2>/dev/null)\" || true",
"timeout": 60
}
]
}
],
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "cat >/dev/null || true; CRG=\"C:/Users/ImRuY/.workbuddy-ai/binaries/python/envs/default/Scripts/code-review-graph.exe\"; [ -x \"$CRG\" ] || exit 0; git rev-parse --git-dir >/dev/null 2>&1 && PYTHONUTF8=1 \"$CRG\" status --repo \"$(git rev-parse --show-toplevel 2>/dev/null)\" || echo 'Not a git repo, skipping'",
"timeout": 30
}
]
}
]
}
}
+28
View File
@@ -0,0 +1,28 @@
---
name: debug-issue
description: Systematically debug issues using graph-powered code navigation
---
## Debug Issue
Use the knowledge graph to systematically trace and debug issues.
### Steps
1. Use `semantic_search_nodes_tool` to find code related to the issue.
2. Use `query_graph_tool` with `callers_of` and `callees_of` to trace call chains.
3. Use `get_flow_tool` to see full execution paths through suspected areas.
4. Run `detect_changes_tool` to check if recent changes caused the issue.
5. Use `get_impact_radius_tool` on suspected files to see what else is affected.
### Tips
- Check both callers and callees to understand the full context.
- Look at affected flows to find the entry point that triggers the bug.
- Recent changes are the most common source of new issues.
## Token Efficiency Rules
- Start with `get_minimal_context_tool(task="<your task>")` before other graph tools.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
- Read the implementation and its tests before changing code. The graph narrows scope; it does not replace the source.
@@ -0,0 +1,29 @@
---
name: explore-codebase
description: Navigate and understand codebase structure using the knowledge graph
---
## Explore Codebase
Use the code-review-graph MCP tools to explore and understand the codebase.
### Steps
1. Run `list_graph_stats_tool` to see overall codebase metrics.
2. Run `get_architecture_overview_tool` for high-level community structure.
3. Use `list_communities_tool` to find major modules, then `get_community_tool` for details.
4. Use `semantic_search_nodes_tool` to find specific functions or classes.
5. Use `query_graph_tool` with patterns like `callers_of`, `callees_of`, `imports_of` to trace relationships.
6. Use `list_flows_tool` and `get_flow_tool` to understand execution paths.
### Tips
- Start broad (stats, architecture) then narrow down to specific areas.
- Use `children_of` on a file to see all its functions and classes.
- Use `find_large_functions_tool` to identify complex code.
## Token Efficiency Rules
- Start with `get_minimal_context_tool(task="<your task>")` before other graph tools.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
- Read the implementation and its tests before changing code. The graph narrows scope; it does not replace the source.
@@ -0,0 +1,29 @@
---
name: refactor-safely
description: Plan and execute safe refactoring using dependency analysis
---
## Refactor Safely
Use the knowledge graph to plan and execute refactoring with confidence.
### Steps
1. Use `refactor_tool` with mode="suggest" for community-driven refactoring suggestions.
2. Use `refactor_tool` with mode="dead_code" to find unreferenced code.
3. For renames, use `refactor_tool` with mode="rename" to preview all affected locations.
4. Use `apply_refactor_tool` with the refactor_id to apply renames.
5. After changes, run `detect_changes_tool` to verify the refactoring impact.
### Safety Checks
- Always preview before applying (rename mode gives you an edit list).
- Check `get_impact_radius_tool` before major refactors.
- Use `get_affected_flows_tool` to ensure no critical paths are broken.
- Run `find_large_functions_tool` to identify decomposition targets.
## Token Efficiency Rules
- Start with `get_minimal_context_tool(task="<your task>")` before other graph tools.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
- Read the implementation and its tests before changing code. The graph narrows scope; it does not replace the source.
+30
View File
@@ -0,0 +1,30 @@
---
name: review-changes
description: Perform a structured code review using change detection and impact
---
## Review Changes
Perform a thorough, risk-aware code review using the knowledge graph.
### Steps
1. Run `detect_changes_tool` to get risk-scored change analysis.
2. Run `get_affected_flows_tool` to find impacted execution paths.
3. For each high-risk function, run `query_graph_tool` with pattern="tests_for" to check test coverage.
4. Run `get_impact_radius_tool` to understand the blast radius.
5. For any untested changes, suggest specific test cases.
### Output Format
Provide findings grouped by risk level (high/medium/low) with:
- What changed and why it matters
- Test coverage status
- Suggested improvements
- Overall merge recommendation
## Token Efficiency Rules
- Start with `get_minimal_context_tool(task="<your task>")` before other graph tools.
- Use `detail_level="minimal"` on all calls. Only escalate to "standard" when minimal is insufficient.
- Target: complete any review/debug/refactor task in ≤5 tool calls and ≤800 total output tokens.
- Read the implementation and its tests before changing code. The graph narrows scope; it does not replace the source.
+2
View File
@@ -11,3 +11,5 @@ runtime/tmp/
runtime/cache/
logs/
config/test_write_perms
# Added by code-review-graph
.code-review-graph/
+15
View File
@@ -0,0 +1,15 @@
{
"mcpServers": {
"code-review-graph": {
"command": "C:\\Users\\ImRuY\\.workbuddy-ai\\binaries\\python\\envs\\default\\Scripts\\code-review-graph.exe",
"args": [
"serve"
],
"cwd": "C:\\Users\\ImRuY\\aios-main-clone",
"env": {
"PYTHONUTF8": "1"
},
"type": "stdio"
}
}
}
+45
View File
@@ -0,0 +1,45 @@
<!-- code-review-graph MCP tools -->
## MCP Tools: code-review-graph
**This project has a knowledge graph. Start with the code-review-graph
MCP tools to narrow scope, then read the source.** The graph is cheaper than scanning files and
gives you structural context (callers, dependents, test coverage) that file search cannot.
### When to use graph tools FIRST
- **Exploring code**: `semantic_search_nodes_tool` or `query_graph_tool` instead of Grep
- **Understanding impact**: `get_impact_radius_tool` instead of manually tracing imports
- **Code review**: `detect_changes_tool` + `get_review_context_tool` instead of reading entire files
- **Finding relationships**: `query_graph_tool` with callers_of/callees_of/imports_of/tests_for
- **Architecture questions**: `get_architecture_overview_tool` + `list_communities_tool`
### Verify in the source
- Narrow scope with the graph, then read the source. Do not change code from graph output alone.
- For any non-trivial change, read the implementation and the relevant tests before concluding.
- Verify the exact source when touching behavior, database logic, migrations, retries, fallbacks,
recovery, or compatibility code.
- When the graph and the source disagree, the source wins. The graph may be stale or may not
model that relationship.
- An empty graph result can mean "not indexed" or "not statically visible", not "does not exist".
### Key Tools
| Tool | Use when |
| ------ | ---------- |
| `detect_changes_tool` | Reviewing code changes — gives risk-scored analysis |
| `get_review_context_tool` | Need source snippets for review — token-efficient |
| `get_impact_radius_tool` | Understanding blast radius of a change |
| `get_affected_flows_tool` | Finding which execution paths are impacted |
| `query_graph_tool` | Tracing callers, callees, imports, tests, dependencies |
| `semantic_search_nodes_tool` | Finding functions/classes by name or keyword |
| `get_architecture_overview_tool` | Understanding high-level codebase structure |
| `refactor_tool` | Planning renames, finding dead code |
### Workflow
1. The graph auto-updates on file changes (via hooks).
2. Use `detect_changes_tool` for code review.
3. Use `get_affected_flows_tool` to understand impact.
4. Use `query_graph_tool` pattern="tests_for" to check coverage.
<!-- /code-review-graph MCP tools -->
+43 -2
View File
@@ -16,6 +16,28 @@ from .agent_observability_store import AgentObservabilityStore
store = AgentObservabilityStore()
dashboard = AgentObservabilityDashboard(store.load())
_ROLE_WHITELIST = {"user", "group_admin", "owner"}
_authenticator = None # callable() -> {"user_id":..., "role":...} | None, injected by host
def set_authenticator(fn):
"""Install a server-side authenticator used to derive the querying tenant.
The callable returns the authenticated principal (dict with "user_id" and
a whitelisted "role") or None for anonymous. The client-supplied role and
user/group ids are NOT trusted to escalate scope.
"""
global _authenticator
_authenticator = fn
def _principal():
if _authenticator is not None:
p = _authenticator()
if isinstance(p, dict) and p.get("role") in _ROLE_WHITELIST:
return p
return None
def record_agent_execution(payload: dict[str, Any]):
record = AgentExecutionRecord(**payload)
@@ -23,11 +45,30 @@ def record_agent_execution(payload: dict[str, Any]):
def get_agent_observability(user_id: str | None = None, group_id: str | None = None, role: str = "user"):
records = store.query(user_id, group_id, role)
# The authenticated principal (tenant) is authoritative; the request params
# role/user_id/group_id can never choose owner or another tenant.
principal = _principal()
pr_user = principal.get("user_id") if principal else None
pr_role = principal.get("role", "user") if principal else "user"
if pr_role == "owner":
# Server-verified owner may browse a requested scope.
q_user, q_group, q_role = user_id, group_id, "owner"
summary_scope = None
elif pr_user:
# Authenticated non-owner: strictly scoped to own tenant.
q_user, q_group, q_role = pr_user, None, "user"
summary_scope = pr_user
else:
# Anonymous: nothing scoped to them.
q_user, q_group, q_role = None, None, "user"
summary_scope = None
records = store.query(q_user, q_group, q_role)
return {
"status": "ok",
"records": records,
"summary": store.load().summary(user_id if role != "owner" else None),
"summary": store.load().summary(summary_scope),
}
@@ -8,11 +8,15 @@ metadata are stored for Web UI statistics and audit.
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
from .agent_observability import AgentExecutionRecord, AgentObservability
# Constrain execution IDs (used as filename) to a single safe path segment.
_SAFE_EXECUTION_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
class AgentObservabilityStore:
def __init__(self, root: str = "runtime/agent-observability"):
@@ -21,9 +25,17 @@ class AgentObservabilityStore:
self.records_dir.mkdir(parents=True, exist_ok=True)
def append(self, record: AgentExecutionRecord) -> dict[str, Any]:
path = self.records_dir / f"{record.execution_id}.json"
execution_id = str(record.execution_id or "")
if not _SAFE_EXECUTION_ID.fullmatch(execution_id) or ".." in execution_id:
raise ValueError(f"invalid execution_id: {execution_id!r}")
base = self.records_dir.resolve()
target = (self.records_dir / f"{execution_id}.json").resolve()
# The whitelist forbids separators/traversal, so the resolved target
# must sit directly inside the records dir; verify defensively.
if target.parent != base:
raise ValueError(f"invalid execution_id: {execution_id!r}")
data = record.to_dict()
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
target.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
return data
def load(self) -> AgentObservability:
+27 -1
View File
@@ -4,6 +4,8 @@ from __future__ import annotations
import hashlib
import json
import os
import re
from pathlib import Path
import stat
from typing import Any
@@ -13,6 +15,29 @@ class RecoveryEvidenceError(RuntimeError):
pass
# Relative evidence path: a safe relative path, never absolute, never with
# traversal segments, never with backslash separators.
_SAFE_EVIDENCE_REL = re.compile(r"^[A-Za-z0-9_./@-]+$")
def _contained_evidence(path: Path, base: Path) -> Path:
"""Resolve an evidence path and guarantee it stays under `base`."""
raw = str(path)
if (
os.path.isabs(raw)
or "\\" in raw
or raw.startswith("/")
or ".." in Path(raw).parts
or not _SAFE_EVIDENCE_REL.fullmatch(raw)
):
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_PATH_INVALID")
base_resolved = base.resolve()
target = (base_resolved / raw).resolve()
if base_resolved != target and not str(target).startswith(str(base_resolved) + os.sep):
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_PATH_ESCAPE")
return target
def _digest(path: Path) -> str:
h = hashlib.sha256()
with path.open("rb") as stream:
@@ -81,10 +106,11 @@ def load_recovered_authorization(path: str | Path) -> dict[str, Any]:
evidence = receipt["evidence_files"]
if not isinstance(evidence, list) or not evidence:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_LIST_INVALID")
base = receipt_path.resolve().parent
for item in evidence:
if not isinstance(item, dict) or set(item) != {"path", "sha256"}:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_ENTRY_INVALID")
target = Path(item["path"])
target = _contained_evidence(Path(item["path"]), base)
if _digest(target) != item["sha256"]:
raise RecoveryEvidenceError("RECOVERY_EVIDENCE_DIGEST_MISMATCH")
return {
+23 -5
View File
@@ -110,6 +110,11 @@ SECRET_VAL_RE = re.compile(
r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})'
)
# Control-plane file read: enforce auth, strict identifier, containment and a
# size cap on the command-detail read exposed by /admin/ai-evolution/api/data.
MAX_COMMAND_READ_BYTES = 1024 * 1024
_COMMAND_FILENAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*\.md$")
READ_ONLY_GET_PATHS = frozenset({
'/', '/health', '/api/health', '/api/openapi.yaml',
'/api/openapi-chatgpt-actions.yaml', '/api/schema', '/api', '/api/status',
@@ -1042,12 +1047,25 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
commands.append(f.name)
command_detail=None
if cmd_filter:
actor = self.actor()
if not actor['ok']:
return self.sendj({'error':'unauthorized','hint':'Bearer token required'},401)
cmd = str(cmd_filter or '')
if not _COMMAND_FILENAME_RE.fullmatch(cmd) or '..' in cmd:
return self.sendj({'command':cmd_filter,'error':'unsafe_command'},400)
for d in cmd_dirs:
if d.exists():
fp=d/cmd_filter
if fp.exists():
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
break
if not d.exists():
continue
base = d.resolve()
fp = (base / cmd).resolve()
# Strict containment: must resolve directly inside one of the
# allowed command directories.
if fp.parent != base or not fp.is_file():
continue
if fp.stat().st_size > MAX_COMMAND_READ_BYTES:
return self.sendj({'command':cmd_filter,'error':'command_too_large'},413)
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
break
if not command_detail:
command_detail={'command':cmd_filter,'error':'not_found'}
return self.sendj(command_detail)
@@ -664,6 +664,23 @@ class GitOpsJobController:
raise JobError("LEASE_DURATION_EXCEEDED")
return _iso_utc(deadline)
@staticmethod
def internal_job_id_for_idempotency_key(client_idempotency_key: str) -> str:
"""Return the durable Job ID for a client idempotency key."""
if (
not isinstance(client_idempotency_key, str)
or not client_idempotency_key
or len(client_idempotency_key) > 128
or not all(
character.isalnum() or character in "._:-"
for character in client_idempotency_key
)
):
raise JobError("IDEMPOTENCY_KEY_INVALID")
return "job-" + sha256_json({
"client_idempotency_key": client_idempotency_key,
})[:32]
@staticmethod
def _binding(request: Mapping[str, Any]) -> dict[str, str]:
if request.get("schema") == "aios.operation-job.v1":
@@ -767,7 +784,12 @@ class GitOpsJobController:
key = sha256_json({"client_idempotency_key": client_idempotency_key})
else:
key = sha256_json(binding)
job_id = "job-" + key[:32]
job_id = (
self.internal_job_id_for_idempotency_key(client_idempotency_key)
if binding.get("schema") != "aios.operation-job.v1"
and client_idempotency_key is not None
else "job-" + key[:32]
)
durable_request = dict(binding)
if payload_ref is not None:
durable_request.update({"payload_ref": str(payload_ref), "payload_size": int(payload_size)})
+32 -4
View File
@@ -57,10 +57,36 @@ def seed_base_models():
if not one('SELECT * FROM gitops_versions WHERE version=?', ('V1R1C0',)):
exec_sql('INSERT INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', ('V1R1C0','V1R1C0','R','MultiUser runtime selector + GitOps + custom LLM priority registry','ready','system',now()))
_ROLE_WHITELIST = {'owner', 'admin', 'user'}
def token_identity(token: str):
"""Resolve the server-authoritative identity bound to a token.
Format of each TOKEN_FILE line (one per line, ':'-separated):
<token> -> authenticated principal, role 'user'
<token>:<user>:<role> -> mapped principal; role restricted to owner/admin/user
Identity NEVER comes from a client-supplied header. In local dev mode the
DEV_ALLOW flag maps a fixed dev principal (must be explicitly enabled).
"""
if DEV_ALLOW:
return {'username': 'dev-owner', 'role': 'owner'}
if not token or not TOKEN_FILE.exists():
return None
for line in TOKEN_FILE.read_text().splitlines():
line = line.strip()
if not line:
continue
parts = [x.strip() for x in line.split(':')]
if not parts or parts[0] != token:
continue
if len(parts) >= 3:
role = parts[2] if parts[2] in _ROLE_WHITELIST else 'user'
return {'username': safe_name(parts[1]), 'role': role}
return {'username': 'user-%s' % token[:8], 'role': 'user'}
return None
def valid_token(token: str) -> bool:
if DEV_ALLOW: return True
if not token or not TOKEN_FILE.exists(): return False
return token.strip() in [x.strip() for x in TOKEN_FILE.read_text().splitlines() if x.strip()]
return token_identity(token) is not None
def git_cmd(args, check=False):
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git command failed')
@@ -90,8 +116,10 @@ class Handler(BaseHTTPRequestHandler):
server_version = 'AIOSGitOpsModelAPI/V1R1C0'
def actor(self):
auth = self.headers.get('Authorization',''); token = auth.replace('Bearer ', '').strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
ok = valid_token(token); user = self.headers.get('X-AIOS-User') or ('dev-owner' if DEV_ALLOW else 'anonymous'); role = self.headers.get('X-AIOS-Role') or ('owner' if DEV_ALLOW else 'user')
return {'username': safe_name(user), 'role': role, 'token_ok': ok}
ident = token_identity(token)
if ident is None:
return {'username': 'anonymous', 'role': 'user', 'token_ok': False}
return {'username': ident['username'], 'role': ident['role'], 'token_ok': True}
def require_write(self, roles=None):
a = self.actor()
if not a['token_ok']: self.send_json({'error':'unauthorized','hint':'write APIs require Bearer token or AIOS_GITOPS_DEV_ALLOW=1'}, 401); return None
@@ -0,0 +1,890 @@
"""Read-only four-authority evidence provider for one canonical C04 attachment.
This module never creates, claims, resumes, reconciles, or mutates a Task or
Job. The expired Owner epoch is verified as historical provenance only.
"""
from __future__ import annotations
import hashlib
import json
import os
import sqlite3
import stat
import time
import urllib.error
import urllib.parse
import urllib.request
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Callable, Mapping
from app.aios_gitops_job_identity import legacy_task_storage_key, task_storage_key
from app.aios_single_host_authority import (
DEFAULT_OWNER_SIGNER_ID,
OWNER_NAMESPACE,
RegistryOwnerApprovalVerifier,
SignerRegistry,
)
from app.task_plane.legacy_attachment import (
ATTACH_EVIDENCE_SCHEMA,
AuthorityReadbackRef,
SOURCE_KIND,
VerifiedLegacyAttachmentEvidence,
)
class LegacyAttachmentRuntimeError(RuntimeError):
"""Stable fail-closed readback error."""
def _fail(code: str) -> None:
raise LegacyAttachmentRuntimeError(code)
def _strict_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
value: dict[str, Any] = {}
for key, item in pairs:
if key in value:
_fail("DUPLICATE_JSON_FIELD")
value[key] = item
return value
def _json_object(raw: bytes | str, code: str) -> dict[str, Any]:
try:
if isinstance(raw, bytes):
raw = raw.decode("utf-8")
value = json.loads(raw, object_pairs_hook=_strict_object)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LegacyAttachmentRuntimeError(code) from exc
if not isinstance(value, dict):
_fail(code)
return value
def _canonical(value: object) -> bytes:
try:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
except (TypeError, ValueError) as exc:
raise LegacyAttachmentRuntimeError("AUTHORITY_JSON_INVALID") from exc
def _sha(value: object) -> str:
return hashlib.sha256(_canonical(value)).hexdigest()
def _hex(value: object, code: str) -> str:
if (
not isinstance(value, str)
or len(value) != 64
or any(ch not in "0123456789abcdef" for ch in value)
):
_fail(code)
return value
def _text(value: object, code: str) -> str:
if not isinstance(value, str) or not value or value != value.strip():
_fail(code)
return value
def _integer(value: object, code: str) -> int:
if type(value) is not int or value < 0:
_fail(code)
return value
def _iso(value: object, code: str) -> str:
text = _text(value, code)
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError as exc:
raise LegacyAttachmentRuntimeError(code) from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
_fail(code)
return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
def _observed(clock: Callable[[], float]) -> str:
return datetime.fromtimestamp(float(clock()), timezone.utc).isoformat().replace(
"+00:00", "Z"
)
def _mapping(value: object, code: str) -> dict[str, Any]:
if not isinstance(value, Mapping):
_fail(code)
return dict(value)
def _secure_file(
path: Path, *, require_root_owner: bool, writable_authority: bool = False
) -> os.stat_result:
try:
info = path.lstat()
except OSError as exc:
raise LegacyAttachmentRuntimeError("AUTHORITY_FILE_UNAVAILABLE") from exc
if path.is_symlink() or not stat.S_ISREG(info.st_mode):
_fail("AUTHORITY_FILE_INVALID")
if os.name == "posix":
if not writable_authority and stat.S_IMODE(info.st_mode) & 0o022:
_fail("AUTHORITY_FILE_WRITABLE")
if require_root_owner and info.st_uid != 0:
_fail("AUTHORITY_FILE_OWNER_INVALID")
return info
def _secure_directory(path: Path) -> os.stat_result:
try:
info = path.lstat()
except OSError as exc:
raise LegacyAttachmentRuntimeError("AUTHORITY_DIRECTORY_UNAVAILABLE") from exc
if path.is_symlink() or not stat.S_ISDIR(info.st_mode):
_fail("AUTHORITY_DIRECTORY_INVALID")
return info
def _stable_read(path: Path, *, require_root_owner: bool, maximum: int = 2_000_000) -> bytes:
before = _secure_file(path, require_root_owner=require_root_owner)
if before.st_size > maximum:
_fail("AUTHORITY_FILE_TOO_LARGE")
try:
raw = path.read_bytes()
except OSError as exc:
raise LegacyAttachmentRuntimeError("AUTHORITY_FILE_UNAVAILABLE") from exc
after = _secure_file(path, require_root_owner=require_root_owner)
if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (
after.st_dev,
after.st_ino,
after.st_size,
after.st_mtime_ns,
):
_fail("AUTHORITY_FILE_CHANGED")
return raw
def _ro_connection(path: Path, *, require_root_owner: bool) -> tuple[sqlite3.Connection, os.stat_result]:
before = _secure_file(
path, require_root_owner=require_root_owner, writable_authority=True
)
try:
resolved = path.resolve(strict=True)
connection = sqlite3.connect(
resolved.as_uri() + "?mode=ro", uri=True, timeout=5.0
)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA query_only=ON")
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("BEGIN")
return connection, before
except (OSError, sqlite3.Error) as exc:
raise LegacyAttachmentRuntimeError("AUTHORITY_DB_UNAVAILABLE") from exc
def _finish_ro(connection: sqlite3.Connection, path: Path, before: os.stat_result) -> None:
connection.close()
after = _secure_file(
path, require_root_owner=False, writable_authority=True
)
if (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino):
_fail("AUTHORITY_DB_CHANGED")
def _columns(connection: sqlite3.Connection, table: str) -> tuple[str, ...]:
return tuple(str(row[1]) for row in connection.execute(f"PRAGMA table_info({table})"))
@dataclass(frozen=True, slots=True)
class LegacyC04Binding:
mission_id: str
root_task_id: str
anchor_task_id: str
owner_principal_id: str
original_acceptance_digest: str
source_task_id: str
external_job_id: str
def validate(self) -> None:
for value in (
self.mission_id,
self.root_task_id,
self.anchor_task_id,
self.owner_principal_id,
self.source_task_id,
self.external_job_id,
):
_text(value, "C04_BINDING_INVALID")
_hex(self.original_acceptance_digest, "C04_ACCEPTANCE_DIGEST_INVALID")
@dataclass(frozen=True, slots=True)
class CredentialFileBearerSupplier:
"""Read a systemd credential for one call without caching or displaying it."""
path: Path
def __post_init__(self) -> None:
if not self.path.is_absolute():
_fail("AGENT_TASK_CREDENTIAL_PATH_INVALID")
if os.name == "posix":
try:
self.path.resolve(strict=False).relative_to(Path("/run/credentials"))
except ValueError as exc:
raise LegacyAttachmentRuntimeError(
"AGENT_TASK_CREDENTIAL_PATH_INVALID"
) from exc
def __call__(self) -> str:
raw = _stable_read(self.path, require_root_owner=False, maximum=8192)
try:
value = raw.decode("utf-8").strip()
except UnicodeDecodeError as exc:
raise LegacyAttachmentRuntimeError(
"AGENT_TASK_CREDENTIAL_INVALID"
) from exc
if len(value) < 32 or len(value) > 4096:
_fail("AGENT_TASK_CREDENTIAL_INVALID")
return value
class _NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001
return None
_AGENT_TASK_COLUMNS = (
"task_id",
"goal",
"instructions",
"success_criteria_json",
"required_capabilities_json",
"constraints_json",
"metadata_json",
"deadline",
"priority",
"max_attempts",
"attempt",
"status",
"claimed_by",
"lease_token_hash",
"lease_expires_at",
"result_json",
"result_digest",
"result_idempotency_key",
"verification_json",
"revision",
"created_at",
"updated_at",
)
@dataclass(slots=True)
class AuthenticatedAgentTaskReader:
"""Authenticated non-mutating proof + read-only authority-row snapshot.
The authenticated HTTP request is intentionally sent to the task events
endpoint because the historical GET /v1/tasks/{id} path calls the global
lease-expiry mutator. The task row itself is read from the authoritative
SQLite database using query_only mode and an exact schema allowlist.
"""
base_url: str
bearer_supplier: Callable[[], str]
task_db_path: Path
timeout_seconds: float = 10.0
clock: Callable[[], float] = time.time
require_db_root_owner: bool = False
def __post_init__(self) -> None:
parsed = urllib.parse.urlsplit(self.base_url)
if (
parsed.scheme != "http"
or parsed.hostname not in {"127.0.0.1", "localhost", "::1"}
or parsed.username is not None
or parsed.password is not None
or parsed.query
or parsed.fragment
):
_fail("AGENT_TASK_URL_INVALID")
if not self.task_db_path.is_absolute():
_fail("AGENT_TASK_DB_PATH_INVALID")
self.base_url = self.base_url.rstrip("/")
def _authenticated_events(self, task_id: str) -> tuple[list[dict[str, Any]], str]:
token = self.bearer_supplier()
if not isinstance(token, str) or len(token.strip()) < 32:
_fail("AGENT_TASK_CREDENTIAL_INVALID")
url = (
self.base_url
+ "/v1/tasks/"
+ urllib.parse.quote(task_id, safe="._:-")
+ "/events"
)
request = urllib.request.Request(
url,
method="GET",
headers={
"Authorization": "Bearer " + token.strip(),
"Accept": "application/json",
},
)
try:
with urllib.request.build_opener(_NoRedirect).open(
request, timeout=self.timeout_seconds
) as response:
if response.status != 200 or response.geturl() != url:
_fail("AGENT_TASK_EVENTS_REJECTED")
raw = response.read(2_000_001)
except (urllib.error.URLError, TimeoutError, OSError) as exc:
raise LegacyAttachmentRuntimeError(
"AGENT_TASK_EVENTS_UNAVAILABLE"
) from exc
finally:
token = ""
if len(raw) > 2_000_000:
_fail("AGENT_TASK_EVENTS_TOO_LARGE")
try:
value = json.loads(raw.decode("utf-8"), object_pairs_hook=_strict_object)
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
raise LegacyAttachmentRuntimeError("AGENT_TASK_EVENTS_INVALID") from exc
if not isinstance(value, list):
_fail("AGENT_TASK_EVENTS_INVALID")
events: list[dict[str, Any]] = []
for event in value:
if not isinstance(event, Mapping) or event.get("task_id") != task_id:
_fail("AGENT_TASK_EVENTS_INVALID")
events.append(dict(event))
return events, url
@staticmethod
def _task_projection(row: sqlite3.Row) -> dict[str, Any]:
try:
body = {
"task_id": row["task_id"],
"goal": row["goal"],
"instructions": row["instructions"],
"success_criteria": json.loads(row["success_criteria_json"]),
"required_capabilities": json.loads(row["required_capabilities_json"]),
"constraints": json.loads(row["constraints_json"]),
"metadata": json.loads(row["metadata_json"]),
"deadline": row["deadline"],
"priority": row["priority"],
"max_attempts": row["max_attempts"],
"attempt": row["attempt"],
"status": row["status"],
"claimed_by": row["claimed_by"],
"lease_expires_at": row["lease_expires_at"],
"revision": row["revision"],
"created_at": row["created_at"],
"updated_at": row["updated_at"],
"result": json.loads(row["result_json"]) if row["result_json"] else None,
"verification": (
json.loads(row["verification_json"])
if row["verification_json"]
else None
),
}
except (KeyError, TypeError, json.JSONDecodeError) as exc:
raise LegacyAttachmentRuntimeError(
"AGENT_TASK_ROW_INVALID"
) from exc
_mapping(body["constraints"], "AGENT_TASK_CONSTRAINTS_INVALID")
_mapping(body["metadata"], "AGENT_TASK_METADATA_INVALID")
_integer(body["revision"], "AGENT_TASK_REVISION_INVALID")
if body["status"] not in {"SUCCEEDED", "FAILED", "EXPIRED", "CANCELLED"}:
_fail("AGENT_TASK_NOT_TERMINAL")
return body
def read(self, task_id: str) -> dict[str, Any]:
events, events_url = self._authenticated_events(task_id)
connection, before = _ro_connection(
self.task_db_path, require_root_owner=self.require_db_root_owner
)
try:
if _columns(connection, "tasks") != _AGENT_TASK_COLUMNS:
_fail("AGENT_TASK_DB_SCHEMA_INVALID")
row = connection.execute(
"SELECT * FROM tasks WHERE task_id=? LIMIT 1", (task_id,)
).fetchone()
if row is None:
_fail("AGENT_TASK_NOT_FOUND")
body = self._task_projection(row)
finally:
_finish_ro(connection, self.task_db_path, before)
event_digest = _sha(events)
return {
"body": body,
"observed_at": _observed(self.clock),
"readback_ref": (
events_url
+ "#authenticated-events-sha256="
+ event_digest
+ ";task-db-revision="
+ str(body["revision"])
),
"canonical_sha256": _sha(body),
"authenticated_events_sha256": event_digest,
}
_ADMISSION_COLUMNS = {
"task_admission_envelopes": (
"idempotency_key", "envelope_id", "request_digest", "envelope_json", "status", "created_at"
),
"task_admission_grants": (
"envelope_id", "grant_id", "grant_digest", "grant_json", "authorization_digest", "created_at"
),
"task_admission_receipts": ("envelope_id", "task_id", "receipt_json", "created_at"),
}
@dataclass(slots=True)
class AdmissionEpochRegistryReader:
db_path: Path
epoch_root: Path
revocation_file: Path
registry_path: Path
require_db_root_owner: bool = False
require_static_root_owner: bool = True
clock: Callable[[], float] = time.time
def __post_init__(self) -> None:
for path in (self.db_path, self.epoch_root, self.revocation_file, self.registry_path):
if not path.is_absolute():
_fail("AUTHORITY_PATH_NOT_ABSOLUTE")
def read(self, task_id: str) -> dict[str, Any]:
connection, before = _ro_connection(
self.db_path, require_root_owner=self.require_db_root_owner
)
try:
if any(_columns(connection, table) != columns for table, columns in _ADMISSION_COLUMNS.items()):
_fail("ADMISSION_SCHEMA_INVALID")
rows = connection.execute(
"SELECT e.*,g.grant_id,g.grant_digest,g.grant_json,g.authorization_digest,"
"g.created_at AS grant_created_at,r.task_id,r.receipt_json,"
"r.created_at AS receipt_created_at FROM task_admission_receipts r "
"JOIN task_admission_envelopes e ON e.envelope_id=r.envelope_id "
"JOIN task_admission_grants g ON g.envelope_id=r.envelope_id WHERE r.task_id=?",
(task_id,),
).fetchall()
if len(rows) != 1:
_fail("ADMISSION_READBACK_AMBIGUOUS")
row = dict(rows[0])
except sqlite3.Error as exc:
raise LegacyAttachmentRuntimeError("ADMISSION_READBACK_FAILED") from exc
finally:
_finish_ro(connection, self.db_path, before)
envelope = _json_object(row["envelope_json"], "ADMISSION_ENVELOPE_INVALID")
grant = _json_object(row["grant_json"], "ADMISSION_GRANT_INVALID")
receipt = _json_object(row["receipt_json"], "ADMISSION_RECEIPT_INVALID")
envelope_id = _text(row["envelope_id"], "ADMISSION_ENVELOPE_INVALID")
grant_id = _text(row["grant_id"], "ADMISSION_GRANT_INVALID")
epoch_id = _text(grant.get("parent_epoch_id"), "ADMISSION_EPOCH_ID_INVALID")
if (
row.get("status") != "ADMITTED"
or envelope.get("schema") != "aios.task-admission-envelope.v1"
or grant.get("schema") != "aios.task-admission-grant.v1"
or receipt.get("schema") != "aios.task-admission-receipt.v1"
or envelope.get("envelope_id") != envelope_id
or grant.get("envelope_id") != envelope_id
or receipt.get("envelope_id") != envelope_id
or envelope.get("task_id") != task_id
or grant.get("task_id") != task_id
or receipt.get("task_id") != task_id
or grant.get("grant_id") != grant_id
or receipt.get("grant_id") != grant_id
or grant.get("authorization_kind") != "delegation_epoch"
or receipt.get("authorization_kind") != "delegation_epoch"
or receipt.get("parent_epoch_id") != epoch_id
):
_fail("ADMISSION_BINDING_INVALID")
request_value = {
key: envelope.get(key)
for key in (
"task_id", "repository", "authority_base_oid", "target_branch",
"generation", "execution_digest", "requested_capabilities",
"file_allowlist", "risk", "trust_domain", "policy_digest",
)
}
if envelope.get("request_digest") != _sha(request_value) or row.get("request_digest") != _sha(request_value):
_fail("ADMISSION_REQUEST_DIGEST_INVALID")
grant_digest = _sha(grant)
if row.get("grant_digest") != grant_digest or receipt.get("grant_digest_sha256") != grant_digest:
_fail("ADMISSION_GRANT_DIGEST_INVALID")
auth_value = {
"envelope_id": envelope_id,
"authorization_kind": "delegation_epoch",
"delegation_epoch_id": epoch_id,
}
if row.get("authorization_digest") != _sha(auth_value):
_fail("ADMISSION_AUTHORIZATION_DIGEST_INVALID")
linked = (
"repository", "authority_base_oid", "target_branch", "generation",
"execution_digest", "policy_digest", "trust_domain", "risk",
)
if any(grant.get(key) != envelope.get(key) for key in linked):
_fail("ADMISSION_GRANT_SCOPE_INVALID")
if grant.get("capabilities") != envelope.get("requested_capabilities") or grant.get("file_allowlist") != envelope.get("file_allowlist"):
_fail("ADMISSION_GRANT_SCOPE_INVALID")
if receipt.get("execution_digest") != grant.get("execution_digest"):
_fail("ADMISSION_RECEIPT_SCOPE_INVALID")
_secure_directory(self.epoch_root)
canonical_path = self.epoch_root / "owner-delegation-epoch.canonical.json"
signature_path = self.epoch_root / "owner-delegation-epoch.sig"
key_path = self.epoch_root / "owner-delegation-epoch.key-id"
canonical = _stable_read(canonical_path, require_root_owner=self.require_static_root_owner)
epoch = _json_object(canonical, "OWNER_EPOCH_INVALID")
if _canonical(epoch) != canonical:
_fail("OWNER_EPOCH_NOT_CANONICAL")
signature = _stable_read(signature_path, require_root_owner=self.require_static_root_owner).decode("utf-8")
key_id = _stable_read(key_path, require_root_owner=self.require_static_root_owner).decode("utf-8").strip()
registry_raw = _stable_read(self.registry_path, require_root_owner=self.require_static_root_owner)
registry_json = _json_object(registry_raw, "OWNER_REGISTRY_INVALID")
revocations = _json_object(
_stable_read(self.revocation_file, require_root_owner=self.require_static_root_owner),
"OWNER_REVOCATIONS_INVALID",
)
if (
set(revocations) != {"schema", "revoked_epoch_ids"}
or revocations.get("schema") != "aios.owner-delegation-revocations.v1"
or not isinstance(revocations.get("revoked_epoch_ids"), list)
or epoch_id in revocations["revoked_epoch_ids"]
):
_fail("OWNER_EPOCH_REVOKED")
try:
registry = SignerRegistry(
self.registry_path, require_root_owner=self.require_static_root_owner
)
record = registry.require(DEFAULT_OWNER_SIGNER_ID, role="OWNER_AUTHORITY")
verified = RegistryOwnerApprovalVerifier(
self.registry_path,
require_root_owner=self.require_static_root_owner,
signer_id=DEFAULT_OWNER_SIGNER_ID,
).verify(
namespace=OWNER_NAMESPACE,
payload=canonical,
signature=signature,
key_id=key_id,
)
except Exception as exc:
raise LegacyAttachmentRuntimeError("OWNER_REGISTRY_VERIFICATION_FAILED") from exc
now = int(self.clock())
provenance_checks = {
"signature": verified is True,
"fingerprint": record.fingerprint == key_id,
"schema": epoch.get("schema") == "aios.owner-delegation-epoch.v1",
"epoch_id": epoch.get("epoch_id") == epoch_id,
"owner": epoch.get("owner_identity") == grant.get("owner_identity"),
"repository": epoch.get("repository") == grant.get("repository"),
"trust_domain": epoch.get("trust_domain") == grant.get("trust_domain"),
"policy": epoch.get("policy_digest") == grant.get("policy_digest"),
"revocation_epoch": epoch.get("revocation_epoch") == 0,
"expires_type": type(epoch.get("expires_at")) is int,
"not_before_type": type(epoch.get("not_before")) is int,
}
failed = [name for name, passed in provenance_checks.items() if not passed]
if failed:
_fail("OWNER_EPOCH_PROVENANCE_INVALID:" + ",".join(sorted(failed)))
if (
now < epoch["expires_at"]
or type(epoch.get("issued_at")) is not int
or epoch["issued_at"] > grant.get("issued_at", 0)
or grant.get("issued_at", 0) < epoch["not_before"]
or grant.get("expires_at", 0) > epoch["expires_at"]
):
_fail("OWNER_EPOCH_PROVENANCE_INVALID:temporal")
if not any(str(grant["target_branch"]).startswith(str(prefix)) for prefix in epoch.get("allowed_branch_prefixes", [])):
_fail("OWNER_EPOCH_SCOPE_INVALID")
if not set(grant.get("capabilities", [])).issubset(set(epoch.get("allowed_capabilities", []))):
_fail("OWNER_EPOCH_SCOPE_INVALID")
risk_rank = {"low": 0, "medium": 1, "high": 2}
if (
grant.get("risk") not in risk_rank
or epoch.get("risk_ceiling") not in risk_rank
or risk_rank[grant["risk"]] > risk_rank[epoch["risk_ceiling"]]
):
_fail("OWNER_EPOCH_SCOPE_INVALID")
if any(
not any(path == prefix.rstrip("/") or path.startswith(prefix.rstrip("/") + "/") for prefix in epoch.get("allowed_file_prefixes", []))
for path in grant.get("file_allowlist", [])
):
_fail("OWNER_EPOCH_SCOPE_INVALID")
admission_projection = {
"envelope": envelope,
"grant": grant,
"receipt": receipt,
"request_digest": row["request_digest"],
"grant_digest": row["grant_digest"],
"authorization_digest": row["authorization_digest"],
"envelope_status": row["status"],
}
owner_projection = {
"epoch": epoch,
"epoch_sha256": hashlib.sha256(canonical).hexdigest(),
"epoch_key_id": key_id,
"signature_verified": True,
"epoch_currently_valid": False,
"revocations": revocations,
"registry_schema": registry_json.get("schema"),
"registry_generation": registry.authority_generation,
"signer": record.to_dict(),
}
observed = _observed(self.clock)
return {
"envelope": envelope,
"grant": grant,
"receipt": receipt,
"epoch": epoch,
"epoch_digest": hashlib.sha256(canonical).hexdigest(),
"signer_id": record.signer_id,
"signer_fingerprint": record.fingerprint,
"registry_generation": registry.authority_generation,
"observed_at": observed,
"admission_ref": f"sqlite:{self.db_path}#task={task_id};envelope={envelope_id}",
"admission_sha256": _sha(admission_projection),
"registry_ref": f"file:{self.registry_path}#generation={registry.authority_generation};epoch={epoch_id}",
"registry_sha256": _sha(owner_projection),
}
_JOB_COLUMNS = {
"job_aliases": (
"external_job_id", "internal_job_id", "task_id", "repository", "target_branch",
"branch_mode", "generation", "commit_idempotency_key", "expected_branch_oid",
"request_digest", "adopted_legacy", "created_at",
),
"jobs": (
"job_id", "idempotency_key", "campaign_id", "repository", "operation", "branch",
"expected_branch_oid", "payload_digest", "payload_ref", "payload_size", "state",
"side_effect_state", "revision", "request_json", "receipt_json", "created_at", "updated_at",
),
"idempotency_keys": ("key", "job_id", "binding_json", "created_at"),
}
@dataclass(slots=True)
class BridgeJobSQLiteReader:
jobs_root: Path
require_root_owner: bool = False
clock: Callable[[], float] = time.time
def __post_init__(self) -> None:
if not self.jobs_root.is_absolute():
_fail("BRIDGE_JOB_ROOT_NOT_ABSOLUTE")
def read(self, task_id: str, external_job_id: str) -> dict[str, Any]:
exact_root = self.jobs_root / task_storage_key(task_id)
legacy_root = self.jobs_root / legacy_task_storage_key(task_id)
db_path = exact_root / "state.sqlite3"
if legacy_root != exact_root and legacy_root.exists():
_fail("BRIDGE_JOB_STORE_AMBIGUOUS")
_secure_directory(self.jobs_root)
_secure_directory(exact_root)
try:
if db_path.resolve(strict=True).parent != exact_root.resolve(strict=True):
_fail("BRIDGE_JOB_STORE_INVALID")
except OSError as exc:
raise LegacyAttachmentRuntimeError("BRIDGE_JOB_STORE_INVALID") from exc
connection, before = _ro_connection(
db_path, require_root_owner=self.require_root_owner
)
try:
if any(_columns(connection, table) != columns for table, columns in _JOB_COLUMNS.items()):
_fail("BRIDGE_JOB_SCHEMA_INVALID")
aliases = connection.execute(
"SELECT * FROM job_aliases WHERE external_job_id=? AND task_id=?",
(external_job_id, task_id),
).fetchall()
if len(aliases) != 1:
_fail("BRIDGE_JOB_ALIAS_AMBIGUOUS")
alias = dict(aliases[0])
job_rows = connection.execute(
"SELECT * FROM jobs WHERE job_id=?", (alias["internal_job_id"],)
).fetchall()
key_rows = connection.execute(
"SELECT * FROM idempotency_keys WHERE job_id=?", (alias["internal_job_id"],)
).fetchall()
if len(job_rows) != 1 or len(key_rows) != 1:
_fail("BRIDGE_JOB_BINDING_MISSING")
job, key = dict(job_rows[0]), dict(key_rows[0])
except sqlite3.Error as exc:
raise LegacyAttachmentRuntimeError("BRIDGE_JOB_READBACK_FAILED") from exc
finally:
_finish_ro(connection, db_path, before)
request = _json_object(job.get("request_json") or "{}", "BRIDGE_JOB_REQUEST_INVALID")
binding = _mapping(request.get("binding", request), "BRIDGE_JOB_REQUEST_INVALID")
key_binding = _json_object(key.get("binding_json") or "{}", "BRIDGE_JOB_KEY_BINDING_INVALID")
external_commit_key = _text(alias.get("commit_idempotency_key"), "BRIDGE_EXTERNAL_IDEMPOTENCY_INVALID")
internal_key = _text(job.get("idempotency_key"), "BRIDGE_INTERNAL_IDEMPOTENCY_INVALID")
if (
alias.get("external_job_id") != external_job_id
or alias.get("task_id") != task_id
or job.get("job_id") != alias.get("internal_job_id")
or job.get("campaign_id") != task_id
or key.get("job_id") != job.get("job_id")
or key.get("key") != internal_key
or binding.get("task_id", binding.get("campaign_id")) != task_id
or binding.get("external_job_id") != external_job_id
or binding.get("idempotency_key") != external_commit_key
or key_binding.get("task_id", key_binding.get("campaign_id")) != task_id
or key_binding.get("external_job_id") != external_job_id
or key_binding.get("idempotency_key") != external_commit_key
or alias.get("repository") != job.get("repository")
or alias.get("repository") != binding.get("repository")
or alias.get("target_branch") != job.get("branch")
or alias.get("target_branch") != binding.get("branch", binding.get("target_branch"))
):
_fail("BRIDGE_JOB_BINDING_INVALID")
try:
if int(alias.get("generation")) != int(binding.get("generation")):
_fail("BRIDGE_JOB_BINDING_INVALID")
except (TypeError, ValueError) as exc:
raise LegacyAttachmentRuntimeError("BRIDGE_JOB_BINDING_INVALID") from exc
projection = {
"alias": alias,
"job": job,
"idempotency": key,
"external_commit_idempotency_key": external_commit_key,
"internal_idempotency_key": internal_key,
}
return {
**projection,
"observed_at": _observed(self.clock),
"readback_ref": f"sqlite:{db_path}#external={external_job_id};internal={job['job_id']}",
"canonical_sha256": _sha(projection),
}
@dataclass(slots=True)
class LegacyAttachmentRuntimeProvider:
binding: LegacyC04Binding
agent_task: AuthenticatedAgentTaskReader
admission: AdmissionEpochRegistryReader
bridge_job: BridgeJobSQLiteReader
def __post_init__(self) -> None:
self.binding.validate()
def read_verified(
self,
*,
mission_id: str,
root_task_id: str,
anchor_task_id: str,
source_kind: str,
source_task_id: str,
external_job_id: str,
original_acceptance_digest: str,
) -> VerifiedLegacyAttachmentEvidence:
expected = self.binding
if (
mission_id != expected.mission_id
or root_task_id != expected.root_task_id
or anchor_task_id != expected.anchor_task_id
or source_kind != SOURCE_KIND
or source_task_id != expected.source_task_id
or external_job_id != expected.external_job_id
or original_acceptance_digest != expected.original_acceptance_digest
):
_fail("C04_AUTHORITY_BINDING_MISMATCH")
task_read = self.agent_task.read(source_task_id)
admission_read = self.admission.read(source_task_id)
job_read = self.bridge_job.read(source_task_id, external_job_id)
task = task_read["body"]
metadata = _mapping(task["metadata"], "AGENT_TASK_METADATA_INVALID")
constraints = _mapping(task["constraints"], "AGENT_TASK_CONSTRAINTS_INVALID")
execution = _mapping(metadata.get("execution"), "AGENT_TASK_EXECUTION_INVALID")
grant = admission_read["grant"]
envelope = admission_read["envelope"]
alias, job = job_read["alias"], job_read["job"]
try:
immutable_metadata = _json_object(
constraints.get("immutable_metadata", ""), "AGENT_TASK_IMMUTABLE_METADATA_INVALID"
)
except LegacyAttachmentRuntimeError:
raise
if (
immutable_metadata != metadata
or metadata.get("grant_id") != grant.get("grant_id")
or metadata.get("grant_digest_sha256") != admission_read["receipt"].get("grant_digest_sha256")
or metadata.get("execution_digest") != grant.get("execution_digest")
or execution.get("execution_digest") != grant.get("execution_digest")
or execution.get("external_job_id") != external_job_id
or execution.get("task_id") != source_task_id
or metadata.get("repository") != grant.get("repository")
or metadata.get("target_branch") != grant.get("target_branch")
or metadata.get("generation") != grant.get("generation")
or constraints.get("repository") != grant.get("repository")
or constraints.get("target_branch") != grant.get("target_branch")
or constraints.get("generation") != grant.get("generation")
or envelope.get("task_id") != source_task_id
or alias.get("repository") != grant.get("repository")
or alias.get("target_branch") != grant.get("target_branch")
or alias.get("generation") != grant.get("generation")
or admission_read["signer_id"] != expected.owner_principal_id
):
_fail("FOUR_AUTHORITY_BINDING_INVALID")
return VerifiedLegacyAttachmentEvidence(
schema=ATTACH_EVIDENCE_SCHEMA,
source_kind=SOURCE_KIND,
source_task_id=source_task_id,
source_task_created_at=_iso(task.get("created_at"), "AGENT_TASK_CREATED_AT_INVALID"),
source_task_status=_text(task.get("status"), "AGENT_TASK_STATUS_INVALID"),
source_task_revision=_integer(task.get("revision"), "AGENT_TASK_REVISION_INVALID"),
source_task_attempt=_integer(task.get("attempt"), "AGENT_TASK_ATTEMPT_INVALID"),
source_task_max_attempts=_integer(task.get("max_attempts"), "AGENT_TASK_MAX_ATTEMPTS_INVALID"),
external_job_id=external_job_id,
internal_job_id=_text(job.get("job_id"), "BRIDGE_INTERNAL_JOB_INVALID"),
internal_idempotency_key=_text(job.get("idempotency_key"), "BRIDGE_INTERNAL_IDEMPOTENCY_INVALID"),
job_branch=_text(job.get("branch"), "BRIDGE_JOB_BRANCH_INVALID"),
job_state=_text(job.get("state"), "BRIDGE_JOB_STATE_INVALID"),
job_revision=_integer(job.get("revision"), "BRIDGE_JOB_REVISION_INVALID"),
side_effect_state=_text(job.get("side_effect_state"), "BRIDGE_SIDE_EFFECT_INVALID"),
grant_id=_text(grant.get("grant_id"), "ADMISSION_GRANT_INVALID"),
grant_digest=_hex(admission_read["receipt"].get("grant_digest_sha256"), "ADMISSION_GRANT_DIGEST_INVALID"),
execution_digest=_hex(grant.get("execution_digest"), "ADMISSION_EXECUTION_DIGEST_INVALID"),
epoch_id=_text(admission_read["epoch"].get("epoch_id"), "OWNER_EPOCH_ID_INVALID"),
epoch_digest=_hex(admission_read["epoch_digest"], "OWNER_EPOCH_DIGEST_INVALID"),
historical_signer_id=admission_read["signer_id"],
historical_signer_fingerprint=admission_read["signer_fingerprint"],
registry_generation=_integer(admission_read["registry_generation"], "OWNER_REGISTRY_GENERATION_INVALID"),
signature_verified=True,
epoch_currently_valid=False,
readbacks=(
AuthorityReadbackRef(
role="agent_task", authority="aios-agent-task-api",
readback_ref=task_read["readback_ref"], canonical_sha256=task_read["canonical_sha256"],
observed_at=task_read["observed_at"], revision=task["revision"],
),
AuthorityReadbackRef(
role="admission", authority="aios-task-admission-store",
readback_ref=admission_read["admission_ref"], canonical_sha256=admission_read["admission_sha256"],
observed_at=admission_read["observed_at"], revision=None,
),
AuthorityReadbackRef(
role="owner_registry", authority="aios-single-host-owner-authority",
readback_ref=admission_read["registry_ref"], canonical_sha256=admission_read["registry_sha256"],
observed_at=admission_read["observed_at"], revision=admission_read["registry_generation"],
),
AuthorityReadbackRef(
role="bridge_job", authority="aios-gitops-job-controller",
readback_ref=job_read["readback_ref"], canonical_sha256=job_read["canonical_sha256"],
observed_at=job_read["observed_at"], revision=job["revision"],
),
),
)
+46 -19
View File
@@ -135,6 +135,11 @@ SECRET_VAL_RE = re.compile(
r'(sk-[A-Za-z0-9_-]{20,}|HPUA[A-Z0-9]{10,}|SDC[A-Za-z0-9]{20,})'
)
# Control-plane file read: enforce auth, strict identifier, containment and a
# size cap on the command-detail read exposed by /admin/ai-evolution/api/data.
MAX_COMMAND_READ_BYTES = 1024 * 1024
_COMMAND_FILENAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*\.md$")
READ_ONLY_GET_PATHS = frozenset({
'/', '/health', '/api/health', '/api/openapi.yaml',
'/api/openapi-chatgpt-actions.yaml', '/api/schema', '/api', '/api/status',
@@ -155,6 +160,7 @@ SILENT_RELEASE_POST_PATHS = frozenset({
'/submitSignedApproval',
'/submitOwnerApproval',
'/startDeployment',
'/reopenOwnerApproval',
})
def reject_non_read_http(handler, method: str, path: str) -> bool:
@@ -689,20 +695,6 @@ class H(BaseHTTPRequestHandler):
return self.sendj(body, exc.code)
except (urllib.error.URLError, TimeoutError) as exc:
return self.sendj({'error': 'GOVERNED_BRIDGE_UNAVAILABLE', 'detail': type(exc).__name__}, 503)
def do_POST(self):
path = urllib.parse.urlparse(self.path).path
if path in {
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
'/buildProductionArtifact', '/executeApprovedProductionRelease',
'/getReleaseConsistency', '/runReleaseAcceptance',
}:
actor = self.need('tag:create')
if not actor:
return
return self.bridge_release(path, self.body())
if reject_non_read_http(self, 'POST', path):
return
return self.sendj({'error': 'not_found', 'path': path}, 404)
def do_GET(self):
u=urllib.parse.urlparse(self.path); path=u.path.rstrip('/') if u.path != '/' else u.path
if path not in READ_ONLY_GET_PATHS: return self.sendj({'error':'not_found','path':path},404)
@@ -948,6 +940,17 @@ class H(BaseHTTPRequestHandler):
path=urllib.parse.urlparse(self.path).path.rstrip('/')
if reject_non_read_http(self, 'POST', path): return
data=self.body()
# Governed V2 bridge release routes (previously shadowed by the
# redefinition of do_POST in this class).
if path in {
'/createTagApprovalRequest', '/requestOrCreateTag', '/getTagPublicationStatus',
'/buildProductionArtifact', '/executeApprovedProductionRelease',
'/getReleaseConsistency', '/runReleaseAcceptance',
}:
actor = self.need('tag:create')
if not actor:
return
return self.bridge_release(path, self.body())
try:
if path == '/createReleaseCandidate':
a=self.need('release:candidate')
@@ -974,6 +977,17 @@ class H(BaseHTTPRequestHandler):
return self.sendj(
silent_release_service().submit_owner_approval(data)
)
if path == "/reopenOwnerApproval":
a = self.need("release:approve")
if not a:
return
from release_approval_reopen import ApprovalReopenError, reopen_owner_approval
try:
result = reopen_owner_approval(silent_release_service(), data)
except ApprovalReopenError as exc:
return self.sendj({"error": exc.code, "message": str(exc)}, 409)
return self.sendj(result)
if path == '/startDeployment':
a=self.need('release:deploy')
if not a: return
@@ -1236,12 +1250,25 @@ return{vs,cmds,ar,cmd,cdet,tot,sok,sfail,runs,baseline,refreshMsg,loadCmd,selVer
commands.append(f.name)
command_detail=None
if cmd_filter:
actor = self.actor()
if not actor['ok']:
return self.sendj({'error':'unauthorized','hint':'Bearer token required'},401)
cmd = str(cmd_filter or '')
if not _COMMAND_FILENAME_RE.fullmatch(cmd) or '..' in cmd:
return self.sendj({'command':cmd_filter,'error':'unsafe_command'},400)
for d in cmd_dirs:
if d.exists():
fp=d/cmd_filter
if fp.exists():
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
break
if not d.exists():
continue
base = d.resolve()
fp = (base / cmd).resolve()
# Strict containment: must resolve directly inside one of the
# allowed command directories.
if fp.parent != base or not fp.is_file():
continue
if fp.stat().st_size > MAX_COMMAND_READ_BYTES:
return self.sendj({'command':cmd_filter,'error':'command_too_large'},413)
command_detail={'command':cmd_filter,'content':fp.read_text(encoding='utf-8')}
break
if not command_detail:
command_detail={'command':cmd_filter,'error':'not_found'}
return self.sendj(command_detail)
@@ -11,10 +11,12 @@ import base64
from dataclasses import dataclass
from datetime import datetime, timezone
import hashlib
import hmac
import importlib.util
import json
import os
import re
import sqlite3
from pathlib import Path
import stat
import time
@@ -29,6 +31,15 @@ from aios_task_admission_bootstrap import (
canonical_json_bytes,
sha256_json,
)
from aios_gitops_job_controller import (
GitOpsJobController as _FLGitOpsJobController,
JobState as _FLJobState,
sha256_json as _fl_job_sha256_json,
)
from aios_gitops_job_identity import (
legacy_task_storage_key as _fl_legacy_task_storage_key,
task_storage_key as _fl_task_storage_key,
)
OWNER_NAMESPACE = "aios-owner-approval-v1"
@@ -1069,6 +1080,8 @@ PLAN_FIELDS_V3=frozenset({
_SHA256=re.compile(r"^[0-9a-f]{64}$")
_OID=re.compile(r"^[0-9a-f]{40}$")
_JOB_ID=re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$")
_TASK_ID=re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,179}$")
_TASK_BRANCH=re.compile(r"^agent/[A-Za-z0-9][A-Za-z0-9._/-]{2,179}$")
class ProducerReceiptError(RuntimeError):
@@ -1130,6 +1143,38 @@ def require_target_grant(grant:object)->None:
fail("TASK_ADMISSION_EXECUTION_DIGEST_INVALID")
def require_reservable_grant(grant:object)->None:
"""Validate the authenticated, persisted grant accepted by the producer.
The historical target keeps its exact tuple. Every other target is driven
by the grant persisted by TaskAdmissionBootstrap; no Mission metadata or
caller-supplied execution identity is consulted here.
"""
task_id=_gv(grant,"task_id")
if task_id==TARGET_TASK_ID:
require_target_grant(grant)
return
if not isinstance(task_id,str) or not _TASK_ID.fullmatch(task_id):
fail("TASK_ADMISSION_POST_OWNER_RESERVATION_TASK_INVALID")
if _gv(grant,"repository")!=TARGET_REPOSITORY:
fail("TASK_ADMISSION_POST_OWNER_RESERVATION_REPOSITORY_INVALID")
base=_gv(grant,"authority_base_oid")
if not isinstance(base,str) or not _OID.fullmatch(base):
fail("TASK_ADMISSION_POST_OWNER_RESERVATION_BASE_INVALID")
branch=_gv(grant,"target_branch")
if not isinstance(branch,str) or not _TASK_BRANCH.fullmatch(branch):
fail("TASK_ADMISSION_POST_OWNER_RESERVATION_BRANCH_INVALID")
generation=_gv(grant,"generation")
if isinstance(generation,bool) or not isinstance(generation,int) or generation<1:
fail("TASK_ADMISSION_POST_OWNER_RESERVATION_GENERATION_INVALID")
digest=_gv(grant,"execution_digest")
if not isinstance(digest,str) or not _SHA256.fullmatch(digest):
fail("TASK_ADMISSION_EXECUTION_DIGEST_INVALID")
grant_id=_gv(grant,"grant_id")
if not isinstance(grant_id,str) or not grant_id:
fail("TASK_ADMISSION_POST_OWNER_RESERVATION_GRANT_INVALID")
def _safe_path(value:object)->str:
if not isinstance(value,str) or not value or "\x00" in value:
fail("TASK_ADMISSION_EXECUTION_PLAN_FILE_PATH_INVALID")
@@ -1152,7 +1197,7 @@ def _unique_strings(value:object,code:str)->list[str]:
def validate_v3_plan(*,grant:object,raw:bytes)->dict[str,Any]:
require_target_grant(grant)
require_reservable_grant(grant)
plan=strict_json_object(raw)
if set(plan)!=PLAN_FIELDS_V3:
fail("TASK_ADMISSION_EXECUTION_PLAN_FIELDS_INVALID")
@@ -1195,14 +1240,17 @@ def validate_v3_plan(*,grant:object,raw:bytes)->dict[str,Any]:
def load_v3_plan(root:Path,grant:object)->dict[str,Any]:
require_target_grant(grant)
require_reservable_grant(grant)
try:
rst=root.lstat()
except OSError:
fail("TASK_ADMISSION_EXECUTION_PLAN_UNAVAILABLE")
if (
stat.S_ISLNK(rst.st_mode) or not stat.S_ISDIR(rst.st_mode)
or rst.st_uid!=0 or rst.st_mode&0o022
or (
_host_posix_metadata_enforced()
and (rst.st_uid!=0 or rst.st_mode&0o022)
)
):
fail("TASK_ADMISSION_EXECUTION_PLAN_DIRECTORY_INVALID")
path=root/(str(_gv(grant,"execution_digest"))+".json")
@@ -1212,7 +1260,10 @@ def load_v3_plan(root:Path,grant:object)->dict[str,Any]:
fail("TASK_ADMISSION_EXECUTION_PLAN_UNAVAILABLE")
if (
stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode)
or st.st_uid!=0 or st.st_mode&0o022
or (
_host_posix_metadata_enforced()
and (st.st_uid!=0 or st.st_mode&0o022)
)
):
fail("TASK_ADMISSION_EXECUTION_PLAN_FILE_INVALID")
try:
@@ -1223,25 +1274,30 @@ def load_v3_plan(root:Path,grant:object)->dict[str,Any]:
def reservation_idempotency_key(grant:object)->str:
require_target_grant(grant)
require_reservable_grant(grant)
material=(
str(_gv(grant,"grant_id"))+"\0"+str(_gv(grant,"execution_digest"))
).encode("utf-8")
return "task-admission-execution-reservation-"+hashlib.sha256(material).hexdigest()[:32]
def build_reservation_request(grant:object)->dict[str,Any]:
require_target_grant(grant)
return {
"task_id":TARGET_TASK_ID,
"repository":TARGET_REPOSITORY,
"base_oid":TARGET_BASE_OID,
"target_branch":TARGET_BRANCH,
"generation":TARGET_GENERATION,
def build_reservation_request(
grant:object,*,expected_branch_oid:str|None=None,
)->dict[str,Any]:
require_reservable_grant(grant)
request={
"task_id":str(_gv(grant,"task_id")),
"repository":str(_gv(grant,"repository")),
"base_oid":str(_gv(grant,"authority_base_oid")),
"target_branch":str(_gv(grant,"target_branch")),
"generation":int(_gv(grant,"generation")),
"risk":str(_gv(grant,"risk")),
"description":"Reserve one inert durable GitOps Job identity after exact Owner TaskAdmission authorization; no Agent Task claim or execution.",
"idempotency_key":reservation_idempotency_key(grant),
}
if expected_branch_oid is not None:
request["expected_branch_oid"]=expected_branch_oid
return request
def _mapping(value:object,code:str)->Mapping[str,Any]:
@@ -1256,21 +1312,22 @@ def validate_bootstrap_response(
request:Mapping[str,Any],
response:object,
expected_side_effect_state:str,
expected_branch_mode:str="CONTINUE_EXISTING",
)->str:
require_target_grant(grant)
require_reservable_grant(grant)
result=_mapping(response,"TASK_ADMISSION_EXECUTION_RESERVATION_RESPONSE_INVALID")
job_id=result.get("job_id")
if not isinstance(job_id,str) or not _JOB_ID.fullmatch(job_id):
fail("TASK_ADMISSION_EXECUTION_RESERVATION_JOB_ID_INVALID")
expected={
"task_id":TARGET_TASK_ID,
"task_id":_gv(grant,"task_id"),
"job_id":job_id,
"repository":TARGET_REPOSITORY,
"base_oid":TARGET_BASE_OID,
"expected_branch_oid":TARGET_BASE_OID,
"target_branch":TARGET_BRANCH,
"branch_mode":"CONTINUE_EXISTING",
"generation":TARGET_GENERATION,
"repository":_gv(grant,"repository"),
"base_oid":_gv(grant,"authority_base_oid"),
"expected_branch_oid":request.get("expected_branch_oid",_gv(grant,"authority_base_oid")),
"target_branch":_gv(grant,"target_branch"),
"branch_mode":expected_branch_mode,
"generation":_gv(grant,"generation"),
"state":"BOUND",
}
for key,value in expected.items():
@@ -1333,13 +1390,20 @@ def validate_bootstrap_replay(
first:object,
replay:object,
replay_side_effect_state:str,
first_side_effect_state:str="QUEUED",
expected_branch_mode:str="CONTINUE_EXISTING",
)->str:
if first_side_effect_state not in {"QUEUED","SAME_JOB"}:
fail("TASK_ADMISSION_EXECUTION_RESERVATION_PHASE_INVALID")
first_id=validate_bootstrap_response(
grant=grant,request=request,response=first,expected_side_effect_state="QUEUED"
grant=grant,request=request,response=first,
expected_side_effect_state=first_side_effect_state,
expected_branch_mode=expected_branch_mode,
)
replay_id=validate_bootstrap_response(
grant=grant,request=request,response=replay,
expected_side_effect_state=replay_side_effect_state
expected_side_effect_state=replay_side_effect_state,
expected_branch_mode=expected_branch_mode,
)
if first_id!=replay_id:
fail("TASK_ADMISSION_EXECUTION_RESERVATION_REPLAY_JOB_CONFLICT")
@@ -1355,6 +1419,12 @@ import inspect as _fl_inspect
_FL_HARDENING_PROVIDER = ExecutionPlanDirectoryProvider
_FL_BUILD_FROM_ACTIVATION = build_task_admission_handlers_from_activation_file
_FL_BUILD_FROM_ENVIRONMENT = globals().get("build_task_admission_handlers_from_environment")
_FL_BUILD_RUNTIME_FROM_ACTIVATION = globals().get(
"build_task_admission_runtime_from_activation_file"
)
_FL_BUILD_RUNTIME_FROM_ENVIRONMENT = globals().get(
"build_task_admission_runtime_from_environment"
)
_FL_APPROVED_BOOTSTRAP_SHA256="758895a8407568890fd2f8eb2f232a3dbf2cfa369e5f65ae8c6975bf8d01e3b5"
REPLAY_SIDE_EFFECT_STATE="SAME_JOB"
@@ -1395,38 +1465,428 @@ def _fl_assert_bootstrap_source(bootstrap:object)->None:
if _fl_sha(path)!=_FL_APPROVED_BOOTSTRAP_SHA256:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_BOOTSTRAP_SOURCE_DRIFT",503)
def _fl_reservation_refs(runtime:object,grant:object)->tuple[str,str|None,str,str]:
require_reservable_grant(grant)
authority=getattr(runtime,"authority",None)
if authority is None or not callable(getattr(authority,"read_ref",None)):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_BACKEND_INVALID",503)
base=str(_gv(grant,"authority_base_oid"))
branch=str(_gv(grant,"target_branch"))
try:
main=authority.read_ref("refs/heads/main")
target=authority.read_ref("refs/heads/"+branch)
except Exception as exc:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_AUTHORITY_UNAVAILABLE",503) from exc
if main!=base:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_AUTHORITY_DRIFT",409)
if _gv(grant,"task_id")==TARGET_TASK_ID:
if target!=base:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_AUTHORITY_DRIFT",409)
return main,target,"CONTINUE_EXISTING",base
if target is None:
if _gv(grant,"generation")!=1:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_GENERATION_CONFLICT",409)
return main,target,"CREATE_NEW",base
if target==base:
return main,target,"CONTINUE_EXISTING",base
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_AUTHORITY_DRIFT",409)
def _fl_validate_durable_binding(
*,grant:object,request:Mapping[str,Any],durable:object,job_id:str,
expected_branch_mode:str,
)->None:
expected={
"task_id":_gv(grant,"task_id"),
"job_id":job_id,
"repository":_gv(grant,"repository"),
"base_oid":_gv(grant,"authority_base_oid"),
"expected_branch_oid":request.get("expected_branch_oid"),
"target_branch":_gv(grant,"target_branch"),
"branch_mode":expected_branch_mode,
"generation":_gv(grant,"generation"),
"risk":_gv(grant,"risk"),
"idempotency_key":request.get("idempotency_key"),
"description":request.get("description"),
}
if any(getattr(durable,field,None)!=value for field,value in expected.items()):
raise _fl_error("TASK_ADMISSION_PRODUCER_DURABLE_BINDING_CONFLICT",409)
def _fl_existing_reservation_readback(
*,bootstrap:object,grant:object,request:Mapping[str,Any],durable:object,
expected_branch_mode:str,
)->dict[str,Any]:
"""Verify an existing producer reservation without opening a mutating store.
``GovernedTaskBootstrap._store`` initializes missing directories and SQLite
state, while ``create`` may submit a new Job. Neither is valid after a
durable task binding already exists: missing producer state is corruption,
not crash recovery. Read the exact Job and idempotency rows through a
read-only SQLite URI and return an explicitly typed readback proof. This
is deliberately not a producer submit receipt: Job lifecycle state may
have advanced since the reservation was first created.
"""
task_id=str(_gv(grant,"task_id"))
job_id=getattr(durable,"job_id",None)
if not isinstance(job_id,str) or not _JOB_ID.fullmatch(job_id):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_JOB_ID_INVALID",409)
key=request.get("idempotency_key")
if not isinstance(key,str) or not key:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_IDEMPOTENCY_INVALID",409)
try:
expected_job_id=_FLGitOpsJobController.internal_job_id_for_idempotency_key(key)
producer_key=_fl_job_sha256_json({"client_idempotency_key":key})
except Exception as exc:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_IDEMPOTENCY_INVALID",409) from exc
if job_id!=expected_job_id or job_id!="job-"+producer_key[:32]:
raise _fl_error("TASK_ADMISSION_PRODUCER_DURABLE_BINDING_CONFLICT",409)
job_root=getattr(bootstrap,"job_root",None)
if not isinstance(job_root,(str,os.PathLike)):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_BACKEND_INVALID",503)
job_root=Path(job_root)
store_root=job_root/_fl_task_storage_key(task_id)
legacy_root=job_root/_fl_legacy_task_storage_key(task_id)
try:
legacy_stat=None
try:
legacy_stat=legacy_root.lstat()
except FileNotFoundError:
pass
if legacy_root!=store_root and legacy_stat is not None:
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STORE_CONFLICT",409)
store_stat=store_root.lstat()
db_path=store_root/"state.sqlite3"
db_stat=db_path.lstat()
except FileNotFoundError as exc:
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STORE_MISSING",409) from exc
except OSError as exc:
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STORE_READ_FAILED",503) from exc
if (
stat.S_ISLNK(store_stat.st_mode) or not stat.S_ISDIR(store_stat.st_mode)
or stat.S_ISLNK(db_stat.st_mode) or not stat.S_ISREG(db_stat.st_mode)
):
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STORE_INVALID",409)
values={
field:str(request[field]) for field in (
"task_id","repository","base_oid","target_branch","risk",
"description","idempotency_key",
)
}
payload=json.dumps(values,sort_keys=True,separators=(",",":")).encode()
payload_digest=hashlib.sha256(payload).hexdigest()
expected_branch_oid=str(
request.get("expected_branch_oid") or values["base_oid"]
)
producer_binding={
"campaign_id":task_id,
"repository":values["repository"],
"operation":"GOVERNED_TASK_CREATE",
"branch":values["target_branch"],
"expected_branch_oid":expected_branch_oid,
"payload_digest":payload_digest,
"idempotency_key":key,
"task_id":task_id,
"lineage_anchor_oid":values["base_oid"],
}
expected_request={
**producer_binding,
"binding":producer_binding,
"payload_ref":"sha256/"+payload_digest,
"payload_size":len(payload),
}
expected_job={
"job_id":job_id,
"idempotency_key":producer_key,
"campaign_id":task_id,
"repository":values["repository"],
"operation":"GOVERNED_TASK_CREATE",
"branch":values["target_branch"],
"expected_branch_oid":expected_branch_oid,
"payload_digest":payload_digest,
"payload_ref":"sha256/"+payload_digest,
"payload_size":len(payload),
}
# mode=ro prevents schema/data creation while still including committed WAL
# rows. query_only supplies a second SQLite-level write prohibition.
try:
resolved_store=store_root.resolve(strict=True)
resolved_db=db_path.resolve(strict=True)
if resolved_db.parent!=resolved_store:
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STORE_INVALID",409)
connection=sqlite3.connect(
resolved_db.as_uri()+"?mode=ro",
uri=True,timeout=5.0,
)
connection.row_factory=sqlite3.Row
try:
connection.execute("PRAGMA query_only=ON")
connection.execute("BEGIN")
job=connection.execute(
"SELECT job_id,idempotency_key,campaign_id,repository,operation,"
"branch,expected_branch_oid,payload_digest,payload_ref,payload_size,"
"state,side_effect_state,revision,request_json FROM jobs WHERE job_id=?",
(job_id,),
).fetchone()
idempotency=connection.execute(
"SELECT key,job_id,binding_json FROM idempotency_keys WHERE key=?",
(producer_key,),
).fetchone()
store_after=store_root.lstat()
db_after=db_path.lstat()
if (
(store_stat.st_dev,store_stat.st_ino)!=(store_after.st_dev,store_after.st_ino)
or (db_stat.st_dev,db_stat.st_ino)!=(db_after.st_dev,db_after.st_ino)
or stat.S_ISLNK(store_after.st_mode)
or not stat.S_ISDIR(store_after.st_mode)
or stat.S_ISLNK(db_after.st_mode)
or not stat.S_ISREG(db_after.st_mode)
):
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STORE_CHANGED",409)
finally:
connection.close()
del connection
except (OSError,sqlite3.Error) as exc:
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STORE_READ_FAILED",503) from exc
if job is None:
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_MISSING",409)
if idempotency is None:
raise _fl_error("TASK_ADMISSION_PRODUCER_IDEMPOTENCY_MISSING",409)
if any(job[field]!=value for field,value in expected_job.items()):
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_CONFLICT",409)
valid_states={item.value for item in _FLJobState}
if (
job["state"] not in valid_states
or not isinstance(job["side_effect_state"],str)
or not job["side_effect_state"]
or not isinstance(job["revision"],int)
or isinstance(job["revision"],bool)
or job["revision"]<0
):
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_STATE_INVALID",409)
try:
recorded_request=json.loads(job["request_json"])
recorded_binding=json.loads(idempotency["binding_json"])
except (TypeError,ValueError) as exc:
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_CONFLICT",409) from exc
if (
recorded_request!=expected_request
or idempotency["key"]!=producer_key
or idempotency["job_id"]!=job_id
or recorded_binding!=producer_binding
):
raise _fl_error("TASK_ADMISSION_PRODUCER_JOB_CONFLICT",409)
payload_path=store_root/"payloads"/payload_digest
descriptor=None
try:
descriptor=os.open(
payload_path,
os.O_RDONLY|getattr(os,"O_NOFOLLOW",0),
)
payload_stat=os.fstat(descriptor)
with os.fdopen(descriptor,"rb",closefd=False) as stream:
payload_bytes=stream.read(len(payload)+1)
payload_after=payload_path.lstat()
except FileNotFoundError as exc:
raise _fl_error("TASK_ADMISSION_PRODUCER_PAYLOAD_MISSING",409) from exc
except OSError as exc:
raise _fl_error("TASK_ADMISSION_PRODUCER_PAYLOAD_READ_FAILED",503) from exc
finally:
if descriptor is not None:
os.close(descriptor)
if (
not stat.S_ISREG(payload_stat.st_mode)
or stat.S_ISLNK(payload_after.st_mode)
or not stat.S_ISREG(payload_after.st_mode)
or (payload_stat.st_dev,payload_stat.st_ino)!=(payload_after.st_dev,payload_after.st_ino)
or len(payload_bytes)!=len(payload)
or not hmac.compare_digest(hashlib.sha256(payload_bytes).hexdigest(),payload_digest)
or payload_bytes!=payload
):
raise _fl_error("TASK_ADMISSION_PRODUCER_PAYLOAD_CONFLICT",409)
response=dict(durable.response(idempotent=True))
response["reservation_readback"]={
"schema":"aios.governed-task-reservation-readback.v1",
"job_id":job_id,
"state":job["state"],
"side_effect_state":job["side_effect_state"],
"revision":job["revision"],
"producer_idempotency_key":producer_key,
"request_idempotency_key":key,
}
return response
def _fl_validate_reservation_readback(
*,grant:object,request:Mapping[str,Any],durable:object,
response:object,expected_branch_mode:str,
)->str:
if not isinstance(response,Mapping):
raise _fl_error("TASK_ADMISSION_PRODUCER_READBACK_INVALID",409)
result=response
expected_response=dict(durable.response(idempotent=True))
if (
set(result)!=set(expected_response)|{"reservation_readback"}
or any(result.get(field)!=value for field,value in expected_response.items())
):
raise _fl_error("TASK_ADMISSION_PRODUCER_READBACK_CONFLICT",409)
proof=result.get("reservation_readback")
if not isinstance(proof,Mapping):
raise _fl_error("TASK_ADMISSION_PRODUCER_READBACK_INVALID",409)
expected_keys={
"schema","job_id","state","side_effect_state","revision",
"producer_idempotency_key","request_idempotency_key",
}
key=request.get("idempotency_key")
try:
producer_key=_fl_job_sha256_json({"client_idempotency_key":key})
except Exception as exc:
raise _fl_error("TASK_ADMISSION_PRODUCER_READBACK_INVALID",409) from exc
job_id=getattr(durable,"job_id",None)
if (
set(proof)!=expected_keys
or proof.get("schema")!="aios.governed-task-reservation-readback.v1"
or proof.get("job_id")!=job_id
or proof.get("producer_idempotency_key")!=producer_key
or proof.get("request_idempotency_key")!=key
or proof.get("state") not in {item.value for item in _FLJobState}
or not isinstance(proof.get("side_effect_state"),str)
or not proof.get("side_effect_state")
or not isinstance(proof.get("revision"),int)
or isinstance(proof.get("revision"),bool)
or proof.get("revision")<0
or getattr(durable,"branch_mode",None)!=expected_branch_mode
):
raise _fl_error("TASK_ADMISSION_PRODUCER_READBACK_CONFLICT",409)
return str(job_id)
class _FLProducerReceiptReserver:
def __init__(self,runtime:object):
self.runtime=runtime
self.readbacks:dict[str,dict[str,Any]]={}
def validate_v2_replay(self,grant:object,plan:Mapping[str,Any])->None:
"""Permit the old v2 contract only as an exact durable replay."""
require_reservable_grant(grant)
bootstrap=getattr(self.runtime,"bootstrap",None)
if bootstrap is None or not callable(getattr(bootstrap,"get_binding",None)):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_BACKEND_INVALID",503)
_fl_assert_bootstrap_source(bootstrap)
task_id=str(_gv(grant,"task_id"))
try:
durable=bootstrap.get_binding(task_id)
except Exception as exc:
raise _fl_error("TASK_ADMISSION_EXECUTION_PLAN_V2_REPLAY_BINDING_REQUIRED",409) from exc
expected={
"task_id":task_id,
"job_id":plan.get("external_job_id"),
"repository":_gv(grant,"repository"),
"base_oid":_gv(grant,"authority_base_oid"),
"expected_branch_oid":_gv(grant,"authority_base_oid"),
"target_branch":_gv(grant,"target_branch"),
"generation":_gv(grant,"generation"),
"risk":_gv(grant,"risk"),
"idempotency_key":"task-admission:"+task_id,
"description":"P0 governed execution projection for officially admitted Agent Task.",
}
if any(getattr(durable,field,None)!=value for field,value in expected.items()):
raise _fl_error("TASK_ADMISSION_EXECUTION_PLAN_V2_REPLAY_BINDING_CONFLICT",409)
if getattr(durable,"branch_mode",None) not in {"CREATE_NEW","CONTINUE_EXISTING"}:
raise _fl_error("TASK_ADMISSION_EXECUTION_PLAN_V2_REPLAY_BINDING_CONFLICT",409)
if not isinstance(plan.get("external_job_id"),str) or not _JOB_ID.fullmatch(plan["external_job_id"]):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_JOB_ID_INVALID",409)
def existing_v3_job(self,grant:object,plan:Mapping[str,Any])->str|None:
"""Reuse an exact durable reservation without reinterpreting live refs.
Once reserved, CREATE_NEW versus CONTINUE_EXISTING is immutable input
lineage. A later TaskPrincipal read must not turn legitimate branch
creation or forward progress into a fresh admission decision.
"""
try:
validate_v3_plan(grant=grant,raw=canonical_json_bytes(dict(plan)))
except ProducerReceiptError as exc:
raise _fl_error(exc.code,403) from exc
bootstrap=getattr(self.runtime,"bootstrap",None)
bindings=getattr(self.runtime,"bindings",None)
if (
bootstrap is None
or not callable(getattr(bootstrap,"get_binding",None))
or not isinstance(bindings,dict)
):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_BACKEND_INVALID",503)
_fl_assert_bootstrap_source(bootstrap)
task_id=str(_gv(grant,"task_id"))
try:
durable=bootstrap.get_binding(task_id)
except Exception as exc:
if str(exc)=="TASK_EXECUTION_PROJECTION_MISSING":
return None
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_READ_FAILED",503) from exc
request=build_reservation_request(
grant,expected_branch_oid=str(_gv(grant,"authority_base_oid")),
)
branch_mode=getattr(durable,"branch_mode",None)
if branch_mode not in {"CREATE_NEW","CONTINUE_EXISTING"}:
raise _fl_error("TASK_ADMISSION_PRODUCER_DURABLE_BINDING_CONFLICT",409)
job_id=getattr(durable,"job_id",None)
if not isinstance(job_id,str) or not _JOB_ID.fullmatch(job_id):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_JOB_ID_INVALID",409)
_fl_validate_durable_binding(
grant=grant,request=request,durable=durable,job_id=job_id,
expected_branch_mode=branch_mode,
)
readback=_fl_existing_reservation_readback(
bootstrap=bootstrap,grant=grant,request=request,durable=durable,
expected_branch_mode=branch_mode,
)
_fl_validate_reservation_readback(
grant=grant,request=request,durable=durable,response=readback,
expected_branch_mode=branch_mode,
)
# Keep readback evidence separate from producer submit receipts. In
# particular, a principal re-read must not overwrite the real receipt
# retained by a fresh admission in this runtime.
self.readbacks[task_id]=dict(readback)
return job_id
def __call__(self,grant:object,plan:Mapping[str,Any])->str:
try:
validate_v3_plan(grant=grant,raw=canonical_json_bytes(dict(plan)))
except ProducerReceiptError as exc:
raise _fl_error(exc.code,403) from exc
authority=getattr(self.runtime,"authority",None)
bootstrap=getattr(self.runtime,"bootstrap",None)
bindings=getattr(self.runtime,"bindings",None)
if (
authority is None or bootstrap is None
or not callable(getattr(bootstrap,"create",None))
or not callable(getattr(bootstrap,"get_binding",None))
or not isinstance(bindings,dict)
):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_BACKEND_INVALID",503)
_fl_assert_bootstrap_source(bootstrap)
try:
main_before=authority.read_ref("refs/heads/main")
branch_before=authority.read_ref("refs/heads/"+TARGET_BRANCH)
except Exception as exc:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_AUTHORITY_UNAVAILABLE",503) from exc
if main_before!=TARGET_BASE_OID or branch_before!=TARGET_BASE_OID:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_AUTHORITY_DRIFT",409)
main_before,branch_before,branch_mode,expected_branch_oid=_fl_reservation_refs(self.runtime,grant)
request=build_reservation_request(grant)
request=build_reservation_request(grant,expected_branch_oid=expected_branch_oid)
request_before=dict(request)
try:
first=bootstrap.create(request)
if request!=request_before:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_REQUEST_MUTATED",503)
validate_bootstrap_response(grant=grant,request=request,response=first,expected_side_effect_state="QUEUED")
first_phase=_mapping(first,"TASK_ADMISSION_EXECUTION_RESERVATION_RESPONSE_INVALID").get("reservation_receipt",{}).get("side_effect_state")
if first_phase not in {"QUEUED","SAME_JOB"}:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_PHASE_INVALID",409)
validate_bootstrap_response(
grant=grant,request=request,response=first,
expected_side_effect_state=first_phase,expected_branch_mode=branch_mode,
)
replay=bootstrap.create(request)
if request!=request_before:
@@ -1434,6 +1894,12 @@ class _FLProducerReceiptReserver:
job_id=validate_bootstrap_replay(
grant=grant,request=request,first=first,replay=replay,
replay_side_effect_state=REPLAY_SIDE_EFFECT_STATE,
first_side_effect_state=first_phase,expected_branch_mode=branch_mode,
)
durable=bootstrap.get_binding(str(_gv(grant,"task_id")))
_fl_validate_durable_binding(
grant=grant,request=request,durable=durable,job_id=job_id,
expected_branch_mode=branch_mode,
)
except ProducerReceiptError as exc:
raise _fl_error(exc.code,409) from exc
@@ -1448,11 +1914,12 @@ class _FLProducerReceiptReserver:
# Existing runtime's public createGovernedTask projection performs this
# in-memory binding update after bootstrap.create(). Mirror it only
# after the exact producer receipt + replay proof has passed.
bindings[TARGET_TASK_ID]=dict(replay)
bindings[str(_gv(grant,"task_id"))]=dict(replay)
self.readbacks.pop(str(_gv(grant,"task_id")),None)
try:
main_after=authority.read_ref("refs/heads/main")
branch_after=authority.read_ref("refs/heads/"+TARGET_BRANCH)
branch_after=authority.read_ref("refs/heads/"+str(_gv(grant,"target_branch")))
except Exception as exc:
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_AUTHORITY_UNAVAILABLE",503) from exc
if main_after!=main_before or branch_after!=branch_before:
@@ -1467,29 +1934,33 @@ class V3ProducerReceiptExecutionPlanDirectoryProvider(_FL_HARDENING_PROVIDER):
raise _fl_error("TASK_ADMISSION_EXECUTION_RESERVATION_BACKEND_INVALID",503)
try:
plan=load_v3_plan(_fl_provider_root(self),grant)
job_id=self._reserver(grant,plan)
job_id=self._reserver.existing_v3_job(grant,plan)
if job_id is None:
job_id=self._reserver(grant,plan)
except ProducerReceiptError as exc:
raise _fl_error(exc.code,403) from exc
return {**plan,"external_job_id":job_id}
return {
**plan,
"external_job_id":job_id,
"execution_digest":_gv(grant,"execution_digest"),
}
def __call__(self,grant:object)->Mapping[str,Any]:
if getattr(grant,"task_id",None)!=TARGET_TASK_ID:
try:
return self._target(grant)
except Exception as v3_error:
parent=getattr(super(),"__call__",None)
if callable(parent):
return parent(grant)
raise _fl_error("TASK_ADMISSION_EXECUTION_PLAN_PROVIDER_CONTRACT_INVALID",503)
return self._target(grant)
if not callable(parent) or self._reserver is None:
raise v3_error
try:
legacy=parent(grant)
except Exception:
raise v3_error
self._reserver.validate_v2_replay(grant,legacy)
return legacy
def load(self,grant:object)->Mapping[str,Any]:
if getattr(grant,"task_id",None)!=TARGET_TASK_ID:
parent=getattr(super(),"load",None)
if callable(parent):
return parent(grant)
parent_call=getattr(super(),"__call__",None)
if callable(parent_call):
return parent_call(grant)
raise _fl_error("TASK_ADMISSION_EXECUTION_PLAN_PROVIDER_CONTRACT_INVALID",503)
return self._target(grant)
return self(grant)
def _fl_runtime_from_post_admission(post_admission:object)->object:
runtime=getattr(post_admission,"__self__",None)
@@ -1509,31 +1980,93 @@ def _fl_call(builder,*args,post_admission=None,**kwargs):
previous=ExecutionPlanDirectoryProvider
ExecutionPlanDirectoryProvider=BoundProvider
try:
return builder(*args,post_admission=post_admission,**kwargs)
def exact_post_admission(grant,result):
task_id=str(_gv(grant,"task_id"))
durable=reserver.runtime.bootstrap.get_binding(task_id)
request=build_reservation_request(
grant,expected_branch_oid=str(_gv(grant,"authority_base_oid")),
)
if getattr(durable,"idempotency_key",None)!=request["idempotency_key"]:
return post_admission(grant,result)
branch_mode=getattr(durable,"branch_mode",None)
if branch_mode not in {"CREATE_NEW","CONTINUE_EXISTING"}:
raise _fl_error("TASK_ADMISSION_PRODUCER_DURABLE_BINDING_CONFLICT",409)
binding=reserver.runtime.bindings.get(task_id)
if isinstance(binding,Mapping) and "reservation_receipt" in binding:
# Fresh admission must retain the actual producer submit replay
# receipt produced by GovernedTaskBootstrap.create().
job_id=validate_bootstrap_response(
grant=grant,request=request,response=binding,
expected_side_effect_state="SAME_JOB",expected_branch_mode=branch_mode,
)
else:
# A process restart may leave only durable producer state. Its
# separately typed readback proves immutable identity without
# claiming that a producer submit happened in this request.
binding=reserver.readbacks.get(task_id)
job_id=_fl_validate_reservation_readback(
grant=grant,request=request,durable=durable,response=binding,
expected_branch_mode=branch_mode,
)
if result.get("task_id")!=task_id or result.get("external_job_id")!=job_id or result.get("state",result.get("status")) not in {"QUEUED","CLAIMED","RUNNING"}:
raise _fl_error("TASK_ADMISSION_PRODUCER_TASK_BINDING_CONFLICT",409)
_fl_validate_durable_binding(
grant=grant,request=request,durable=durable,job_id=job_id,
expected_branch_mode=branch_mode,
)
return dict(binding)
return builder(*args,post_admission=exact_post_admission,**kwargs)
finally:
ExecutionPlanDirectoryProvider=previous
def build_task_admission_runtime_from_activation_file(
activation_file:Path,*,
caller_service:str="aios-chatgpt-bridge-v2-staging.service",
post_admission=None,
):
if not callable(_FL_BUILD_RUNTIME_FROM_ACTIVATION):
raise RuntimeError("TASK_ADMISSION_RUNTIME_ACTIVATION_BUILDER_MISSING")
return _fl_call(
_FL_BUILD_RUNTIME_FROM_ACTIVATION,activation_file,
caller_service=caller_service,post_admission=post_admission,
)
def build_task_admission_handlers_from_activation_file(
activation_file:Path,*,
caller_service:str="aios-chatgpt-bridge-v2-staging.service",
post_admission=None,
):
return build_task_admission_runtime_from_activation_file(
activation_file,
caller_service=caller_service,post_admission=post_admission,
).handlers
def build_task_admission_runtime_from_environment(
*,
caller_service:str="aios-chatgpt-bridge-v2-staging.service",
post_admission=None,
):
if not callable(_FL_BUILD_RUNTIME_FROM_ENVIRONMENT):
raise RuntimeError("TASK_ADMISSION_RUNTIME_ENVIRONMENT_BUILDER_MISSING")
return _fl_call(
_FL_BUILD_FROM_ACTIVATION,activation_file,
_FL_BUILD_RUNTIME_FROM_ENVIRONMENT,
caller_service=caller_service,post_admission=post_admission,
)
def build_task_admission_handlers_from_environment(
*,
caller_service:str="aios-chatgpt-bridge-v2-staging.service",
post_admission=None,
):
if not callable(_FL_BUILD_FROM_ENVIRONMENT):
raise RuntimeError("TASK_ADMISSION_ENVIRONMENT_BUILDER_MISSING")
return _fl_call(
_FL_BUILD_FROM_ENVIRONMENT,
return build_task_admission_runtime_from_environment(
caller_service=caller_service,post_admission=post_admission,
)
).handlers
ExecutionPlanDirectoryProvider=V3ProducerReceiptExecutionPlanDirectoryProvider
# Keep the standalone legacy provider contract. The exported admission builders
# install a runtime-bound v3 subclass only while composing their trusted port;
# an unbound provider cannot prove a durable producer receipt.
ExecutionPlanDirectoryProvider=_FL_HARDENING_PROVIDER
# ===== AIOS FORMAL LINEAGE POST-OWNER PRODUCER NATIVE V12 — END =====
+779
View File
@@ -0,0 +1,779 @@
from __future__ import annotations
from dataclasses import dataclass, field, replace
from datetime import datetime, timezone
from enum import Enum
from hashlib import sha256
import re
from typing import Callable, Iterable, Mapping, Protocol, Sequence
class LineageError(ValueError):
pass
class RelationType(str, Enum):
ROOT = "ROOT"
CHILD = "CHILD"
SUCCESSOR = "SUCCESSOR"
RECOVERY = "RECOVERY"
class NextLegalAction(str, Enum):
ROOT_COMPLETE = "ROOT_COMPLETE"
CREATE_OR_RESUME_NEXT_CHILD = "CREATE_OR_RESUME_NEXT_CHILD"
WAIT_DEPENDENCY = "WAIT_DEPENDENCY"
RECOVER_EXISTING_CHILD = "RECOVER_EXISTING_CHILD"
OWNER_BOUNDARY = "OWNER_BOUNDARY"
TERMINAL_STATUSES = frozenset(
{"COMPLETE", "COMPLETED", "SUCCEEDED", "FAILED", "FAILED_FINAL", "CANCELLED"}
)
RECOVERY_STATUSES = frozenset(
{"RECONCILIATION_REQUIRED", "RECOVERY_REQUIRED", "FAILED_RECOVERABLE"}
)
BLOCKED_STATUSES = frozenset({"BLOCKED", "WAITING_DEPENDENCY", "WAITING_CAPACITY"})
_ALIAS_RE = re.compile(r"^[A-Z0-9][A-Z0-9_-]{1,31}$")
_SLUG_RE = re.compile(r"^[A-Z0-9][A-Z0-9-]{1,63}$")
def _require_utc(ts: datetime) -> datetime:
if ts.tzinfo is None or ts.utcoffset() is None:
raise LineageError("created_at/reviewed_at must be timezone-aware")
return ts.astimezone(timezone.utc)
def derive_safe_owner_alias(principal_id: str, preferred_alias: str | None = None) -> str:
"""Return a non-secret display alias.
A trusted identity adapter may supply a safe human alias. Otherwise we use a
deterministic pseudonym derived from the principal identifier. We never put
raw email addresses, tokens, or principal strings into task names.
"""
if preferred_alias and "@" not in preferred_alias and ":" not in preferred_alias:
candidate = re.sub(r"[^A-Za-z0-9_-]+", "-", preferred_alias.strip()).strip("-_").upper()
if 2 <= len(candidate) <= 32 and _ALIAS_RE.fullmatch(candidate):
return candidate
if not principal_id:
raise LineageError("principal_id is required")
return "U" + sha256(principal_id.encode("utf-8")).hexdigest()[:9].upper()
def normalize_goal_slug(value: str) -> str:
value = re.sub(r"[^A-Za-z0-9]+", "-", value.strip()).strip("-").upper()
value = re.sub(r"-+", "-", value)
if len(value) > 64:
value = value[:64].rstrip("-")
if not _SLUG_RE.fullmatch(value):
raise LineageError("goal_slug must normalize to 2-64 [A-Z0-9-] characters")
return value
def short_ref(task_id: str) -> str:
if not task_id:
raise LineageError("task_id is required")
compact = re.sub(r"[^A-Za-z0-9]", "", task_id).upper()
if len(compact) >= 8:
return compact[-8:]
return sha256(task_id.encode("utf-8")).hexdigest()[:8].upper()
@dataclass(frozen=True)
class TaskLineage:
task_id: str
display_name: str
owner_principal_id: str
owner_alias: str
created_at: datetime
mission_id: str
root_task_id: str
parent_task_id: str | None
lineage_depth: int
relation_type: RelationType
sibling_sequence: int
goal_slug: str
root_acceptance_ref: str
parent_revision: int | None = None
@dataclass(frozen=True)
class TaskRecord:
task_id: str
revision: int
status: str
lineage: TaskLineage
evidence_refs: tuple[str, ...] = ()
terminal_evidence_proven: bool = False
@dataclass(frozen=True)
class AcceptanceCondition:
key: str
satisfied: bool
evidence_refs: tuple[str, ...] = ()
owner_boundary: bool = False
@dataclass(frozen=True)
class RootAcceptanceSnapshot:
conditions: tuple[AcceptanceCondition, ...]
@property
def remaining(self) -> tuple[str, ...]:
return tuple(c.key for c in self.conditions if not c.satisfied)
@property
def complete(self) -> bool:
return all(c.satisfied for c in self.conditions)
@property
def owner_boundary_required(self) -> bool:
return any((not c.satisfied) and c.owner_boundary for c in self.conditions)
@property
def evidence_refs(self) -> tuple[str, ...]:
values: list[str] = []
for condition in self.conditions:
if condition.satisfied:
values.extend(condition.evidence_refs)
return tuple(dict.fromkeys(values))
def canonicalize_acceptance(
acceptance: RootAcceptanceSnapshot,
) -> RootAcceptanceSnapshot:
if not isinstance(acceptance, RootAcceptanceSnapshot):
raise LineageError("acceptance evaluator returned an invalid snapshot")
normalized: list[AcceptanceCondition] = []
keys: set[str] = set()
for condition in acceptance.conditions:
if not isinstance(condition, AcceptanceCondition):
raise LineageError("acceptance snapshot contains an invalid condition")
if not isinstance(condition.key, str) or not condition.key:
raise LineageError("acceptance condition key must be a non-empty string")
if condition.key in keys:
raise LineageError("acceptance condition keys must be unique")
if not isinstance(condition.satisfied, bool) or not isinstance(
condition.owner_boundary, bool
):
raise LineageError("acceptance condition flags must be booleans")
if not all(isinstance(ref, str) and ref for ref in condition.evidence_refs):
raise LineageError("acceptance evidence refs must be non-empty strings")
keys.add(condition.key)
normalized.append(
replace(
condition,
evidence_refs=tuple(sorted(set(condition.evidence_refs))),
)
)
return RootAcceptanceSnapshot(tuple(sorted(normalized, key=lambda c: c.key)))
@dataclass(frozen=True)
class RootReviewRecord:
review_id: str
root_task_id: str
reviewed_at: datetime
trigger_child_task_id: str
root_revision_before: int
root_revision_after: int
completed_children: tuple[str, ...]
active_children: tuple[str, ...]
blocked_children: tuple[str, ...]
remaining_acceptance_conditions: tuple[str, ...]
next_legal_action: NextLegalAction
root_complete: bool
evidence_refs: tuple[str, ...]
class RootReviewStore(Protocol):
def get_task(self, task_id: str) -> TaskRecord: ...
def list_descendants(self, root_task_id: str) -> Sequence[TaskRecord]: ...
def get_root_review(self, review_id: str) -> RootReviewRecord | None: ...
def compare_and_swap_root_review(
self,
root_task_id: str,
expected_root_revision: int,
review: RootReviewRecord,
) -> bool: ...
AcceptanceEvaluator = Callable[[TaskRecord, Sequence[TaskRecord]], RootAcceptanceSnapshot]
def _timestamp(ts: datetime) -> str:
return _require_utc(ts).strftime("%Y%m%dT%H%M%SZ")
def _relation_token(relation: RelationType, sequence: int) -> str:
if sequence < 1 or sequence > 99:
raise LineageError("non-root sibling_sequence must be 1..99")
prefix = {
RelationType.CHILD: "C",
RelationType.SUCCESSOR: "S",
RelationType.RECOVERY: "X",
}.get(relation)
if not prefix:
raise LineageError("ROOT does not use a child relation token")
return f"{prefix}{sequence:02d}"
def build_display_name(lineage: TaskLineage) -> str:
alias = lineage.owner_alias.upper()
if not _ALIAS_RE.fullmatch(alias):
raise LineageError("owner_alias must be a non-secret [A-Z0-9_-] alias")
goal = normalize_goal_slug(lineage.goal_slug)
ts = _timestamp(lineage.created_at)
if lineage.relation_type == RelationType.ROOT:
return f"TASK-{alias}-{ts}-ROOT-{goal}-{short_ref(lineage.task_id)[:4]}"
relation = _relation_token(lineage.relation_type, lineage.sibling_sequence)
return (
f"TASK-{alias}-{ts}"
f"-R{short_ref(lineage.root_task_id)}"
f"-P{short_ref(lineage.parent_task_id or '')}"
f"-D{lineage.lineage_depth:02d}"
f"-{relation}-{goal}"
)
def make_root_lineage(
*,
task_id: str,
owner_principal_id: str,
owner_alias: str,
created_at: datetime,
mission_id: str,
goal_slug: str,
root_acceptance_ref: str,
) -> TaskLineage:
lineage = TaskLineage(
task_id=task_id,
display_name="",
owner_principal_id=owner_principal_id,
owner_alias=owner_alias.upper(),
created_at=_require_utc(created_at),
mission_id=mission_id,
root_task_id=task_id,
parent_task_id=None,
lineage_depth=0,
relation_type=RelationType.ROOT,
sibling_sequence=0,
goal_slug=normalize_goal_slug(goal_slug),
root_acceptance_ref=root_acceptance_ref,
parent_revision=None,
)
lineage = replace(lineage, display_name=build_display_name(lineage))
validate_lineage(lineage)
return lineage
def make_child_lineage(
*,
task_id: str,
created_at: datetime,
parent: TaskRecord,
root: TaskRecord,
relation_type: RelationType,
sibling_sequence: int,
goal_slug: str,
) -> TaskLineage:
if relation_type == RelationType.ROOT:
raise LineageError("child relation_type cannot be ROOT")
lineage = TaskLineage(
task_id=task_id,
display_name="",
owner_principal_id=root.lineage.owner_principal_id,
owner_alias=root.lineage.owner_alias,
created_at=_require_utc(created_at),
mission_id=root.lineage.mission_id,
root_task_id=root.task_id,
parent_task_id=parent.task_id,
lineage_depth=parent.lineage.lineage_depth + 1,
relation_type=relation_type,
sibling_sequence=sibling_sequence,
goal_slug=normalize_goal_slug(goal_slug),
root_acceptance_ref=root.lineage.root_acceptance_ref,
parent_revision=parent.revision,
)
lineage = replace(lineage, display_name=build_display_name(lineage))
validate_lineage(lineage, parent=parent, root=root)
return lineage
def validate_lineage(
lineage: TaskLineage,
*,
parent: TaskRecord | None = None,
root: TaskRecord | None = None,
) -> None:
_validate_lineage_shape(lineage)
if lineage.relation_type == RelationType.ROOT:
return
if parent is None or root is None:
raise LineageError("non-root validation requires parent and root authority records")
if root.lineage.relation_type != RelationType.ROOT:
raise LineageError("root authority record must have ROOT lineage")
if parent.lineage.root_task_id != root.task_id:
raise LineageError("parent is not attached to the selected root")
if parent.lineage.owner_principal_id != root.lineage.owner_principal_id:
raise LineageError("parent owner does not match root owner")
if parent.lineage.mission_id != root.lineage.mission_id:
raise LineageError("parent mission does not match root mission")
if parent.lineage.root_acceptance_ref != root.lineage.root_acceptance_ref:
raise LineageError("parent acceptance reference does not match root")
if lineage.root_task_id != root.task_id:
raise LineageError("root_task_id mismatch")
if lineage.parent_task_id != parent.task_id:
raise LineageError("parent_task_id mismatch")
if lineage.lineage_depth != parent.lineage.lineage_depth + 1:
raise LineageError("lineage_depth must equal parent depth + 1")
if lineage.owner_principal_id != root.lineage.owner_principal_id:
raise LineageError("cross-owner lineage attachment rejected")
if lineage.owner_alias != root.lineage.owner_alias:
raise LineageError("owner_alias must match root")
if lineage.mission_id != root.lineage.mission_id:
raise LineageError("mission_id must match root")
if lineage.root_acceptance_ref != root.lineage.root_acceptance_ref:
raise LineageError("root_acceptance_ref must match root")
if lineage.parent_revision != parent.revision:
raise LineageError("stale or mismatched parent revision")
def _validate_lineage_shape(lineage: TaskLineage) -> None:
if not lineage.task_id or not lineage.owner_principal_id or not lineage.mission_id:
raise LineageError("task_id, owner_principal_id and mission_id are required")
if not lineage.root_acceptance_ref:
raise LineageError("root_acceptance_ref is required")
if not _ALIAS_RE.fullmatch(lineage.owner_alias):
raise LineageError("invalid owner_alias")
if normalize_goal_slug(lineage.goal_slug) != lineage.goal_slug:
raise LineageError("goal_slug must already be normalized")
if not lineage.display_name or len(lineage.display_name) > 200:
raise LineageError("display_name must be non-empty and <= 200 characters")
if lineage.relation_type == RelationType.ROOT:
if lineage.root_task_id != lineage.task_id:
raise LineageError("root_task_id must equal task_id for ROOT")
if lineage.parent_task_id is not None:
raise LineageError("ROOT parent_task_id must be null")
if lineage.lineage_depth != 0:
raise LineageError("ROOT lineage_depth must be 0")
if lineage.sibling_sequence != 0:
raise LineageError("ROOT sibling_sequence must be 0")
if lineage.parent_revision is not None:
raise LineageError("ROOT parent_revision must be null")
return
if not lineage.root_task_id or not lineage.parent_task_id:
raise LineageError("non-root root_task_id and parent_task_id are required")
if lineage.lineage_depth < 1:
raise LineageError("non-root lineage_depth must be >= 1")
if lineage.parent_revision is None or lineage.parent_revision < 0:
raise LineageError("non-root parent_revision must be >= 0")
_relation_token(lineage.relation_type, lineage.sibling_sequence)
def attach_legacy_lineage(
*,
existing_task_id: str,
created_at: datetime,
parent: TaskRecord,
root: TaskRecord,
relation_type: RelationType,
sibling_sequence: int,
goal_slug: str,
) -> TaskLineage:
"""Return lineage metadata for an existing durable task without changing its ID."""
return make_child_lineage(
task_id=existing_task_id,
created_at=created_at,
parent=parent,
root=root,
relation_type=relation_type,
sibling_sequence=sibling_sequence,
goal_slug=goal_slug,
)
def rename_display_only(lineage: TaskLineage, new_display_name: str) -> TaskLineage:
if not new_display_name or len(new_display_name) > 200:
raise LineageError("display name must be non-empty and <= 200 characters")
return replace(lineage, display_name=new_display_name)
LINEAGE_SCHEMA = "aios.task-lineage.v1"
ROOT_REVIEW_SCHEMA = "aios.root-review.v1"
_LINEAGE_FIELDS = frozenset(
{
"schema",
"task_id",
"display_name",
"owner_principal_id",
"owner_alias",
"created_at",
"mission_id",
"root_task_id",
"parent_task_id",
"lineage_depth",
"relation_type",
"sibling_sequence",
"goal_slug",
"root_acceptance_ref",
"parent_revision",
}
)
_ROOT_REVIEW_FIELDS = frozenset(
{
"schema",
"review_id",
"root_task_id",
"reviewed_at",
"trigger_child_task_id",
"root_revision_before",
"root_revision_after",
"completed_children",
"active_children",
"blocked_children",
"remaining_acceptance_conditions",
"next_legal_action",
"root_complete",
"evidence_refs",
}
)
def _require_exact_fields(metadata: Mapping[str, object], expected: frozenset[str]) -> None:
if not all(isinstance(key, str) for key in metadata):
raise LineageError("metadata field names must be strings")
actual = frozenset(metadata)
if actual != expected:
missing = sorted(expected - actual)
extra = sorted(actual - expected)
raise LineageError(f"metadata fields mismatch: missing={missing} extra={extra}")
def _require_string(metadata: Mapping[str, object], key: str) -> str:
value = metadata[key]
if not isinstance(value, str) or not value:
raise LineageError(f"{key} must be a non-empty string")
return value
def _require_int(metadata: Mapping[str, object], key: str) -> int:
value = metadata[key]
if isinstance(value, bool) or not isinstance(value, int):
raise LineageError(f"{key} must be an integer")
return value
def _require_string_list(metadata: Mapping[str, object], key: str) -> tuple[str, ...]:
value = metadata[key]
if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value):
raise LineageError(f"{key} must be a list of non-empty strings")
items = tuple(value)
if len(items) != len(set(items)):
raise LineageError(f"{key} must not contain duplicates")
return items
def lineage_to_metadata(lineage: TaskLineage) -> dict[str, object]:
return {
"schema": LINEAGE_SCHEMA,
"task_id": lineage.task_id,
"display_name": lineage.display_name,
"owner_principal_id": lineage.owner_principal_id,
"owner_alias": lineage.owner_alias,
"created_at": _require_utc(lineage.created_at).isoformat().replace("+00:00", "Z"),
"mission_id": lineage.mission_id,
"root_task_id": lineage.root_task_id,
"parent_task_id": lineage.parent_task_id,
"lineage_depth": lineage.lineage_depth,
"relation_type": lineage.relation_type.value,
"sibling_sequence": lineage.sibling_sequence,
"goal_slug": lineage.goal_slug,
"root_acceptance_ref": lineage.root_acceptance_ref,
"parent_revision": lineage.parent_revision,
}
def lineage_from_metadata(metadata: Mapping[str, object]) -> TaskLineage:
_require_exact_fields(metadata, _LINEAGE_FIELDS)
if metadata.get("schema") != LINEAGE_SCHEMA:
raise LineageError("unsupported lineage metadata schema")
try:
created_raw = _require_string(metadata, "created_at")
if created_raw.endswith("Z"):
created_raw = created_raw[:-1] + "+00:00"
parent_task_id = metadata["parent_task_id"]
if parent_task_id is not None and (
not isinstance(parent_task_id, str) or not parent_task_id
):
raise LineageError("parent_task_id must be null or a non-empty string")
parent_revision = metadata["parent_revision"]
if parent_revision is not None and (
isinstance(parent_revision, bool) or not isinstance(parent_revision, int)
):
raise LineageError("parent_revision must be null or an integer")
lineage = TaskLineage(
task_id=_require_string(metadata, "task_id"),
display_name=_require_string(metadata, "display_name"),
owner_principal_id=_require_string(metadata, "owner_principal_id"),
owner_alias=_require_string(metadata, "owner_alias"),
created_at=datetime.fromisoformat(created_raw),
mission_id=_require_string(metadata, "mission_id"),
root_task_id=_require_string(metadata, "root_task_id"),
parent_task_id=parent_task_id,
lineage_depth=_require_int(metadata, "lineage_depth"),
relation_type=RelationType(_require_string(metadata, "relation_type")),
sibling_sequence=_require_int(metadata, "sibling_sequence"),
goal_slug=_require_string(metadata, "goal_slug"),
root_acceptance_ref=_require_string(metadata, "root_acceptance_ref"),
parent_revision=parent_revision,
)
except (KeyError, TypeError, ValueError, LineageError) as exc:
raise LineageError(f"invalid lineage metadata: {exc}") from exc
_validate_lineage_shape(lineage)
return lineage
def root_review_to_record(review: RootReviewRecord) -> dict[str, object]:
return {
"schema": ROOT_REVIEW_SCHEMA,
"review_id": review.review_id,
"root_task_id": review.root_task_id,
"reviewed_at": _require_utc(review.reviewed_at).isoformat().replace("+00:00", "Z"),
"trigger_child_task_id": review.trigger_child_task_id,
"root_revision_before": review.root_revision_before,
"root_revision_after": review.root_revision_after,
"completed_children": list(review.completed_children),
"active_children": list(review.active_children),
"blocked_children": list(review.blocked_children),
"remaining_acceptance_conditions": list(review.remaining_acceptance_conditions),
"next_legal_action": review.next_legal_action.value,
"root_complete": review.root_complete,
"evidence_refs": list(review.evidence_refs),
}
def root_review_from_record(
record: Mapping[str, object], *, expected_review_id: str | None = None
) -> RootReviewRecord:
_require_exact_fields(record, _ROOT_REVIEW_FIELDS)
if record.get("schema") != ROOT_REVIEW_SCHEMA:
raise LineageError("unsupported root-review metadata schema")
try:
reviewed_raw = _require_string(record, "reviewed_at")
if reviewed_raw.endswith("Z"):
reviewed_raw = reviewed_raw[:-1] + "+00:00"
root_complete = record["root_complete"]
if not isinstance(root_complete, bool):
raise LineageError("root_complete must be a boolean")
review = RootReviewRecord(
review_id=_require_string(record, "review_id"),
root_task_id=_require_string(record, "root_task_id"),
reviewed_at=_require_utc(datetime.fromisoformat(reviewed_raw)),
trigger_child_task_id=_require_string(record, "trigger_child_task_id"),
root_revision_before=_require_int(record, "root_revision_before"),
root_revision_after=_require_int(record, "root_revision_after"),
completed_children=_require_string_list(record, "completed_children"),
active_children=_require_string_list(record, "active_children"),
blocked_children=_require_string_list(record, "blocked_children"),
remaining_acceptance_conditions=_require_string_list(
record, "remaining_acceptance_conditions"
),
next_legal_action=NextLegalAction(
_require_string(record, "next_legal_action")
),
root_complete=root_complete,
evidence_refs=_require_string_list(record, "evidence_refs"),
)
except (KeyError, TypeError, ValueError, LineageError) as exc:
raise LineageError(f"invalid root-review metadata: {exc}") from exc
if expected_review_id is not None and review.review_id != expected_review_id:
raise LineageError("root-review id does not match lookup key")
if review.root_revision_before < 0 or (
review.root_revision_after != review.root_revision_before + 1
):
raise LineageError("root-review revision transition is invalid")
if review.root_complete != (
review.next_legal_action == NextLegalAction.ROOT_COMPLETE
):
raise LineageError("root_complete does not match next_legal_action")
child_sets = (
set(review.completed_children),
set(review.active_children),
set(review.blocked_children),
)
if any(child_sets[i] & child_sets[j] for i in range(3) for j in range(i + 1, 3)):
raise LineageError("root-review child classifications overlap")
return review
def _review_id(
root_task_id: str,
trigger_child_task_id: str,
trigger_revision: int,
acceptance: RootAcceptanceSnapshot,
) -> str:
acceptance_digest = sha256(
"\n".join(
f"{c.key}:{int(c.satisfied)}:{int(c.owner_boundary)}:"
f"{','.join(sorted(set(c.evidence_refs)))}"
for c in acceptance.conditions
).encode("utf-8")
).hexdigest()
payload = f"{root_task_id}|{trigger_child_task_id}|{trigger_revision}|{acceptance_digest}"
return "RR-" + sha256(payload.encode("utf-8")).hexdigest()[:24]
def _next_action(
descendants: Sequence[TaskRecord],
acceptance: RootAcceptanceSnapshot,
) -> NextLegalAction:
if acceptance.complete:
return NextLegalAction.ROOT_COMPLETE
if acceptance.owner_boundary_required:
return NextLegalAction.OWNER_BOUNDARY
if any(d.status in RECOVERY_STATUSES for d in descendants):
return NextLegalAction.RECOVER_EXISTING_CHILD
if any(d.status in BLOCKED_STATUSES for d in descendants):
return NextLegalAction.WAIT_DEPENDENCY
return NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD
def review_root_after_child_terminal(
*,
store: RootReviewStore,
trigger_child_task_id: str,
acceptance_evaluator: AcceptanceEvaluator,
reviewed_at: datetime,
max_cas_retries: int = 4,
) -> RootReviewRecord:
"""Persist a root review after a proven child terminal transition.
This function is deliberately storage-agnostic. TaskPlane remains the
authority and supplies the CAS implementation. The function never creates a
new root or mutates a child identity.
"""
if max_cas_retries < 1:
raise LineageError("max_cas_retries must be >= 1")
reviewed_at = _require_utc(reviewed_at)
for _ in range(max_cas_retries):
child = store.get_task(trigger_child_task_id)
if child.lineage.relation_type == RelationType.ROOT:
raise LineageError("root review hook requires a child task")
if child.status not in TERMINAL_STATUSES:
raise LineageError("child must be terminal before root review")
if not child.terminal_evidence_proven:
raise LineageError("child terminal evidence is not proven")
root = store.get_task(child.lineage.root_task_id)
if root.lineage.relation_type != RelationType.ROOT:
raise LineageError("root_task_id does not resolve to a ROOT task")
if child.lineage.owner_principal_id != root.lineage.owner_principal_id:
raise LineageError("trigger child owner does not match root owner")
if child.lineage.mission_id != root.lineage.mission_id:
raise LineageError("trigger child mission does not match root mission")
if child.lineage.root_acceptance_ref != root.lineage.root_acceptance_ref:
raise LineageError("trigger child acceptance reference does not match root")
descendants = tuple(
sorted(store.list_descendants(root.task_id), key=lambda item: item.task_id)
)
descendant_ids = tuple(item.task_id for item in descendants)
if len(descendant_ids) != len(set(descendant_ids)):
raise LineageError("descendant authority returned duplicate task ids")
if child.task_id not in descendant_ids:
raise LineageError("trigger child missing from authoritative descendants")
for descendant in descendants:
if descendant.lineage.root_task_id != root.task_id:
raise LineageError("descendant is attached to a different root")
if descendant.lineage.owner_principal_id != root.lineage.owner_principal_id:
raise LineageError("descendant owner does not match root owner")
if descendant.lineage.mission_id != root.lineage.mission_id:
raise LineageError("descendant mission does not match root mission")
if (
descendant.lineage.root_acceptance_ref
!= root.lineage.root_acceptance_ref
):
raise LineageError("descendant acceptance reference does not match root")
acceptance = canonicalize_acceptance(acceptance_evaluator(root, descendants))
review_id = _review_id(root.task_id, child.task_id, child.revision, acceptance)
completed = tuple(
d.task_id for d in descendants
if d.status in TERMINAL_STATUSES and d.terminal_evidence_proven
)
active = tuple(
d.task_id for d in descendants
if d.status not in TERMINAL_STATUSES and d.status not in BLOCKED_STATUSES
)
blocked = tuple(
d.task_id for d in descendants
if d.status in BLOCKED_STATUSES
)
evidence = tuple(sorted(set(child.evidence_refs + acceptance.evidence_refs)))
action = _next_action(descendants, acceptance)
existing = store.get_root_review(review_id)
if existing is not None:
if (
existing.root_task_id != root.task_id
or existing.trigger_child_task_id != child.task_id
):
raise LineageError("persisted root review identity conflicts with trigger")
if existing.root_revision_after > root.revision:
raise LineageError("persisted root review revision is ahead of authority")
expected_semantics = (
completed,
active,
blocked,
acceptance.remaining,
action,
action == NextLegalAction.ROOT_COMPLETE,
evidence,
)
actual_semantics = (
existing.completed_children,
existing.active_children,
existing.blocked_children,
existing.remaining_acceptance_conditions,
existing.next_legal_action,
existing.root_complete,
existing.evidence_refs,
)
if actual_semantics != expected_semantics:
raise LineageError("persisted root review conflicts with current semantics")
return existing
review = RootReviewRecord(
review_id=review_id,
root_task_id=root.task_id,
reviewed_at=reviewed_at,
trigger_child_task_id=child.task_id,
root_revision_before=root.revision,
root_revision_after=root.revision + 1,
completed_children=completed,
active_children=active,
blocked_children=blocked,
remaining_acceptance_conditions=acceptance.remaining,
next_legal_action=action,
root_complete=(action == NextLegalAction.ROOT_COMPLETE),
evidence_refs=evidence,
)
if store.compare_and_swap_root_review(root.task_id, root.revision, review):
return review
# Another sibling may have updated root progress. Re-read authority and
# recompute; never overwrite a newer root review.
raise LineageError("root review CAS retry budget exhausted")
@@ -0,0 +1,285 @@
from __future__ import annotations
from dataclasses import dataclass, replace
from datetime import datetime
from typing import Mapping
if not __package__:
from aios_task_lineage import AcceptanceEvaluator, RelationType, TaskLineage, lineage_to_metadata, lineage_from_metadata, root_review_to_record
from aios_task_lineage_integration import (
DISPLAY_NAME_METADATA_KEY,
IntegrationError,
LINEAGE_METADATA_KEY,
NewTaskIdentityRequest,
PreparedTaskIdentity,
TaskIdentityLineageService,
TerminalReviewOutcome,
)
from aios_task_lineage_taskplane_adapter import (
AuthenticatedTaskContext,
InProcessTaskPlaneLineageAdapter,
require_context_matches_task,
)
else:
from .aios_task_lineage import AcceptanceEvaluator, RelationType, TaskLineage, lineage_to_metadata, lineage_from_metadata, root_review_to_record
from .aios_task_lineage_integration import (
DISPLAY_NAME_METADATA_KEY,
IntegrationError,
LINEAGE_METADATA_KEY,
NewTaskIdentityRequest,
PreparedTaskIdentity,
TaskIdentityLineageService,
TerminalReviewOutcome,
)
from .aios_task_lineage_taskplane_adapter import (
AuthenticatedTaskContext,
InProcessTaskPlaneLineageAdapter,
require_context_matches_task,
)
class LifecycleHookError(IntegrationError):
pass
@dataclass(frozen=True)
class IdentityPersistenceOutcome:
prepared: PreparedTaskIdentity
persisted_revision_at_least: int
independently_verified: bool
@dataclass(frozen=True)
class LegacyAttachOutcome:
lineage: TaskLineage
independently_verified: bool
@dataclass(frozen=True)
class TerminalHookOutcome:
outcome: TerminalReviewOutcome
root_review_persisted: bool
dispatch_persisted: bool
independently_verified: bool
class TaskLineageLifecycleHooks:
"""Concrete lifecycle hook coordinator.
It does not own TaskPlane state. It orchestrates existing TaskPlane CAS,
journal/continuation and optional independent authority readback.
"""
def __init__(self, adapter: InProcessTaskPlaneLineageAdapter):
self._adapter = adapter
self._service = TaskIdentityLineageService(adapter)
def persist_new_task_identity(
self,
*,
request: NewTaskIdentityRequest,
context: AuthenticatedTaskContext,
max_cas_retries: int = 4,
require_independent_readback: bool = True,
) -> IdentityPersistenceOutcome:
if max_cas_retries < 1:
raise LifecycleHookError("max_cas_retries must be >= 1")
if require_independent_readback and not self._adapter.has_independent_readback:
raise LifecycleHookError("independent TaskPlane readback is not configured")
if request.owner_principal_id != context.principal_id:
raise LifecycleHookError("request principal does not match authenticated context")
if request.mission_id != context.mission_id:
raise LifecycleHookError("request mission does not match authenticated context")
for _ in range(max_cas_retries):
current = self._adapter.get_task_snapshot(request.task_id)
require_context_matches_task(context, current)
if current.created_at != request.created_at:
raise LifecycleHookError("request created_at does not match authority")
if (request.relation_type == RelationType.ROOT
and request.preferred_owner_alias not in (None, context.preferred_alias)):
raise LifecycleHookError("request alias does not match authenticated context")
request = replace(request, preferred_owner_alias=context.preferred_alias)
prepared = self._service.prepare_new_task_identity(request)
# Idempotent replay and legacy display-name repair are fail-closed:
# never overwrite an existing conflicting lineage record.
raw = current.metadata.get(LINEAGE_METADATA_KEY)
display = current.metadata.get(DISPLAY_NAME_METADATA_KEY)
if raw is not None and not isinstance(raw, Mapping):
raise LifecycleHookError("existing lineage metadata is malformed")
if isinstance(raw, Mapping):
# parent_revision records the creation observation, not the
# parent's current progress revision on an idempotent replay.
expected = dict(prepared.metadata_patch[LINEAGE_METADATA_KEY])
if request.parent_task_id is not None:
stored = lineage_from_metadata(dict(raw))
if (stored.parent_revision is None
or stored.parent_revision > prepared.lineage.parent_revision):
raise LifecycleHookError("existing parent revision conflicts with authority")
expected["parent_revision"] = stored.parent_revision
if raw != expected:
raise LifecycleHookError(
"existing lineage metadata conflicts with requested identity"
)
prepared = replace(prepared, lineage=lineage_from_metadata(dict(raw)),
metadata_patch={LINEAGE_METADATA_KEY: dict(raw),
DISPLAY_NAME_METADATA_KEY: prepared.display_name})
if display == prepared.display_name:
verified = False
if require_independent_readback:
self._adapter.verify_task_readback(
task_id=request.task_id,
expected_revision_at_least=current.revision,
expected_metadata=prepared.metadata_patch,
)
verified = True
return IdentityPersistenceOutcome(
prepared=prepared,
persisted_revision_at_least=current.revision,
independently_verified=verified,
)
if display not in (None, ""):
raise LifecycleHookError(
"existing display name conflicts with requested identity"
)
metadata_patch = {
DISPLAY_NAME_METADATA_KEY: prepared.display_name,
}
else:
if display not in (None, ""):
raise LifecycleHookError(
"display name exists without authoritative lineage"
)
metadata_patch = prepared.metadata_patch
ok = self._adapter.compare_and_swap_task_metadata(
task_id=request.task_id,
expected_revision=current.revision,
metadata_patch=metadata_patch,
)
if not ok:
continue
verified = False
if require_independent_readback:
self._adapter.verify_task_readback(
task_id=request.task_id,
expected_revision_at_least=current.revision + 1,
expected_metadata=prepared.metadata_patch,
)
verified = True
return IdentityPersistenceOutcome(
prepared=prepared,
persisted_revision_at_least=current.revision + 1,
independently_verified=verified,
)
raise LifecycleHookError("identity persistence CAS retry budget exhausted")
def attach_legacy_identity(
self,
*,
task_id: str,
parent_task_id: str,
relation_type: RelationType,
goal_slug: str,
context: AuthenticatedTaskContext,
max_cas_retries: int = 4,
require_independent_readback: bool = True,
) -> LegacyAttachOutcome:
if require_independent_readback and not self._adapter.has_independent_readback:
raise LifecycleHookError("independent TaskPlane readback is not configured")
before = self._adapter.get_task_snapshot(task_id)
require_context_matches_task(context, before)
lineage = self._service.attach_legacy_task(
task_id=task_id,
parent_task_id=parent_task_id,
relation_type=relation_type,
goal_slug=goal_slug,
max_cas_retries=max_cas_retries,
expected_owner_principal_id=context.principal_id,
expected_mission_id=context.mission_id,
)
verified = False
if require_independent_readback:
self._adapter.verify_task_readback(
task_id=task_id,
expected_revision_at_least=before.revision,
expected_metadata={
LINEAGE_METADATA_KEY: lineage_to_metadata(lineage),
DISPLAY_NAME_METADATA_KEY: lineage.display_name,
},
)
# Domain serialization compatibility check below avoids requiring
# TaskLineage to expose a method in older versions.
verified = True
return LegacyAttachOutcome(lineage=lineage, independently_verified=verified)
def on_proven_terminal_child(
self,
*,
child_task_id: str,
context: AuthenticatedTaskContext,
acceptance_evaluator: AcceptanceEvaluator,
reviewed_at: datetime,
max_cas_retries: int = 4,
require_independent_readback: bool = True,
) -> TerminalHookOutcome:
if require_independent_readback and not self._adapter.has_independent_readback:
raise LifecycleHookError("independent TaskPlane readback is not configured")
child = self._adapter.get_task_snapshot(child_task_id)
require_context_matches_task(context, child)
if not child.terminal_evidence_proven:
raise LifecycleHookError(
"terminal child hook requires proven terminal evidence"
)
raw = child.metadata.get(LINEAGE_METADATA_KEY)
if not isinstance(raw, Mapping):
raise LifecycleHookError("terminal child lineage missing")
lineage = lineage_from_metadata(dict(raw))
root = self._adapter.get_task_snapshot(lineage.root_task_id)
require_context_matches_task(context, root)
def authorized_acceptance(root_record, descendants):
if (root_record.lineage.owner_principal_id != context.principal_id
or root_record.lineage.mission_id != context.mission_id):
raise LifecycleHookError("root authority changed during terminal review")
return acceptance_evaluator(root_record, descendants)
outcome = self._service.on_proven_terminal_child(
child_task_id=child_task_id,
acceptance_evaluator=authorized_acceptance,
reviewed_at=reviewed_at,
max_cas_retries=max_cas_retries,
)
persisted_review = self._adapter.get_root_review_record(
outcome.review.review_id
)
if persisted_review is None:
raise LifecycleHookError("root review was not persisted")
if persisted_review != root_review_to_record(outcome.review):
raise LifecycleHookError("persisted review does not match returned review")
dispatch = self._adapter.get_root_action_dispatch(outcome.review.review_id)
if dispatch is None:
raise LifecycleHookError("root action dispatch was not persisted")
if dispatch != outcome.dispatch:
raise LifecycleHookError("persisted dispatch does not match returned receipt")
verified = False
if require_independent_readback:
self._adapter.verify_root_readback(
root_task_id=outcome.review.root_task_id,
expected_review_id=outcome.review.review_id,
)
verified = True
return TerminalHookOutcome(
outcome=outcome,
root_review_persisted=True,
dispatch_persisted=True,
independently_verified=verified,
)
@@ -0,0 +1,539 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Mapping, MutableMapping, Protocol, Sequence
if not __package__:
from aios_task_lineage import (
AcceptanceEvaluator,
LineageError,
NextLegalAction,
RelationType,
RootReviewRecord,
RootReviewStore,
TaskLineage,
TaskRecord,
attach_legacy_lineage,
derive_safe_owner_alias,
lineage_from_metadata,
lineage_to_metadata,
make_child_lineage,
make_root_lineage,
normalize_goal_slug,
review_root_after_child_terminal,
root_review_from_record,
root_review_to_record,
)
else:
from .aios_task_lineage import (
AcceptanceEvaluator,
LineageError,
NextLegalAction,
RelationType,
RootReviewRecord,
RootReviewStore,
TaskLineage,
TaskRecord,
attach_legacy_lineage,
derive_safe_owner_alias,
lineage_from_metadata,
lineage_to_metadata,
make_child_lineage,
make_root_lineage,
normalize_goal_slug,
review_root_after_child_terminal,
root_review_from_record,
root_review_to_record,
)
LINEAGE_METADATA_KEY = "aios_task_lineage"
LINEAGE_REQUEST_DIGEST_KEY = "aios_lineage_request_digest"
ROOT_REVIEW_RECORD_KEY = "aios_root_review"
DISPLAY_NAME_METADATA_KEY = "aios_task_display_name"
class IntegrationError(RuntimeError):
pass
@dataclass(frozen=True)
class TaskAuthoritySnapshot:
task_id: str
revision: int
status: str
owner_principal_id: str
mission_id: str
created_at: datetime
metadata: Mapping[str, object]
evidence_refs: tuple[str, ...] = ()
terminal_evidence_proven: bool = False
@dataclass(frozen=True)
class NewTaskIdentityRequest:
task_id: str
owner_principal_id: str
preferred_owner_alias: str | None
created_at: datetime
mission_id: str
goal_slug: str
root_acceptance_ref: str
relation_type: RelationType = RelationType.ROOT
parent_task_id: str | None = None
@dataclass(frozen=True)
class PreparedTaskIdentity:
task_id: str
display_name: str
lineage: TaskLineage
metadata_patch: Mapping[str, object]
@dataclass(frozen=True)
class TerminalReviewOutcome:
review: RootReviewRecord
dispatch: "RootActionDispatchReceipt"
@dataclass(frozen=True)
class RootActionDispatchReceipt:
review_id: str
root_task_id: str
action: NextLegalAction
idempotency_key: str
status: str
mission_id: str = ""
root_revision: int = 0
plan_digest: str | None = None
execution_key: str | None = None
PERSISTED_DISPATCH_STATUSES = frozenset(
{
"PENDING",
"CLAIMED",
"EFFECT_UNKNOWN",
"EXECUTED_PENDING_REREVIEW",
"WAITING_DEPENDENCY",
"WAITING_OWNER",
"CLOSED",
"SUPERSEDED",
"BLOCKED",
# Read compatibility only. A consumer must reconcile this legacy
# state unless a complete durable receipt is present.
"DISPATCHED",
}
)
class TaskPlaneLineagePort(Protocol):
"""Narrow authority port required by the lineage integration service.
The live adapter must implement these methods over the existing TaskPlane /
mission / journal / CAS mechanisms. This protocol deliberately avoids
guessing concrete DB tables, HTTP endpoints, or handler names.
"""
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot: ...
def list_descendant_snapshots(
self, root_task_id: str
) -> Sequence[TaskAuthoritySnapshot]: ...
def reserve_sibling_sequence(
self,
*,
parent_task_id: str,
relation_type: RelationType,
child_task_id: str,
) -> int:
"""Idempotently reserve one sibling sequence for child_task_id."""
def compare_and_swap_task_metadata(
self,
*,
task_id: str,
expected_revision: int,
metadata_patch: Mapping[str, object],
) -> bool: ...
def get_root_review_record(
self, review_id: str
) -> Mapping[str, object] | None: ...
def compare_and_swap_root_review_record(
self,
*,
root_task_id: str,
expected_root_revision: int,
review_record: Mapping[str, object],
) -> bool: ...
def get_root_action_dispatch(
self, review_id: str
) -> RootActionDispatchReceipt | None: ...
def dispatch_root_action(
self,
*,
review: RootReviewRecord,
idempotency_key: str,
) -> RootActionDispatchReceipt: ...
def _extract_lineage(snapshot: TaskAuthoritySnapshot) -> TaskLineage:
raw = snapshot.metadata.get(LINEAGE_METADATA_KEY)
if not isinstance(raw, dict):
raise IntegrationError(
f"task {snapshot.task_id} does not have authoritative lineage metadata"
)
lineage = lineage_from_metadata(raw)
if lineage.task_id != snapshot.task_id:
raise IntegrationError("lineage task_id does not match authority task")
if lineage.owner_principal_id != snapshot.owner_principal_id:
raise IntegrationError("lineage owner does not match authority owner")
if lineage.mission_id != snapshot.mission_id:
raise IntegrationError("lineage mission does not match authority mission")
if snapshot.created_at.tzinfo is None or snapshot.created_at.utcoffset() is None:
raise IntegrationError("authority created_at must be timezone-aware")
if lineage.created_at != snapshot.created_at:
raise IntegrationError("lineage created_at does not match authority task")
display = snapshot.metadata.get(DISPLAY_NAME_METADATA_KEY)
if display not in (None, "", lineage.display_name):
raise IntegrationError("lineage display name conflicts with authority metadata")
return lineage
def _as_task_record(snapshot: TaskAuthoritySnapshot) -> TaskRecord:
return TaskRecord(
task_id=snapshot.task_id,
revision=snapshot.revision,
status=snapshot.status,
lineage=_extract_lineage(snapshot),
evidence_refs=snapshot.evidence_refs,
terminal_evidence_proven=snapshot.terminal_evidence_proven,
)
def _validate_record_root_binding(record: TaskRecord, root: TaskRecord) -> None:
if root.lineage.relation_type != RelationType.ROOT:
raise IntegrationError("root_task_id does not resolve to a ROOT task")
if record.lineage.root_task_id != root.task_id:
raise IntegrationError("task lineage points to a different root")
if record.lineage.owner_principal_id != root.lineage.owner_principal_id:
raise IntegrationError("task lineage owner does not match root owner")
if record.lineage.mission_id != root.lineage.mission_id:
raise IntegrationError("task lineage mission does not match root mission")
if record.lineage.root_acceptance_ref != root.lineage.root_acceptance_ref:
raise IntegrationError("task lineage acceptance reference does not match root")
def _load_authority_record(
port: TaskPlaneLineagePort, task_id: str
) -> tuple[TaskRecord, TaskRecord]:
record = _as_task_record(port.get_task_snapshot(task_id))
if record.lineage.relation_type == RelationType.ROOT:
return record, record
root = _as_task_record(port.get_task_snapshot(record.lineage.root_task_id))
current = record
seen = {record.task_id}
while current.lineage.relation_type != RelationType.ROOT:
_validate_record_root_binding(current, root)
parent_id = current.lineage.parent_task_id
if not parent_id or parent_id in seen:
raise IntegrationError("task lineage contains a parent cycle")
seen.add(parent_id)
parent = _as_task_record(port.get_task_snapshot(parent_id))
_validate_record_root_binding(parent, root)
if current.lineage.parent_task_id != parent.task_id:
raise IntegrationError("task lineage parent does not match authority parent")
if current.lineage.lineage_depth != parent.lineage.lineage_depth + 1:
raise IntegrationError("task lineage depth does not match authority parent")
if current.lineage.parent_revision is None or (
current.lineage.parent_revision > parent.revision
):
raise IntegrationError("task lineage parent revision is not authoritative")
current = parent
if current.task_id != root.task_id:
raise IntegrationError("task lineage parent chain ends at a different root")
return record, root
def _validate_dispatch_receipt(
receipt: RootActionDispatchReceipt,
*,
review: RootReviewRecord,
idempotency_key: str,
existing: bool,
) -> None:
if (
receipt.review_id != review.review_id
or receipt.root_task_id != review.root_task_id
or receipt.action != review.next_legal_action
or receipt.idempotency_key != idempotency_key
or receipt.status not in PERSISTED_DISPATCH_STATUSES
):
noun = "existing root-action dispatch" if existing else "root-action dispatch receipt"
raise IntegrationError(f"{noun} conflicts with persisted review")
class _RootReviewStoreAdapter(RootReviewStore):
def __init__(self, port: TaskPlaneLineagePort):
self._port = port
def get_task(self, task_id: str) -> TaskRecord:
record, _ = _load_authority_record(self._port, task_id)
return record
def list_descendants(self, root_task_id: str) -> Sequence[TaskRecord]:
root, _ = _load_authority_record(self._port, root_task_id)
if root.lineage.relation_type != RelationType.ROOT:
raise IntegrationError("descendant lookup key is not a ROOT task")
records: list[TaskRecord] = []
for snapshot in self._port.list_descendant_snapshots(root_task_id):
record, resolved_root = _load_authority_record(self._port, snapshot.task_id)
if resolved_root.task_id != root_task_id:
raise IntegrationError("descendant authority returned a cross-root task")
_validate_record_root_binding(record, root)
records.append(record)
return tuple(records)
def get_root_review(self, review_id: str) -> RootReviewRecord | None:
raw = self._port.get_root_review_record(review_id)
if raw is None:
return None
if not isinstance(raw, Mapping):
raise IntegrationError("persisted root-review record must be a mapping")
try:
return root_review_from_record(
raw,
expected_review_id=review_id,
)
except LineageError as exc:
raise IntegrationError(
f"invalid persisted root-review record: {exc}"
) from exc
def compare_and_swap_root_review(
self,
root_task_id: str,
expected_root_revision: int,
review: RootReviewRecord,
) -> bool:
return self._port.compare_and_swap_root_review_record(
root_task_id=root_task_id,
expected_root_revision=expected_root_revision,
review_record=root_review_to_record(review),
)
class TaskIdentityLineageService:
"""Application service that wires the lineage domain to TaskPlane authority."""
def __init__(self, port: TaskPlaneLineagePort):
self._port = port
def prepare_new_task_identity(
self, request: NewTaskIdentityRequest
) -> PreparedTaskIdentity:
alias = derive_safe_owner_alias(
request.owner_principal_id, request.preferred_owner_alias
)
if request.relation_type == RelationType.ROOT:
if request.parent_task_id is not None:
raise IntegrationError("ROOT task cannot have parent_task_id")
lineage = make_root_lineage(
task_id=request.task_id,
owner_principal_id=request.owner_principal_id,
owner_alias=alias,
created_at=request.created_at,
mission_id=request.mission_id,
goal_slug=request.goal_slug,
root_acceptance_ref=request.root_acceptance_ref,
)
else:
if not request.parent_task_id:
raise IntegrationError("non-root task requires parent_task_id")
parent, root = _load_authority_record(
self._port, request.parent_task_id
)
if request.owner_principal_id != root.lineage.owner_principal_id:
raise IntegrationError("cross-owner child creation rejected")
if request.mission_id != root.lineage.mission_id:
raise IntegrationError("cross-mission child creation rejected")
if request.root_acceptance_ref != root.lineage.root_acceptance_ref:
raise IntegrationError("root acceptance reference mismatch")
sequence = self._port.reserve_sibling_sequence(
parent_task_id=parent.task_id,
relation_type=request.relation_type,
child_task_id=request.task_id,
)
lineage = make_child_lineage(
task_id=request.task_id,
created_at=request.created_at,
parent=parent,
root=root,
relation_type=request.relation_type,
sibling_sequence=sequence,
goal_slug=request.goal_slug,
)
return PreparedTaskIdentity(
task_id=request.task_id,
display_name=lineage.display_name,
lineage=lineage,
metadata_patch={
LINEAGE_METADATA_KEY: lineage_to_metadata(lineage),
DISPLAY_NAME_METADATA_KEY: lineage.display_name,
},
)
def attach_legacy_task(
self,
*,
task_id: str,
parent_task_id: str,
relation_type: RelationType,
goal_slug: str,
expected_owner_principal_id: str | None = None,
expected_mission_id: str | None = None,
max_cas_retries: int = 4,
) -> TaskLineage:
if max_cas_retries < 1:
raise IntegrationError("max_cas_retries must be >= 1")
for _ in range(max_cas_retries):
current = self._port.get_task_snapshot(task_id)
if (
expected_owner_principal_id is not None
and current.owner_principal_id != expected_owner_principal_id
):
raise IntegrationError("legacy task owner changed after authorization")
if (
expected_mission_id is not None
and current.mission_id != expected_mission_id
):
raise IntegrationError("legacy task mission changed after authorization")
raw = current.metadata.get(LINEAGE_METADATA_KEY)
if isinstance(raw, Mapping):
record, _ = _load_authority_record(self._port, task_id)
lineage = record.lineage
expected_goal_slug = normalize_goal_slug(goal_slug)
if (
lineage.parent_task_id != parent_task_id
or lineage.relation_type != relation_type
or lineage.goal_slug != expected_goal_slug
):
raise IntegrationError(
"existing legacy lineage conflicts with requested binding"
)
display = current.metadata.get(DISPLAY_NAME_METADATA_KEY)
if display == lineage.display_name:
return lineage
if display not in (None, ""):
raise IntegrationError(
"legacy display name conflicts with authoritative lineage"
)
if self._port.compare_and_swap_task_metadata(
task_id=current.task_id,
expected_revision=current.revision,
metadata_patch={
DISPLAY_NAME_METADATA_KEY: lineage.display_name,
},
):
return lineage
continue
parent, root = _load_authority_record(self._port, parent_task_id)
if root.status == "COMPLETED":
raise IntegrationError("cannot attach lineage below a completed root")
if (
expected_owner_principal_id is not None
and root.lineage.owner_principal_id != expected_owner_principal_id
):
raise IntegrationError("legacy root owner does not match authorization")
if (
expected_mission_id is not None
and root.lineage.mission_id != expected_mission_id
):
raise IntegrationError("legacy root mission does not match authorization")
if current.owner_principal_id != root.lineage.owner_principal_id:
raise IntegrationError("legacy task owner does not match root owner")
if current.mission_id != root.lineage.mission_id:
raise IntegrationError("legacy task mission does not match root mission")
sequence = self._port.reserve_sibling_sequence(
parent_task_id=parent.task_id,
relation_type=relation_type,
child_task_id=current.task_id,
)
lineage = attach_legacy_lineage(
existing_task_id=current.task_id,
created_at=current.created_at,
parent=parent,
root=root,
relation_type=relation_type,
sibling_sequence=sequence,
goal_slug=goal_slug,
)
if self._port.compare_and_swap_task_metadata(
task_id=current.task_id,
expected_revision=current.revision,
metadata_patch={
LINEAGE_METADATA_KEY: lineage_to_metadata(lineage),
DISPLAY_NAME_METADATA_KEY: lineage.display_name,
},
):
return lineage
raise IntegrationError("legacy lineage CAS retry budget exhausted")
def on_proven_terminal_child(
self,
*,
child_task_id: str,
acceptance_evaluator: AcceptanceEvaluator,
reviewed_at: datetime,
max_cas_retries: int = 4,
) -> TerminalReviewOutcome:
store = _RootReviewStoreAdapter(self._port)
review = review_root_after_child_terminal(
store=store,
trigger_child_task_id=child_task_id,
acceptance_evaluator=acceptance_evaluator,
reviewed_at=reviewed_at,
max_cas_retries=max_cas_retries,
)
existing = self._port.get_root_action_dispatch(review.review_id)
if existing is not None:
idempotency_key = (
f"root-review:{review.review_id}:{review.next_legal_action.value}"
)
_validate_dispatch_receipt(
existing,
review=review,
idempotency_key=idempotency_key,
existing=True,
)
return TerminalReviewOutcome(review=review, dispatch=existing)
idempotency_key = f"root-review:{review.review_id}:{review.next_legal_action.value}"
receipt = self._port.dispatch_root_action(
review=review,
idempotency_key=idempotency_key,
)
_validate_dispatch_receipt(
receipt,
review=review,
idempotency_key=idempotency_key,
existing=False,
)
return TerminalReviewOutcome(review=review, dispatch=receipt)
@@ -0,0 +1,464 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import json
from pathlib import Path
import sqlite3
from typing import Callable, Mapping, Protocol, Sequence, runtime_checkable
if not __package__:
from aios_task_lineage import RelationType, RootReviewRecord, lineage_from_metadata
from aios_task_lineage_integration import (
IntegrationError,
LINEAGE_METADATA_KEY,
RootActionDispatchReceipt,
TaskAuthoritySnapshot,
TaskPlaneLineagePort,
)
else:
from .aios_task_lineage import RelationType, RootReviewRecord, lineage_from_metadata
from .aios_task_lineage_integration import (
IntegrationError,
LINEAGE_METADATA_KEY,
RootActionDispatchReceipt,
TaskAuthoritySnapshot,
TaskPlaneLineagePort,
)
class AuthorityAdapterError(IntegrationError):
pass
def _parse_datetime(value: object) -> datetime:
if isinstance(value, datetime):
if value.tzinfo is None or value.utcoffset() is None:
raise AuthorityAdapterError("authority datetime must be timezone-aware")
return value.astimezone(timezone.utc)
if not isinstance(value, str) or not value:
raise AuthorityAdapterError("authority datetime missing")
try:
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise AuthorityAdapterError("authority datetime invalid") from exc
if dt.tzinfo is None or dt.utcoffset() is None:
raise AuthorityAdapterError("authority datetime must include timezone")
return dt.astimezone(timezone.utc)
def _load_json_object(value: object, *, field: str) -> dict[str, object]:
if value in (None, ""):
return {}
if isinstance(value, Mapping):
return dict(value)
if not isinstance(value, str):
raise AuthorityAdapterError(f"{field} must be a JSON object")
try:
obj = json.loads(value)
except json.JSONDecodeError as exc:
raise AuthorityAdapterError(f"{field} is invalid JSON") from exc
if not isinstance(obj, dict):
raise AuthorityAdapterError(f"{field} must decode to an object")
return obj
def _metadata_identity(metadata: Mapping[str, object]) -> tuple[str, str]:
lineage = metadata.get(LINEAGE_METADATA_KEY)
if isinstance(lineage, Mapping):
owner = lineage.get("owner_principal_id")
mission = lineage.get("mission_id")
if isinstance(owner, str) and owner and isinstance(mission, str) and mission:
return owner, mission
# Pre-lineage/legacy compatibility. These keys are accepted only as
# authority-provided metadata; missing values fail closed rather than being
# invented by this adapter.
owner = metadata.get("owner_principal_id")
mission = metadata.get("mission_id")
if not isinstance(owner, str) or not owner:
raise AuthorityAdapterError("task owner_principal_id is not discoverable")
if not isinstance(mission, str) or not mission:
raise AuthorityAdapterError("task mission_id is not discoverable")
return owner, mission
def _metadata_evidence(metadata: Mapping[str, object]) -> tuple[str, ...]:
raw = metadata.get("evidence_refs")
if raw is None:
return ()
if not isinstance(raw, list) or not all(isinstance(x, str) for x in raw):
raise AuthorityAdapterError("evidence_refs must be a string list")
return tuple(dict.fromkeys(raw))
@runtime_checkable
class TaskPlaneReadback(Protocol):
"""Independent authority readback surface.
This may be backed by the TaskPlane query service, a read replica, or a
read-only SQLite authority. It must not mutate task state.
"""
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot: ...
def list_descendant_snapshots(
self, root_task_id: str
) -> Sequence[TaskAuthoritySnapshot]: ...
def get_root_review_record(self, review_id: str) -> Mapping[str, object] | None: ...
def get_root_action_dispatch(self, review_id: str) -> RootActionDispatchReceipt | None: ...
@dataclass(frozen=True)
class CallableTaskPlaneReadback:
"""Readback adapter for an existing official TaskPlane query/service."""
get_task: Callable[[str], TaskAuthoritySnapshot]
list_descendants: Callable[[str], Sequence[TaskAuthoritySnapshot]]
get_review: Callable[[str], Mapping[str, object] | None] | None = None
get_dispatch: Callable[[str], RootActionDispatchReceipt | None] | None = None
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot:
snapshot = self.get_task(task_id)
if not isinstance(snapshot, TaskAuthoritySnapshot):
raise AuthorityAdapterError("readback returned invalid task snapshot")
if snapshot.task_id != task_id:
raise AuthorityAdapterError("readback returned wrong task identity")
return snapshot
def list_descendant_snapshots(
self, root_task_id: str
) -> Sequence[TaskAuthoritySnapshot]:
values = tuple(self.list_descendants(root_task_id))
if not all(isinstance(v, TaskAuthoritySnapshot) for v in values):
raise AuthorityAdapterError("readback returned invalid descendant snapshot")
return values
def get_root_review_record(self, review_id: str) -> Mapping[str, object] | None:
if self.get_review is None:
raise AuthorityAdapterError("independent review readback is not configured")
return self.get_review(review_id)
def get_root_action_dispatch(self, review_id: str) -> RootActionDispatchReceipt | None:
if self.get_dispatch is None:
raise AuthorityAdapterError("independent dispatch readback is not configured")
return self.get_dispatch(review_id)
@dataclass(frozen=True)
class AuthoritySchemaReport:
task_table: str
columns: tuple[str, ...]
required_columns_present: bool
missing_columns: tuple[str, ...]
task_count: int
class ReadOnlySQLiteTaskPlane:
"""Read-only discovery/readback helper for the current TaskPlane SQLite authority.
This class never writes. It opens SQLite read-only, including committed WAL
data, and exists only for discovery, diagnostics and independent readback.
Execution writes must go through the live TaskPlane repository/service and
are supplied separately through ``TaskPlaneServiceBindings``.
"""
REQUIRED_TASK_COLUMNS = (
"task_id",
"status",
"revision",
"metadata_json",
"created_at",
)
def __init__(self, db_path: Path):
self.db_path = Path(db_path)
def _connect(self) -> sqlite3.Connection:
if not self.db_path.is_file():
raise AuthorityAdapterError(f"TaskPlane DB missing: {self.db_path}")
# A live WAL database is not immutable: ignoring its WAL can return
# stale state. as_uri also quotes URI metacharacters in file names.
uri = self.db_path.resolve().as_uri() + "?mode=ro"
con = sqlite3.connect(uri, uri=True)
con.row_factory = sqlite3.Row
return con
def inspect_schema(self) -> AuthoritySchemaReport:
con = self._connect()
try:
rows = list(con.execute("PRAGMA table_info(tasks)"))
columns = tuple(str(r["name"]) for r in rows)
missing = tuple(c for c in self.REQUIRED_TASK_COLUMNS if c not in columns)
count = int(con.execute("SELECT COUNT(*) FROM tasks").fetchone()[0]) if rows else 0
return AuthoritySchemaReport(
task_table="tasks",
columns=columns,
required_columns_present=not missing,
missing_columns=missing,
task_count=count,
)
finally:
con.close()
def _row_to_snapshot(self, row: sqlite3.Row) -> TaskAuthoritySnapshot:
keys = set(row.keys())
metadata = _load_json_object(
row["metadata_json"] if "metadata_json" in keys else None,
field="metadata_json",
)
owner, mission = _metadata_identity(metadata)
created_value = row["created_at"] if "created_at" in keys else None
return TaskAuthoritySnapshot(
task_id=str(row["task_id"]),
revision=int(row["revision"]),
status=str(row["status"]),
owner_principal_id=owner,
mission_id=mission,
created_at=_parse_datetime(created_value),
metadata=metadata,
evidence_refs=_metadata_evidence(metadata),
# A metadata flag cannot stand in for execution-integrity proof.
# The official service adapter must resolve verified receipts.
terminal_evidence_proven=False,
)
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot:
con = self._connect()
try:
row = con.execute("SELECT * FROM tasks WHERE task_id=? LIMIT 1", (task_id,)).fetchone()
if row is None:
raise AuthorityAdapterError(f"task not found: {task_id}")
return self._row_to_snapshot(row)
finally:
con.close()
def list_descendant_snapshots(self, root_task_id: str) -> tuple[TaskAuthoritySnapshot, ...]:
con = self._connect()
try:
rows = list(con.execute("SELECT * FROM tasks ORDER BY task_id"))
finally:
con.close()
out: list[TaskAuthoritySnapshot] = []
for row in rows:
try:
snap = self._row_to_snapshot(row)
except AuthorityAdapterError:
# Legacy tasks without discoverable identity cannot safely be
# attached to a root by this read-only adapter.
continue
raw = snap.metadata.get(LINEAGE_METADATA_KEY)
if (
isinstance(raw, Mapping)
and raw.get("root_task_id") == root_task_id
and snap.task_id != root_task_id
):
out.append(snap)
return tuple(out)
@dataclass(frozen=True)
class TaskPlaneServiceBindings:
"""In-process bindings to the existing TaskPlane authority.
These callables must be backed by the real TaskPlane repository/service,
including its existing transactions, revision/CAS, journal and continuation
mechanisms. They must NOT be implemented as out-of-band SQLite mutations.
"""
get_task_snapshot: Callable[[str], TaskAuthoritySnapshot]
list_descendant_snapshots: Callable[[str], Sequence[TaskAuthoritySnapshot]]
reserve_sibling_sequence: Callable[[str, RelationType, str], int]
compare_and_swap_task_metadata: Callable[[str, int, Mapping[str, object]], bool]
get_root_review_record: Callable[[str], Mapping[str, object] | None]
compare_and_swap_root_review_record: Callable[
[str, int, Mapping[str, object]], bool
]
get_root_action_dispatch: Callable[[str], RootActionDispatchReceipt | None]
dispatch_root_action: Callable[
[RootReviewRecord, str], RootActionDispatchReceipt
]
class InProcessTaskPlaneLineageAdapter(TaskPlaneLineagePort):
"""Concrete TaskPlaneLineagePort backed by existing TaskPlane service calls."""
def __init__(
self,
bindings: TaskPlaneServiceBindings,
*,
readback: TaskPlaneReadback | None = None,
):
self._b = bindings
self._readback = readback
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot:
return self._b.get_task_snapshot(task_id)
def list_descendant_snapshots(self, root_task_id: str) -> Sequence[TaskAuthoritySnapshot]:
return tuple(self._b.list_descendant_snapshots(root_task_id))
def reserve_sibling_sequence(
self,
*,
parent_task_id: str,
relation_type: RelationType,
child_task_id: str,
) -> int:
seq = self._b.reserve_sibling_sequence(
parent_task_id, relation_type, child_task_id
)
if not isinstance(seq, int) or seq < 1:
raise AuthorityAdapterError("TaskPlane returned invalid sibling sequence")
return seq
def compare_and_swap_task_metadata(
self,
*,
task_id: str,
expected_revision: int,
metadata_patch: Mapping[str, object],
) -> bool:
return bool(
self._b.compare_and_swap_task_metadata(
task_id, expected_revision, metadata_patch
)
)
def get_root_review_record(self, review_id: str) -> Mapping[str, object] | None:
return self._b.get_root_review_record(review_id)
def compare_and_swap_root_review_record(
self,
*,
root_task_id: str,
expected_root_revision: int,
review_record: Mapping[str, object],
) -> bool:
return bool(
self._b.compare_and_swap_root_review_record(
root_task_id, expected_root_revision, review_record
)
)
def get_root_action_dispatch(
self, review_id: str
) -> RootActionDispatchReceipt | None:
return self._b.get_root_action_dispatch(review_id)
def dispatch_root_action(
self,
*,
review: RootReviewRecord,
idempotency_key: str,
) -> RootActionDispatchReceipt:
return self._b.dispatch_root_action(review, idempotency_key)
def verify_task_readback(
self,
*,
task_id: str,
expected_revision_at_least: int,
expected_metadata: Mapping[str, object],
) -> TaskAuthoritySnapshot:
if self._readback is None:
raise AuthorityAdapterError("independent TaskPlane readback is not configured")
snap = self._readback.get_task_snapshot(task_id)
if snap.task_id != task_id:
raise AuthorityAdapterError("TaskPlane readback task identity mismatch")
if snap.revision < expected_revision_at_least:
raise AuthorityAdapterError("TaskPlane readback revision did not advance")
for key, expected in expected_metadata.items():
if snap.metadata.get(key) != expected:
raise AuthorityAdapterError(
f"TaskPlane readback metadata mismatch for {key}"
)
return snap
def verify_root_readback(
self,
*,
root_task_id: str,
expected_review_id: str,
) -> tuple[TaskAuthoritySnapshot, ...]:
"""Verify that independent authority descendants remain bound to root.
The persisted ROOT_REVIEW record itself may live in a journal rather
than the task row. This method verifies the task/lineage projection
independently while the service binding verifies the journal receipt.
"""
if self._readback is None:
raise AuthorityAdapterError("independent TaskPlane readback is not configured")
review = self.get_root_review_record(expected_review_id)
dispatch = self.get_root_action_dispatch(expected_review_id)
if (review is None or review.get("review_id") != expected_review_id
or review.get("root_task_id") != root_task_id or dispatch is None):
raise AuthorityAdapterError("root review or dispatch identity mismatch")
read_review = getattr(self._readback, "get_root_review_record", None)
read_dispatch = getattr(self._readback, "get_root_action_dispatch", None)
if not callable(read_review) or not callable(read_dispatch):
raise AuthorityAdapterError("independent review/dispatch readback is not configured")
if read_review(expected_review_id) != review:
raise AuthorityAdapterError("independent root review readback mismatch")
if read_dispatch(expected_review_id) != dispatch:
raise AuthorityAdapterError("independent dispatch readback mismatch")
root = self._readback.get_task_snapshot(root_task_id)
raw_root = root.metadata.get(LINEAGE_METADATA_KEY)
if root.task_id != root_task_id or not isinstance(raw_root, Mapping):
raise AuthorityAdapterError("root readback identity or lineage missing")
root_lineage = lineage_from_metadata(dict(raw_root))
if (root_lineage.relation_type != RelationType.ROOT
or root_lineage.task_id != root_task_id
or root_lineage.owner_principal_id != root.owner_principal_id
or root_lineage.mission_id != root.mission_id):
raise AuthorityAdapterError("root readback authority mismatch")
revision = review.get("root_revision_after")
if type(revision) is not int or root.revision < revision:
raise AuthorityAdapterError("root readback revision is stale")
descendants = tuple(self._readback.list_descendant_snapshots(root_task_id))
seen = set()
for snap in descendants:
raw = snap.metadata.get(LINEAGE_METADATA_KEY)
if not isinstance(raw, Mapping):
raise AuthorityAdapterError("descendant lineage missing")
lineage = lineage_from_metadata(dict(raw))
if (snap.task_id in seen or snap.task_id == root_task_id
or lineage.task_id != snap.task_id
or lineage.root_task_id != root_task_id
or lineage.owner_principal_id != root.owner_principal_id
or lineage.mission_id != root.mission_id
or snap.owner_principal_id != root.owner_principal_id
or snap.mission_id != root.mission_id
or lineage.root_acceptance_ref != root_lineage.root_acceptance_ref):
raise AuthorityAdapterError(
f"descendant {snap.task_id} is projected under wrong root"
)
seen.add(snap.task_id)
if review.get("trigger_child_task_id") not in seen:
raise AuthorityAdapterError("trigger child missing from independent readback")
return descendants
@property
def has_independent_readback(self) -> bool:
return self._readback is not None
@dataclass(frozen=True)
class AuthenticatedTaskContext:
principal_id: str
preferred_alias: str | None
mission_id: str
def require_context_matches_task(
context: AuthenticatedTaskContext,
task: TaskAuthoritySnapshot,
) -> None:
if context.principal_id != task.owner_principal_id:
raise AuthorityAdapterError("authenticated principal does not own task")
if context.mission_id != task.mission_id:
raise AuthorityAdapterError("authenticated mission does not match task")
@@ -0,0 +1,242 @@
"""Narrow staging Mission ingress authenticated by the registered Owner signer.
The signed payload binds the method, resource, body and idempotency key. This
adapter neither signs requests nor grants Git/worker/production capabilities.
"""
from __future__ import annotations
import argparse
import importlib.util
import json
from pathlib import Path
import re
import stat
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any
from .task_plane.errors import (
ForbiddenError, TaskPlaneHTTPError, UnauthorizedError, ValidationError,
)
AUDIENCE = "aios.task-mission-authority.staging.v1"
NAMESPACE = "aios-owner-approval-v1"
MAX_BODY = 131072
REQUEST_FIELDS = frozenset({
"schema", "audience", "method", "path", "body", "idempotency_key",
"issued_at", "expires_at", "nonce",
})
def canonical_bytes(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"),
ensure_ascii=False, allow_nan=False).encode("utf-8")
def strict_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValidationError("duplicate JSON key")
result[key] = value
return result
class SignedMissionIngress:
def __init__(
self, service, verifier, *, terminal_proof_importer=None, clock=time.time
):
if not callable(getattr(verifier, "verify", None)):
raise ValueError("registered Owner verifier required")
identity = getattr(verifier, "signer_id", None)
if not isinstance(identity, str) or not identity.strip():
raise ValueError("registered signer identity required")
self.service = service
self.verifier = verifier
self.identity = identity
self.terminal_proof_importer = terminal_proof_importer
self.clock = clock
def handle(self, envelope: dict) -> tuple[int, dict]:
if not isinstance(envelope, dict) or set(envelope) != {
"request", "signature", "key_id"
}:
raise ValidationError("invalid signed mission envelope")
request = envelope["request"]
if not isinstance(request, dict) or set(request) != REQUEST_FIELDS:
raise ValidationError("invalid mission request fields")
if (request["schema"] != "aios.signed-mission-request.v1"
or request["audience"] != AUDIENCE):
raise ForbiddenError("mission request audience mismatch")
issued, expires = request["issued_at"], request["expires_at"]
if (type(issued) is not int or type(expires) is not int
or not 0 < expires - issued <= 300
or not issued <= self.clock() < expires):
raise UnauthorizedError("mission request expired or not yet valid")
if (not isinstance(request["nonce"], str)
or re.fullmatch(r"[A-Za-z0-9_-]{16,128}", request["nonce"]) is None):
raise ValidationError("invalid mission request nonce")
if (not isinstance(envelope["signature"], str)
or not isinstance(envelope["key_id"], str)):
raise UnauthorizedError("Owner signature required")
try:
verified = self.verifier.verify(
namespace=NAMESPACE, payload=canonical_bytes(request),
signature=envelope["signature"], key_id=envelope["key_id"],
)
except Exception:
verified = False
if verified is not True:
raise UnauthorizedError("Owner signature invalid")
method, path, body = request["method"], request["path"], request["body"]
key = request["idempotency_key"]
if method == "POST" and path == "/missions":
if not isinstance(body, dict) or not isinstance(key, str) or not key.strip():
raise ValidationError("mission body and idempotency key required")
return 201, self.service.create_mission(
body, key, actor_identity=self.identity,
)
if (method == "GET" and isinstance(path, str)
and re.fullmatch(r"/missions/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}", path)
and body is None and key is None):
return 200, self.service.get_mission(
path.split("/")[2], actor_identity=self.identity,
)
if (method == "POST" and isinstance(path, str)
and re.fullmatch(r"/tasks/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/delegate", path)):
if not isinstance(body, dict) or not isinstance(key, str) or not key.strip():
raise ValidationError("child body and idempotency key required")
return 201, self.service.delegate_mission(
path.split("/")[2], body, key, actor_identity=self.identity,
)
if (method == "POST" and isinstance(path, str)
and re.fullmatch(
r"/missions/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/legacy-attachments",
path,
)):
if not isinstance(body, dict) or not isinstance(key, str) or not key.strip():
raise ValidationError(
"legacy attachment body and idempotency key required"
)
result = self.service.attach_legacy_attachment(
path.split("/")[2], body, key, actor_identity=self.identity,
)
return (200 if result.get("idempotent") is True else 201), result
if (method == "GET" and isinstance(path, str)
and re.fullmatch(
r"/missions/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/"
r"legacy-attachments/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}",
path,
) and body is None and key is None):
pieces = path.split("/")
return 200, self.service.get_legacy_attachment(
pieces[2], pieces[4], actor_identity=self.identity,
)
if (method == "POST" and isinstance(path, str)
and re.fullmatch(
r"/tasks/[A-Za-z0-9][A-Za-z0-9._:-]{2,179}/reconcile-terminal-proof",
path,
)):
if body != {} or not isinstance(key, str) or not key.strip():
raise ValidationError(
"terminal proof trigger requires an empty body and idempotency key"
)
if self.terminal_proof_importer is None:
raise ForbiddenError("terminal proof importer is not configured")
return 200, self.terminal_proof_importer.reconcile(
path.split("/")[2], key, actor_identity=self.identity,
)
raise ForbiddenError("operation outside mission ingress scope")
def load_registered_verifier(path: Path):
metadata = path.lstat()
if (not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0
or metadata.st_mode & 0o022):
raise ValueError("Owner verifier must be a root-owned immutable regular file")
spec = importlib.util.spec_from_file_location("aios_mission_owner_verifier", path)
if spec is None or spec.loader is None:
raise ValueError("Owner verifier module unavailable")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.GrantRootOwnerApprovalVerifier()
def make_handler(ingress: SignedMissionIngress):
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_args):
pass # Do not log signed payloads or transport credentials.
def respond(self, status, value):
raw = canonical_bytes(value)
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
def do_POST(self):
try:
if self.path != "/v1/signed-request":
raise ForbiddenError("unsupported mission ingress route")
if self.headers.get("Transfer-Encoding"):
raise ValidationError("transfer encoding is unsupported")
lengths = self.headers.get_all("Content-Length", [])
if len(lengths) != 1 or not lengths[0].isdigit():
raise ValidationError("one content length required")
length = int(lengths[0])
if not 0 < length <= MAX_BODY:
raise ValidationError("mission request too large or empty")
self.connection.settimeout(10)
raw = self.rfile.read(length)
if len(raw) != length:
raise ValidationError("truncated mission request")
envelope = json.loads(raw, object_pairs_hook=strict_object)
status, result = ingress.handle(envelope)
self.respond(status, result)
except TaskPlaneHTTPError as exc:
self.respond(exc.status_code, exc.to_payload())
except (ValueError, UnicodeError, RecursionError):
self.respond(400, {"error": "invalid_json"})
except Exception:
self.respond(500, {"error": "mission_authority_unavailable"})
return Handler
def main() -> None:
from .task_plane.api import TaskPlaneService
from .task_plane.mission import MissionAuthority, MissionPrincipalBinding
from .task_plane.policy import IdentityVerifier, PolicyEngine
from .task_plane.store import TaskPlaneStore
parser = argparse.ArgumentParser()
parser.add_argument("--db", type=Path, required=True)
parser.add_argument("--owner-verifier", type=Path, required=True)
parser.add_argument("--port", type=int, required=True)
args = parser.parse_args()
if not 1024 <= args.port <= 65535:
parser.error("unprivileged loopback port required")
verifier = load_registered_verifier(args.owner_verifier)
identity = verifier.signer_id
store = TaskPlaneStore(str(args.db), high_assurance=True)
authority = MissionAuthority(store, principal_bindings=[MissionPrincipalBinding(
authenticated_identity=identity, owner_principal_id=identity,
acceptance_verifier_id="identity-lineage-live-closure-v1",
allowed_task_capabilities=(
"code.read", "code.write", "test.execute",
"governed.write.file", "governed.write.branch",
),
max_risk="medium",
)])
service = TaskPlaneService(store, policy=PolicyEngine(
identity_verifier=IdentityVerifier({identity}), execution_enabled=False,
identity_capabilities={identity: {"task.creator", "task.read", "task.delegate"}},
), mission_authority=authority)
ingress = SignedMissionIngress(service, verifier)
# SQLite's existing connection and transaction lock share one request thread.
HTTPServer(("127.0.0.1", args.port), make_handler(ingress)).serve_forever()
if __name__ == "__main__":
main()
@@ -0,0 +1,511 @@
"""Authenticated, trigger-only import of authoritative terminal proof.
The caller authorizes reconciliation but supplies no proof fields. Proof is
read by a configured server-side provider and is accepted only after the
provider validates an authenticated authority document and its immutable
cross-system tuple.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from hashlib import sha256
import json
import re
from typing import Any, Callable, Mapping, Protocol
from .aios_task_lineage_integration import LINEAGE_METADATA_KEY
from .task_plane.errors import ConflictError, ForbiddenError, ValidationError
from .task_plane.models import ResultEnvelope, TaskRecord
from .task_plane.policy import PolicyDecision
PROOF_SCHEMA = "aios.task-terminal-authority-proof.v1"
RECEIPT_SCHEMA = "aios.task-terminal-import-receipt.v1"
REQUIRED_SOURCES = frozenset(
{
"agent_task",
"admission",
"job_producer",
"execution_alias",
"governed_job",
"bridge",
"authority_git",
}
)
DOCUMENT_FIELDS = frozenset(
{
"schema",
"task_id",
"mission_id",
"root_task_id",
"parent_task_id",
"acceptance_condition",
"agent_task_status",
"admission_status",
"execution_outcome",
"external_job_id",
"internal_job_id",
"repository",
"base_revision",
"target_branch",
"generation",
"execution_plan_digest",
"task_principal_ref",
"task_lease_ref",
"writer_lease_ref",
"governed_commit_oid",
"authority_ref_oid",
"postimage_hashes",
"source_receipt_digests",
"attestation_ref",
"started_at",
"completed_at",
}
)
_HEX64 = re.compile(r"[0-9a-f]{64}")
_GIT_OID = re.compile(r"[0-9a-f]{40}(?:[0-9a-f]{24})?")
_IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/-]{2,255}")
def _canonical_bytes(value: Mapping[str, object]) -> bytes:
return json.dumps(
dict(value), ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
def _nonempty_string(value: object, field: str) -> str:
if not isinstance(value, str) or not value or len(value) > 1024:
raise ForbiddenError(f"authoritative proof {field} invalid")
return value
def _identifier(value: object, field: str) -> str:
text = _nonempty_string(value, field)
if _IDENTIFIER.fullmatch(text) is None:
raise ForbiddenError(f"authoritative proof {field} invalid")
return text
def _utc_time(value: object, field: str) -> str:
text = _nonempty_string(value, field)
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError as exc:
raise ForbiddenError(f"authoritative proof {field} invalid") from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ForbiddenError(f"authoritative proof {field} lacks timezone")
return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
@dataclass(frozen=True)
class VerifiedTerminalProof:
document: dict[str, object]
@property
def task_id(self) -> str:
return str(self.document["task_id"])
@property
def attestation_ref(self) -> str:
return str(self.document["attestation_ref"])
@property
def started_at(self) -> str:
return str(self.document["started_at"])
@property
def completed_at(self) -> str:
return str(self.document["completed_at"])
def evidence(self) -> dict[str, object]:
return {
"kind": "acceptance-proof",
"proof_schema": PROOF_SCHEMA,
"authority_document_digest": sha256(
_canonical_bytes(self.document)
).hexdigest(),
"task_id": self.task_id,
"mission_id": self.document["mission_id"],
"root_task_id": self.document["root_task_id"],
"parent_task_id": self.document["parent_task_id"],
"acceptance_condition": self.document["acceptance_condition"],
"external_job_id": self.document["external_job_id"],
"internal_job_id": self.document["internal_job_id"],
"repository": self.document["repository"],
"base_revision": self.document["base_revision"],
"target_branch": self.document["target_branch"],
"generation": self.document["generation"],
"execution_plan_digest": self.document["execution_plan_digest"],
"task_principal_ref": self.document["task_principal_ref"],
"task_lease_ref": self.document["task_lease_ref"],
"writer_lease_ref": self.document["writer_lease_ref"],
"governed_commit_oid": self.document["governed_commit_oid"],
"authority_ref_oid": self.document["authority_ref_oid"],
"postimage_hashes": self.document["postimage_hashes"],
"source_receipt_digests": self.document["source_receipt_digests"],
"attestation_ref": self.attestation_ref,
}
def outputs(self) -> list[dict[str, object]]:
return [
{
"kind": "governed-commit-readback",
"repository": self.document["repository"],
"target_branch": self.document["target_branch"],
"governed_commit_oid": self.document["governed_commit_oid"],
"postimage_hashes": self.document["postimage_hashes"],
}
]
class AuthorityDocumentReader(Protocol):
def __call__(self, task_id: str) -> Mapping[str, object]: ...
class AuthorityDocumentVerifier(Protocol):
def __call__(self, document: Mapping[str, object]) -> bool: ...
class AuthenticatedTerminalProofProvider:
"""Read and validate one aggregate document backed by existing authorities.
``reader`` and ``verifier`` are mandatory runtime bindings. The verifier
must authenticate the referenced AgentTask/admission/Job/Bridge/Git
receipts using already configured trust roots. This class does not confer
trust on an arbitrary mapping.
"""
def __init__(
self,
*,
reader: AuthorityDocumentReader,
verifier: AuthorityDocumentVerifier,
task_reader: Callable[[str], TaskRecord],
worker_identity: str,
) -> None:
if not callable(reader) or not callable(verifier) or not callable(task_reader):
raise ValueError("authenticated proof reader, verifier and task reader required")
self._reader = reader
self._verifier = verifier
self._task_reader = task_reader
self.worker_identity = _identifier(worker_identity, "worker_identity")
@staticmethod
def _parse(raw: Mapping[str, object]) -> dict[str, object]:
if not isinstance(raw, Mapping) or set(raw) != DOCUMENT_FIELDS:
raise ForbiddenError("authoritative proof document shape invalid")
data = dict(raw)
if data["schema"] != PROOF_SCHEMA:
raise ForbiddenError("authoritative proof schema invalid")
for field in (
"task_id", "mission_id", "root_task_id", "parent_task_id",
"external_job_id", "internal_job_id", "task_principal_ref", "task_lease_ref",
"writer_lease_ref",
):
data[field] = _identifier(data[field], field)
for field in ("acceptance_condition", "repository", "base_revision", "target_branch"):
data[field] = _nonempty_string(data[field], field)
if data["agent_task_status"] != "SUCCEEDED":
raise ForbiddenError("AgentTask is not authoritatively successful")
if data["admission_status"] != "ADMITTED":
raise ForbiddenError("task admission is not authoritative")
if data["execution_outcome"] != "APPLIED_VERIFIED":
raise ForbiddenError("governed execution outcome is not verified")
if type(data["generation"]) is not int or int(data["generation"]) < 1:
raise ForbiddenError("authoritative proof generation invalid")
if not isinstance(data["execution_plan_digest"], str) or _HEX64.fullmatch(
data["execution_plan_digest"]
) is None:
raise ForbiddenError("authoritative execution plan digest invalid")
for field in ("governed_commit_oid", "authority_ref_oid"):
if not isinstance(data[field], str) or _GIT_OID.fullmatch(data[field]) is None:
raise ForbiddenError(f"authoritative proof {field} invalid")
if data["governed_commit_oid"] != data["authority_ref_oid"]:
raise ForbiddenError("authority ref does not read back governed commit")
images = data["postimage_hashes"]
if (
not isinstance(images, Mapping)
or not images
or len(images) > 256
or any(
not isinstance(path, str)
or not path
or path.startswith(("/", "\\"))
or ".." in path.replace("\\", "/").split("/")
or not isinstance(digest, str)
or _HEX64.fullmatch(digest) is None
for path, digest in images.items()
)
):
raise ForbiddenError("authoritative postimage hashes invalid")
data["postimage_hashes"] = dict(sorted(images.items()))
sources = data["source_receipt_digests"]
if (
not isinstance(sources, Mapping)
or set(sources) != REQUIRED_SOURCES
or any(
not isinstance(digest, str) or _HEX64.fullmatch(digest) is None
for digest in sources.values()
)
):
raise ForbiddenError("authoritative source receipt set invalid")
data["source_receipt_digests"] = dict(sorted(sources.items()))
data["attestation_ref"] = _nonempty_string(data["attestation_ref"], "attestation_ref")
data["started_at"] = _utc_time(data["started_at"], "started_at")
data["completed_at"] = _utc_time(data["completed_at"], "completed_at")
if datetime.fromisoformat(data["completed_at"].replace("Z", "+00:00")) < datetime.fromisoformat(
data["started_at"].replace("Z", "+00:00")
):
raise ForbiddenError("authoritative proof time order invalid")
return data
@staticmethod
def _assert_task_binding(task: TaskRecord, data: Mapping[str, object]) -> None:
lineage = task.task_plan.metadata.get(LINEAGE_METADATA_KEY)
if not isinstance(lineage, Mapping) or lineage.get("relation_type") == "ROOT":
raise ForbiddenError("terminal proof requires a canonical lineage child")
expected = {
"task_id": task.task_id,
"mission_id": task.mission_id,
"root_task_id": task.root_task_id,
"parent_task_id": task.task_plan.parent_task_id,
"acceptance_condition": task.task_plan.metadata.get("acceptance_condition"),
}
if any(data.get(field) != value or value in (None, "") for field, value in expected.items()):
raise ForbiddenError("authoritative proof TaskPlane binding mismatch")
if any(lineage.get(field) != expected[field] for field in expected if field != "acceptance_condition"):
raise ForbiddenError("authoritative proof lineage binding mismatch")
def collect(self, task: TaskRecord) -> VerifiedTerminalProof:
raw = self._reader(task.task_id)
if not isinstance(raw, Mapping) or self._verifier(raw) is not True:
raise ForbiddenError("authoritative terminal proof authentication failed")
data = self._parse(raw)
self._assert_task_binding(task, data)
return VerifiedTerminalProof(data)
def verify_evidence(self, task: TaskRecord, evidences: list[dict]) -> bool:
proof = self.collect(task)
return evidences == [proof.evidence()]
def verify_result(self, result: ResultEnvelope) -> bool:
if result.worker_identity != self.worker_identity or result.status != "COMPLETED":
return False
try:
task = self._task_reader(result.task_id)
proof = self.collect(task)
except Exception:
return False
return bool(
result.attestation_ref == proof.attestation_ref
and result.evidence == [proof.evidence()]
and result.outputs == proof.outputs()
and result.started_at == proof.started_at
and result.completed_at == proof.completed_at
and result.operation_digest == task.task_plan.operation_digest()
)
class TerminalProofEvidenceVerifier:
"""PolicyEngine evidence verifier backed by authenticated authority reads."""
def __init__(self, provider: AuthenticatedTerminalProofProvider) -> None:
self.provider = provider
def verify(self, task: TaskRecord, evidences: list[dict]) -> PolicyDecision:
try:
allowed = self.provider.verify_evidence(task, evidences)
except Exception:
allowed = False
return PolicyDecision(allowed, "" if allowed else "terminal proof evidence invalid")
class TerminalProofImporter:
"""Reconcile a server-verified proof through TaskPlane claim/result paths."""
def __init__(
self,
service: Any,
*,
provider: AuthenticatedTerminalProofProvider | None,
worker_id: str | None,
worker_identity: str | None,
lease_seconds: int = 300,
) -> None:
self.service = service
self.provider = provider
self.worker_id = worker_id
self.worker_identity = worker_identity
if provider is not None:
if not worker_id or not worker_identity:
raise ValueError("terminal proof worker binding required")
_identifier(worker_id, "worker_id")
_identifier(worker_identity, "worker_identity")
if provider.worker_identity != worker_identity:
raise ValueError("terminal proof provider identity mismatch")
if not 30 <= int(lease_seconds) <= 3600:
raise ValueError("terminal proof lease out of range")
self.lease_seconds = int(lease_seconds)
@staticmethod
def expected_idempotency_key(task: TaskRecord) -> str:
return f"terminal-proof:{task.mission_id}:{task.task_id}"
def _authorized_task(self, task_id: str, actor_identity: str) -> TaskRecord:
self.service.get_task(task_id, actor_identity)
task = self.service.store.get_task(task_id)
lineage = task.task_plan.metadata.get(LINEAGE_METADATA_KEY)
if (
not isinstance(lineage, Mapping)
or lineage.get("relation_type") == "ROOT"
or lineage.get("task_id") != task.task_id
or lineage.get("mission_id") != task.mission_id
or lineage.get("root_task_id") != task.root_task_id
or lineage.get("parent_task_id") != task.task_plan.parent_task_id
):
raise ForbiddenError("terminal proof target is not a canonical child")
return task
def _existing_receipt(
self, task: TaskRecord, actor_identity: str
) -> dict[str, object] | None:
if self.service.lineage is None:
raise ForbiddenError("TaskPlane lineage is not configured")
snapshot = self.service.lineage.binding.get_task_snapshot(task.task_id)
if not snapshot.terminal_evidence_proven:
return None
if snapshot.status != "COMPLETED":
raise ConflictError("terminal proof target did not complete successfully")
review = self.service.lineage.binding.get_root_review_for_trigger(task.task_id)
if review is None:
raise ConflictError("terminal proof lacks durable root review")
evidence_rows = self.service.store.list_evidence(task.task_id)
by_id = {str(row.get("evidence_id")): row for row in evidence_rows}
if not snapshot.evidence_refs or any(ref not in by_id for ref in snapshot.evidence_refs):
raise ConflictError("terminal proof evidence readback mismatch")
envelopes = []
for ref in snapshot.evidence_refs:
try:
envelope = json.loads(str(by_id[ref]["envelope_json"]))
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as exc:
raise ConflictError("terminal proof evidence readback invalid") from exc
envelopes.append(envelope)
claim_ids = {item.get("claim_id") for item in envelopes}
evidence_payloads = [item.get("payload") for item in envelopes]
if (
len(claim_ids) != 1
or len(evidence_payloads) != 1
or any(item.get("worker_identity") != self.worker_identity for item in envelopes)
or envelopes[0].get("kind") != "acceptance-proof"
or not isinstance(evidence_payloads[0], dict)
or not self.provider.verify_evidence(task, evidence_payloads)
or not self.service.store.verify_evidence_integrity(task.task_id)
or not self.service.store.verify_event_chain(task.task_id)
):
raise ConflictError("terminal proof receipt integrity failed")
claim = self.service.store.get_claim(str(next(iter(claim_ids))))
if (
claim.task_id != task.task_id
or claim.worker_id != self.worker_id
or claim.worker_identity != self.worker_identity
or claim.operation_digest != task.task_plan.operation_digest()
or claim.status != "COMPLETED"
):
raise ConflictError("terminal proof claim binding mismatch")
dispatch = self.service.lineage.binding.get_root_action_dispatch(str(review["review_id"]))
if dispatch is None:
raise ConflictError("terminal proof lacks durable root action dispatch")
body: dict[str, object] = {
"schema": RECEIPT_SCHEMA,
"task_id": task.task_id,
"mission_id": task.mission_id,
"root_task_id": task.root_task_id,
"claim_id": next(iter(claim_ids)),
"task_status": snapshot.status,
"task_revision": snapshot.revision,
"evidence_refs": list(snapshot.evidence_refs),
"review_id": review["review_id"],
"root_revision_after": review["root_revision_after"],
"next_legal_action": review["next_legal_action"],
"dispatch_status": dispatch.status,
"dispatch_idempotency_key": dispatch.idempotency_key,
}
body["receipt_digest"] = sha256(_canonical_bytes(body)).hexdigest()
return body
def _exact_active_claim(self, task: TaskRecord):
active = self.service.store.active_claim(task.task_id)
if active is None:
return None
if (
active.worker_id != self.worker_id
or active.worker_identity != self.worker_identity
or active.operation_digest != task.task_plan.operation_digest()
):
raise ConflictError("active claim is owned by another execution binding")
return active
def reconcile(
self, task_id: str, idempotency_key: str | None, *, actor_identity: str
) -> dict[str, object]:
if self.provider is None:
raise ForbiddenError("terminal proof provider is not configured")
task = self._authorized_task(task_id, actor_identity)
if idempotency_key != self.expected_idempotency_key(task):
raise ValidationError("terminal proof idempotency key mismatch")
existing = self._existing_receipt(task, actor_identity)
if existing is not None:
return existing
proof = self.provider.collect(task)
claim = self._exact_active_claim(task)
if claim is None:
try:
claimed = self.service.claim(
task.task_id,
{
"worker_id": self.worker_id,
"lease_seconds": self.lease_seconds,
"actor_identity": self.worker_identity,
},
)
claim_id = str(claimed["claim_id"])
except Exception:
refreshed = self._authorized_task(task_id, actor_identity)
replay = self._existing_receipt(refreshed, actor_identity)
if replay is not None:
return replay
claim = self._exact_active_claim(refreshed)
if claim is None:
raise
claim_id = claim.claim_id
else:
claim_id = claim.claim_id
try:
self.service.complete(
task.task_id,
{
"claim_id": claim_id,
"actor_identity": self.worker_identity,
"status": "COMPLETED",
"outputs": proof.outputs(),
"evidence": [proof.evidence()],
"started_at": proof.started_at,
"completed_at": proof.completed_at,
"attestation_ref": proof.attestation_ref,
},
)
except Exception:
refreshed = self._authorized_task(task_id, actor_identity)
replay = self._existing_receipt(refreshed, actor_identity)
if replay is None:
raise
return replay
refreshed = self._authorized_task(task_id, actor_identity)
receipt = self._existing_receipt(refreshed, actor_identity)
if receipt is None:
raise ConflictError("terminal proof completion lacks authoritative readback")
return receipt
+40 -8
View File
@@ -4,6 +4,7 @@ import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse
from http import cookies
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import hmac # SEC-P0-BOOTSTRAP-CREDENTIAL
ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]))
DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3'))
@@ -13,7 +14,11 @@ PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top')
WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top')
API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top')
WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top')
SECRET = os.getenv('AIOS_WEBOPS_SECRET','change-me-before-production')
SECRET = os.getenv('AIOS_WEBOPS_SECRET') or ''
# SEC-P0: fail closed — never boot with a missing/insecure default secret.
if SECRET in ('', 'change-me-before-production'):
raise SystemExit('AIOS_WEBOPS_SECRET not set (or insecure default). Refusing to start: fail closed.')
SESSION_TTL = int(os.getenv('AIOS_SESSION_TTL','3600'))
ROLES = ['owner','admin','user']
NODES = ['server-hw','server-local']
PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')]
@@ -43,19 +48,46 @@ def audit(actor, action, target='', payload=None):
exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now()))
def notify(event_type,payload):
exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now()))
def hash_pw(p): return hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
def check_pw(stored,p): return stored in (p, hash_pw(p))
_PBKDF2_ITERS = int(os.getenv('AIOS_PBKDF2_ITERS','260000'))
def _is_pbkdf2(stored): return isinstance(stored,str) and stored.startswith('pbkdf2$') and stored.count('$')==2
def hash_pw(p):
salt=os.urandom(16).hex()
dk=hashlib.pbkdf2_hmac('sha256', p.encode(), bytes.fromhex(salt), _PBKDF2_ITERS).hex()
return f'pbkdf2${salt}${dk}'
def check_pw(stored,p):
if not stored or not isinstance(stored,str) or not stored: return False
if _is_pbkdf2(stored):
_,salt,dk=stored.split('$')
calc=hashlib.pbkdf2_hmac('sha256', p.encode(), bytes.fromhex(salt), _PBKDF2_ITERS).hex()
return hmac.compare_digest(dk,calc)
# Legacy record path: NEVER accept the plaintext form as a credential.
# A legacy (unsalted) sha256 is honored ONLY as a one-time owner migration
# credential and never for the known bootstrap default; anything else is
# rejected so the account must be re-seeded via a secure channel (fail closed).
if stored == p: return False
try:
old=hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
except Exception:
return False
return (hmac.compare_digest(old,stored)) and (p not in {'bootstrap-change-me'})
def token(username,role):
data=f'{username}|{role}|{int(time.time())}'; sig=hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]; return data+'|'+sig
exp=int(time.time())+SESSION_TTL
data=f'{username}|{role}|{exp}'
sig=hmac.new(SECRET.encode(), data.encode(), hashlib.sha256).hexdigest()
return data+'|'+sig
def parse_session(header):
if not header: return None
c=cookies.SimpleCookie(); c.load(header)
if 'aios_session' not in c: return None
parts=c['aios_session'].value.split('|')
if len(parts)!=4: return None
data='|'.join(parts[:3]); sig=parts[3]
if hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]!=sig: return None
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(parts[0],parts[1],'approved'))
username,role,exp,sig=parts
data=f'{username}|{role}|{exp}'
if not hmac.compare_digest(hmac.new(SECRET.encode(), data.encode(), hashlib.sha256).hexdigest(), sig): return None
try:
if int(exp) <= int(time.time()): return None
except Exception: return None
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(username,role,'approved'))
def require(user, roles): return bool(user and user['role'] in roles)
def table(data):
if not data: return '<p class="muted">No records.</p>'
@@ -215,7 +247,7 @@ class H(BaseHTTPRequestHandler):
if path=='/login':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved'))
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Lax"]); return
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Strict"]); return
self.send_body(403,layout('Login Failed','<p>登录失败。</p>',None)); return
if path=='/register':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
+40 -8
View File
@@ -4,6 +4,7 @@ import hashlib, html, json, os, sqlite3, time, uuid, urllib.parse
from http import cookies
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import hmac # SEC-P0-BOOTSTRAP-CREDENTIAL
ROOT = Path(os.getenv('AIOS_WEBOPS_ROOT', Path(__file__).resolve().parents[1]))
DB_PATH = Path(os.getenv('AIOS_WEBOPS_DB', ROOT / 'data' / 'webops.sqlite3'))
@@ -13,7 +14,11 @@ PUBLIC_DOMAIN = os.getenv('AIOS_PUBLIC_DOMAIN','https://cnnetmkt.top')
WEBOPS_DOMAIN = os.getenv('AIOS_WEBOPS_DOMAIN','https://ops.cnnetmkt.top')
API_DOMAIN = os.getenv('AIOS_API_DOMAIN','https://api.cnnetmkt.top')
WECHAT_DOMAIN = os.getenv('AIOS_WECHAT_CALLBACK_DOMAIN','https://wechat.cnnetmkt.top')
SECRET = os.getenv('AIOS_WEBOPS_SECRET','change-me-before-production')
SECRET = os.getenv('AIOS_WEBOPS_SECRET') or ''
# SEC-P0: fail closed — never boot with a missing/insecure default secret.
if SECRET in ('', 'change-me-before-production'):
raise SystemExit('AIOS_WEBOPS_SECRET not set (or insecure default). Refusing to start: fail closed.')
SESSION_TTL = int(os.getenv('AIOS_SESSION_TTL','3600'))
ROLES = ['owner','admin','user']
NODES = ['server-hw','server-local']
PAGES = [('/dashboard','Dashboard'),('/nodes','Nodes'),('/roles','Roles'),('/routing','Routing'),('/failover','Failover'),('/approvals','Approvals'),('/operations','Operations'),('/users','Users'),('/quota','Quota'),('/models','Models'),('/memory','Memory'),('/channels/wechat','WeChat'),('/life','Life Core'),('/patches','Patch Studio'),('/optimization','Optimization'),('/simulation','Simulation'),('/logs','Logs'),('/config','Config'),('/uat','UAT'),('/versions','Versions'),('/evidence','Evidence')]
@@ -43,19 +48,46 @@ def audit(actor, action, target='', payload=None):
exec_sql('INSERT INTO audit_logs(id,actor,action,target,payload,created_at) VALUES(?,?,?,?,?,?)',(uid('audit'),actor,action,target,jdump(payload or {}),now()))
def notify(event_type,payload):
exec_sql('INSERT INTO owner_notifications(id,event_type,payload,status,created_at) VALUES(?,?,?,?,?)',(uid('notify'),event_type,jdump(payload),'queued',now()))
def hash_pw(p): return hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
def check_pw(stored,p): return stored in (p, hash_pw(p))
_PBKDF2_ITERS = int(os.getenv('AIOS_PBKDF2_ITERS','260000'))
def _is_pbkdf2(stored): return isinstance(stored,str) and stored.startswith('pbkdf2$') and stored.count('$')==2
def hash_pw(p):
salt=os.urandom(16).hex()
dk=hashlib.pbkdf2_hmac('sha256', p.encode(), bytes.fromhex(salt), _PBKDF2_ITERS).hex()
return f'pbkdf2${salt}${dk}'
def check_pw(stored,p):
if not stored or not isinstance(stored,str) or not stored: return False
if _is_pbkdf2(stored):
_,salt,dk=stored.split('$')
calc=hashlib.pbkdf2_hmac('sha256', p.encode(), bytes.fromhex(salt), _PBKDF2_ITERS).hex()
return hmac.compare_digest(dk,calc)
# Legacy record path: NEVER accept the plaintext form as a credential.
# A legacy (unsalted) sha256 is honored ONLY as a one-time owner migration
# credential and never for the known bootstrap default; anything else is
# rejected so the account must be re-seeded via a secure channel (fail closed).
if stored == p: return False
try:
old=hashlib.sha256((SECRET+'|'+p).encode()).hexdigest()
except Exception:
return False
return (hmac.compare_digest(old,stored)) and (p not in {'bootstrap-change-me'})
def token(username,role):
data=f'{username}|{role}|{int(time.time())}'; sig=hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]; return data+'|'+sig
exp=int(time.time())+SESSION_TTL
data=f'{username}|{role}|{exp}'
sig=hmac.new(SECRET.encode(), data.encode(), hashlib.sha256).hexdigest()
return data+'|'+sig
def parse_session(header):
if not header: return None
c=cookies.SimpleCookie(); c.load(header)
if 'aios_session' not in c: return None
parts=c['aios_session'].value.split('|')
if len(parts)!=4: return None
data='|'.join(parts[:3]); sig=parts[3]
if hashlib.sha256((SECRET+'|'+data).encode()).hexdigest()[:16]!=sig: return None
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(parts[0],parts[1],'approved'))
username,role,exp,sig=parts
data=f'{username}|{role}|{exp}'
if not hmac.compare_digest(hmac.new(SECRET.encode(), data.encode(), hashlib.sha256).hexdigest(), sig): return None
try:
if int(exp) <= int(time.time()): return None
except Exception: return None
return one('SELECT * FROM users WHERE username=? AND role=? AND status=?',(username,role,'approved'))
def require(user, roles): return bool(user and user['role'] in roles)
def table(data):
if not data: return '<p class="muted">No records.</p>'
@@ -215,7 +247,7 @@ class H(BaseHTTPRequestHandler):
if path=='/login':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
u=one('SELECT * FROM users WHERE username=? AND status=?',(data.get('username',[''])[0],'approved'))
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Lax"]); return
if u and check_pw(u.get('password_hash') or '',data.get('password',[''])[0]): audit(u['username'],'login','webops',{}); self.redirect('/dashboard',[f"aios_session={token(u['username'],u['role'])}; Path=/; HttpOnly; SameSite=Strict"]); return
self.send_body(403,layout('Login Failed','<p>登录失败。</p>',None)); return
if path=='/register':
n=int(self.headers.get('Content-Length','0') or 0); data=urllib.parse.parse_qs(self.rfile.read(n).decode())
@@ -0,0 +1,359 @@
#!/usr/bin/env python3
"""Governed, append-only reopen of one expired Owner approval cycle."""
from __future__ import annotations
import hashlib
import json
import os
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Mapping
REQUEST_SCHEMA = "aios.owner-approval-reopen-request.v1"
EVENT_NAME = "owner_approval_reopen_authorized"
_IDEMPOTENCY_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$")
_HEX40 = re.compile(r"^[0-9a-f]{40}$")
_HEX64 = re.compile(r"^[0-9a-f]{64}$")
class ApprovalReopenError(RuntimeError):
def __init__(self, code: str, detail: str | None = None):
self.code = code
self.detail = detail
super().__init__(code if not detail else f"{code}:{detail}")
@dataclass(frozen=True)
class ReopenRequest:
candidate_id: str
expected_approval_fingerprint: str
expected_source_commit: str
expected_tree_oid: str
expected_artifact_sha256: str
expected_manifest_sha256: str
expected_release_plan_sha256: str
expected_policy_sha256: str
reason: str
idempotency_key: str
@classmethod
def from_mapping(cls, raw: Mapping[str, Any]) -> "ReopenRequest":
if raw.get("schema") != REQUEST_SCHEMA:
raise ApprovalReopenError("APPROVAL_REOPEN_SCHEMA_INVALID")
allowed = {
"schema", "candidate_id", "expected_approval_fingerprint",
"expected_source_commit", "expected_tree_oid",
"expected_artifact_sha256", "expected_manifest_sha256",
"expected_release_plan_sha256", "expected_policy_sha256",
"reason", "idempotency_key",
}
if set(raw) != allowed:
raise ApprovalReopenError("APPROVAL_REOPEN_REQUEST_FIELDS_INVALID")
req = cls(
candidate_id=str(raw["candidate_id"]),
expected_approval_fingerprint=str(raw["expected_approval_fingerprint"]),
expected_source_commit=str(raw["expected_source_commit"]),
expected_tree_oid=str(raw["expected_tree_oid"]),
expected_artifact_sha256=str(raw["expected_artifact_sha256"]),
expected_manifest_sha256=str(raw["expected_manifest_sha256"]),
expected_release_plan_sha256=str(raw["expected_release_plan_sha256"]),
expected_policy_sha256=str(raw["expected_policy_sha256"]),
reason=str(raw["reason"]),
idempotency_key=str(raw["idempotency_key"]),
)
if not req.candidate_id or "/" in req.candidate_id or ".." in req.candidate_id:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_ID_INVALID")
if not _HEX64.fullmatch(req.expected_approval_fingerprint):
raise ApprovalReopenError("APPROVAL_REOPEN_FINGERPRINT_INVALID")
if not _HEX40.fullmatch(req.expected_source_commit):
raise ApprovalReopenError("APPROVAL_REOPEN_SOURCE_INVALID")
if not _HEX40.fullmatch(req.expected_tree_oid):
raise ApprovalReopenError("APPROVAL_REOPEN_TREE_INVALID")
for value, code in (
(req.expected_artifact_sha256, "APPROVAL_REOPEN_ARTIFACT_INVALID"),
(req.expected_manifest_sha256, "APPROVAL_REOPEN_MANIFEST_INVALID"),
(req.expected_release_plan_sha256, "APPROVAL_REOPEN_RELEASE_PLAN_INVALID"),
(req.expected_policy_sha256, "APPROVAL_REOPEN_POLICY_INVALID"),
):
if not _HEX64.fullmatch(value):
raise ApprovalReopenError(code)
if not (1 <= len(req.reason.strip()) <= 512):
raise ApprovalReopenError("APPROVAL_REOPEN_REASON_INVALID")
if not _IDEMPOTENCY_RE.fullmatch(req.idempotency_key):
raise ApprovalReopenError("APPROVAL_REOPEN_IDEMPOTENCY_KEY_INVALID")
return req
def _canon(value: Any) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode()
def _sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _time(value: Any, code: str) -> datetime:
if not isinstance(value, str) or not value:
raise ApprovalReopenError(code)
try:
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise ApprovalReopenError(code) from exc
if dt.tzinfo is None:
raise ApprovalReopenError(code)
return dt.astimezone(timezone.utc)
def _secure_json(path: Path) -> dict[str, Any]:
if path.is_symlink() or not path.is_file():
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_INVALID")
st = path.stat()
if st.st_mode & 0o022:
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_WRITABLE")
if st.st_uid not in {0, os.geteuid()}:
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_OWNER_INVALID")
try:
obj = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_INVALID") from exc
if not isinstance(obj, dict):
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_FILE_INVALID")
return obj
def _fsync_dir(path: Path) -> None:
fd = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try:
os.fsync(fd)
finally:
os.close(fd)
def _paths(root: Path, cid: str) -> dict[str, Path]:
return {
"candidate": root / "candidates" / f"{cid}.json",
"saga": root / "sagas" / f"{cid}.json",
"plan": root / "release-plans" / f"{cid}.json",
"acceptance": root / "acceptance" / f"{cid}.json",
"approval": root / "approvals" / f"{cid}.json",
}
def _history(root: Path, cid: str, fp: str) -> Path:
return root / "approval-history" / cid / f"{fp}.json"
def _archive(active: Path, history: Path) -> None:
history.parent.mkdir(parents=True, exist_ok=True, mode=0o750)
try:
os.chmod(history.parent, 0o750)
except PermissionError:
pass
original = active.read_bytes()
if history.exists():
if history.is_symlink() or not history.is_file() or history.read_bytes() != original:
raise ApprovalReopenError("APPROVAL_REOPEN_HISTORY_COLLISION")
return
try:
os.link(active, history)
except OSError:
fd = os.open(history, os.O_WRONLY | os.O_CREAT | os.O_EXCL,
active.stat().st_mode & 0o777)
try:
view = memoryview(original)
while view:
n = os.write(fd, view)
view = view[n:]
os.fsync(fd)
finally:
os.close(fd)
if history.read_bytes() != original:
raise ApprovalReopenError("APPROVAL_REOPEN_HISTORY_COPY_FAILED")
_fsync_dir(history.parent)
def _details(event: Mapping[str, Any]) -> Mapping[str, Any]:
val = event.get("details")
return val if isinstance(val, Mapping) else {}
def _cycle_event(saga: Mapping[str, Any], cycle_id: str) -> Mapping[str, Any] | None:
for event in saga.get("events") or []:
if isinstance(event, Mapping) and event.get("event") == EVENT_NAME \
and _details(event).get("approval_cycle_id") == cycle_id:
return event
return None
def _state(name: str) -> Any:
from silent_release_hardening import SagaState # lazy: avoids import cycle
return getattr(SagaState, name)
def _cycle_id(req: ReopenRequest) -> str:
return "approval-reopen-" + _sha(_canon({
"schema": REQUEST_SCHEMA,
"candidate_id": req.candidate_id,
"old_fingerprint": req.expected_approval_fingerprint,
"source_commit": req.expected_source_commit,
"tree_oid": req.expected_tree_oid,
"artifact_sha256": req.expected_artifact_sha256,
"manifest_sha256": req.expected_manifest_sha256,
"release_plan_sha256": req.expected_release_plan_sha256,
"policy_sha256": req.expected_policy_sha256,
"idempotency_key": req.idempotency_key,
}))[:24]
def _validate(req: ReopenRequest, candidate: Mapping[str, Any],
saga: Mapping[str, Any], plan: Mapping[str, Any],
acceptance: Mapping[str, Any], approval: Mapping[str, Any],
now: datetime) -> None:
if saga.get("journal_integrity") is False:
raise ApprovalReopenError("APPROVAL_REOPEN_JOURNAL_INTEGRITY_INVALID")
if saga.get("state") != "APPROVED":
raise ApprovalReopenError("APPROVAL_REOPEN_SAGA_STATE_INVALID")
if candidate.get("candidate_id") != req.candidate_id:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_MISMATCH")
if candidate.get("state") not in {"READY_FOR_APPROVAL", "APPROVED"}:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_STATE_INVALID")
if candidate.get("approval_state") not in {None, "PENDING"}:
raise ApprovalReopenError("APPROVAL_REOPEN_CANDIDATE_APPROVAL_STATE_INVALID")
# ReleaseCandidate v1 durable records do not persist side_effects_executed.
# Compatibility rule is intentionally fail-closed:
# - an absent key is accepted only together with the independent Saga
# invariant below that the accepted Owner-approval event is the final
# Saga event;
# - if a future durable schema persists the key, it must be exactly False;
# - explicit True/None/number/string values are rejected.
if "side_effects_executed" in candidate and candidate.get("side_effects_executed") is not False:
raise ApprovalReopenError("APPROVAL_REOPEN_SIDE_EFFECTS_ALREADY_EXECUTED")
checks = {
"source": candidate.get("source_commit") == req.expected_source_commit,
"tree": candidate.get("tree_oid") == req.expected_tree_oid,
"artifact": candidate.get("artifact_sha256") == req.expected_artifact_sha256,
"manifest": candidate.get("manifest_sha256") == req.expected_manifest_sha256,
"approval_fingerprint": approval.get("fingerprint") == req.expected_approval_fingerprint,
"approval_source": approval.get("source_commit") == req.expected_source_commit,
"approval_tree": approval.get("tree_oid") == req.expected_tree_oid,
"approval_artifact": approval.get("artifact_sha256") == req.expected_artifact_sha256,
"approval_manifest": approval.get("manifest_sha256") == req.expected_manifest_sha256,
"approval_release_plan": approval.get("release_plan_sha256") == req.expected_release_plan_sha256,
"approval_policy": approval.get("policy_sha256") == req.expected_policy_sha256,
"acceptance": acceptance.get("status") == "ACCEPTED",
"acceptance_release_plan": acceptance.get("release_plan_sha256") == req.expected_release_plan_sha256,
"plan_sha": (plan.get("sha256") or plan.get("release_plan_sha256")) == req.expected_release_plan_sha256,
"plan_policy": plan.get("policy_sha256") == req.expected_policy_sha256,
}
bad = sorted(k for k, ok in checks.items() if not ok)
if bad:
raise ApprovalReopenError("APPROVAL_REOPEN_BINDING_MISMATCH", ",".join(bad))
if now <= _time(approval.get("expires_at"), "APPROVAL_REOPEN_APPROVAL_EXPIRY_INVALID"):
raise ApprovalReopenError("APPROVAL_REOPEN_APPROVAL_NOT_EXPIRED")
events = [e for e in saga.get("events") or [] if isinstance(e, Mapping)]
accepted = None
for i, event in enumerate(events):
if event.get("event") == "signed_owner_approval_accepted" \
and _details(event).get("approval_fingerprint") == req.expected_approval_fingerprint:
accepted = i
if accepted is None:
raise ApprovalReopenError("APPROVAL_REOPEN_ACCEPTED_EVENT_MISSING")
if accepted != len(events) - 1:
raise ApprovalReopenError("APPROVAL_REOPEN_SAGA_ALREADY_ADVANCED")
def reopen_owner_approval(service: Any, raw: Mapping[str, Any], *,
state_root: Path | None = None,
now: datetime | None = None) -> dict[str, Any]:
req = ReopenRequest.from_mapping(raw)
raw_root = state_root or os.environ.get("AIOS_SILENT_RELEASE_STATE_ROOT", "")
if not str(raw_root):
raise ApprovalReopenError("APPROVAL_REOPEN_STATE_ROOT_MISSING")
root = Path(raw_root).expanduser().resolve()
paths = _paths(root, req.candidate_id)
cycle = _cycle_id(req)
history = _history(root, req.candidate_id, req.expected_approval_fingerprint)
saga = _secure_json(paths["saga"])
prior = _cycle_event(saga, cycle)
if prior is not None:
if saga.get("state") != "READY_FOR_APPROVAL":
raise ApprovalReopenError("APPROVAL_REOPEN_CYCLE_ALREADY_CONSUMED")
if not history.is_file():
raise ApprovalReopenError("APPROVAL_REOPEN_HISTORY_MISSING")
if paths["approval"].exists():
active = _secure_json(paths["approval"])
if active.get("fingerprint") != req.expected_approval_fingerprint:
raise ApprovalReopenError("APPROVAL_REOPEN_ACTIVE_APPROVAL_CHANGED")
paths["approval"].unlink()
_fsync_dir(paths["approval"].parent)
return {
"schema": "aios.owner-approval-reopen-result.v1",
"status": "IDEMPOTENT",
"candidate_id": req.candidate_id,
"approval_cycle_id": cycle,
"supersedes_fingerprint": req.expected_approval_fingerprint,
"saga_state": "READY_FOR_APPROVAL",
"history_ref": str(history.relative_to(root)),
}
candidate = _secure_json(paths["candidate"])
plan = _secure_json(paths["plan"])
acceptance = _secure_json(paths["acceptance"])
approval = _secure_json(paths["approval"])
_validate(req, candidate, saga, plan, acceptance, approval,
(now or datetime.now(timezone.utc)).astimezone(timezone.utc))
_archive(paths["approval"], history)
event_details = {
"schema": "aios.owner-approval-reopen-event.v1",
"approval_cycle_id": cycle,
"supersedes_approval_fingerprint": req.expected_approval_fingerprint,
"expired_at": approval.get("expires_at"),
"release_plan_sha256": req.expected_release_plan_sha256,
"source_commit": req.expected_source_commit,
"tree_oid": req.expected_tree_oid,
"artifact_sha256": req.expected_artifact_sha256,
"reason": req.reason.strip(),
"idempotency_digest": _sha(req.idempotency_key.encode()),
}
try:
service.sagas.compare_and_set(
req.candidate_id,
expected_revision=saga.get("revision"),
expected_states={_state("APPROVED")},
target=_state("READY_FOR_APPROVAL"),
event=EVENT_NAME,
details=event_details,
)
except Exception as exc:
raise ApprovalReopenError("APPROVAL_REOPEN_SAGA_CAS_FAILED") from exc
try:
active = _secure_json(paths["approval"])
if active.get("fingerprint") != req.expected_approval_fingerprint:
raise ApprovalReopenError("APPROVAL_REOPEN_ACTIVE_APPROVAL_CHANGED")
paths["approval"].unlink()
_fsync_dir(paths["approval"].parent)
except ApprovalReopenError:
raise
except OSError as exc:
raise ApprovalReopenError("APPROVAL_REOPEN_ACTIVE_APPROVAL_REMOVE_FAILED") from exc
return {
"schema": "aios.owner-approval-reopen-result.v1",
"status": "REOPENED",
"candidate_id": req.candidate_id,
"approval_cycle_id": cycle,
"supersedes_fingerprint": req.expected_approval_fingerprint,
"saga_state": "READY_FOR_APPROVAL",
"history_ref": str(history.relative_to(root)),
"release_plan_sha256": req.expected_release_plan_sha256,
"source_commit": req.expected_source_commit,
"tree_oid": req.expected_tree_oid,
}
+167 -71
View File
@@ -105,6 +105,28 @@ pre { white-space: pre-wrap; word-break: break-word; }
</div>
</div>
<script>
// Security: all dynamic values (API/git/agent/owner-gate) are rendered with
// textContent / DOM APIs. innerHTML is never used to inject untrusted data.
function clearNode(node) {
if (!node) return;
while (node.firstChild) node.removeChild(node.firstChild);
}
function setText(id, text) {
const n = document.getElementById(id);
if (n) n.textContent = text;
}
function addClass(node, cls) { if (node) node.classList.add(cls); }
function div(cls) { const d = document.createElement('div'); if (cls) addClass(d, cls); return d; }
function span(cls, text) { const s = document.createElement('span'); if (cls) addClass(s, cls); if (text != null) s.textContent = text; return s; }
// Centralized CSRF token source: server-issued meta tag, or double-submit cookie.
function csrfToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
if (meta && meta.content) return meta.content;
const m = document.cookie.match(/(?:^|;\s*)aios_csrf=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
async function fetchData() {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
@@ -124,130 +146,204 @@ async function fetchData() {
async function loadVersions() {
const el = document.getElementById('versions-list');
el.innerHTML = '<div class="loading">Loading versions...</div>';
clearNode(el);
const loading = div('loading'); loading.textContent = 'Loading versions...'; el.appendChild(loading);
try {
const r = await fetch('/admin/ai-evolution/api/versions');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '<div class="grid">';
clearNode(el);
if (data.error) { const e = div('error'); e.textContent = data.error; el.appendChild(e); return; }
const grid = div('grid');
for (const v of data.versions || []) {
html += '<div class="card"><div class="card-title">' + v.tag + '</div><div class="card-value">' + (v.version || v.tag) + '</div></div>';
const card = div('card');
const t = div('card-title'); t.textContent = v.tag || '';
const val = div('card-value'); val.textContent = v.version || v.tag || '';
card.appendChild(t); card.appendChild(val); grid.appendChild(card);
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load versions: ' + e.message + '</div>';
el.appendChild(grid);
} catch (e) {
const err = div('error'); err.textContent = 'Failed to load versions: ' + e.message; el.appendChild(err);
}
}
async function loadCommands() {
const el = document.getElementById('commands-list');
el.innerHTML = '<div class="loading">Loading commands...</div>';
clearNode(el);
const loading = div('loading'); loading.textContent = 'Loading commands...'; el.appendChild(loading);
try {
const r = await fetch('/admin/ai-evolution/api/data');
const data = await r.json();
let cmds = data.commands || [];
let html = '<div style="display:flex;gap:24px;margin-bottom:16px;">';
html += '<div><div class="card"><div class="card-title">Total</div><div class="card-value">' + cmds.length + '</div></div></div>';
html += '<div><div class="card"><div class="card-title">By Category</div>';
const cmds = data.commands || [];
clearNode(el);
const wrapper = div('');
wrapper.style.display = 'flex'; wrapper.style.gap = '24px'; wrapper.style.marginBottom = '16px';
const totalCard = div('card');
const tcTitle = div('card-title'); tcTitle.textContent = 'Total';
const tcVal = div('card-value'); tcVal.textContent = String(cmds.length);
totalCard.appendChild(tcTitle); totalCard.appendChild(tcVal); wrapper.appendChild(totalCard);
const catCard = div('card');
const ccTitle = div('card-title'); ccTitle.textContent = 'By Category';
catCard.appendChild(ccTitle);
const cats = {};
for (const c of cmds) { const k = c.category || 'uncategorized'; cats[k] = (cats[k] || 0) + 1; }
for (const [k, v] of Object.entries(cats)) html += '<div style="font-size:14px;color:#8b949e;margin:4px 0;">' + k + ': ' + v + '</div>';
html += '</div></div></div>';
for (const cmd of cmds.slice().reverse()) {
html += '<div class="command-item"><div class="meta">' + cmd.category + ' | ' + cmd.filename + '</div><div class="name">' + cmd.title + '</div><div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + cmd.summary + '</div></div>';
for (const [k, v] of Object.entries(cats)) {
const line = div(''); line.style.fontSize = '14px'; line.style.color = '#8b949e'; line.style.margin = '4px 0';
line.textContent = k + ': ' + v; catCard.appendChild(line);
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load commands: ' + e.message + '</div>';
wrapper.appendChild(catCard); el.appendChild(wrapper);
for (const cmd of cmds.slice().reverse()) {
const item = div('command-item');
const meta = div('meta'); meta.textContent = (cmd.category || '') + ' | ' + (cmd.filename || '');
const name = div('name'); name.textContent = cmd.title || '';
const summary = div(''); summary.style.fontSize = '13px'; summary.style.color = '#c9d1d9'; summary.style.marginTop = '4px';
summary.textContent = cmd.summary || '';
item.appendChild(meta); item.appendChild(name); item.appendChild(summary);
el.appendChild(item);
}
} catch (e) {
const err = div('error'); err.textContent = 'Failed to load commands: ' + e.message; el.appendChild(err);
}
}
async function loadAgents() {
const el = document.getElementById('agents-list');
el.innerHTML = '<div class="loading">Loading agent runs...</div>';
clearNode(el);
const loading = div('loading'); loading.textContent = 'Loading agent runs...'; el.appendChild(loading);
try {
const r = await fetch('/admin/ai-evolution/api/agents');
const data = await r.json();
if (data.error) { el.innerHTML = '<div class="error">' + data.error + '</div>'; return; }
let html = '';
for (const agent of data.agents || []) {
html += '<div class="agent-item"><div class="agent-title">' + agent.name + ' <span class="state state-' + (agent.status === 'running' ? 'running' : (agent.status === 'ok' ? 'completed' : 'failed')) + '">' + agent.status + '</span></div><div class="agent-meta">' + (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-') + '</div></div>';
clearNode(el);
if (data.error) { const e = div('error'); e.textContent = data.error; el.appendChild(e); return; }
const agents = data.agents || [];
if (!agents.length) {
const none = div(''); none.style.padding = '24px'; none.style.color = '#8b949e';
none.textContent = 'No agent runs recorded yet.'; el.appendChild(none); return;
}
el.innerHTML = html || '<div style="padding:24px;color:#8b949e;">No agent runs recorded yet.</div>';
} catch(e) {
el.innerHTML = '<div class="error">Failed to load agents: ' + e.message + '</div>';
for (const agent of agents) {
const item = div('agent-item');
const title = div('agent-title');
title.appendChild(document.createTextNode(agent.name || ''));
const st = (agent.status === 'running') ? 'running' : ((agent.status === 'ok') ? 'completed' : 'failed');
const badge = span('state state-' + st, agent.status || '');
title.appendChild(badge);
const meta = div('agent-meta');
meta.textContent = (agent.runs || 0) + ' runs | last: ' + (agent.last_run || '-');
item.appendChild(title); item.appendChild(meta); el.appendChild(item);
}
} catch (e) {
const err = div('error'); err.textContent = 'Failed to load agents: ' + e.message; el.appendChild(err);
}
}
function showPanel(name) {
async function loadOwnerGate() {
const el = document.getElementById('arch-owner-content');
el.innerHTML = '<div class="loading">Loading owner gate data...</div>';
clearNode(el);
const loading = div('loading'); loading.textContent = 'Loading owner gate data...'; el.appendChild(loading);
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '<div class="row">';
html += '<div class="col"><div class="card"><div class="card-title">Pending Requests</div><div class="card-value">' + data.pending_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Approved</div><div class="card-value">' + data.approved_count + '</div></div></div>';
html += '<div class="col"><div class="card"><div class="card-title">Rejected</div><div class="card-value">' + data.rejected_count + '</div></div></div>';
html += '</div>';
html += '<div class="card"><div class="card-title">Pending Approval Requests</div>';
if (data.pending_requests && data.pending_requests.length) {
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">ID: ' + req.change_id + ' | Risk: ' + req.risk + '</div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin-top:4px;">' + (req.reason || '') + '</div>';
html += '</div>';
clearNode(el);
const row = div('row');
const cols = [['Pending Requests', data.pending_count], ['Approved', data.approved_count], ['Rejected', data.rejected_count]];
for (const [label, value] of cols) {
const col = div('col'); const card = div('card');
const t = div('card-title'); t.textContent = label;
const val = div('card-value'); val.textContent = value;
card.appendChild(t); card.appendChild(val); col.appendChild(card); row.appendChild(col);
}
el.appendChild(row);
const listCard = div('card');
const lt = div('card-title'); lt.textContent = 'Pending Approval Requests'; listCard.appendChild(lt);
const reqs = data.pending_requests || [];
if (reqs.length) {
for (const req of reqs) {
const item = div('command-item');
const name = div('name'); name.textContent = req.title || req.change_id || '';
const meta = div('meta'); meta.textContent = 'ID: ' + (req.change_id || '') + ' | Risk: ' + (req.risk || '');
const reason = div(''); reason.style.fontSize = '13px'; reason.style.color = '#c9d1d9'; reason.style.marginTop = '4px';
reason.textContent = req.reason || '';
item.appendChild(name); item.appendChild(meta); item.appendChild(reason);
listCard.appendChild(item);
}
} else {
html += '<div style="color:#8b949e;padding:12px;">No pending requests.</div>';
const none = div(''); none.style.color = '#8b949e'; none.style.padding = '12px';
none.textContent = 'No pending requests.'; listCard.appendChild(none);
}
html += '</div>';
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
el.appendChild(listCard);
} catch (e) {
const err = div('error'); err.textContent = 'Failed to load: ' + e.message; el.appendChild(err);
}
}
// Real governed approve: posts the decision to the server-side governed endpoint.
async function approveDecision(changeId, decision, btn) {
if (btn) btn.disabled = true;
try {
const res = await fetch('/admin/ai-evolution/api/owner-gate/decision', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken() },
body: JSON.stringify({ change_id: changeId, decision: decision })
});
const payload = await res.json();
if (!res.ok) throw new Error(payload.error || ('HTTP ' + res.status));
await loadApprove();
} catch (e) {
if (btn) btn.disabled = false;
const out = document.getElementById('arch-approve-content');
const err = div('error'); err.textContent = 'Decision failed: ' + e.message;
if (out) out.appendChild(err);
}
}
async function loadApprove() {
const el = document.getElementById('arch-approve-content');
el.innerHTML = '<div class="loading">Loading pending approvals...</div>';
clearNode(el);
const loading = div('loading'); loading.textContent = 'Loading pending approvals...'; el.appendChild(loading);
try {
const r = await fetch('/admin/ai-evolution/api/owner-gate');
const data = await r.json();
let html = '';
if (data.pending_requests && data.pending_requests.length) {
html += '<div class="card"><div class="card-title">Pending changes requiring your approval</div>';
for (const req of data.pending_requests) {
html += '<div class="command-item">';
html += '<div class="name">' + (req.title || req.change_id) + '</div>';
html += '<div class="meta">Risk: <strong>' + req.risk + '</strong></div>';
html += '<div style="font-size:13px;color:#c9d1d9;margin:8px 0;">' + (req.reason || '') + '</div>';
html += '<div style="display:flex;gap:8px;">';
html += '<button onclick="ownerAction.classList.add('\'approved\');this.disabled=true" style="padding:6px 16px;background:#238636;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">✓ Approve</button>';
html += '</div></div>';
clearNode(el);
const reqs = data.pending_requests || [];
if (reqs.length) {
const card = div('card');
const t = div('card-title'); t.textContent = 'Pending changes requiring your approval'; card.appendChild(t);
for (const req of reqs) {
const item = div('command-item');
const name = div('name'); name.textContent = req.title || req.change_id || '';
const meta = div('meta');
const strongRisk = document.createElement('strong'); strongRisk.textContent = req.risk || '';
meta.appendChild(document.createTextNode('Risk: ')); meta.appendChild(strongRisk);
const reason = div(''); reason.style.fontSize = '13px'; reason.style.color = '#c9d1d9'; reason.style.margin = '8px 0';
reason.textContent = req.reason || '';
const actions = div(''); actions.style.display = 'flex'; actions.style.gap = '8px';
const btn = document.createElement('button');
btn.textContent = '✓ Approve';
btn.style.padding = '6px 16px'; btn.style.background = '#238636'; btn.style.color = '#fff';
btn.style.border = 'none'; btn.style.borderRadius = '6px'; btn.style.cursor = 'pointer'; btn.style.fontSize = '13px';
const changeId = req.change_id;
btn.addEventListener('click', function () { approveDecision(changeId, 'approved', btn); });
actions.appendChild(btn);
item.appendChild(name); item.appendChild(meta); item.appendChild(reason); item.appendChild(actions);
card.appendChild(item);
}
html += '</div>';
el.appendChild(card);
} else {
html += '<div class="card"><div style="padding:24px;text-align:center;color:#8b949e;">✅ All changes processed. No pending approval requests.</div></div>';
const done = div('card');
const inner = div(''); inner.style.padding = '24px'; inner.style.textAlign = 'center'; inner.style.color = '#8b949e';
inner.textContent = '✅ All changes processed. No pending approval requests.';
done.appendChild(inner); el.appendChild(done);
}
el.innerHTML = html;
} catch(e) {
el.innerHTML = '<div class="error">Failed to load: ' + e.message + '</div>';
} catch (e) {
const err = div('error'); err.textContent = 'Failed to load: ' + e.message; el.appendChild(err);
}
}
function showPanel(name) {
document.querySelectorAll('.panel').forEach(p => p.style.display = 'none');
document.getElementById('panel-' + name).style.display = '';
document.querySelectorAll('.sidebar-nav li').forEach(li => li.classList.remove('active'));
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4}[name] || 1) + ')').classList.add('active');
if (name === 'overview') {
document.getElementById('panel-overview').innerHTML = '<div class="main-header"><h2>System Overview</h2><div id="header-status" class="status-badge">Loading...</div></div><div id="loading" class="loading">Loading...</div><div id="error" class="error" style="display:none;"></div><div id="content"><div class="row"><div class="col"><div class="card"><div class="card-title">Current Baseline</div><div id="base" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Git Branch</div><div id="branch" class="card-value">-</div></div></div><div class="col"><div class="card"><div class="card-title">Uptime</div><div id="uptime" class="card-value">-</div></div></div></div><div class="card" style="margin-top:16px"><div class="card-title">Event Bus Status</div><pre id="bus-status">-</pre></div></div>';
fetchData();
}
document.querySelector('.sidebar-nav li:nth-child(' + ({overview:1,versions:2,commands:3,agents:4,['arch-owner']:5,['arch-approve']:6}[name] || 1) + ')').classList.add('active');
if (name === 'overview') fetchData();
const loaders = {
'arch-owner': loadOwnerGate,
'arch-approve': loadApprove,
+25 -1
View File
@@ -12,6 +12,29 @@ from .task_lifecycle import TaskLifecycle
lifecycle = TaskLifecycle()
# Role must come from an authenticated principal, never from the client body.
_ROLE_WHITELIST = {"user", "agent", "executor", "dispatcher", "notifier", "group_admin", "owner"}
_authenticator = None # callable(payload) -> {"role": ...} | None, injected by host
def set_authenticator(fn):
"""Install a server-side authenticator used to derive the actor role.
`fn` receives the raw request payload and must return either None (or a
dict without a whitelisted "role") for anonymous, or a dict containing a
server-verified "role". The client-supplied `payload["role"]` is ignored.
"""
global _authenticator
_authenticator = fn
def effective_role(payload: dict[str, Any]) -> str:
if _authenticator is not None:
principal = _authenticator(payload)
if isinstance(principal, dict) and principal.get("role") in _ROLE_WHITELIST:
return principal["role"]
return "user"
def get_task_actions(task: dict[str, Any], role: str) -> dict[str, Any]:
return {
@@ -31,7 +54,8 @@ def build_router():
@router.post("/{task_id}/actions/{action}", operation_id="executeTaskAction")
def action(task_id: str, action: str, payload: dict[str, Any]):
return execute_task_action(payload.get("task", {}), action, payload.get("role", "user"), payload.get("reason"))
# Role is derived from the authenticated principal; body role is ignored.
return execute_task_action(payload.get("task", {}), action, effective_role(payload), payload.get("reason"))
return router
+386 -26
View File
@@ -8,6 +8,7 @@ import time
import uuid
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from datetime import datetime, timezone
from typing import Any, Callable
from .delegation import DelegationEngine
@@ -23,6 +24,8 @@ from .errors import (
from .evidence import EvidenceService
from .journal import TaskJournal
from .lease import LeaseManager
from .lineage_store import TaskLineagePolicy, TaskPlaneLineageCoordinator
from .mission import MissionAuthority
from .models import (
EvidenceEnvelope,
ResultEnvelope,
@@ -33,6 +36,11 @@ from .models import (
validate_identifier,
)
from .policy import PolicyEngine
from .root_action_consumer import (
RootActionConsumer,
RootActionExecutor,
RootActionRunResult,
)
from .store import TaskPlaneStore
from .worker_registry import WorkerRegistry
@@ -53,7 +61,15 @@ def _bounded_int(value: Any, name: str, minimum: int, maximum: int) -> int:
class TaskPlaneService:
def __init__(self, store: TaskPlaneStore, policy: PolicyEngine | None = None):
def __init__(
self,
store: TaskPlaneStore,
policy: PolicyEngine | None = None,
*,
lineage_policy: TaskLineagePolicy | None = None,
mission_authority: MissionAuthority | None = None,
root_action_executor: RootActionExecutor | None = None,
):
self.store = store
self.policy = policy or PolicyEngine(
execution_enabled=False,
@@ -66,30 +82,257 @@ class TaskPlaneService:
self.workers = WorkerRegistry(store)
self.evidence = EvidenceService(store)
self.delegation = DelegationEngine(store)
if mission_authority is not None and mission_authority.store is not store:
raise ValueError("mission authority must use the TaskPlane store")
self.missions = mission_authority
effective_lineage_policy = lineage_policy
if mission_authority is not None:
effective_lineage_policy = (
lineage_policy
or TaskLineagePolicy(
principal_bindings=(), acceptance_evaluators={}
)
).with_acceptance_evaluator_resolver(
mission_authority.resolve_acceptance_evaluator
)
self.lineage = (
TaskPlaneLineageCoordinator(store, effective_lineage_policy)
if effective_lineage_policy is not None
else None
)
self.root_actions = (
RootActionConsumer(
store,
plan_builder=lambda review: mission_authority.build_root_action_plan(
review=review, lineage=self.lineage
),
rereviewer=lambda plan, receipt: mission_authority.rereview_root_action(
plan=plan,
execution_receipt=receipt,
lineage=self.lineage,
),
executor=root_action_executor,
)
if mission_authority is not None and self.lineage is not None
else None
)
self.store.probe_attestation_verifier = self.policy.probe_attestation_verifier
def create_task(self, body: dict[str, Any], idempotency_key: str | None = None) -> dict[str, Any]:
def consume_one_root_action(
self,
*,
consumer_id: str,
now: datetime | None = None,
lease_seconds: int = 60,
) -> RootActionRunResult:
if self.root_actions is None:
raise ForbiddenError("root action consumer is not configured")
return self.root_actions.run_once(
consumer_id=consumer_id,
now=now or datetime.now(tz=timezone.utc),
lease_seconds=lease_seconds,
)
def create_mission(
self,
body: dict[str, Any],
idempotency_key: str | None,
*,
actor_identity: str | None,
) -> dict[str, Any]:
if self.missions is None or self.lineage is None:
raise ForbiddenError("mission authority is not configured")
if actor_identity is None:
raise ForbiddenError("mission creation requires authenticated identity")
if set(body) != {"goal_slug", "original_acceptance"}:
raise ValidationError(
"mission body requires only goal_slug and original_acceptance"
)
self.policy.assert_allowed("create", actor_identity)
mission = self.missions.create_mission(
goal_slug=body["goal_slug"],
original_acceptance=body["original_acceptance"],
idempotency_key=idempotency_key,
authenticated_identity=actor_identity,
lineage=self.lineage,
)
return {
**mission.to_dict(),
"root_task": self.store.get_task(mission.root_task_id).to_dict(),
}
def get_mission(
self, mission_id: str, *, actor_identity: str | None
) -> dict[str, Any]:
if self.missions is None:
raise ForbiddenError("mission authority is not configured")
if actor_identity is None:
raise ForbiddenError("mission read requires authenticated identity")
self.policy.assert_allowed("read", actor_identity)
mission = self.missions.get_mission(
mission_id, authenticated_identity=actor_identity
)
return {
**mission.to_dict(),
"root_task": self.store.get_task(mission.root_task_id).to_dict(),
}
def delegate_mission(
self,
parent_task_id: str,
body: dict[str, Any],
idempotency_key: str | None,
*,
actor_identity: str | None,
) -> dict[str, Any]:
"""Admit one server-identified child under an owned canonical Mission."""
if self.missions is None or self.lineage is None:
raise ForbiddenError("mission authority is not configured")
if actor_identity is None:
raise ForbiddenError("mission delegation requires authenticated identity")
# Mission delegation is an admission operation. It deliberately does
# not enable the worker execution actions guarded by execution_enabled.
self.policy.assert_allowed("create", actor_identity)
granted = set(
self.policy.identity_capabilities.get(
actor_identity,
self.policy.identity_capabilities.get("*", set()),
)
)
if "task.delegate" not in granted:
raise ForbiddenError("action delegate not permitted")
child = self.missions.delegate_child(
parent_task_id=parent_task_id,
body=body,
idempotency_key=idempotency_key,
authenticated_identity=actor_identity,
lineage=self.lineage,
delegation=self.delegation,
)
return child.to_dict()
def attach_legacy_attachment(
self,
mission_id: str,
body: dict[str, Any],
idempotency_key: str | None,
*,
actor_identity: str | None,
) -> dict[str, Any]:
if self.missions is None:
raise ForbiddenError("mission authority is not configured")
if actor_identity is None:
raise ForbiddenError("legacy attachment requires authenticated identity")
self.policy.assert_allowed("create", actor_identity)
granted = set(
self.policy.identity_capabilities.get(
actor_identity, self.policy.identity_capabilities.get("*", set())
)
)
if "task.delegate" not in granted:
raise ForbiddenError("action delegate not permitted")
return self.missions.attach_legacy(
mission_id=mission_id,
body=body,
idempotency_key=idempotency_key,
authenticated_identity=actor_identity,
)
def get_legacy_attachment(
self,
mission_id: str,
attachment_id: str,
*,
actor_identity: str | None,
) -> dict[str, Any]:
if self.missions is None:
raise ForbiddenError("mission authority is not configured")
if actor_identity is None:
raise ForbiddenError("legacy attachment read requires authenticated identity")
self.policy.assert_allowed("read", actor_identity)
return self.missions.get_legacy_attachment(
mission_id=mission_id,
attachment_id=attachment_id,
authenticated_identity=actor_identity,
)
def create_task(
self,
body: dict[str, Any],
idempotency_key: str | None = None,
*,
actor_identity: str | None = None,
) -> dict[str, Any]:
if body.get("parent_task_id"):
raise ForbiddenError("child tasks must use the delegate endpoint")
issuer = body.get("issuer", {})
actor = issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
if actor_identity is not None:
if issuer and (
not isinstance(issuer, dict) or issuer.get("id") != actor_identity
):
raise ForbiddenError("issuer identity mismatch")
body = dict(body)
body["issuer"] = {"type": "authenticated", "id": actor_identity}
actor = actor_identity or (
issuer.get("id", "anon") if isinstance(issuer, dict) else "anon"
)
self.policy.assert_allowed("create", actor)
try:
plan = TaskPlan.from_dict(body)
except (TypeError, ValueError) as exc:
raise ValidationError("invalid task plan") from exc
task = self.store.create_task_with_event(plan, idempotency_key=idempotency_key)
if self.lineage is not None:
if actor_identity is None:
raise ForbiddenError("lineage admission requires authenticated identity")
task = self.lineage.create_root(
plan=plan,
authenticated_identity=actor_identity,
request_digest=self.lineage._request_digest(body),
idempotency_key=idempotency_key,
)
else:
task = self.store.create_task_with_event(
plan, idempotency_key=idempotency_key
)
return task.to_dict()
def _can_read_mission_task(self, task, actor: str | None) -> bool:
# Every service sharing the store must honor persisted Mission ownership,
# including a legacy launcher that has no Mission authority configured.
if self.missions is not None:
return self.missions.can_read_task(
task, authenticated_identity=actor or ""
)
with self.store._lock:
return self.store.conn.execute(
"SELECT 1 FROM task_missions WHERE root_task_id=?",
(task.root_task_id,),
).fetchone() is None
def _assert_mission_read(self, task, actor: str | None) -> None:
if not self._can_read_mission_task(task, actor):
raise ForbiddenError("mission task requires its configured owner authority")
def list_tasks(self, actor: str | None = None) -> list[dict[str, Any]]:
if actor is not None:
self.policy.assert_allowed("read", actor)
return [task.to_dict(include_task=False) for task in self.store.list_tasks()]
tasks = self.store.list_tasks()
tasks = [task for task in tasks if self._can_read_mission_task(task, actor)]
return [task.to_dict(include_task=False) for task in tasks]
def get_task(self, task_id: str, actor: str | None = None) -> dict[str, Any]:
task = self.store.get_task(task_id)
self._assert_mission_read(task, actor)
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
return self.store.get_task(task_id).to_dict()
self.policy.assert_allowed("read", actor, task=task)
if self.missions is not None and self.missions.manages_root(
task.root_task_id
):
self.missions.assert_task_read(
task, authenticated_identity=actor
)
return task.to_dict()
def get_worker(self, worker_id: str, actor: str | None = None) -> dict[str, Any]:
if actor is not None:
@@ -307,17 +550,51 @@ class TaskPlaneService:
)
evidence_entries.append(envelope)
previous = envelope.evidence_digest
self.store.complete_claim_with_evidence_and_event(
task_id,
claim_id,
final_status = (
status
if status
in {"COMPLETED", "FAILED_FINAL", "FAILED_RECOVERABLE", "CANCELLED"}
else "COMPLETED",
result.to_dict(),
evidence_entries,
else "COMPLETED"
)
return result.to_dict()
lineage_metadata = task.task_plan.metadata.get("aios_task_lineage")
is_lineage_root = bool(
self.lineage is not None
and isinstance(lineage_metadata, dict)
and lineage_metadata.get("relation_type") == "ROOT"
)
if is_lineage_root and final_status == "COMPLETED":
raise ForbiddenError("lineage root completion requires root acceptance review")
review_outcome = None
with self.store._transaction():
self.store.complete_claim_with_evidence_and_event(
task_id,
claim_id,
final_status,
result.to_dict(),
evidence_entries,
)
is_lineage_child = bool(
self.lineage is not None
and isinstance(lineage_metadata, dict)
and lineage_metadata.get("relation_type") != "ROOT"
)
if is_lineage_child and final_status in {"COMPLETED", "FAILED_FINAL", "CANCELLED"}:
review_outcome = self.lineage.review_proven_terminal_child(
child_task_id=task_id,
claim_id=str(claim_id),
result_digest=str(result.result_digest),
evidence_refs=[entry.evidence_id for entry in evidence_entries],
)
response = result.to_dict()
if review_outcome is not None:
response["root_review"] = {
"review_id": review_outcome.review.review_id,
"root_task_id": review_outcome.review.root_task_id,
"root_complete": review_outcome.review.root_complete,
"next_legal_action": review_outcome.review.next_legal_action.value,
"dispatch_status": review_outcome.dispatch.status,
}
return response
def fail(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
claim_id = body.get("claim_id")
@@ -346,14 +623,38 @@ class TaskPlaneService:
)
return updated.to_dict()
def delegate(self, task_id: str, body: dict[str, Any]) -> dict[str, Any]:
def delegate(
self,
task_id: str,
body: dict[str, Any],
idempotency_key: str | None = None,
) -> dict[str, Any]:
actor = body.get("actor_identity") or "owner"
self.policy.assert_allowed("delegate", actor)
parent = self.store.get_task(task_id)
mission_managed = bool(
self.lineage is not None
and self.missions is not None
and self.missions.manages_root(parent.root_task_id)
)
if mission_managed:
plan_body = body.get("task") if "task" in body else {
key: value
for key, value in body.items()
if key != "actor_identity"
}
return self.delegate_mission(
task_id,
plan_body,
idempotency_key,
actor_identity=str(actor),
)
self.policy.assert_allowed("delegate", actor)
self.policy.assert_allowed("delegate", actor, task=parent)
if "task" in body:
plan_body = body["task"]
try:
child_plan = TaskPlan.from_dict(body["task"])
child_plan = TaskPlan.from_dict(plan_body)
except (TypeError, ValueError) as exc:
raise ValidationError("invalid child task plan") from exc
else:
@@ -364,25 +665,67 @@ class TaskPlaneService:
except (TypeError, ValueError) as exc:
raise ValidationError("invalid child task plan") from exc
child_plan.parent_task_id = task_id
child = self.delegation.delegate(task_id, child_plan, caller=actor)
if self.lineage is not None:
if not isinstance(plan_body, dict):
raise ValidationError("invalid child task plan")
if "task_id" not in plan_body:
raise ValidationError("lineage delegation requires a stable task_id")
plan_body = dict(plan_body)
issuer = plan_body.get("issuer")
if issuer and (
not isinstance(issuer, dict) or issuer.get("id") != actor
):
raise ForbiddenError("child issuer identity mismatch")
plan_body["issuer"] = {"type": "authenticated", "id": actor}
child_plan.issuer = dict(plan_body["issuer"])
child = self.lineage.delegate_child(
parent_task_id=task_id,
plan=child_plan,
authenticated_identity=str(actor),
request_digest=self.lineage._request_digest(plan_body),
delegation=self.delegation,
)
else:
child = self.delegation.delegate(task_id, child_plan, caller=actor)
return child.to_dict()
def get_events(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
task = self.store.get_task(task_id)
self._assert_mission_read(task, actor)
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
self.policy.assert_allowed("audit", actor, task=task)
if self.missions is not None and self.missions.manages_root(
task.root_task_id
):
self.missions.assert_task_read(
task, authenticated_identity=actor
)
return self.store.list_events(task_id)
def get_evidence(self, task_id: str, actor: str | None = None) -> list[dict[str, Any]]:
task = self.store.get_task(task_id)
self._assert_mission_read(task, actor)
if actor is not None:
self.policy.assert_allowed("audit", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
self.policy.assert_allowed("audit", actor, task=task)
if self.missions is not None and self.missions.manages_root(
task.root_task_id
):
self.missions.assert_task_read(
task, authenticated_identity=actor
)
return self.evidence.list(task_id)
def get_children(self, task_id: str, actor: str | None = None) -> list[str]:
task = self.store.get_task(task_id)
self._assert_mission_read(task, actor)
if actor is not None:
self.policy.assert_allowed("read", actor, task=self.store.get_task(task_id))
self.store.get_task(task_id)
self.policy.assert_allowed("read", actor, task=task)
if self.missions is not None and self.missions.manages_root(
task.root_task_id
):
self.missions.assert_task_read(
task, authenticated_identity=actor
)
return self.store.list_children(task_id)
@@ -474,6 +817,10 @@ class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
if len(parts) == 1 and parts[0] == "tasks":
return 200, self.service.list_tasks(identity)
if len(parts) == 2 and parts[0] == "missions":
return 200, self.service.get_mission(
parts[1], actor_identity=identity
)
if len(parts) == 2 and parts[0] == "tasks":
task_id = parts[1]
return 200, self.service.get_task(task_id, identity)
@@ -499,6 +846,13 @@ class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
parts = [p for p in path.split("/") if p]
headers = {k.lower(): v for k, v in self.headers.items()}
if len(parts) == 1 and parts[0] == "missions":
return 201, self.service.create_mission(
body or {},
headers.get("idempotency-key"),
actor_identity=identity,
)
if len(parts) == 1 and parts[0] == "tasks":
issuer = body.get("issuer")
if issuer and (
@@ -506,7 +860,11 @@ class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
):
raise ForbiddenError("issuer identity mismatch")
body["issuer"] = {"type": "authenticated", "id": identity}
return 201, self.service.create_task(body or {}, headers.get("idempotency-key"))
return 201, self.service.create_task(
body or {},
headers.get("idempotency-key"),
actor_identity=identity,
)
if body.get("actor_identity") not in {None, identity}:
raise ForbiddenError("actor identity mismatch")
body["actor_identity"] = identity
@@ -523,7 +881,9 @@ class TaskPlaneRequestHandler(BaseHTTPRequestHandler):
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "cancel":
return 200, self.service.cancel(parts[1], body or {})
if len(parts) == 3 and parts[0] == "tasks" and parts[2] == "delegate":
return 200, self.service.delegate(parts[1], body or {})
return 200, self.service.delegate(
parts[1], body or {}, headers.get("idempotency-key")
)
if len(parts) == 2 and parts[0] == "workers" and parts[1] == "register":
if (
@@ -0,0 +1,94 @@
"""Agent discovery/claim coordinator over diagnostic schedulability evidence."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping
from .schedulability import (
DiscoveryDiagnostics,
SchedulabilityProof,
SchedulabilityService,
)
@dataclass(frozen=True)
class DiscoveryResult:
status: str
proof: SchedulabilityProof | None
blocker_codes: tuple[str, ...] = ()
candidate_count: int = 0
def to_dict(self) -> dict[str, Any]:
value: dict[str, Any] = {
"status": self.status,
"blocker_codes": list(self.blocker_codes),
"candidate_count": self.candidate_count,
"proof": None,
}
if self.proof is not None:
value["proof"] = {
"schema": self.proof.schema,
"proof_id": self.proof.proof_id,
"task_id": self.proof.task_id,
"worker_id": self.proof.worker_id,
"created_at": self.proof.created_at,
"expires_at": self.proof.expires_at,
"blockers": [b.to_dict() for b in self.proof.blockers],
"proof_digest": self.proof.proof_digest,
}
return value
def _waiting_status(diag: DiscoveryDiagnostics) -> str:
codes = set(diag.blocker_codes)
if diag.candidate_count == 0:
return "NO_CANDIDATES"
if not codes:
return "NO_CLAIMABLE_TASK"
if codes <= {"CAPACITY_EXHAUSTED"}:
return "WAITING_CAPACITY"
if "AUTHORIZATION_BLOCKED" in codes or "EXACT_SCOPE_MISMATCH" in codes:
return "WAITING_AUTHORIZATION"
if "DEPENDENCY_BLOCKED" in codes:
return "WAITING_DEPENDENCY"
if "FREEZE_BLOCKED" in codes:
return "WAITING_FREEZE"
if "CONFLICTING_LEASE" in codes:
return "WAITING_LEASE"
if {"STALE_OR_INACTIVE_WORKER", "MISSING_CAPABILITY", "INVALID_AUTHORITY_VALUE"} & codes:
return "WORKER_NOT_ELIGIBLE"
if "TASK_STATE_NOT_CLAIMABLE" in codes:
return "NO_CLAIMABLE_TASK"
return "BLOCKED"
class AutoClaimCoordinator:
"""No implicit credential derivation; authenticated identity is supplied by host."""
def __init__(self, schedulability: SchedulabilityService) -> None:
self.schedulability = schedulability
def discover(self, *, worker_id: str, actor_identity: str) -> DiscoveryResult:
proof, diag = self.schedulability.discover_with_diagnostics(
worker_id=worker_id, actor_identity=actor_identity
)
if proof is not None:
return DiscoveryResult(
"CLAIMABLE", proof, diag.blocker_codes, diag.candidate_count
)
return DiscoveryResult(
_waiting_status(diag), None, diag.blocker_codes, diag.candidate_count
)
def claim(
self,
*,
proof_id: str,
worker_id: str,
actor_identity: str,
lease_seconds: int = 30,
) -> Mapping[str, Any]:
return self.schedulability.claim_with_recheck(
proof_id=proof_id,
worker_id=worker_id,
actor_identity=actor_identity,
lease_seconds=lease_seconds,
)
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -31,7 +31,16 @@ class DelegationEngine:
current = self.store.parent_of(current)
return True
def delegate(self, parent_id: str, child_plan: TaskPlan, *, caller: str) -> TaskRecord:
def delegate(
self,
parent_id: str,
child_plan: TaskPlan,
*,
caller: str,
owner_principal_id: str = "",
mission_id: str = "",
created_at: float | None = None,
) -> TaskRecord:
self.can_delegate(parent_id, child_plan)
parent = self.store.get_task(parent_id)
if child_plan.required_capabilities and not set(child_plan.required_capabilities).issubset(
@@ -80,7 +89,12 @@ class DelegationEngine:
raise ConflictError("delegation task_id conflict")
return self.store.delegate_task_with_budget_and_events(
parent_id, child_plan, caller=caller
parent_id,
child_plan,
caller=caller,
owner_principal_id=owner_principal_id,
mission_id=mission_id,
created_at=created_at,
)
def _depth(self, task_id: str) -> int:
@@ -0,0 +1,536 @@
"""Mission-owned, append-only bindings for verified external legacy executions."""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Mapping, Protocol, Sequence
from .errors import ConflictError, NotFoundError, ValidationError
from .models import compute_sha256_hex
ATTACH_REQUEST_SCHEMA = "aios.mission-legacy-attach-request.v1"
ATTACH_EVIDENCE_SCHEMA = "aios.verified-legacy-attachment-evidence.v1"
ATTACH_RECEIPT_SCHEMA = "aios.mission-legacy-attachment-receipt.v1"
ATTACHMENT_KIND = "LEGACY_EXECUTION_SUBJECT"
RELATION_TYPE = "RECOVERY"
SOURCE_KIND = "agent-task-bridge-job"
REQUIRED_READBACK_ROLES = frozenset(
{"agent_task", "admission", "owner_registry", "bridge_job"}
)
REQUIRED_C04_EVIDENCE = frozenset(
{
"verified legacy ownership and acceptance relevance",
"unchanged task/job/idempotency/reconciliation identity",
"authoritative attach readback",
}
)
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
def _canonical(value: object) -> str:
try:
return json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
)
except (TypeError, ValueError) as exc:
raise ValidationError("legacy attachment value must be JSON data") from exc
def _object(value: object, fields: frozenset[str], name: str) -> dict[str, object]:
if not isinstance(value, Mapping) or set(value) != fields:
raise ValidationError(f"invalid {name} fields")
return json.loads(_canonical(dict(value)))
def _identifier(value: object, name: str, *, maximum: int = 256) -> str:
if (
not isinstance(value, str)
or not value
or len(value) > maximum
or any(ord(character) < 33 for character in value)
):
raise ValidationError(f"invalid {name}")
return value
def _integer(value: object, name: str) -> int:
if type(value) is not int or value < 0:
raise ValidationError(f"invalid {name}")
return value
def _utc(value: object, name: str) -> str:
if not isinstance(value, str) or not value:
raise ValidationError(f"invalid {name}")
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError as exc:
raise ValidationError(f"invalid {name}") from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ValidationError(f"invalid {name}")
return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
@dataclass(frozen=True)
class AuthorityReadbackRef:
"""A non-secret pointer and digest issued by one independent authority."""
role: str
authority: str
readback_ref: str
canonical_sha256: str
observed_at: str
revision: int | None = None
def to_dict(self) -> dict[str, object]:
if self.role not in REQUIRED_READBACK_ROLES:
raise ValidationError("invalid legacy authority readback role")
authority = _identifier(self.authority, "readback authority")
reference = _identifier(self.readback_ref, "readback ref", maximum=1024)
if not _SHA256_RE.fullmatch(self.canonical_sha256):
raise ValidationError("invalid readback canonical SHA-256")
observed = _utc(self.observed_at, "readback observed_at")
revision = self.revision
if revision is not None:
revision = _integer(revision, "readback revision")
return {
"role": self.role,
"authority": authority,
"readback_ref": reference,
"canonical_sha256": self.canonical_sha256,
"observed_at": observed,
"revision": revision,
}
@dataclass(frozen=True)
class VerifiedLegacyAttachmentEvidence:
"""Typed output of a configured authority reader, never a caller payload."""
schema: str
source_kind: str
source_task_id: str
source_task_created_at: str
source_task_status: str
source_task_revision: int
source_task_attempt: int
source_task_max_attempts: int
external_job_id: str
internal_job_id: str
internal_idempotency_key: str
job_branch: str
job_state: str
job_revision: int
side_effect_state: str
grant_id: str
grant_digest: str
execution_digest: str
epoch_id: str
epoch_digest: str
historical_signer_id: str
historical_signer_fingerprint: str
registry_generation: int
signature_verified: bool
epoch_currently_valid: bool
readbacks: Sequence[AuthorityReadbackRef]
def to_dict(self) -> dict[str, object]:
if self.schema != ATTACH_EVIDENCE_SCHEMA or self.source_kind != SOURCE_KIND:
raise ValidationError("invalid legacy attachment evidence schema")
if self.signature_verified is not True or self.epoch_currently_valid is not False:
raise ConflictError("legacy historical Owner evidence is not verified-expired")
values = {
"source_task_id": self.source_task_id,
"source_task_status": self.source_task_status,
"external_job_id": self.external_job_id,
"internal_job_id": self.internal_job_id,
"internal_idempotency_key": self.internal_idempotency_key,
"job_branch": self.job_branch,
"job_state": self.job_state,
"side_effect_state": self.side_effect_state,
"grant_id": self.grant_id,
"historical_signer_id": self.historical_signer_id,
"historical_signer_fingerprint": self.historical_signer_fingerprint,
"epoch_id": self.epoch_id,
}
normalized = {
key: _identifier(value, key, maximum=1024)
for key, value in values.items()
}
for name, value in {
"grant_digest": self.grant_digest,
"execution_digest": self.execution_digest,
"epoch_digest": self.epoch_digest,
}.items():
if not _SHA256_RE.fullmatch(value):
raise ValidationError(f"invalid {name}")
normalized[name] = value
task_revision = _integer(self.source_task_revision, "source_task_revision")
task_attempt = _integer(self.source_task_attempt, "source_task_attempt")
task_max_attempts = _integer(
self.source_task_max_attempts, "source_task_max_attempts"
)
if task_attempt > task_max_attempts:
raise ConflictError("legacy task attempt exceeds max attempts")
job_revision = _integer(self.job_revision, "job_revision")
registry_generation = _integer(
self.registry_generation, "registry_generation"
)
readbacks = [value.to_dict() for value in self.readbacks]
roles = [str(value["role"]) for value in readbacks]
if len(roles) != len(set(roles)) or set(roles) != REQUIRED_READBACK_ROLES:
raise ValidationError("legacy evidence requires four authority readbacks")
readbacks.sort(key=lambda value: str(value["role"]))
return {
"schema": ATTACH_EVIDENCE_SCHEMA,
"source_kind": SOURCE_KIND,
**normalized,
"source_task_created_at": _utc(
self.source_task_created_at, "source_task_created_at"
),
"source_task_revision": task_revision,
"source_task_attempt": task_attempt,
"source_task_max_attempts": task_max_attempts,
"job_revision": job_revision,
"registry_generation": registry_generation,
"signature_verified": True,
"epoch_currently_valid": False,
"readbacks": readbacks,
}
class LegacyAttachmentEvidenceProvider(Protocol):
"""Configured server-side reader for the four independent authority sources."""
def read_verified(
self,
*,
mission_id: str,
root_task_id: str,
anchor_task_id: str,
source_kind: str,
source_task_id: str,
external_job_id: str,
original_acceptance_digest: str,
) -> VerifiedLegacyAttachmentEvidence: ...
def normalize_attach_request(value: object) -> dict[str, object]:
body = _object(
value,
frozenset({"schema", "anchor_task_id", "source", "expected"}),
"legacy attachment request",
)
if body["schema"] != ATTACH_REQUEST_SCHEMA:
raise ValidationError("invalid legacy attachment request schema")
source = _object(
body["source"],
frozenset({"kind", "task_id", "external_job_id"}),
"legacy attachment source",
)
if source["kind"] != SOURCE_KIND:
raise ValidationError("invalid legacy attachment source kind")
expected = _object(
body["expected"],
frozenset(
{
"root_revision",
"anchor_revision",
"agent_task_revision",
"agent_task_attempt",
"internal_job_id",
"job_revision",
"internal_idempotency_key",
}
),
"legacy attachment expected",
)
return {
"schema": ATTACH_REQUEST_SCHEMA,
"anchor_task_id": _identifier(body["anchor_task_id"], "anchor_task_id"),
"source": {
"kind": SOURCE_KIND,
"task_id": _identifier(source["task_id"], "source task_id"),
"external_job_id": _identifier(
source["external_job_id"], "external_job_id"
),
},
"expected": {
"root_revision": _integer(expected["root_revision"], "root_revision"),
"anchor_revision": _integer(
expected["anchor_revision"], "anchor_revision"
),
"agent_task_revision": _integer(
expected["agent_task_revision"], "agent_task_revision"
),
"agent_task_attempt": _integer(
expected["agent_task_attempt"], "agent_task_attempt"
),
"internal_job_id": _identifier(
expected["internal_job_id"], "internal_job_id"
),
"job_revision": _integer(expected["job_revision"], "job_revision"),
"internal_idempotency_key": _identifier(
expected["internal_idempotency_key"], "internal_idempotency_key"
),
},
}
def validate_c04_acceptance(
acceptance: Mapping[str, object], *, source_task_id: str, external_job_id: str
) -> None:
candidate = acceptance.get("legacy_candidate")
if (
not isinstance(candidate, Mapping)
or candidate.get("task_id") != source_task_id
or candidate.get("job_id") != external_job_id
):
raise ConflictError("mission legacy candidate authority mismatch")
conditions = acceptance.get("conditions")
if not isinstance(conditions, list):
raise ConflictError("mission C04 acceptance condition is unavailable")
matches = [
condition
for condition in conditions
if isinstance(condition, Mapping) and condition.get("key") == "C04"
]
if len(matches) != 1:
raise ConflictError("mission C04 acceptance condition is unavailable")
condition = matches[0]
evidence = condition.get("required_evidence")
if (
condition.get("purpose") != "LEGACY-LINEAGE-MIGRATION"
or not isinstance(evidence, list)
or not REQUIRED_C04_EVIDENCE.issubset(
value for value in evidence if isinstance(value, str)
)
):
raise ConflictError("mission C04 acceptance contract mismatch")
def validate_evidence_matches_request(
evidence: Mapping[str, object], request: Mapping[str, object]
) -> None:
source = request["source"]
expected = request["expected"]
assert isinstance(source, Mapping) and isinstance(expected, Mapping)
pairs = {
"source_kind": source["kind"],
"source_task_id": source["task_id"],
"external_job_id": source["external_job_id"],
"source_task_revision": expected["agent_task_revision"],
"source_task_attempt": expected["agent_task_attempt"],
"internal_job_id": expected["internal_job_id"],
"job_revision": expected["job_revision"],
"internal_idempotency_key": expected["internal_idempotency_key"],
}
mismatches = [name for name, value in pairs.items() if evidence.get(name) != value]
if mismatches:
raise ConflictError(
"legacy provider evidence mismatch: " + ",".join(sorted(mismatches))
)
class LegacyAttachmentLedger:
"""Append-only persistence owned by one Mission authority store."""
def __init__(self, store) -> None:
self.store = store
with store._transaction():
store.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_mission_legacy_attachments (
attachment_id TEXT PRIMARY KEY,
mission_id TEXT NOT NULL,
root_task_id TEXT NOT NULL,
anchor_task_id TEXT NOT NULL,
authenticated_identity TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
attachment_kind TEXT NOT NULL,
relation_type TEXT NOT NULL,
attachment_sequence INTEGER NOT NULL,
display_name TEXT NOT NULL,
source_kind TEXT NOT NULL,
source_task_id TEXT NOT NULL,
external_job_id TEXT NOT NULL,
internal_job_id TEXT NOT NULL,
internal_idempotency_key TEXT NOT NULL,
request_digest TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
evidence_digest TEXT NOT NULL,
receipt_digest TEXT NOT NULL,
receipt_json TEXT NOT NULL,
created_at REAL NOT NULL,
UNIQUE (authenticated_identity, idempotency_key),
UNIQUE (source_kind, source_task_id),
UNIQUE (source_kind, external_job_id),
UNIQUE (mission_id, anchor_task_id, attachment_sequence),
UNIQUE (mission_id, anchor_task_id, source_kind, source_task_id, external_job_id)
)
"""
)
store.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_mission_legacy_attachments "
"ON task_mission_legacy_attachments(mission_id, anchor_task_id)"
)
store.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_mission_acceptance_child_bindings (
mission_id TEXT NOT NULL,
condition_key TEXT NOT NULL,
child_task_id TEXT NOT NULL UNIQUE,
original_acceptance_digest TEXT NOT NULL,
binding_digest TEXT NOT NULL,
created_at REAL NOT NULL,
PRIMARY KEY (mission_id, condition_key)
)
"""
)
def acceptance_child_binding(self, mission_id: str, condition_key: str):
return self.store.conn.execute(
"SELECT * FROM task_mission_acceptance_child_bindings "
"WHERE mission_id=? AND condition_key=?",
(mission_id, condition_key),
).fetchone()
def bind_acceptance_child(
self,
*,
mission_id: str,
condition_key: str,
child_task_id: str,
original_acceptance_digest: str,
binding_digest: str,
created_at: float,
) -> None:
existing = self.acceptance_child_binding(mission_id, condition_key)
if existing is not None:
if (
existing["child_task_id"] != child_task_id
or existing["original_acceptance_digest"]
!= original_acceptance_digest
or existing["binding_digest"] != binding_digest
):
raise ConflictError(
"mission acceptance condition already has a canonical child"
)
return
child_binding = self.store.conn.execute(
"SELECT mission_id,condition_key FROM "
"task_mission_acceptance_child_bindings WHERE child_task_id=?",
(child_task_id,),
).fetchone()
if child_binding is not None:
raise ConflictError("mission child already belongs to an acceptance condition")
self.store.conn.execute(
"INSERT INTO task_mission_acceptance_child_bindings "
"(mission_id,condition_key,child_task_id,original_acceptance_digest,"
"binding_digest,created_at) VALUES(?,?,?,?,?,?)",
(
mission_id,
condition_key,
child_task_id,
original_acceptance_digest,
binding_digest,
created_at,
),
)
def by_idempotency(self, identity: str, key: str):
return self.store.conn.execute(
"SELECT * FROM task_mission_legacy_attachments "
"WHERE authenticated_identity=? AND idempotency_key=?",
(identity, key),
).fetchone()
def by_source(self, source_kind: str, task_id: str, external_job_id: str):
return self.store.conn.execute(
"SELECT * FROM task_mission_legacy_attachments "
"WHERE source_kind=? AND (source_task_id=? OR external_job_id=?)",
(source_kind, task_id, external_job_id),
).fetchone()
def by_id(self, mission_id: str, attachment_id: str):
row = self.store.conn.execute(
"SELECT * FROM task_mission_legacy_attachments "
"WHERE mission_id=? AND attachment_id=?",
(mission_id, attachment_id),
).fetchone()
if row is None:
raise NotFoundError("legacy attachment not found")
return row
def next_sequence(self, mission_id: str, anchor_task_id: str) -> int:
row = self.store.conn.execute(
"SELECT COALESCE(MAX(attachment_sequence),0)+1 "
"FROM task_mission_legacy_attachments "
"WHERE mission_id=? AND anchor_task_id=?",
(mission_id, anchor_task_id),
).fetchone()
sequence = int(row[0])
if not 1 <= sequence <= 99:
raise ConflictError("legacy attachment sequence exhausted")
return sequence
def insert(
self,
*,
receipt: Mapping[str, object],
authenticated_identity: str,
idempotency_key: str,
request_digest: str,
evidence_digest: str,
created_at: float,
) -> None:
self.store.conn.execute(
"INSERT INTO task_mission_legacy_attachments "
"(attachment_id,mission_id,root_task_id,anchor_task_id,"
"authenticated_identity,owner_principal_id,attachment_kind,"
"relation_type,attachment_sequence,display_name,source_kind,"
"source_task_id,external_job_id,internal_job_id,"
"internal_idempotency_key,request_digest,idempotency_key,"
"evidence_digest,receipt_digest,receipt_json,created_at) "
"VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
receipt["attachment_id"], receipt["mission_id"],
receipt["root_task_id"], receipt["anchor_task_id"],
authenticated_identity, receipt["owner_principal_id"],
receipt["attachment_kind"], receipt["relation_type"],
receipt["attachment_sequence"], receipt["display_name"],
receipt["source"]["source_kind"],
receipt["source"]["source_task_id"],
receipt["source"]["external_job_id"],
receipt["source"]["internal_job_id"],
receipt["source"]["internal_idempotency_key"],
request_digest, idempotency_key, evidence_digest,
receipt["receipt_digest"], _canonical(receipt), created_at,
),
)
@staticmethod
def receipt(row) -> dict[str, object]:
try:
value = json.loads(row["receipt_json"])
except (TypeError, ValueError, json.JSONDecodeError) as exc:
raise ConflictError("persisted legacy attachment receipt is invalid") from exc
if (
not isinstance(value, dict)
or value.get("schema") != ATTACH_RECEIPT_SCHEMA
or value.get("receipt_digest") != row["receipt_digest"]
):
raise ConflictError("persisted legacy attachment receipt is invalid")
digest_payload = dict(value)
digest_payload.pop("receipt_digest", None)
digest_payload.pop("idempotent", None)
if compute_sha256_hex(digest_payload) != row["receipt_digest"]:
raise ConflictError("persisted legacy attachment receipt digest mismatch")
return value
@@ -0,0 +1,846 @@
"""Actual TaskPlane binding for task identity, lineage and root review.
All writes use the existing :class:`TaskPlaneStore` connection and transaction
fence. Request metadata is never used as an authorization source.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Callable, Mapping, Sequence
if __package__ and "." in __package__: # app.task_plane package import
from ..aios_task_lineage import (
AcceptanceCondition,
AcceptanceEvaluator,
NextLegalAction,
RelationType,
RootAcceptanceSnapshot,
RootReviewRecord,
TaskRecord as LineageTaskRecord,
lineage_from_metadata,
normalize_goal_slug,
root_review_from_record,
)
from ..aios_task_lineage_integration import (
DISPLAY_NAME_METADATA_KEY,
LINEAGE_METADATA_KEY,
LINEAGE_REQUEST_DIGEST_KEY,
NewTaskIdentityRequest,
PERSISTED_DISPATCH_STATUSES,
RootActionDispatchReceipt,
TaskAuthoritySnapshot,
TaskIdentityLineageService,
TerminalReviewOutcome,
_as_task_record,
)
from ..aios_task_lineage_taskplane_adapter import (
AuthenticatedTaskContext,
InProcessTaskPlaneLineageAdapter,
TaskPlaneServiceBindings,
)
from ..aios_task_lineage_hooks import TaskLineageLifecycleHooks
else: # top-level task_plane import used by direct-module test harnesses
from aios_task_lineage import (
AcceptanceCondition,
AcceptanceEvaluator,
NextLegalAction,
RelationType,
RootAcceptanceSnapshot,
RootReviewRecord,
TaskRecord as LineageTaskRecord,
lineage_from_metadata,
normalize_goal_slug,
root_review_from_record,
)
from aios_task_lineage_integration import (
DISPLAY_NAME_METADATA_KEY,
LINEAGE_METADATA_KEY,
LINEAGE_REQUEST_DIGEST_KEY,
NewTaskIdentityRequest,
PERSISTED_DISPATCH_STATUSES,
RootActionDispatchReceipt,
TaskAuthoritySnapshot,
TaskIdentityLineageService,
TerminalReviewOutcome,
_as_task_record,
)
from aios_task_lineage_taskplane_adapter import (
AuthenticatedTaskContext,
InProcessTaskPlaneLineageAdapter,
TaskPlaneServiceBindings,
)
from aios_task_lineage_hooks import TaskLineageLifecycleHooks
from .delegation import DelegationEngine
from .errors import ConflictError, ForbiddenError, NotFoundError, ValidationError
from .models import TaskPlan, TaskRecord, canonical_json, compute_sha256_hex
from .store import TaskPlaneStore
RESERVED_LINEAGE_METADATA_KEYS = frozenset(
{LINEAGE_METADATA_KEY, DISPLAY_NAME_METADATA_KEY, LINEAGE_REQUEST_DIGEST_KEY}
)
PROVEN_TERMINAL_STATES = frozenset({"COMPLETED", "FAILED_FINAL", "CANCELLED"})
@dataclass(frozen=True)
class PrincipalLineageBinding:
"""Server-owned projection of one authenticated service identity."""
authenticated_identity: str
owner_principal_id: str
mission_id: str
preferred_owner_alias: str | None
root_acceptance_ref: str
def __post_init__(self) -> None:
for name in (
"authenticated_identity",
"owner_principal_id",
"mission_id",
"root_acceptance_ref",
):
if not str(getattr(self, name)).strip():
raise ValueError(f"{name} is required")
class TaskLineagePolicy:
"""Immutable service policy for authenticated identity and acceptance."""
def __init__(
self,
*,
principal_bindings: Sequence[PrincipalLineageBinding],
acceptance_evaluators: Mapping[str, AcceptanceEvaluator],
acceptance_evaluator_resolver: Callable[
[str], AcceptanceEvaluator | None
]
| None = None,
) -> None:
self._bindings: dict[str, PrincipalLineageBinding] = {}
for binding in principal_bindings:
if binding.authenticated_identity in self._bindings:
raise ValueError("duplicate authenticated lineage identity")
self._bindings[binding.authenticated_identity] = binding
self._evaluators = dict(acceptance_evaluators)
self._acceptance_evaluator_resolver = acceptance_evaluator_resolver
def with_acceptance_evaluator_resolver(
self, resolver: Callable[[str], AcceptanceEvaluator | None]
) -> "TaskLineagePolicy":
previous = self._acceptance_evaluator_resolver
def combined(reference: str) -> AcceptanceEvaluator | None:
resolved = resolver(reference)
if resolved is not None:
return resolved
return previous(reference) if previous is not None else None
return TaskLineagePolicy(
principal_bindings=tuple(self._bindings.values()),
acceptance_evaluators=self._evaluators,
acceptance_evaluator_resolver=combined,
)
def resolve_principal(self, authenticated_identity: str) -> PrincipalLineageBinding:
try:
return self._bindings[authenticated_identity]
except KeyError as exc:
raise ForbiddenError("authenticated identity has no lineage policy binding") from exc
def _acceptance_evaluator(self, reference: str) -> AcceptanceEvaluator | None:
evaluator = self._evaluators.get(reference)
if evaluator is None and self._acceptance_evaluator_resolver is not None:
evaluator = self._acceptance_evaluator_resolver(reference)
return evaluator
def assert_acceptance_ref_registered(self, reference: str) -> None:
if self._acceptance_evaluator(reference) is None:
raise ForbiddenError("root acceptance policy is not registered")
def evaluate_acceptance(
self,
root: LineageTaskRecord,
descendants: Sequence[LineageTaskRecord],
) -> RootAcceptanceSnapshot:
reference = root.lineage.root_acceptance_ref
evaluator = self._acceptance_evaluator(reference)
if evaluator is None:
raise ForbiddenError("root acceptance policy is not registered")
snapshot = evaluator(root, descendants)
if not isinstance(snapshot, RootAcceptanceSnapshot) or not snapshot.conditions:
return RootAcceptanceSnapshot(
(
AcceptanceCondition(
key="root-acceptance-proof-unavailable",
satisfied=False,
owner_boundary=True,
),
)
)
normalized: list[AcceptanceCondition] = []
for condition in snapshot.conditions:
if condition.satisfied and not condition.evidence_refs:
normalized.append(
AcceptanceCondition(
key=condition.key,
satisfied=False,
owner_boundary=True,
)
)
else:
normalized.append(condition)
return RootAcceptanceSnapshot(tuple(normalized))
class TaskPlaneLineageStoreBinding:
"""Official in-process read/write surface over the existing TaskPlane DB."""
def __init__(self, store: TaskPlaneStore) -> None:
self.store = store
def service_bindings(self) -> TaskPlaneServiceBindings:
return TaskPlaneServiceBindings(
get_task_snapshot=self.get_task_snapshot,
list_descendant_snapshots=self.list_descendant_snapshots,
reserve_sibling_sequence=self.reserve_sibling_sequence,
compare_and_swap_task_metadata=self.compare_and_swap_task_metadata,
get_root_review_record=self.get_root_review_record,
compare_and_swap_root_review_record=self.compare_and_swap_root_review_record,
get_root_action_dispatch=self.get_root_action_dispatch,
dispatch_root_action=self.dispatch_root_action,
)
def _terminal_proof(self, task: TaskRecord) -> Mapping[str, object] | None:
row = self.store.conn.execute(
"SELECT * FROM task_terminal_proofs WHERE task_id=?", (task.task_id,)
).fetchone()
if row is None or task.status not in PROVEN_TERMINAL_STATES:
return None
claim = self.store.conn.execute(
"SELECT status,result_payload FROM claims WHERE claim_id=? AND task_id=?",
(row["claim_id"], task.task_id),
).fetchone()
if claim is None or claim["status"] != "COMPLETED" or not claim["result_payload"]:
return None
try:
result = json.loads(claim["result_payload"])
refs = tuple(json.loads(row["evidence_refs_json"]))
except (TypeError, ValueError, json.JSONDecodeError):
return None
if result.get("result_digest") != row["result_digest"]:
return None
if task.revision < int(row["task_revision"]):
return None
evidence_ids = {entry["evidence_id"] for entry in self.store.list_evidence(task.task_id)}
if not set(refs).issubset(evidence_ids):
return None
if not self.store.verify_event_chain(task.task_id):
return None
terminal_event = any(
event["type"] in {"TASK_COMPLETED", "TASK_FAILED"}
and event["data"].get("claim_id") == row["claim_id"]
for event in self.store.list_events(task.task_id)
)
if not terminal_event or not self.store.verify_evidence_integrity(task.task_id):
return None
return dict(row)
def get_task_snapshot(self, task_id: str) -> TaskAuthoritySnapshot:
task = self.store.get_task(task_id)
if not task.owner_principal_id or not task.mission_id:
raise ForbiddenError("task lacks server-owned lineage authority binding")
proof = self._terminal_proof(task)
return TaskAuthoritySnapshot(
task_id=task.task_id,
revision=task.revision,
status=task.status,
owner_principal_id=task.owner_principal_id,
mission_id=task.mission_id,
created_at=datetime.fromtimestamp(task.created_at, tz=timezone.utc),
metadata=dict(task.task_plan.metadata),
evidence_refs=(
tuple(json.loads(proof["evidence_refs_json"])) if proof else ()
),
terminal_evidence_proven=proof is not None,
)
def list_descendant_snapshots(
self, root_task_id: str
) -> tuple[TaskAuthoritySnapshot, ...]:
with self.store._lock:
rows = self.store.conn.execute(
"SELECT task_id FROM tasks WHERE root_task_id=? AND task_id<>? "
"ORDER BY created_at,task_id",
(root_task_id, root_task_id),
).fetchall()
return tuple(self.get_task_snapshot(row["task_id"]) for row in rows)
def reserve_sibling_sequence(
self, parent_task_id: str, relation_type: RelationType, child_task_id: str
) -> int:
with self.store._transaction():
existing = self.store.conn.execute(
"SELECT parent_task_id,relation_type,sibling_sequence "
"FROM task_lineage_sequences WHERE child_task_id=?",
(child_task_id,),
).fetchone()
if existing is not None:
if (
existing["parent_task_id"] != parent_task_id
or existing["relation_type"] != relation_type.value
):
raise ConflictError("child sibling reservation conflicts with lineage")
return int(existing["sibling_sequence"])
row = self.store.conn.execute(
"SELECT COALESCE(MAX(sibling_sequence),0)+1 AS next_sequence "
"FROM task_lineage_sequences WHERE parent_task_id=? AND relation_type=?",
(parent_task_id, relation_type.value),
).fetchone()
sequence = int(row["next_sequence"])
if sequence > 99:
raise ConflictError("lineage sibling sequence exhausted")
self.store.conn.execute(
"INSERT INTO task_lineage_sequences "
"(parent_task_id,relation_type,child_task_id,sibling_sequence,created_at) "
"VALUES(?,?,?,?,?)",
(parent_task_id, relation_type.value, child_task_id, sequence, datetime.now(tz=timezone.utc).timestamp()),
)
return sequence
def compare_and_swap_task_metadata(
self, task_id: str, expected_revision: int, metadata_patch: Mapping[str, object]
) -> bool:
if not set(metadata_patch).issubset(RESERVED_LINEAGE_METADATA_KEYS):
raise ForbiddenError("lineage CAS contains non-lineage metadata")
with self.store._transaction():
task = self.store.get_task(task_id)
if task.revision != expected_revision:
return False
lineage_value = metadata_patch.get(LINEAGE_METADATA_KEY)
if isinstance(lineage_value, Mapping):
lineage = lineage_from_metadata(lineage_value)
if lineage.relation_type != RelationType.ROOT:
# The root check and child metadata CAS share this BEGIN
# IMMEDIATE transaction, closing the legacy attach race.
self.store.assert_root_open_for_child(lineage.root_task_id)
if task.status in PROVEN_TERMINAL_STATES:
raise ConflictError("terminal task lineage metadata is immutable")
payload = task.task_plan.to_dict(redact=False)
metadata = dict(payload.get("metadata") or {})
for key, value in metadata_patch.items():
if key in metadata and metadata[key] != value:
raise ConflictError("authoritative lineage metadata conflict")
metadata[key] = value
payload["metadata"] = metadata
now = datetime.now(tz=timezone.utc).timestamp()
updated = self.store.conn.execute(
"UPDATE tasks SET task_json=?,revision=revision+1,updated_at=? "
"WHERE task_id=? AND revision=?",
(json.dumps(payload, ensure_ascii=False), now, task_id, expected_revision),
)
if updated.rowcount != 1:
return False
self.store.append_event(
task_id,
"TASK_LINEAGE_BOUND",
{"metadata_keys": sorted(metadata_patch)},
subject=f"task/{task_id}",
)
return True
def record_terminal_proof(
self,
*,
task_id: str,
claim_id: str,
result_digest: str,
evidence_refs: Sequence[str],
) -> None:
with self.store._transaction():
task = self.store.get_task(task_id)
if task.status not in PROVEN_TERMINAL_STATES:
raise ConflictError("terminal proof requires a final task status")
claim = self.store.conn.execute(
"SELECT status,result_payload FROM claims WHERE claim_id=? AND task_id=?",
(claim_id, task_id),
).fetchone()
if claim is None or claim["status"] != "COMPLETED" or not claim["result_payload"]:
raise ConflictError("terminal proof requires a completed claim")
stored_result = json.loads(claim["result_payload"])
if stored_result.get("result_digest") != result_digest:
raise ConflictError("terminal proof result digest mismatch")
refs = tuple(str(value) for value in evidence_refs)
evidence_ids = {entry["evidence_id"] for entry in self.store.list_evidence(task_id)}
if not set(refs).issubset(evidence_ids):
raise ConflictError("terminal proof evidence reference mismatch")
existing = self.store.conn.execute(
"SELECT * FROM task_terminal_proofs WHERE task_id=?", (task_id,)
).fetchone()
expected = (claim_id, result_digest, canonical_json(list(refs)))
if existing is not None:
actual = (
existing["claim_id"],
existing["result_digest"],
canonical_json(json.loads(existing["evidence_refs_json"])),
)
if actual != expected:
raise ConflictError("terminal proof replay conflict")
return
if not self.store.verify_event_chain(task_id) or not self.store.verify_evidence_integrity(task_id):
raise ConflictError("terminal proof journal integrity failed")
self.store.conn.execute(
"INSERT INTO task_terminal_proofs "
"(task_id,claim_id,task_revision,result_digest,evidence_refs_json,created_at) "
"VALUES(?,?,?,?,?,?)",
(
task_id,
claim_id,
task.revision,
result_digest,
json.dumps(list(refs), ensure_ascii=False),
datetime.now(tz=timezone.utc).timestamp(),
),
)
def get_root_review_record(self, review_id: str) -> Mapping[str, object] | None:
row = self.store.conn.execute(
"SELECT record_json FROM task_root_reviews WHERE review_id=?", (review_id,)
).fetchone()
return None if row is None else json.loads(row["record_json"])
def get_root_review_for_trigger(
self, trigger_child_task_id: str
) -> Mapping[str, object] | None:
row = self.store.conn.execute(
"SELECT record_json FROM task_root_reviews "
"WHERE trigger_child_task_id=? ORDER BY created_at LIMIT 1",
(trigger_child_task_id,),
).fetchone()
return None if row is None else json.loads(row["record_json"])
def compare_and_swap_root_review_record(
self,
root_task_id: str,
expected_root_revision: int,
review_record: Mapping[str, object],
) -> bool:
with self.store._transaction():
review_id = str(review_record.get("review_id") or "")
trigger_id = str(review_record.get("trigger_child_task_id") or "")
if (
review_record.get("root_task_id") != root_task_id
or int(review_record.get("root_revision_before", -1)) != expected_root_revision
or int(review_record.get("root_revision_after", -1)) != expected_root_revision + 1
):
raise ConflictError("root review authority binding mismatch")
existing = self.get_root_review_record(review_id)
if existing is not None:
if canonical_json(existing) != canonical_json(dict(review_record)):
raise ConflictError("root review replay conflict")
return True
trigger = self.get_task_snapshot(trigger_id)
if not trigger.terminal_evidence_proven:
raise ConflictError("root review trigger is not proven terminal")
trigger_revision = trigger.revision
frozen = self.store.conn.execute(
"SELECT review_id FROM task_root_reviews "
"WHERE trigger_child_task_id=? AND trigger_child_revision=?",
(trigger_id, trigger_revision),
).fetchone()
if frozen is not None:
raise ConflictError("terminal trigger already has a different root review")
root = self.store.get_task(root_task_id)
if root.revision != expected_root_revision:
return False
now = datetime.now(tz=timezone.utc).timestamp()
self.store.conn.execute(
"INSERT INTO task_root_reviews "
"(review_id,root_task_id,trigger_child_task_id,trigger_child_revision,"
"root_revision_before,root_revision_after,record_json,created_at) "
"VALUES(?,?,?,?,?,?,?,?)",
(
review_id,
root_task_id,
trigger_id,
trigger_revision,
expected_root_revision,
expected_root_revision + 1,
json.dumps(dict(review_record), ensure_ascii=False, sort_keys=True),
now,
),
)
updated = self.store.conn.execute(
"UPDATE tasks SET revision=revision+1,updated_at=? "
"WHERE task_id=? AND revision=?",
(now, root_task_id, expected_root_revision),
)
if updated.rowcount != 1:
raise ConflictError("root review CAS lost after reservation")
self.store.append_event(
root_task_id,
"ROOT_REVIEW",
{
"review_id": review_id,
"trigger_child_task_id": trigger_id,
"next_legal_action": review_record.get("next_legal_action"),
"root_complete": bool(review_record.get("root_complete")),
},
subject=f"task/{root_task_id}",
)
return True
def get_root_action_dispatch(self, review_id: str) -> RootActionDispatchReceipt | None:
row = self.store.conn.execute(
"SELECT * FROM task_root_action_dispatches WHERE review_id=?", (review_id,)
).fetchone()
if row is None:
return None
return RootActionDispatchReceipt(
review_id=row["review_id"],
root_task_id=row["root_task_id"],
action=NextLegalAction(row["action"]),
idempotency_key=row["idempotency_key"],
status=row["status"],
mission_id=row["mission_id"],
root_revision=int(row["root_revision"]),
plan_digest=row["plan_digest"],
execution_key=row["execution_key"],
)
def dispatch_root_action(
self, review: RootReviewRecord, idempotency_key: str
) -> RootActionDispatchReceipt:
with self.store._transaction():
persisted = self.get_root_review_record(review.review_id)
if persisted is None:
raise ConflictError("root action requires a persisted root review")
existing = self.get_root_action_dispatch(review.review_id)
if existing is not None:
if (
existing.root_task_id != review.root_task_id
or existing.action != review.next_legal_action
or existing.idempotency_key != idempotency_key
):
raise ConflictError("root action dispatch replay conflict")
return existing
now = datetime.now(tz=timezone.utc).timestamp()
self.store.conn.execute(
"INSERT INTO task_root_action_dispatches "
"(review_id,root_task_id,action,idempotency_key,status,created_at,"
"mission_id,root_revision,updated_at) VALUES(?,?,?,?,?,?,?,?,?)",
(
review.review_id,
review.root_task_id,
review.next_legal_action.value,
idempotency_key,
"PENDING",
now,
self.store.get_task(review.root_task_id).mission_id,
review.root_revision_after,
now,
),
)
self.store.append_event(
review.root_task_id,
"ROOT_NEXT_ACTION_PENDING",
{
"review_id": review.review_id,
"action": review.next_legal_action.value,
"idempotency_key": idempotency_key,
},
subject=f"task/{review.root_task_id}",
)
return self.get_root_action_dispatch(review.review_id) # type: ignore[return-value]
class TaskPlaneLineageCoordinator:
"""Coordinates lineage with real TaskPlane admission and completion paths."""
def __init__(self, store: TaskPlaneStore, policy: TaskLineagePolicy) -> None:
self.store = store
self.policy = policy
self.binding = TaskPlaneLineageStoreBinding(store)
self.adapter = InProcessTaskPlaneLineageAdapter(
self.binding.service_bindings(), readback=self.binding
)
self.service = TaskIdentityLineageService(self.adapter)
self.hooks = TaskLineageLifecycleHooks(self.adapter)
@staticmethod
def _request_digest(payload: Mapping[str, object]) -> str:
return compute_sha256_hex(dict(payload))
@staticmethod
def _reject_reserved_metadata(plan: TaskPlan) -> None:
if set(plan.metadata).intersection(RESERVED_LINEAGE_METADATA_KEYS):
raise ValidationError("lineage metadata is server-owned")
@staticmethod
def _goal_slug(plan: TaskPlan) -> str:
for candidate in (plan.intent, plan.task_type, f"TASK-{plan.task_id}"):
if not candidate:
continue
try:
return normalize_goal_slug(str(candidate))
except ValueError:
continue
raise ValidationError("task goal cannot be normalized for lineage")
def _existing_by_idempotency(self, key: str | None) -> TaskRecord | None:
if not key:
return None
row = self.store.conn.execute(
"SELECT task_id FROM tasks WHERE idempotency_key=?", (key,)
).fetchone()
return None if row is None else self.store.get_task(row["task_id"])
@staticmethod
def _verify_replay(
task: TaskRecord,
*,
request_digest: str,
principal: PrincipalLineageBinding,
parent_task_id: str | None,
) -> TaskRecord:
metadata = task.task_plan.metadata
if (
metadata.get(LINEAGE_REQUEST_DIGEST_KEY) != request_digest
or task.owner_principal_id != principal.owner_principal_id
or task.mission_id != principal.mission_id
or task.task_plan.parent_task_id != parent_task_id
):
raise ConflictError("lineage admission replay conflict")
return task
def create_root(
self,
*,
plan: TaskPlan,
authenticated_identity: str,
request_digest: str,
idempotency_key: str | None,
) -> TaskRecord:
principal = self.policy.resolve_principal(authenticated_identity)
return self.create_root_for_principal(
plan=plan,
principal=principal,
request_digest=request_digest,
idempotency_key=idempotency_key,
)
def create_root_for_principal(
self,
*,
plan: TaskPlan,
principal: PrincipalLineageBinding,
request_digest: str,
idempotency_key: str | None,
created_at: datetime | None = None,
) -> TaskRecord:
self.policy.assert_acceptance_ref_registered(principal.root_acceptance_ref)
self._reject_reserved_metadata(plan)
with self.store._transaction():
existing = self._existing_by_idempotency(idempotency_key)
if existing is not None:
return self._verify_replay(
existing,
request_digest=request_digest,
principal=principal,
parent_task_id=None,
)
created_at = created_at or datetime.now(tz=timezone.utc)
if created_at.tzinfo is None or created_at.utcoffset() is None:
raise ValidationError("lineage created_at must be timezone-aware")
created_at = created_at.astimezone(timezone.utc)
prepared = self.service.prepare_new_task_identity(
NewTaskIdentityRequest(
task_id=str(plan.task_id),
owner_principal_id=principal.owner_principal_id,
preferred_owner_alias=principal.preferred_owner_alias,
created_at=created_at,
mission_id=principal.mission_id,
goal_slug=self._goal_slug(plan),
root_acceptance_ref=principal.root_acceptance_ref,
relation_type=RelationType.ROOT,
)
)
plan.metadata = {
**plan.metadata,
**prepared.metadata_patch,
LINEAGE_REQUEST_DIGEST_KEY: request_digest,
}
return self.store.create_task_with_event(
plan,
idempotency_key=idempotency_key,
owner_principal_id=principal.owner_principal_id,
mission_id=principal.mission_id,
created_at=created_at.timestamp(),
)
def delegate_child(
self,
*,
parent_task_id: str,
plan: TaskPlan,
authenticated_identity: str,
request_digest: str,
delegation: DelegationEngine,
principal: PrincipalLineageBinding | None = None,
) -> TaskRecord:
if not plan.task_id:
raise ValidationError("lineage delegation requires a stable task_id")
principal = principal or self.policy.resolve_principal(authenticated_identity)
if principal.authenticated_identity != authenticated_identity:
raise ForbiddenError("delegation principal identity mismatch")
self._reject_reserved_metadata(plan)
with self.store._transaction():
parent = self.store.get_task(parent_task_id)
root = self.store.get_task(parent.root_task_id)
if (
root.owner_principal_id != principal.owner_principal_id
or root.mission_id != principal.mission_id
):
raise ForbiddenError("delegation actor does not match root authority")
try:
existing = self.store.get_task(str(plan.task_id))
except NotFoundError:
existing = None
if existing is not None:
return self._verify_replay(
existing,
request_digest=request_digest,
principal=principal,
parent_task_id=parent_task_id,
)
self.store.assert_root_open_for_child(root.task_id)
created_at = datetime.now(tz=timezone.utc)
prepared = self.service.prepare_new_task_identity(
NewTaskIdentityRequest(
task_id=str(plan.task_id),
owner_principal_id=root.owner_principal_id,
preferred_owner_alias=principal.preferred_owner_alias,
created_at=created_at,
mission_id=root.mission_id,
goal_slug=self._goal_slug(plan),
root_acceptance_ref=str(
root.task_plan.metadata[LINEAGE_METADATA_KEY]["root_acceptance_ref"]
),
relation_type=RelationType.CHILD,
parent_task_id=parent_task_id,
)
)
plan.metadata = {
**plan.metadata,
**prepared.metadata_patch,
LINEAGE_REQUEST_DIGEST_KEY: request_digest,
}
return delegation.delegate(
parent_task_id,
plan,
caller=authenticated_identity,
owner_principal_id=root.owner_principal_id,
mission_id=root.mission_id,
created_at=created_at.timestamp(),
)
def current_acceptance_snapshot(self, *, root_task_id: str) -> RootAcceptanceSnapshot:
"""Evaluate the current authoritative root/descendant projection."""
root = _as_task_record(self.binding.get_task_snapshot(root_task_id))
descendants = tuple(
_as_task_record(snapshot)
for snapshot in self.binding.list_descendant_snapshots(root_task_id)
)
return self.policy.evaluate_acceptance(root, descendants)
def review_proven_terminal_child(
self,
*,
child_task_id: str,
claim_id: str,
result_digest: str,
evidence_refs: Sequence[str],
):
with self.store._transaction():
self.binding.record_terminal_proof(
task_id=child_task_id,
claim_id=claim_id,
result_digest=result_digest,
evidence_refs=evidence_refs,
)
return self.resume_terminal_review(child_task_id=child_task_id)
def resume_terminal_review(self, *, child_task_id: str) -> TerminalReviewOutcome:
"""Resume after restart without re-completing a claim or re-evaluating a review.
Once a trigger has a persisted review, that exact decision is frozen.
Policy/evaluator drift therefore cannot create a second decision for the
same terminal transition.
"""
with self.store._transaction():
child = self.binding.get_task_snapshot(child_task_id)
if not child.terminal_evidence_proven:
raise ConflictError("terminal review resume requires durable proof")
raw_lineage = child.metadata.get(LINEAGE_METADATA_KEY)
if not isinstance(raw_lineage, Mapping):
raise ConflictError("lineage child lacks authoritative lineage metadata")
try:
child_lineage = lineage_from_metadata(dict(raw_lineage))
except ValueError as exc:
raise ConflictError("lineage child metadata is invalid") from exc
persisted = self.binding.get_root_review_for_trigger(child_task_id)
if persisted is not None:
review_id = persisted.get("review_id")
if not isinstance(review_id, str) or not review_id:
raise ConflictError("persisted root review identity is invalid")
try:
review = root_review_from_record(
persisted, expected_review_id=review_id
)
except ValueError as exc:
raise ConflictError("persisted root review is invalid") from exc
root = self.binding.get_task_snapshot(child_lineage.root_task_id)
if (
review.trigger_child_task_id != child_task_id
or review.root_task_id != child_lineage.root_task_id
or child_lineage.task_id != child_task_id
or child_lineage.owner_principal_id != child.owner_principal_id
or child_lineage.mission_id != child.mission_id
or root.owner_principal_id != child.owner_principal_id
or root.mission_id != child.mission_id
or root.revision < review.root_revision_after
):
raise ConflictError("persisted root review authority mismatch")
dispatch = self.binding.get_root_action_dispatch(review.review_id)
if dispatch is None:
dispatch = self.binding.dispatch_root_action(
review,
f"root-review:{review.review_id}:{review.next_legal_action.value}",
)
expected_key = (
f"root-review:{review.review_id}:{review.next_legal_action.value}"
)
if (
dispatch.review_id != review.review_id
or dispatch.root_task_id != review.root_task_id
or dispatch.action != review.next_legal_action
or dispatch.idempotency_key != expected_key
or dispatch.status not in PERSISTED_DISPATCH_STATUSES
):
raise ConflictError("persisted root action dispatch is invalid")
return TerminalReviewOutcome(review=review, dispatch=dispatch)
hook_outcome = self.hooks.on_proven_terminal_child(
child_task_id=child_task_id,
context=AuthenticatedTaskContext(
principal_id=child.owner_principal_id,
preferred_alias=child_lineage.owner_alias,
mission_id=child.mission_id,
),
acceptance_evaluator=self.policy.evaluate_acceptance,
reviewed_at=datetime.now(tz=timezone.utc),
require_independent_readback=False,
)
return hook_outcome.outcome
File diff suppressed because it is too large Load Diff
+7
View File
@@ -388,6 +388,8 @@ class TaskRecord:
root_task_id: str
idempotency_key: str | None = None
failure_reason: str | None = None
owner_principal_id: str = ""
mission_id: str = ""
def with_status(self, status: str) -> "TaskRecord":
allowed = TASK_NEXT_STATES.get(self.status, set())
@@ -407,6 +409,8 @@ class TaskRecord:
root_task_id=self.root_task_id,
idempotency_key=self.idempotency_key,
failure_reason=self.failure_reason,
owner_principal_id=self.owner_principal_id,
mission_id=self.mission_id,
)
def to_dict(self, *, include_task=True, redact_payload: bool = True) -> dict[str, Any]:
@@ -422,6 +426,9 @@ class TaskRecord:
}
if include_task:
payload["plan"] = self.task_plan.to_dict(redact=redact_payload)
display_name = self.task_plan.metadata.get("aios_task_display_name")
if self.owner_principal_id and self.mission_id and isinstance(display_name, str):
payload["display_name"] = display_name
if self.idempotency_key:
payload["idempotency_key"] = self.idempotency_key
return payload
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,513 @@
"""Diagnostic schedulability proof and claim-time authoritative recheck.
The proof is evidence, never a bearer credential.
A claim proceeds in three stages:
1. read-only preflight/discovery;
2. claim-time read-only recheck;
3. final *atomic* authority operation that revalidates all mutable claim
conditions at the linearization point and returns lease/fencing receipt.
This module does not create a second scheduler or a second TaskPlane authority.
The host adapter must bind ``claim_atomically`` to the existing authoritative
claim transaction / repository operation.
"""
from __future__ import annotations
from dataclasses import dataclass, asdict
from typing import Any, Mapping, Protocol, Sequence
import hashlib
import time
import uuid
from .models import canonical_json
CLAIMABLE_TASK_STATES = frozenset({"CREATED", "QUEUED", "FAILED_RECOVERABLE"})
class SchedulabilityError(RuntimeError):
pass
class SchedulabilityChanged(SchedulabilityError):
pass
def _canonical(value: object) -> bytes:
# Reuse the existing TaskPlane canonical JSON contract. Do not introduce
# another sort/separator encoder for schedulability evidence.
return canonical_json(value).encode("utf-8")
def _sha(value: object) -> str:
return hashlib.sha256(_canonical(value)).hexdigest()
def _text(value: object, name: str, *, max_len: int = 256) -> str:
if not isinstance(value, str):
raise SchedulabilityError(f"{name} must be a string")
value = value.strip()
if not value or len(value) > max_len or any(ord(ch) < 32 for ch in value):
raise SchedulabilityError(f"invalid {name}")
return value
def _int_authority(
value: object,
*,
name: str,
default: int | None = None,
minimum: int = 0,
) -> tuple[int, "Blocker | None"]:
if value is None:
if default is None:
return 0, Blocker("INVALID_AUTHORITY_VALUE", "worker", f"{name} missing")
return default, None
if isinstance(value, bool) or not isinstance(value, int):
return 0, Blocker("INVALID_AUTHORITY_VALUE", "worker", f"{name} must be integer")
if value < minimum:
return value, Blocker("INVALID_AUTHORITY_VALUE", "worker", f"{name} below {minimum}")
return value, None
@dataclass(frozen=True)
class Blocker:
code: str
plane: str
detail: str
def to_dict(self) -> dict[str, str]:
return asdict(self)
@dataclass(frozen=True)
class ClaimConditions:
"""Mutable authority conditions that MUST be checked atomically with claim."""
task_revision: int
task_generation: int
task_status: str
worker_generation: int
worker_identity: str
authorization_ref: str
authorization_revision: str
scope_digest: str
capacity_limit: int
def to_dict(self) -> dict[str, object]:
return asdict(self)
@dataclass(frozen=True)
class SchedulabilityObservation:
task_id: str
task_revision: int
task_generation: int
task_status: str
worker_id: str
worker_generation: int
worker_identity: str
heartbeat_observed_at: float
heartbeat_fresh: bool
required_capabilities: tuple[str, ...]
worker_capabilities: tuple[str, ...]
scope_digest: str
authorization_ref: str
authorization_revision: str
authorization_allowed: bool
capacity_limit: int
active_claims: int
dependencies_satisfied: bool
freeze_blocked: bool
conflicting_lease: bool
@property
def capacity_remaining(self) -> int:
return max(0, self.capacity_limit - self.active_claims)
def stable_binding(self) -> dict[str, object]:
"""Fields whose change invalidates an earlier proof."""
return {
"task_id": self.task_id,
"task_revision": self.task_revision,
"task_generation": self.task_generation,
"task_status": self.task_status,
"worker_id": self.worker_id,
"worker_generation": self.worker_generation,
"worker_identity": self.worker_identity,
"required_capabilities": list(self.required_capabilities),
"worker_capabilities": list(self.worker_capabilities),
"scope_digest": self.scope_digest,
"authorization_ref": self.authorization_ref,
"authorization_revision": self.authorization_revision,
"authorization_allowed": self.authorization_allowed,
"capacity_limit": self.capacity_limit,
"active_claims": self.active_claims,
"dependencies_satisfied": self.dependencies_satisfied,
"freeze_blocked": self.freeze_blocked,
"conflicting_lease": self.conflicting_lease,
"heartbeat_fresh": self.heartbeat_fresh,
}
def claim_conditions(self) -> ClaimConditions:
return ClaimConditions(
task_revision=self.task_revision,
task_generation=self.task_generation,
task_status=self.task_status,
worker_generation=self.worker_generation,
worker_identity=self.worker_identity,
authorization_ref=self.authorization_ref,
authorization_revision=self.authorization_revision,
scope_digest=self.scope_digest,
capacity_limit=self.capacity_limit,
)
@dataclass(frozen=True)
class SchedulabilityProof:
schema: str
proof_id: str
task_id: str
worker_id: str
actor_identity: str
created_at: float
expires_at: float
observation: SchedulabilityObservation
blockers: tuple[Blocker, ...]
observation_digest: str
proof_digest: str
@classmethod
def create(
cls,
*,
task_id: str,
worker_id: str,
actor_identity: str,
observation: SchedulabilityObservation,
blockers: Sequence[Blocker],
now: float,
ttl_seconds: float,
proof_id: str | None = None,
) -> "SchedulabilityProof":
if ttl_seconds <= 0 or ttl_seconds > 300:
raise ValueError("schedulability proof ttl must be in (0,300]")
payload = {
"schema": "aios.schedulability-proof.v2",
"proof_id": proof_id or f"sched-{uuid.uuid4()}",
"task_id": _text(task_id, "task_id"),
"worker_id": _text(worker_id, "worker_id"),
"actor_identity": _text(actor_identity, "actor_identity", max_len=512),
"created_at": float(now),
"expires_at": float(now + ttl_seconds),
"observation": observation.stable_binding(),
"blockers": [b.to_dict() for b in blockers],
}
obs_digest = _sha(payload["observation"])
proof_digest = _sha({**payload, "observation_digest": obs_digest})
return cls(
schema=payload["schema"],
proof_id=payload["proof_id"],
task_id=payload["task_id"],
worker_id=payload["worker_id"],
actor_identity=payload["actor_identity"],
created_at=payload["created_at"],
expires_at=payload["expires_at"],
observation=observation,
blockers=tuple(blockers),
observation_digest=obs_digest,
proof_digest=proof_digest,
)
@property
def claimable(self) -> bool:
return not self.blockers
@dataclass(frozen=True)
class DiscoveryDiagnostics:
candidate_count: int
blocker_codes: tuple[str, ...]
class SchedulabilityAuthority(Protocol):
"""Read authorities and perform the final existing claim transaction."""
def read_task(self, task_id: str) -> Mapping[str, Any]: ...
def list_candidate_tasks(self) -> Sequence[Mapping[str, Any]]: ...
def read_worker(self, worker_id: str) -> Mapping[str, Any]: ...
def read_authorization(
self, *, task: Mapping[str, Any], worker: Mapping[str, Any], actor_identity: str
) -> Mapping[str, Any]: ...
def active_claim_count(self, worker_id: str) -> int: ...
def dependencies_satisfied(self, task: Mapping[str, Any]) -> bool: ...
def freeze_blocked(self, task: Mapping[str, Any]) -> bool: ...
def conflicting_lease(self, task_id: str) -> bool: ...
def exact_scope_digest(
self, *, task: Mapping[str, Any], worker: Mapping[str, Any], actor_identity: str
) -> tuple[bool, str]: ...
def claim_atomically(
self,
*,
task_id: str,
worker_id: str,
actor_identity: str,
expected: ClaimConditions,
lease_seconds: int,
schedulability_proof_digest: str,
) -> Mapping[str, Any]:
"""Claim at the existing TaskPlane linearization point.
The implementation MUST, in the same authoritative transaction / fenced
operation that writes the claim:
- verify task revision/generation/status is still claimable;
- verify worker registration/identity/health/capacity;
- verify authorization ref/revision remains allowed;
- verify exact scope;
- verify dependencies/freeze/conflicting lease;
- create claim + lease/fencing receipt.
This protocol is an adapter contract; it is not evidence that a specific
production adapter already implements the transaction.
"""
class ProofRepository(Protocol):
def put(self, proof: SchedulabilityProof) -> None: ...
def get(self, proof_id: str) -> SchedulabilityProof | None: ...
class MemoryProofRepository:
def __init__(self) -> None:
self._values: dict[str, SchedulabilityProof] = {}
def put(self, proof: SchedulabilityProof) -> None:
existing = self._values.get(proof.proof_id)
if existing is not None and existing.proof_digest != proof.proof_digest:
raise SchedulabilityError("proof id replay conflict")
self._values[proof.proof_id] = proof
def get(self, proof_id: str) -> SchedulabilityProof | None:
return self._values.get(proof_id)
class SchedulabilityService:
def __init__(
self,
authority: SchedulabilityAuthority,
proofs: ProofRepository,
*,
clock=time.time,
proof_ttl_seconds: float = 30.0,
heartbeat_grace_factor: float = 3.0,
) -> None:
self.authority = authority
self.proofs = proofs
self.clock = clock
self.proof_ttl_seconds = proof_ttl_seconds
self.heartbeat_grace_factor = heartbeat_grace_factor
@staticmethod
def _caps(value: object) -> tuple[str, ...]:
if not isinstance(value, (list, tuple)):
return ()
return tuple(sorted({str(v) for v in value if isinstance(v, str) and v}))
def observe(
self, task_id: str, worker_id: str, actor_identity: str
) -> tuple[SchedulabilityObservation, tuple[Blocker, ...]]:
task = self.authority.read_task(task_id)
worker = self.authority.read_worker(worker_id)
auth = self.authority.read_authorization(
task=task, worker=worker, actor_identity=actor_identity
)
required = self._caps(task.get("required_capabilities") or ())
offered = self._caps(worker.get("capabilities") or ())
now = float(self.clock())
last = float(worker.get("last_heartbeat") or worker.get("last_seen_at_epoch") or 0.0)
interval = float(worker.get("heartbeat_interval_seconds") or 15.0)
fresh = (
str(worker.get("status") or "").lower() == "active"
and not bool(worker.get("disabled"))
and last <= now
and now - last <= max(interval * self.heartbeat_grace_factor, 1.0)
)
scope_ok, scope_digest = self.authority.exact_scope_digest(
task=task, worker=worker, actor_identity=actor_identity
)
deps = bool(self.authority.dependencies_satisfied(task))
freeze = bool(self.authority.freeze_blocked(task))
lease = bool(self.authority.conflicting_lease(task_id))
active_raw = self.authority.active_claim_count(worker_id)
active, active_error = _int_authority(active_raw, name="active_claim_count", minimum=0)
capacity, capacity_error = _int_authority(
worker.get("max_concurrency"),
name="max_concurrency",
default=1,
minimum=0,
)
auth_allowed = bool(auth.get("allowed"))
task_status = str(task.get("status") or "")
blockers: list[Blocker] = []
if task_status not in CLAIMABLE_TASK_STATES:
blockers.append(
Blocker(
"TASK_STATE_NOT_CLAIMABLE",
"task",
f"task status {task_status or '<missing>'} is not claimable",
)
)
if active_error:
blockers.append(active_error)
if capacity_error:
blockers.append(capacity_error)
if not fresh:
blockers.append(Blocker("STALE_OR_INACTIVE_WORKER", "worker", "worker heartbeat is not fresh"))
missing = sorted(set(required) - set(offered))
if missing:
blockers.append(Blocker("MISSING_CAPABILITY", "worker", ",".join(missing)))
if not scope_ok:
blockers.append(Blocker("EXACT_SCOPE_MISMATCH", "authorization", "worker/task signed scope mismatch"))
if not auth_allowed:
blockers.append(Blocker("AUTHORIZATION_BLOCKED", "authorization", "current authorization does not allow claim"))
if capacity >= 0 and active >= capacity:
blockers.append(Blocker("CAPACITY_EXHAUSTED", "worker", f"{active}/{capacity} active"))
if not deps:
blockers.append(Blocker("DEPENDENCY_BLOCKED", "task", "task dependencies are not satisfied"))
if freeze:
blockers.append(Blocker("FREEZE_BLOCKED", "task", "task/environment freeze is active"))
if lease:
blockers.append(Blocker("CONFLICTING_LEASE", "task", "a live conflicting lease exists"))
obs = SchedulabilityObservation(
task_id=_text(task.get("task_id"), "task_id"),
task_revision=int(task.get("revision") or 0),
task_generation=int(task.get("generation") or task.get("metadata", {}).get("generation") or 0),
task_status=task_status,
worker_id=_text(worker.get("worker_id"), "worker_id"),
worker_generation=int(worker.get("registration_revision") or worker.get("generation") or 0),
worker_identity=_text(worker.get("identity"), "worker_identity", max_len=512),
heartbeat_observed_at=last,
heartbeat_fresh=fresh,
required_capabilities=required,
worker_capabilities=offered,
scope_digest=_text(scope_digest, "scope_digest", max_len=256),
authorization_ref=_text(auth.get("authorization_ref"), "authorization_ref", max_len=512),
authorization_revision=_text(auth.get("revision"), "authorization_revision", max_len=256),
authorization_allowed=auth_allowed,
capacity_limit=capacity,
active_claims=active,
dependencies_satisfied=deps,
freeze_blocked=freeze,
conflicting_lease=lease,
)
return obs, tuple(blockers)
def preflight(
self,
*,
task_id: str,
worker_id: str,
actor_identity: str,
persist: bool = False,
) -> SchedulabilityProof:
now = float(self.clock())
obs, blockers = self.observe(task_id, worker_id, actor_identity)
proof = SchedulabilityProof.create(
task_id=task_id,
worker_id=worker_id,
actor_identity=actor_identity,
observation=obs,
blockers=blockers,
now=now,
ttl_seconds=self.proof_ttl_seconds,
)
if persist:
self.proofs.put(proof)
return proof
def discover_with_diagnostics(
self, *, worker_id: str, actor_identity: str
) -> tuple[SchedulabilityProof | None, DiscoveryDiagnostics]:
candidates = sorted(
self.authority.list_candidate_tasks(),
key=lambda task: (
-int(task.get("priority") or 0),
str(task.get("created_at") or ""),
str(task.get("task_id") or ""),
),
)
codes: set[str] = set()
observed = 0
for task in candidates:
task_id = str(task.get("task_id") or "")
if not task_id:
continue
observed += 1
proof = self.preflight(
task_id=task_id,
worker_id=worker_id,
actor_identity=actor_identity,
persist=False,
)
if proof.claimable:
self.proofs.put(proof)
return proof, DiscoveryDiagnostics(observed, tuple(sorted(codes)))
codes.update(b.code for b in proof.blockers)
return None, DiscoveryDiagnostics(observed, tuple(sorted(codes)))
def discover(
self, *, worker_id: str, actor_identity: str
) -> SchedulabilityProof | None:
proof, _ = self.discover_with_diagnostics(
worker_id=worker_id, actor_identity=actor_identity
)
return proof
@staticmethod
def _validate_claim_receipt(result: Mapping[str, Any]) -> None:
required = ("claim_id", "lease_id", "fencing_generation", "receipt_revision")
missing = [name for name in required if result.get(name) in (None, "")]
if missing:
raise SchedulabilityChanged(
"claim authority did not return required lease/fence receipt: "
+ ",".join(missing)
)
def claim_with_recheck(
self,
*,
proof_id: str,
worker_id: str,
actor_identity: str,
lease_seconds: int = 30,
) -> Mapping[str, Any]:
proof = self.proofs.get(proof_id)
if proof is None:
raise SchedulabilityChanged("proof not found")
now = float(self.clock())
if proof.expires_at <= now:
raise SchedulabilityChanged("proof expired")
if proof.worker_id != worker_id or proof.actor_identity != actor_identity:
raise SchedulabilityChanged("proof identity mismatch")
current, blockers = self.observe(proof.task_id, worker_id, actor_identity)
if blockers:
raise SchedulabilityChanged(
"claim-time blockers: " + ",".join(sorted(b.code for b in blockers))
)
if current.stable_binding() != proof.observation.stable_binding():
raise SchedulabilityChanged("claim-time authority changed since proof")
result = self.authority.claim_atomically(
task_id=proof.task_id,
worker_id=worker_id,
actor_identity=actor_identity,
expected=current.claim_conditions(),
lease_seconds=lease_seconds,
schedulability_proof_digest=proof.proof_digest,
)
self._validate_claim_receipt(result)
return result
+237 -9
View File
@@ -272,6 +272,13 @@ class TaskPlaneStore:
)
"""
)
self._ensure_columns(
"tasks",
{
"owner_principal_id": "TEXT NOT NULL DEFAULT ''",
"mission_id": "TEXT NOT NULL DEFAULT ''",
},
)
self._ensure_columns(
"workers",
{
@@ -305,6 +312,167 @@ class TaskPlaneStore:
"CREATE UNIQUE INDEX IF NOT EXISTS ux_events_task_seq ON events(task_id, seq)"
)
self.conn.execute("CREATE INDEX IF NOT EXISTS ix_outbox_status ON outbox(delivery_status)")
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_lineage_sequences (
parent_task_id TEXT NOT NULL,
relation_type TEXT NOT NULL,
child_task_id TEXT NOT NULL UNIQUE,
sibling_sequence INTEGER NOT NULL,
created_at REAL NOT NULL,
PRIMARY KEY(parent_task_id, relation_type, sibling_sequence)
)
"""
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_terminal_proofs (
task_id TEXT PRIMARY KEY,
claim_id TEXT NOT NULL UNIQUE,
task_revision INTEGER NOT NULL,
result_digest TEXT NOT NULL,
evidence_refs_json TEXT NOT NULL,
created_at REAL NOT NULL
)
"""
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_root_reviews (
review_id TEXT PRIMARY KEY,
root_task_id TEXT NOT NULL,
trigger_child_task_id TEXT NOT NULL,
trigger_child_revision INTEGER NOT NULL,
root_revision_before INTEGER NOT NULL,
root_revision_after INTEGER NOT NULL,
record_json TEXT NOT NULL,
created_at REAL NOT NULL
)
"""
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_root_action_dispatches (
review_id TEXT PRIMARY KEY,
root_task_id TEXT NOT NULL,
action TEXT NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
status TEXT NOT NULL,
created_at REAL NOT NULL
)
"""
)
self._ensure_columns(
"task_root_action_dispatches",
{
"mission_id": "TEXT NOT NULL DEFAULT ''",
"root_revision": "INTEGER NOT NULL DEFAULT 0",
"row_version": "INTEGER NOT NULL DEFAULT 0",
"plan_json": "TEXT",
"plan_digest": "TEXT",
"execution_key": "TEXT",
"claim_owner": "TEXT",
"claim_token": "TEXT",
"claim_fence": "INTEGER",
"claim_expires_at": "REAL",
"execution_receipt_json": "TEXT",
"execution_receipt_digest": "TEXT",
"rereview_json": "TEXT",
"rereview_digest": "TEXT",
"retry_at": "REAL",
"last_error_code": "TEXT",
"updated_at": "REAL",
},
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_root_action_effects (
execution_key TEXT PRIMARY KEY,
root_task_id TEXT NOT NULL,
target_task_id TEXT,
target_job_id TEXT,
request_digest TEXT NOT NULL,
state TEXT NOT NULL,
effect_phase TEXT NOT NULL DEFAULT 'LEGACY_UNKNOWN',
receipt_json TEXT,
receipt_digest TEXT,
retry_at REAL,
last_error_code TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
)
"""
)
self._ensure_columns(
"task_root_action_effects",
{
# Existing v1/v2 EFFECT_UNKNOWN rows may already have sent
# an external request. They must never migrate to a phase
# that permits a new lease or commit.
"effect_phase": "TEXT NOT NULL DEFAULT 'LEGACY_UNKNOWN'",
},
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_root_action_fences (
root_task_id TEXT PRIMARY KEY,
next_fence INTEGER NOT NULL,
active_review_id TEXT,
active_fence INTEGER,
active_phase TEXT,
updated_at REAL NOT NULL
)
"""
)
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_root_action_dispatch_ready "
"ON task_root_action_dispatches(status,retry_at,created_at)"
)
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_root_action_dispatch_root "
"ON task_root_action_dispatches(root_task_id,root_revision)"
)
self.conn.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS ux_root_review_trigger "
"ON task_root_reviews(trigger_child_task_id, trigger_child_revision)"
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_missions (
mission_id TEXT PRIMARY KEY,
root_task_id TEXT NOT NULL UNIQUE,
authenticated_identity TEXT NOT NULL,
owner_principal_id TEXT NOT NULL,
owner_alias TEXT NOT NULL,
goal_slug TEXT NOT NULL,
root_acceptance_ref TEXT NOT NULL UNIQUE,
acceptance_verifier_id TEXT NOT NULL,
original_acceptance_json TEXT NOT NULL,
request_digest TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
created_at REAL NOT NULL,
UNIQUE(authenticated_identity, idempotency_key)
)
"""
)
self.conn.execute(
"CREATE INDEX IF NOT EXISTS ix_task_missions_owner "
"ON task_missions(authenticated_identity, mission_id)"
)
self.conn.execute(
"""
CREATE TABLE IF NOT EXISTS task_mission_delegations (
authenticated_identity TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
mission_id TEXT NOT NULL,
parent_task_id TEXT NOT NULL,
child_task_id TEXT NOT NULL UNIQUE,
request_digest TEXT NOT NULL,
created_at REAL NOT NULL,
PRIMARY KEY(authenticated_identity, idempotency_key)
)
"""
)
def _ensure_columns(self, table: str, columns: dict[str, str]) -> None:
existing = {
@@ -359,6 +527,8 @@ class TaskPlaneStore:
root_task_id=row["root_task_id"],
idempotency_key=row["idempotency_key"],
failure_reason=row["failure_reason"],
owner_principal_id=row["owner_principal_id"],
mission_id=row["mission_id"],
)
def _row_to_worker(self, row: sqlite3.Row | None) -> WorkerRegistration:
@@ -385,20 +555,32 @@ class TaskPlaneStore:
disabled_reason=row["disabled_reason"],
)
def create_task(self, task_plan: TaskPlan, idempotency_key: str | None = None) -> TaskRecord:
def create_task(
self,
task_plan: TaskPlan,
idempotency_key: str | None = None,
*,
owner_principal_id: str = "",
mission_id: str = "",
created_at: float | None = None,
) -> TaskRecord:
task_id = task_plan.task_id or f"task-{uuid.uuid4()}"
task_plan.task_id = task_id
task_json = redact_value(task_plan.to_dict(redact=False))
now = _now()
now = _now() if created_at is None else float(created_at)
with self._transaction():
if idempotency_key:
existing = self.conn.execute(
"SELECT task_id,task_json FROM tasks WHERE idempotency_key=?",
"SELECT task_id,task_json,owner_principal_id,mission_id FROM tasks WHERE idempotency_key=?",
(idempotency_key,),
).fetchone()
if existing:
if _canonical(json.loads(existing["task_json"])) != _canonical(task_json):
raise ConflictError("idempotency key payload mismatch")
if owner_principal_id and existing["owner_principal_id"] != owner_principal_id:
raise ConflictError("idempotency key owner mismatch")
if mission_id and existing["mission_id"] != mission_id:
raise ConflictError("idempotency key mission mismatch")
return self.get_task(existing["task_id"])
root_task_id = task_id
for dependency in task_plan.dependencies:
@@ -412,8 +594,8 @@ class TaskPlaneStore:
self.conn.execute(
"""
INSERT INTO tasks
(task_id, revision, status, task_json, task_type, root_task_id, parent_task_id, correlation_id, idempotency_key, created_at, updated_at, failure_reason)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
(task_id, revision, status, task_json, task_type, root_task_id, parent_task_id, correlation_id, idempotency_key, created_at, updated_at, failure_reason, owner_principal_id, mission_id)
VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
""",
(
task_id,
@@ -426,6 +608,8 @@ class TaskPlaneStore:
idempotency_key,
now,
now,
owner_principal_id,
mission_id,
),
)
self._set_budget(task_id, task_plan.budgets)
@@ -437,10 +621,22 @@ class TaskPlaneStore:
return self.get_task(task_id)
def create_task_with_event(
self, task_plan: TaskPlan, idempotency_key: str | None = None
self,
task_plan: TaskPlan,
idempotency_key: str | None = None,
*,
owner_principal_id: str = "",
mission_id: str = "",
created_at: float | None = None,
) -> TaskRecord:
with self._transaction():
task = self.create_task(task_plan, idempotency_key=idempotency_key)
task = self.create_task(
task_plan,
idempotency_key=idempotency_key,
owner_principal_id=owner_principal_id,
mission_id=mission_id,
created_at=created_at,
)
if not self.list_events(task.task_id):
self._fault("create.after_task_before_event")
self.append_event(
@@ -461,6 +657,26 @@ class TaskPlaneStore:
raise NotFoundError(f"task {task_id} not found")
return self._row_to_task(row)
def assert_root_open_for_child(self, root_task_id: str) -> TaskRecord:
"""Fence child admission against root completion/action finalization."""
root = self.get_task(root_task_id)
if root.task_id != root.root_task_id:
raise ConflictError("child admission authority is not a root task")
if root.status == "COMPLETED":
raise ConflictError("cannot add a child below a completed root")
fence = self.conn.execute(
"SELECT active_phase FROM task_root_action_fences WHERE root_task_id=?",
(root_task_id,),
).fetchone()
if fence is not None and fence["active_phase"] in {
"CLAIMED",
"EFFECT_UNKNOWN",
"EXECUTED_PENDING_REREVIEW",
"ROOT_CLOSING",
}:
raise ConflictError("root action fence blocks child admission")
return root
def list_tasks(self) -> list[TaskRecord]:
with self._lock:
rows = self.conn.execute(
@@ -1251,7 +1467,14 @@ class TaskPlaneStore:
return updated
def delegate_task_with_budget_and_events(
self, parent_task_id: str, child_plan: TaskPlan, *, caller: str
self,
parent_task_id: str,
child_plan: TaskPlan,
*,
caller: str,
owner_principal_id: str = "",
mission_id: str = "",
created_at: float | None = None,
) -> TaskRecord:
with self._transaction():
parent_budget = self.get_task_budget(parent_task_id)
@@ -1270,7 +1493,12 @@ class TaskPlaneStore:
self.consume_budget(parent_task_id, needed)
self._fault("delegate.after_budget_before_child")
child_plan.parent_task_id = parent_task_id
child = self.create_task(child_plan)
child = self.create_task(
child_plan,
owner_principal_id=owner_principal_id,
mission_id=mission_id,
created_at=created_at,
)
self.append_event(
parent_task_id,
"TASK_DELEGATED",
@@ -1,5 +1,10 @@
INSERT OR IGNORE INTO node_status(node_id, health, active, score, last_seen_at, note) VALUES ('server-hw','ok',1,100,datetime('now'),'primary logical node'),('server-local','ok',0,95,datetime('now'),'backup logical node');
INSERT OR IGNORE INTO routing_policy(role, primary_node, backup_node, active_node, failover_priority, updated_at) VALUES ('owner','server-hw','server-local','server-hw',1,datetime('now')),('admin','server-hw','server-local','server-hw',2,datetime('now')),('user','server-hw','server-local','server-hw',3,datetime('now'));
INSERT OR IGNORE INTO users(id, username, display_name, role, status, password_hash, created_at, updated_at) VALUES ('bootstrap-owner','owner','Bootstrap Owner','owner','approved','bootstrap-change-me',datetime('now'),datetime('now')),('bootstrap-admin','admin','Bootstrap Admin','admin','approved','bootstrap-change-me',datetime('now'),datetime('now')),('sample-user','sample-user','Sample User','user','approved','bootstrap-change-me',datetime('now'),datetime('now'));
-- SEC-P0-BOOTSTRAP-CREDENTIAL hardening: seed NO usable password. Empty hash
-- means "not set / forced reset" -> owner/admin must set a strong password via
-- the bootstrap flow (scripts/patch_runtime_owner_admin_user.py) over a secure channel.
INSERT OR IGNORE INTO users(id, username, display_name, role, status, password_hash, created_at, updated_at) VALUES ('bootstrap-owner','owner','Bootstrap Owner','owner','approved','',datetime('now'),datetime('now')),('bootstrap-admin','admin','Bootstrap Admin','admin','approved','',datetime('now'),datetime('now')),('sample-user','sample-user','Sample User','user','approved','',datetime('now'),datetime('now'));
-- One-time cleanup: neutralize any legacy known plaintext bootstrap seed already present.
UPDATE users SET password_hash='', updated_at=datetime('now') WHERE password_hash='bootstrap-change-me';
INSERT OR IGNORE INTO model_policy(role, default_model, allowed_models, updated_at) VALUES ('owner','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b","gpt-5.5"]',datetime('now')),('admin','deepseekv4-flash','["deepseekv4-flash","qwen3.6-35b"]',datetime('now')),('user','deepseekv4-flash','["deepseekv4-flash"]',datetime('now'));
INSERT OR IGNORE INTO quota_policy(username, daily_limit, used_today, updated_at) VALUES ('owner',10000,0,datetime('now')),('admin',2000,0,datetime('now')),('sample-user',200,0,datetime('now'));
+32 -4
View File
@@ -55,10 +55,36 @@ def seed_base_models():
if not one('SELECT * FROM gitops_versions WHERE version=?', ('V1R1C0',)):
exec_sql('INSERT INTO gitops_versions(version,tag,level,summary,status,created_by,created_at) VALUES(?,?,?,?,?,?,?)', ('V1R1C0','V1R1C0','R','MultiUser runtime selector + GitOps + custom LLM priority registry','ready','system',now()))
_ROLE_WHITELIST = {'owner', 'admin', 'user'}
def token_identity(token: str):
"""Resolve the server-authoritative identity bound to a token.
Format of each TOKEN_FILE line (one per line, ':'-separated):
<token> -> authenticated principal, role 'user'
<token>:<user>:<role> -> mapped principal; role restricted to owner/admin/user
Identity NEVER comes from a client-supplied header. In local dev mode the
DEV_ALLOW flag maps a fixed dev principal (must be explicitly enabled).
"""
if DEV_ALLOW:
return {'username': 'dev-owner', 'role': 'owner'}
if not token or not TOKEN_FILE.exists():
return None
for line in TOKEN_FILE.read_text().splitlines():
line = line.strip()
if not line:
continue
parts = [x.strip() for x in line.split(':')]
if not parts or parts[0] != token:
continue
if len(parts) >= 3:
role = parts[2] if parts[2] in _ROLE_WHITELIST else 'user'
return {'username': safe_name(parts[1]), 'role': role}
return {'username': 'user-%s' % token[:8], 'role': 'user'}
return None
def valid_token(token: str) -> bool:
if DEV_ALLOW: return True
if not token or not TOKEN_FILE.exists(): return False
return token.strip() in [x.strip() for x in TOKEN_FILE.read_text().splitlines() if x.strip()]
return token_identity(token) is not None
def git_cmd(args, check=False):
WORKSPACE.mkdir(parents=True, exist_ok=True); p = subprocess.run(['git'] + args, cwd=str(WORKSPACE), text=True, capture_output=True)
if check and p.returncode != 0: raise RuntimeError(p.stderr or p.stdout or 'git command failed')
@@ -88,8 +114,10 @@ class Handler(BaseHTTPRequestHandler):
server_version = 'AIOSGitOpsModelAPI/V1R1C0'
def actor(self):
auth = self.headers.get('Authorization',''); token = auth.replace('Bearer ', '').strip() if auth.startswith('Bearer ') else self.headers.get('X-AIOS-Token','')
ok = valid_token(token); user = self.headers.get('X-AIOS-User') or ('dev-owner' if DEV_ALLOW else 'anonymous'); role = self.headers.get('X-AIOS-Role') or ('owner' if DEV_ALLOW else 'user')
return {'username': safe_name(user), 'role': role, 'token_ok': ok}
ident = token_identity(token)
if ident is None:
return {'username': 'anonymous', 'role': 'user', 'token_ok': False}
return {'username': ident['username'], 'role': ident['role'], 'token_ok': True}
def require_write(self, roles=None):
a = self.actor()
if not a['token_ok']: self.send_json({'error':'unauthorized','hint':'write APIs require Bearer token or AIOS_GITOPS_DEV_ALLOW=1'}, 401); return None
+14 -29
View File
@@ -1,8 +1,8 @@
<<<<<<< HEAD
# AIOS Agent Command Plane Registry
# Git is the single source of truth for all agent commands.
version: 1.0.0
created_at: 2026-07-08T15:20:00+08:00
registry_version: 1.0.0
created_at: 2026-07-08T15:44:00+08:00
baseline: V1R3C1
commands:
@@ -12,6 +12,12 @@ commands:
completed_path: "agents/commands/completed/"
failed_path: "agents/commands/failed/"
directories:
commands/active: "New commands pending execution — Git is the only entry point"
commands/running: "Commands currently being executed by an agent"
commands/completed: "Commands that finished successfully — moved here on success"
commands/failed: "Commands that finished with errors — moved here on failure"
policies:
path: "agents/policies/"
source: "git"
@@ -31,6 +37,12 @@ agents:
type: reviewer
role: review
lifecycle:
new: active/
running: running/
success: completed/
failure: failed/
# Audit plane configuration
audit:
enabled: true
@@ -38,30 +50,3 @@ audit:
agent_runs: "audit/agent-runs/"
version_history: "audit/version-history/"
command_history: "audit/command-history/"
=======
# AIOS Agent Command Registry
# Git is the single source of truth for all agent commands.
registry_version: 1.0.0
created_at: "2026-07-08T15:44:00+08:00"
baseline: V1R3C1
directories:
commands/active: "New commands pending execution — Git is the only entry point"
commands/running: "Commands currently being executed by an agent"
commands/completed: "Commands that finished successfully — moved here on success"
commands/failed: "Commands that finished with errors — moved here on failure"
state: agents/state/
policies: agents/policies/
lifecycle:
new: active/
running: running/
success: completed/
failure: failed/
# Command lifecycle state machine:
# active/ → locked & moved to running/ → execute → success → completed/
# → failure → failed/
>>>>>>> 4d18ee0 (feat(cmd): enable Agent Command Plane, audit plane, and Web UI dashboard)
@@ -0,0 +1,68 @@
[Unit]
Description=AIOS GitOps Model API V2R2C2 Baseline1.1 isolated candidate (2852c2a) - control-plane bootstrap only
After=network-online.target
Wants=network-online.target
Requires=aios-trusted-candidate-builder.socket
[Service]
Type=simple
User=aiman
Group=gitops
WorkingDirectory=/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime
UMask=0077
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONDONTWRITEBYTECODE=1
Environment=AIOS_GITOPS_ROOT=/var/lib/aios-gitops
Environment=AIOS_GITOPS_WORKSPACE=/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime
Environment=AIOS_GITOPS_REPO=/data/aios-gitops/repo.git
Environment=AIOS_GITOPS_BARE_REPO=/data/aios-gitops/repo.git
Environment=AIOS_GITOPS_AUTHORITATIVE_GIT_DIR=/data/aios-gitops/repo.git
Environment=AIOS_GITOPS_REPOSITORY_IDENTITY=aios/aios
Environment=AIOS_GITOPS_BASELINE_AUTHORITY=/var/lib/aios-gitops-runtime-anchor/baseline/current.json
Environment=AIOS_GITOPS_DURABLE_JOB_EXECUTION_ENABLED=false
Environment=AIOS_GITOPS_JOB_CONTROLLER_ENABLED=false
Environment=AIOS_GITOPS_WORKSPACE_ROLE=runtime-anchor
Environment=AIOS_GITOPS_RUNTIME_OID=2852c2a40394195192c6c02a215ba31764e2cdda
Environment=AIOS_GITOPS_ARTIFACT=/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime/governed-runtime-package.json
Environment=AIOS_GITOPS_ARTIFACT_SHA256=932431dbbdee55522342d9c94457f40d8f6f7e4fc7eaadc71c66d0a6bdf8472f
Environment=AIOS_GITOPS_DEPLOYMENT_STATE=CANDIDATE_ISOLATED
Environment=AIOS_GITOPS_MODEL_API_SERVICE=aios-gitops-model-api-v2r2c2-baseline11-candidate.service
Environment=AIOS_RUNTIME_CANDIDATE_TREE_OID=ccd67c2357e7a4cd74e06be61ab81bba820d7671
Environment=AIOS_RUNTIME_PROVENANCE_COMMIT=2852c2a40394195192c6c02a215ba31764e2cdda
Environment=AIOS_AUTHORITY_SOURCE_PUBLISHED=true
Environment=AIOS_GITOPS_WRITES_ENABLED=false
Environment=AIOS_GITOPS_LEGACY_GLOBAL_WRITES_ENABLED=false
Environment=AIOS_SILENT_RELEASE_EXECUTION_ENABLED=false
Environment=AIOS_SILENT_RELEASE_STATE_ROOT=/var/lib/aios-gitops/model-api-v2r2c2-baseline11-candidate/silent-release
Environment=AIOS_BRIDGE_V2_URL=http://127.0.0.1:18892
Environment=AIOS_GITOPS_HOST=127.0.0.1
Environment=AIOS_GITOPS_PORT=18897
Environment=PYTHONPATH=/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime:/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime/app:/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime/runtime-agent-os
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_ENABLED=true
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_SOCKET=/run/aios-trusted-candidate-builder.sock
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_POLICY=/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime/config/gitops/trusted-candidate-builder-policy.json
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_PUBLIC_KEYRING=%d/builder_public_keyring
Environment=AIOS_TRUSTED_CANDIDATE_BUILDER_CREDENTIALS_DIRECTORY=%d
Environment=AIOS_TRUSTED_BUILDER_SOURCE_ADMISSION_ROOT=/var/lib/aios-release-source-admission
Environment=AIOS_SILENT_RELEASE_TRUSTED_POLICY=/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime/config/gitops/trusted-release-policy.json
LoadCredentialEncrypted=builder_hmac:/etc/credstore.encrypted/aios-trusted-candidate-builder/builder_hmac
LoadCredentialEncrypted=source_admission_hmac:/etc/credstore.encrypted/aios-release-source-admission/source_admission_hmac
LoadCredential=builder_public_keyring:/etc/aios-gitops/trusted-builder/public-keyring.json
ExecStart=/usr/bin/python3 -B /opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime/app/aios_model_api_staging.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET
ReadOnlyPaths=/opt/aios/gitops-model-api/2852c2a40394195192c6c02a215ba31764e2cdda/runtime /data/aios-gitops/repo.git /etc/aios-gitops/trusted-builder/public-keyring.json /var/lib/aios-release-source-admission /var/lib/aios-release-migration
ReadWritePaths=/var/lib/aios-gitops/model-api-v2r2c2-baseline11-candidate
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,28 @@
{
"schema": "aios.unified-platform-program-contract.v1",
"goal_slug": "AIOS-UNIFIED-PLATFORM-CONVERGENCE",
"authority_base": "beabd053961047f170e87e1110f2636dbd50f5fb",
"existing_identity_lineage_dependency": {
"mission_id": "mission-f3af205b-7dd6-455f-a561-88f0294cc815",
"root_task_id": "task-133ad96e-e0d7-4202-934f-80c75053b11f",
"acceptance_mutation_allowed": false
},
"tracks": {
"A": "IDENTITY_LINEAGE_EXISTING_MISSION",
"B": "ANNEX_2_3_R2_AUTO_CLAIM",
"C": "PLATFORM_GENERATION_G0",
"D": "GOVERNED_MERGE_STAGING_RELEASE"
},
"program_acceptance": [
"U01",
"U02",
"U03",
"U04",
"U05",
"U06",
"U07",
"U08"
],
"deployment_requires_review_go": true,
"new_business_authority_created": false
}
+1
View File
@@ -0,0 +1 @@
{"bindings":[{"acceptance_key":"C01","acceptance_position":0,"lineage_request_digest":"e9946481e7c9fe8dee94aa42ffa0a9b41546ce9d46601ff221a958710c72f6cc","mission_id":"mission-f3af205b-7dd6-455f-a561-88f0294cc815","owner_principal_id":"local-owner-authority","parent_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","root_acceptance_ref":"mission-acceptance:mission-f3af205b-7dd6-455f-a561-88f0294cc815:identity-lineage-live-closure-v1","root_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","route":"GOVERNED_GIT","schema":"aios.autonomous-child-binding.v1","sibling_sequence":1,"task_id":"task-1668131d-7484-4c8e-83b4-2cf9927842fe","task_plan_digest":"3f0824f51d824de7d38821205c012c2a23ac11e9f5889b8b0aafb07cce85333f"},{"acceptance_key":"C02","acceptance_position":1,"lineage_request_digest":"84f28d350ef3a2dce47fb5f3163b61bfd329ce5fe85bd59c886d4813e806b602","mission_id":"mission-f3af205b-7dd6-455f-a561-88f0294cc815","owner_principal_id":"local-owner-authority","parent_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","root_acceptance_ref":"mission-acceptance:mission-f3af205b-7dd6-455f-a561-88f0294cc815:identity-lineage-live-closure-v1","root_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","route":"SEMANTIC_IMPORT","schema":"aios.autonomous-child-binding.v1","sibling_sequence":2,"task_id":"task-24fe3d9a-783e-4a68-bf9c-5b1a9d2b56c7","task_plan_digest":"e01680583b8cc2ec3d33026dda9f7d095692d0ae79fc43f380066511566e305e"},{"acceptance_key":"C03","acceptance_position":2,"lineage_request_digest":"148fa8b55224649ac80f4d9d5c344019dfef24b1d5ed614ef62ee71ccaf8a6d3","mission_id":"mission-f3af205b-7dd6-455f-a561-88f0294cc815","owner_principal_id":"local-owner-authority","parent_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","root_acceptance_ref":"mission-acceptance:mission-f3af205b-7dd6-455f-a561-88f0294cc815:identity-lineage-live-closure-v1","root_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","route":null,"schema":"aios.autonomous-child-binding.v1","sibling_sequence":3,"task_id":"task-78e0dff5-c1fe-491a-99d2-81ad5cf180da","task_plan_digest":"aef0c0cfee2686ad7bff122328408c5094370bf29fac37a49ddc50173fa60986"},{"acceptance_key":"C04","acceptance_position":3,"lineage_request_digest":"927dde8068b9dde4d57e0f9eb6bd52d95d761533974a7437d01707ea3c5c807d","mission_id":"mission-f3af205b-7dd6-455f-a561-88f0294cc815","owner_principal_id":"local-owner-authority","parent_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","root_acceptance_ref":"mission-acceptance:mission-f3af205b-7dd6-455f-a561-88f0294cc815:identity-lineage-live-closure-v1","root_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","route":null,"schema":"aios.autonomous-child-binding.v1","sibling_sequence":4,"task_id":"task-9b6e0cb1-c54a-4f91-8762-7c72112cf71d","task_plan_digest":"d1eae4e90a5892dd851fbe80c2ee0920e7060cd6ce701ae0522d110dcd9af638"},{"acceptance_key":"C05","acceptance_position":4,"lineage_request_digest":"4336de284be15d504af661c18a98c820da3c3518ef5df25d4607c19c3ba724b2","mission_id":"mission-f3af205b-7dd6-455f-a561-88f0294cc815","owner_principal_id":"local-owner-authority","parent_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","root_acceptance_ref":"mission-acceptance:mission-f3af205b-7dd6-455f-a561-88f0294cc815:identity-lineage-live-closure-v1","root_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","route":null,"schema":"aios.autonomous-child-binding.v1","sibling_sequence":5,"task_id":"task-bd8c44c5-7081-4a3f-a3b0-4de4ad6e3b29","task_plan_digest":"dd7e02ef51f601210295c9bda1061dcd7ff944bc7a5dcb2e813db2d601b138a4"},{"acceptance_key":"C06","acceptance_position":5,"lineage_request_digest":"54bdc5cf4414bc52105e966072009c9b0ce8235392f767b38e856072dd01148c","mission_id":"mission-f3af205b-7dd6-455f-a561-88f0294cc815","owner_principal_id":"local-owner-authority","parent_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","root_acceptance_ref":"mission-acceptance:mission-f3af205b-7dd6-455f-a561-88f0294cc815:identity-lineage-live-closure-v1","root_task_id":"task-133ad96e-e0d7-4202-934f-80c75053b11f","route":null,"schema":"aios.autonomous-child-binding.v1","sibling_sequence":6,"task_id":"task-591a87fa-e722-4647-a71d-80624b7e6b6a","task_plan_digest":"9b0d092f6a0a987844edae129eaedfc6bed87c135d8d3db390b0f01b43f896c8"}],"policy_version":"identity-lineage-autonomous-next-child-v1","schema":"aios.autonomous-child-binding-set.v1"}
@@ -0,0 +1,96 @@
# G7 — Security-Critical Closure 证据与判定
- **Gate**: G7_SECURITY_CRITICAL_CLOSURE
- **判定**: `G7_CHANGES_REQUIRED`(修复已就绪,等待上游治理合入 + staging_readback + ROOT_REVIEW 后升为 APPROVED_FOR_GOVERNED_SUBMISSION)
- **Authority HEAD (base OID)**: `beabd053961047f170e87e1110f2636dbd50f5fb`
- **工作分支**: `agent/g7-security-critical-closure-20260914`(基于 origin/main;本地修复分支,待 PR + Owner 合入 main,不绕过保护 main)
- **日期**: 2026-09-14
- **扫描源**: `aios-scan-full.json` (1.08MB, 1039 findings, 高召回含误报)
- **规模**: 729 文件 / 24 个指派文件修改 / 5 个新增负面测试套件
## 摘要
全仓 review 为高召回设计并含误报,故未机械修复全部 1039 条发现。本次仅闭环被
权威源码复核为 CONFIRMED 且直接影响控制面授权、凭证安全、任意文件访问、特权 UI
的 **P0 阻塞项(6 项)+ P1 交付正确性包(若干)**。DEAD_PATH/FALSE_POSITIVE 项已
按契约排除,并记录依据。
## 修复分账(S1–S5)
| 阻塞项 | 角色 | 分类 | 文件 | pre→post SHA | 修复要点 |
|---|---|---|---|---|---|
| SEC-P0-AUTH-ROLE-HEADER | S1 | CONFIRMED | adapters/gitops/aios_gitops_model_api.py | 60396e0→8990317 | role/user 由服务端 token→identity 映射派生,忽略 X-AIOS-User/Role 头 |
| SEC-P0-AUTH-ROLE-HEADER | S1 | CONFIRMED | active-runtime/app/aios_gitops_model_api.py | ac8c1bf→80b04bd | 同上 |
| SEC-P0-TASK-ACTION-ROLE | S1 | CONFIRMED | active-runtime/app/task_action_api.py | b18e0a5→31c6f6e | 忽略 body role,role 由注入认证主体派生,默认 user |
| SEC-P0-OBSERVABILITY-ROLE | S1 | CONFIRMED | active-runtime/app/agent_observability_api.py | f516ef9→6b065fb | 认证 tenant 决定查询范围,客户端 role=owner 被忽略 |
| SEC-P0-BOOTSTRAP-CREDENTIAL | S2 | CONFIRMED | active-runtime/migrations/002_seed_owner_admin_user.sql | 7fff9e5→4cb5a33 | 种子空哈希(强制重置),清除 bootstrap-change-me |
| SEC-P0-BOOTSTRAP-CREDENTIAL | S2 | CONFIRMED | active-runtime/app/aios_webops_app.py | 83387a8→7001142 | PBKDF2 随机盐;SECRET 缺失 fail-closed;拒绝明文回退;会话 HMAC+过期 |
| SEC-P0-BOOTSTRAP-CREDENTIAL | S2 | CONFIRMED | active-runtime/app/aios_v5_webops_app.py | 83387a8→7001142 | 同上 |
| SEC-P0-CONTROL-PLANE-FILE-READ | S3 | CONFIRMED | active-runtime/app/aios_chatgpt_actions_api.py | 47ae2c8→4f345a0 | 命令读取:认证+严格标识+包含校验+1MiB 上限 |
| SEC-P0-CONTROL-PLANE-FILE-READ | S3 | CONFIRMED | active-runtime/app/aios_model_api_staging.py | 1616b37→e94d0dc | 同上 |
| SEC-P1-PATH-CONTAINMENT | S3 | CONFIRMED | services/aios-event-ingress/server.py | c790d41→c4e93cb | ID 白名单+resolve 包含校验 |
| SEC-P1-PATH-CONTAINMENT | S3 | CONFIRMED | active-runtime/app/agent_observability_store.py | 40cc555→4746b3a | execution_id 白名单+包含校验 |
| SEC-P1-PATH-CONTAINMENT | S3 | CONFIRMED | active-runtime/app/aios_campaign_recovery.py | 0d09fd2→06e208f | 证据路径 resolve+realpath 包含校验 |
| SEC-P1-PATH-CONTAINMENT | S3 | CONFIRMED | scripts/gitops/task_worktree.sh | 72d33f4→19ea429 | TASK_ID 正则收紧,拒绝 .. ../ ..\ |
| SEC-P1-PATH-CONTAINMENT | S3 | CONFIRMED | scripts/init_gitops_baseline.sh | 472e60c→48f584e | zip-slip 解包前条目校验 |
| SEC-P1-PATH-CONTAINMENT | S3 | CONFIRMED | ops/gitops-migration/sync_gitea_owner_password.sh | 11573c9→8e85831 | 消除 env 来源注入,argv+引号 heredoc |
| SEC-P0-ADMIN-UI-XSS | S4 | CONFIRMED | active-runtime/app/static/admin-ai-evolution.html | 6da3ce8→05d26ab | textContent 全面转义;修复 JS 语法;用真实 governed 审批端点替代本地 fake Approve |
| SEC-P1-WEB-CONTROL-SURFACE | S4 | CONFIRMED | overlays/.../gitops-dashboard/index.html | 7f459a1→484447b | 客户端 CSRF token 传播;状态变更带 token;branch 服务端校验 |
| SEC-P1-RUNTIME-DELIVERY | S5 | CONFIRMED | agents/registry.yaml | 7b69d96→0037e73 | 解决阻塞性 merge conflict,恢复可解析 YAML |
| SEC-P1-RUNTIME-DELIVERY | S5 | CONFIRMED | active-runtime/app/aios_model_api_staging.py | 1616b37→e94d0dc | 消除 do_POST 遮蔽(合并 bridge 路由) |
| SEC-P1-RUNTIME-DELIVERY | S5 | CONFIRMED | docs/gitops/openapi-chatgpt-actions.yaml | 5278b91→9f339c0 | 补全 securitySchemes.taskPrincipalAuth |
| SEC-P1-RUNTIME-DELIVERY | S5 | CONFIRMED | deploy/install/install.sh | ab1f433→b90b759 | 移除 `\|\| true`,启动失败传播 |
| SEC-P1-RUNTIME-DELIVERY | S5 | CONFIRMED | scripts/silent_install_v5.sh | 59e9552→e26a3b3 | 缺失脚本显式报错 |
| SEC-P1-RUNTIME-DELIVERY | S5 | CONFIRMED | delta-fix/apply_delta_fix_only.sh | de1e2fd→d09d2ab | 修复未定义变量 |
| SEC-P1-RUNTIME-DELIVERY | S5 | **DEAD_PATH** | active-runtime/app/architecture_owner_gate_http.py | 未改 | 全库零引用,非活路径 |
| SEC-P1-RUNTIME-DELIVERY | S5 | **DEAD_PATH** | runtime/agent-os/api.py | 未改 | 全库零引用独立模块,facade 签名不符但无活路径 |
## 负面测试(S6 独立 QA 验证)
新增 5 个测试套件,均针对**真实已修复模块**(经 tests/conftest.py 的 active_runtime 命名空间映射到实际文件):
- `tests/test_g7_auth_negative.py` — 9 passed
- `tests/test_g7_credential_negative.py` — 4 passed
- `tests/test_g7_path_containment_negative.py` — 7 passed(+8 bash 用例如下)
- `tests/test_g7_runtime_delivery.py` — 4 passed
- `tests/test_g7_web_surface_negative.py` — 42 passed
**合计 66 passed / 0 failed**。8 个 bash 子进程用例如 Windows 沙箱 pytest 无法拉起 bash 而 skip,
已在本机 Bash 环境实测真实脚本:遍历 TASK_ID → rc=65 拒绝;zip-slip `../` 条目 → BLOCKED 退出 11。
### 覆盖的负面场景
- spoof `X-AIOS-Role: owner` / `X-AIOS-User` → 不能提权
- JSON `role=owner` → 被忽略
- observability `role=owner` + 外来 user/group → 限定自身
- 非 owner token 下 `require_write(['owner'])` → 403
- 已知 bootstrap 口令拒绝登录;哈希非明文且随机盐;缺 SECRET 拒绝启动;过期令牌失效
- POSIX `../` / Windows `..\` / 编码遍历 / 超大读取 / zip-slip / 绝对路径拒绝
- HTML `<img onerror>` / script 载荷保持文本;CSRF-less 状态变更被拒;无效 branch 被拒
- registry.yaml / openapi 可解析且引用完整;install.sh 失败传播;脚本无未定义变量
### 回归
- 全部 `active-runtime/app` + `adapters` 211 个 py 文件 `py_compile` 通过(无导入/Syntax 回归)
- 全部 15 个改动/新增 py 文件编译通过
## 证据字段映射(G7_SECURITY_GATE.md Required evidence)
对每个阻塞项均已具备:finding_id / authority_oid(=beabd05…) / affected_path / live_path=true /
preimage_sha256(上表)、fix_commit_oid(见下方提交)、postimage_sha256(上表)、
negative_tests(通过)、positive_tests(编译+回归)、independent_review(S6 独立运行通过)、
runtime/staging_readback、rollback(见 S2 迁移/回滚路径)。
## 判定与下一步
- **当前判定**: `G7_CHANGES_REQUIRED` → 修复就绪,待合入流程推进。
- **下一步**(需 Owner/治理,非本 Agent 越权执行):
1. 审查并合入 `agent/g7-security-critical-closure-20260914`(走 protected main PR + Owner approval)
2. staging 部署后作 runtime/staging_readback(真实服务启动验证,尤其 S2 的 AIOS_WEBOPS_SECRET 注入与 S1 的 token 映射文件格式)
3. 执行 S2 owner 引导设密流程(scripts/patch_runtime_owner_admin_user.py)设置真实 owner 强口令
4. 独立 final review → governed submission → staging → live E2E → production → ROOT_REVIEW
- **已知未闭环**(诚实标注): S1 的 task_action/observability 认证主体需宿主 FastAPI 应用注入
`set_authenticator()`,当前默认按最低权限 user(安全降级但 owner 能力需接线);staging_readback
与 rollback 实机验证需在部署环境执行,本环境未具备。
## 备注(Triage 纪律)
- 未机械处理 1000+ 条 medium/low/维护性/样式/历史死代码注释(契约明确非当前发布阻塞项)。
- DEAD_PATH 项未改动并给出全库引用证据。
+1 -1
View File
@@ -15,5 +15,5 @@ bash "$PKG_ROOT/scripts/smoke_test.sh" "$AIOS_RUNTIME_ROOT"
python3 "$PKG_ROOT/scripts/generate_release_manifest.py" "$AIOS_RUNTIME_ROOT"
echo "DELTA_FIX_ONLY_APPLY_OK release=$AIOS_RELEASE_ID runtime=$AIOS_RUNTIME_ROOT"
python3 "$PKG/fixes/patch_runtime_v5_guards.py" "$TARGET"
python3 "$PKG_ROOT/fixes/patch_runtime_v5_guards.py" "$AIOS_RUNTIME_ROOT"
echo "V5_DIGITAL_LIFE_DELTA_APPLIED"
+4 -4
View File
@@ -33,11 +33,11 @@ cp deploy/systemd/aios-autonomous-daemon.service /etc/systemd/system/
cp deploy/systemd/aios-v1r1-wechat-worker.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable aios-autonomous-daemon.service || true
systemctl enable aios-v1r1-wechat-worker.service || true
systemctl enable aios-autonomous-daemon.service
systemctl enable aios-v1r1-wechat-worker.service
systemctl restart aios-autonomous-daemon.service || true
systemctl restart aios-v1r1-wechat-worker.service || true
systemctl restart aios-autonomous-daemon.service
systemctl restart aios-v1r1-wechat-worker.service
sleep 2
systemctl status aios-autonomous-daemon.service --no-pager || true
+51
View File
@@ -0,0 +1,51 @@
# ADR: R2 evidence digests reuse TaskPlane canonical JSON
Status: ACCEPTED FOR R2 REVIEW CANDIDATE
Date: 2026-09-14
## Context
R1 introduced local sorted/separator JSON encoders in schedulability proof and
Retry/Toil fingerprint code. Platform Baseline V4 §1.3/§4.4/§12 prohibits new
parallel canonical encoders unless a scoped ADR proves the existing contract is
not applicable.
The candidate already carries the established TaskPlane contract:
`app.task_plane.models.canonical_json(value)`
which serializes UTF-8 JSON deterministically with sorted keys and compact
separators.
## Decision
R2 reuses `task_plane.models.canonical_json` for:
1. schedulability observation/proof digests;
2. Retry/Toil operation fingerprint payloads.
Retry/Toil adds a domain schema wrapper
`aios.retry-toil-fingerprint.v1` before hashing so the digest domain is
separated without inventing a new byte encoder.
No generic canonical platform, second encoder, or new write authority is added.
## Bytes and algorithm
- canonical text producer: `task_plane.models.canonical_json`
- bytes: UTF-8 encoding of that exact text
- digest: SHA-256 hex
- schedulability schema: `aios.schedulability-proof.v2`
- retry fingerprint schema: `aios.retry-toil-fingerprint.v1`
## Migration / compatibility
These R2 modules are not yet live authorities. Their R1 digests are review
candidate evidence only and do not require backwards-compatible production
decoding. Any future persisted proof/fingerprint consumer must bind schema
version explicitly.
## Guard
R2 tests reject a reintroduction of local `json.dumps(sort_keys=True,
separators=...)` encoders in the two modules.
+232
View File
@@ -0,0 +1,232 @@
# AIOS Unified Platform Convergence — Architecture Change V2
Status: R2 REVIEW CANDIDATE
Date: 2026-09-14
Authority base for rebind: `beabd053961047f170e87e1110f2636dbd50f5fb`
## 1. Purpose
R2 preserves the R1 architectural direction but corrects the eight review
findings F01–F08. It does not authorize deployment.
The program still has four tracks:
1. existing Identity / Lineage S3;
2. ANNEX-2.3 R2 Auto-Claim / Execution Integrity;
3. Platform Generation G0 (WP0a–e/WP1/WP2 only);
4. governed merge, staging, release and root/program acceptance.
Existing Identity/Lineage mission/root/children remain authoritative and are
not recreated or redefined by this program.
## 2. Authority split
- Git authority main: `beabd053961047f170e87e1110f2636dbd50f5fb`
- release-control runtime: `3c320aeb89d9ba8f0ed516abeb57252682b18983`
- production V2R2C2: `4642b74af62c89a505ae49241e805a5a88ca897f`
A review candidate is not a governed commit. All target preimages must be
refreshed from live Git authority before submission.
## 3. R2 safety corrections
### 3.1 Caller evidence is fail-closed
`caller_scan v2` separates:
- static source caller evidence;
- active runtime caller evidence;
- test-only evidence;
- opaque/dynamic uncertainty;
- complete negative evidence.
Static source imports alone do not prove runtime LIVE.
`PROVEN_ORPHAN` requires all of:
- zero production static caller;
- zero active systemd/API/scheduler/runtime-loaded caller;
- relevant runtime host set non-empty and completely scanned;
- loaded bytes negative proof;
- systemd/API/scheduler scan coverage;
- complete observation window;
- no probe failures/opaque dynamic references;
- rollback reference;
- independent acceptance `GO`.
Incomplete negative evidence returns `LIKELY_ORPHAN`; probe failure, opaque
dynamic import, or unresolved production source caller returns `UNKNOWN`.
UNKNOWN can never drive Sunset.
### 3.2 Schedulability proof is diagnostic, final claim is authoritative
R2 checks task lifecycle state and treats explicit `max_concurrency=0` as zero.
Invalid capacity values are structured blockers.
The proof remains non-bearer. Claim has three stages:
1. preflight observation;
2. claim-time read-only recheck;
3. existing TaskPlane authority performs `claim_atomically` at the
linearization point.
The atomic adapter contract must revalidate task revision/generation/status,
worker generation/identity/health/capacity, authorization ref/revision,
scope, dependencies, freeze and conflicting lease inside the same
authoritative transaction/fenced operation that creates the claim.
The returned receipt must include claim id, lease id, fencing generation and
receipt revision.
R2 reference tests verify the adapter contract only. They do **not** prove a
production adapter is atomic; real staging A18–A23 remain mandatory.
### 3.3 Retry Governor is whitelist-only
- UNKNOWN/indeterminate → RECONCILE;
- governance/dependency boundary → BLOCK;
- proven success → SUPPRESS/REUSE;
- unrecognized state → BLOCK;
- only explicitly executable states may return EXECUTE.
A prior success for the same operation conflicting with a later NOT_APPLIED
observation must reconcile rather than re-execute.
### 3.4 Canonical bytes are reused
Schedulability and Retry/Toil reuse
`app.task_plane.models.canonical_json`; no second JSON canonical encoder is
introduced. See `ADR_CANONICAL_EVIDENCE_DIGEST_V1.md`.
### 3.5 Platform G0 discovery is not G0 acceptance
WP0a–d and WP1 now expose completeness/missing evidence. Lexical/static
discovery cannot by itself satisfy Generation-G0 Exit.
See `G0_EVIDENCE_COVERAGE_V2.md`.
## 4. Non-circular review/deploy gates
R1's production gate was circular because existing root C06 and Program
production evidence could be required before production itself.
R2 replaces it with:
```text
REVIEW_READY
↓
STAGING_ACTIVATION_READY
↓
STAGING_ACCEPTED
↓
PRODUCTION_ELIGIBLE
↓
PRODUCTION_ACCEPTED
↓
PROGRAM_COMPLETE
```
### REVIEW_READY
Requires:
- live authority rebind + exact preimages;
- ChangeEnvelope complete;
- complete affected repository regression;
- independent review GO.
### STAGING_ACTIVATION_READY
Requires:
- governed commit + authority readback;
- bounded staging activation scope;
- rollback/recovery plan ready;
- pre-activation lab/contract evidence.
It does **not** require evidence that only real staging activation can produce.
### STAGING_ACCEPTED
Requires actual staging:
- TaskPlane adapter/claim/lease/fencing;
- root action executor;
- ANNEX A01–A23;
- restart/response-loss/sibling concurrency/recovery evidence.
### PRODUCTION_ELIGIBLE
Requires:
- STAGING_ACCEPTED;
- existing release authorization valid;
- production rollback ready;
- freeze/NO-GO clear.
### PRODUCTION_ACCEPTED
Requires actual deployment:
- production deployment/runtime readback;
- authenticated business E2E;
- external projection readback.
### PROGRAM_COMPLETE
Only after production acceptance:
- existing Identity/Lineage original C01–C06 all accepted;
- Program final acceptance all satisfied;
- final root review proves no remaining acceptance gaps.
The existing root acceptance contract is unchanged. Its full completion is
consumed only by the final program-complete gate.
## 5. State authority rules
No new business authority is added.
- caller_scan: read-only evidence producer;
- WP0 inventories: read-only discovery;
- Environment Profile: read-only host profile;
- Retry Governor: decision/audit only;
- SchedulabilityProof: short-lived diagnostic evidence;
- SystemTruth: read-only derived view;
- TaskPlane / existing claim transaction: authoritative task/claim/lease state;
- existing mission/continuation path: root-action executor;
- existing release Saga: release authority.
No new Task DB, scheduler, mission store, release controller or authorization
authority is introduced in R2.
## 6. Migration
- Existing mission/root/children: preserve IDs and acceptance.
- Legacy durable task/job: attach metadata only; do not replace identity.
- Schedulability proof: persist only through existing evidence/journal if
persistence is required.
- No direct SQLite mutation outside existing TaskPlane repository.
- G0 outputs remain shadow/read-only.
## 7. Deployment status
R2 code may proceed to live-authority rebase and repository-native review after
its focused regressions pass.
Deployment remains NO-GO until:
- live preimages are refreshed;
- full repository regressions pass;
- atomic TaskPlane adapter is implemented and verified;
- ANNEX A18–A23 pass;
- real root-action executor consumes PENDING intents exactly once;
- independent staging review is GO.
## 8. Rollback
Review candidate: discard candidate.
Governed but undeployed commit: use existing successor/revert path; no shared
branch reset/force push.
Staging: disable feature/policy binding and preserve evidence.
Production: existing release Saga rollback only.
## 9. Deferred platform work
WP3–WP10 remain dependency-gated backlog from Platform Baseline V4. R2 does not
claim their acceptance and does not pre-build them.
+21
View File
@@ -0,0 +1,21 @@
# G0 Evidence Coverage V2
R2 corrects the R1 wording: WP0a–d and WP1 outputs are **discovery evidence**,
not complete Generation-G0 acceptance by themselves.
| WP | R2 output | What it proves | Still required for G0 Exit |
|---|---|---|---|
| WP0a | PARTIAL_DISCOVERY | authority/runtime/production OID relation | VERSION/current/baseline/README/versions/release metadata reader-writer-caller/live mapping |
| WP0b | PARTIAL_DISCOVERY | lexical Git-writer candidates | writer→auth→repo→ref→CAS→receipt→caller graph and live classification |
| WP0c | PARTIAL_DISCOVERY | authorization references by term/subdomain hint | owner/broker, TaskPrincipal, ClaimLease/TaskLease, WriterLease, release/recovery, signer chain + revocation/live caller |
| WP0d | PARTIAL_DISCOVERY | function-level canonical/digest candidates | producer/consumer, input schema, bytes rule, algorithm and duplicate semantic groups |
| WP0e | caller_scan v2 | fail-closed source/runtime caller evidence | five historical known-case acceptance + complete runtime evidence for any PROVEN_ORPHAN |
| WP1 | capability discovery / requirements evaluated | actual local capability probes | declared host requirements for each key execution host, missing requirements resolved |
| WP2 | conservative decision layer | retry/suppress/reconcile/block decision | audit baseline on real operations; no scheduler authority |
`g0_exit_eligible=false` in WP0a–d output is intentional. The G0 gate is a
separate acceptance aggregation step and must reference the missing evidence
listed above.
WP3–WP10 remain dependency-gated backlog exactly as Platform Baseline V4
defines; R2 does not silently redefine the long-term platform goal to U01–U09.
+65
View File
@@ -0,0 +1,65 @@
{
"schema": "aios.unified-review-gates.v2",
"gates": [
{
"id": "REVIEW_READY",
"requires": [
"BASE_REBOUND",
"CHANGE_ENVELOPE_COMPLETE",
"REPO_REGRESSION_PASS",
"INDEPENDENT_REVIEW_GO"
]
},
{
"id": "STAGING_ACTIVATION_READY",
"requires": [
"REVIEW_READY",
"GOVERNED_COMMIT_READBACK",
"LIMITED_SCOPE_PLAN",
"ROLLBACK_READY",
"LAB_CONTRACT_PASS"
]
},
{
"id": "STAGING_ACCEPTED",
"requires": [
"STAGING_ACTIVATION_READY",
"LIVE_ADAPTER_PASS",
"ROOT_EXECUTOR_PASS",
"ANNEX_A01_A23_PASS",
"FAILURE_RECOVERY_PASS"
]
},
{
"id": "PRODUCTION_ELIGIBLE",
"requires": [
"STAGING_ACCEPTED",
"RELEASE_AUTHORIZATION_VALID",
"PRODUCTION_ROLLBACK_READY",
"NO_GO_CLEAR"
]
},
{
"id": "PRODUCTION_ACCEPTED",
"requires": [
"PRODUCTION_ELIGIBLE",
"PRODUCTION_DEPLOYMENT_READBACK",
"BUSINESS_E2E_PASS",
"EXTERNAL_PROJECTION_READBACK"
]
},
{
"id": "PROGRAM_COMPLETE",
"requires": [
"PRODUCTION_ACCEPTED",
"EXISTING_ROOT_C01_C06_ACCEPTED",
"PROGRAM_U01_U09_ACCEPTED",
"FINAL_ROOT_REVIEW_COMPLETE"
]
}
],
"notes": [
"No gate may require evidence produced only by its own downstream action.",
"Existing Identity/Lineage root acceptance is unchanged; full C01-C06 is consumed only at PROGRAM_COMPLETE."
]
}
+56
View File
@@ -0,0 +1,56 @@
# AIOS Unified Platform Convergence — Task Contract V2
## Program goal
Preserve existing governance and tenant isolation while converging
Identity/Lineage, ANNEX-2.3 execution integrity, Platform Generation G0 and the
existing release/recovery path into one reviewable and recoverable program.
## Existing dependency
Identity/Lineage Mission:
- `mission-f3af205b-7dd6-455f-a561-88f0294cc815`
- root `task-133ad96e-e0d7-4202-934f-80c75053b11f`
Its C01–C06 acceptance is immutable.
## Program acceptance
| Key | Acceptance |
|---|---|
| U01 | authority/runtime/production baseline reconciled and candidate bound to current Git authority |
| U02 | existing Identity/Lineage mission identity and acceptance contract preserved; pre-production dependencies C01–C05 reconciled without claiming root complete |
| U03 | ANNEX-2.3 A01–A23 accepted in real staging/lab as specified |
| U04 | Platform Generation G0 Exit independently accepted; partial discovery output alone is insufficient |
| U05 | unified governed commit + authority readback |
| U06 | staging integrated E2E root→child→proof→ROOT_REVIEW→actual action→re-review |
| U07 | production deployment + runtime/business/external readback accepted |
| U08 | restart/response-loss/sibling concurrency/rollback/recovery accepted |
| U09 | existing Identity/Lineage original C01–C06 fully accepted and final Program ROOT_REVIEW proves no remaining gaps |
## Gate consumption
- REVIEW_READY consumes no staging/production evidence.
- STAGING_ACTIVATION_READY consumes governed commit/readback and lab evidence,
but no evidence that requires staging activation.
- STAGING_ACCEPTED consumes live staging evidence.
- PRODUCTION_ELIGIBLE consumes staging acceptance + release authorization.
- PRODUCTION_ACCEPTED consumes live production evidence.
- PROGRAM_COMPLETE consumes U01–U09 including final existing-root acceptance.
## Autonomous rule
Any child terminal:
`child evidence → ROOT_REVIEW → reload original acceptance → query descendants
→ next legal action`.
A child/local/gate PASS closes only that scope. It never closes the Program by
itself.
## Naming
New task display names retain:
`TASK-<SAFE_USER_ALIAS>-<UTC>-R<ROOT8>-P<PARENT8>-D<DEPTH>-C<SEQ>-<PURPOSE>`.
Authoritative lineage is persisted separately; existing durable IDs are not
renamed/recreated.
+66
View File
@@ -0,0 +1,66 @@
# Unified Workstream DAG & Acceptance Matrix V2
## Workstream DAG
```text
Baseline Reconciliation
├─ Track A Identity/Lineage existing mission
│ ├─ C01 Governed commit/readback
│ ├─ C02 Staging activation
│ ├─ C03 Root-action executor
│ ├─ C04 Legacy attach
│ ├─ C05 Live failure/replay/concurrency E2E
│ └─ C06 Production convergence
├─ Track B ANNEX-2.3 R2
│ ├─ B1 Schedulability proof
│ ├─ B2 Discovery diagnostics
│ ├─ B3 Claim-time read recheck
│ ├─ B4 Atomic TaskPlane claim/lease/fencing
│ ├─ B5 Execution/heartbeat
│ └─ B6 Evidence/readback A01–A23
└─ Track C Platform G0
├─ WP0a Version/Baseline discovery
├─ WP0b Git writer discovery
├─ WP0c Authorization discovery
├─ WP0d Canonical/Digest discovery
├─ WP0e caller_scan
├─ WP1 Environment Profile
└─ WP2 Retry/Toil Governor
A + B + C REVIEW_READY
↓
governed commit/readback
↓
STAGING_ACTIVATION_READY
↓
actual staging activation + A01–A23 + executor
↓
STAGING_ACCEPTED
↓
PRODUCTION_ELIGIBLE
↓
actual production deployment/readback
↓
PRODUCTION_ACCEPTED
↓
existing root C01–C06 + Program final review
↓
PROGRAM_COMPLETE
```
No gate depends on evidence generated only by its own downstream action.
## R2 matrix
| Gate/Area | R2 code/review status | Deployment evidence still required |
|---|---|---|
| caller_scan v2 | focused tests required | historical known-case + live runtime proof |
| schedulability proof | focused tests required | live adapter linearization/lease/fence |
| atomic claim contract | reference contract/tests only | TaskPlane adapter + race/revoke/freeze staging |
| Retry Governor v2 | focused tests required | real audit baseline |
| WP0a–d | PARTIAL_DISCOVERY by design | complete G0 mapping and independent acceptance |
| WP1 | capability/requirement probe | key-host requirement profiles |
| canonical reuse | ADR + guard test | producer/consumer compatibility in live rebase |
| governed commit | NOT RUN | writer receipt + authority readback |
| staging | NOT RUN | A01–A23, root executor, failure recovery |
| production | NOT RUN | release + business/runtime/external readback |
@@ -0,0 +1,215 @@
# AIOS Unified Platform Convergence R1 — 架构变更说明
## 0. 决策摘要
本变更将 **Identity/Lineage S3、ANNEX-2.3 R2 Auto-Claim、平台化 Phase-0、
Governed Merge/Release** 收敛为一个新的上层 Program,而不是改写已存在
Identity/Lineage Mission 的原始验收合同。
核心原则:
1. **新 Program,不重写旧 root。** 已有 Mission `mission-f3af205b-7dd6-455f-a561-88f0294cc815` 和 root
`task-133ad96e-e0d7-4202-934f-80c75053b11f` 原样保留,C01–C06 的 acceptance 不变。
2. **唯一 Git authority 是 `beabd053961047f170e87e1110f2636dbd50f5fb`。** release-control runtime
`3c320aeb89d9ba8f0ed516abeb57252682b18983` 和 production `4642b74af62c89a505ae49241e805a5a88ca897f` 是不同平面,不得互相冒充。
3. **恢复语义,不恢复旧拓扑。** 从 `3c320ae…` 选择性移植 SystemTruth/Execution
Integrity 语义,但不把该后继分支整体推回 main。
4. **单一权威、只读聚合。** 新 inventory/caller_scan/Environment Profile/
Retry Governor 都是 evidence producer,不成为新持久化权威。
5. **先评审,后部署。** 本 R1 是 Review Candidate;禁止在 Review Gate 通过前
进行 staging/production activation。
## 1. 当前权威三平面
| 平面 | OID | 含义 |
|---|---|---|
| Git authority / main | `beabd053961047f170e87e1110f2636dbd50f5fb` | 新候选唯一 source base |
| release-control runtime | `3c320aeb89d9ba8f0ed516abeb57252682b18983` | 现有控制面运行字节,不是 main |
| production baseline | `4642b74af62c89a505ae49241e805a5a88ca897f` | V2R2C2 生产 evidence closure |
`beabd053961047f170e87e1110f2636dbd50f5fb -> 3c320aeb89d9ba8f0ed516abeb57252682b18983` 仅包含 10 个 SystemTruth/consistency 相关路径;
现有 Identity/Lineage 27 文件候选与这 10 个路径无交集。因此 27 文件候选可以
在 **重新读取 authority preimage 并通过 apply/check 后** 重新绑定
`beabd053961047f170e87e1110f2636dbd50f5fb`,但不能复用旧 `3c320ae…` approval/base 记录。
## 2. 新 Program 与既有 Mission 的关系
### 2.1 新上层 Program(尚未在 live TaskPlane 创建)
`goal_slug = AIOS-UNIFIED-PLATFORM-CONVERGENCE`
实际 `mission_id/root_task_id` 必须由 live TaskPlane authority 创建并返回,
本候选不预造 ID。
### 2.2 既有 Identity/Lineage Mission(保留)
- mission: `mission-f3af205b-7dd6-455f-a561-88f0294cc815`
- root: `task-133ad96e-e0d7-4202-934f-80c75053b11f`
- owner: `local-owner-authority`
- alias: `U9CA8400F7`
- legacy durable task: `TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-INTEGRATE-20260913T184412Z-19b7`
- legacy job: `job-7457a322d5f9d6931c7cd99ce3ec6c18`
该 Mission 作为新 Program 的 Track A 依赖/子目标;其历史 task/job/idempotency
身份不可为了新命名而重建。
## 3. 四条 Track
### Track A — Identity / Lineage S3
目标保持:
`root → child → terminal proof → ROOT_REVIEW → actual next action → execution → root re-review`
包含:
- C01 governed candidate commit/readback
- C02 staging lineage policy activation
- C03 root-action executor binding
- C04 legacy attach
- C05 live failure/replay/concurrency E2E
- C06 production convergence
### Track B — ANNEX-2.3 R2 Auto-Claim / Execution Integrity
新链:
`admission/preflight → SchedulabilityProof → discovery → claim-time authoritative recheck
→ lease/fencing/CAS → execution → heartbeat/progress → result/evidence → readback/acceptance`
R1 已实现 review-candidate 代码:
- `task_plane/schedulability.py`
- `task_plane/auto_claim.py`
- A01 风格多 blocker、scope/capability/heartbeat/auth/capacity/freeze/dependency/lease
检查
- proof 仅为诊断证据,不是 bearer credential
- claim 前完整 authority recheck
**R1 不直接替换现有 claim 路径**;必须在 staging 评审时通过 adapter 绑定后才可启用。
### Track C — Platform Phase-0 / Generation G0
仅包含批准的第一 generation:
- WP0a Version/Baseline inventory
- WP0b Git writer inventory
- WP0c Authorization inventory
- WP0d Canonical/Digest inventory
- WP0e caller_scan
- WP1 Environment Capability Profile
- WP2 Retry/Toil Governor
这些组件只读或纯决策:
- 不新增 version authority
- 不新增 canonical authority
- 不写业务状态
- `UNKNOWN` 永远不能被 caller_scan 降级为 orphan
- Retry Governor 只返回决策,不执行 retry
禁止提前引入 WP8 MissionRun store / WP9 新 Release Saga 控制面。
### Track D — Governed Merge / Staging / Release
顺序:
`Review Gate → governed child commit/readback → staging activation → integrated live E2E
→ release candidate/acceptance → production convergence`
部署不是本 Review Candidate 的交付内容。
## 4. 状态权威边界
| 维度 | 权威 |
|---|---|
| Mission/root/lineage | TaskPlane/Mission authority |
| claim/heartbeat/task revision | TaskPlane |
| Owner/TaskPrincipal authorization | existing auth/Admission authority |
| Writer lease / governed write | existing writer/lease authority |
| source bytes/ref | Git authority |
| deployed runtime | runtime readback |
| external projection | external projection readback |
| caller liveness evidence | caller_scan evidence only |
| retry decision | Retry/Toil Governor decision only |
新模块不得建立第三份真值数据库。
## 5. 关键架构优化
### 5.1 动态 readiness 与不可变身份分离
Planner 选择身份(mission/root/child/route/authority digest)不可变;
`readiness_class` 是动态观察,可在未激活、未终态时通过 revision/CAS 单调向
“更可执行”方向推进。active claim/READY/DONE 后冻结。
### 5.2 Legacy reader 无副作用
旧 `GET /v1/tasks/{id}` 可能触发全局 lease expiry。R1 改为:
1. 通过 `/v1/tasks/{id}/events` 做 Bearer 认证;
2. Task row 从 authority SQLite 以 `mode=ro` + `PRAGMA query_only=ON` 精确读取;
3. 只允许终态;
4. readback ref 同时绑定 events SHA 和 DB revision;
5. 不暴露 `lease_token_hash`。
### 5.3 SchedulabilityProof 是短寿命诊断证据
Proof 绑定 task/worker/actor、revision/generation、capabilities、scope digest、
authorization revision、capacity/dependency/freeze/lease 观察;TTL ≤ 300s。
真正 claim 必须重读 authority 并完全一致,proof 本身不能授权。
### 5.4 Root Action Provider / Executor 分离
Provider 只把 root review 映射到准备计划;Executor 使用现有 continuation/recovery
路径执行。不能让 planner 把“PENDING”当成“已执行”。
## 6. 数据迁移策略
- 既有 Mission/root/children:不重建。
- legacy `TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-INTEGRATE-20260913T184412Z-19b7` / `job-7457a322d5f9d6931c7cd99ce3ec6c18`:仅 attachment,不改 identity。
- 新 Platform inventory:无持久业务迁移。
- 新 schedulability proof:短寿命诊断记录;若持久化,必须复用现有 TaskPlane
evidence/journal,禁止新数据库。
- SystemTruth:只读派生,不能成为第三控制面。
## 7. Review Gate
### R0 — 架构与代码评审
必须同时满足:
- 新 Program 与旧 Mission 边界明确
- base 重新绑定 `beabd053961047f170e87e1110f2636dbd50f5fb`
- 变更路径均有 ChangeEnvelope
- 新代码定向测试 PASS
- 旧候选继承证据有效
- 无新增业务权威/数据库
- legacy reader 不再使用可变 GET
- planner readiness 动态规则经过测试
- R2 proof 不能直接授权 claim
### R1 — Governed Commit Gate
在 R0 GO 后:
- live reread authority HEAD
- 所有 preimage SHA 重新采集
- exact apply/diff
- governed writer commit
- authority branch/commit readback
### R2 — Staging Gate
- authenticated principal/tenant
- identity/lineage/read isolation
- auto-claim adapter
- root action real executor
- restart/response-loss/sibling race
- ANNEX A01–A23
### R3 — Production Gate
只有 original root acceptance + Program acceptance 全部证据闭环,
并完成 rollback/recovery 验证后才能生产收敛。
## 8. 回滚
- Review candidate:直接丢弃候选,无线上副作用。
- Governed commit 未发布:通过正式 successor/revert,不 reset/force push。
- Staging activation:关闭 feature/policy binding,保留 evidence。
- Production:只走既有 release Saga/rollback contract。
## 9. 明确不做
- 不把 `3c320aeb89d9ba8f0ed516abeb57252682b18983` 恢复为 main。
- 不新建任务数据库、第二 scheduler、第二 release authority。
- 不把 caller_scan/Retry Governor/SystemTruth 作为写权威。
- 不把局部测试 PASS 当 root/program 完成。
@@ -0,0 +1,56 @@
# Unified Workstream DAG & Acceptance Matrix V1
```text
U01 Baseline Reconciliation
├─ Track A Identity/Lineage existing mission
│ ├─ C01 Governed commit/readback
│ ├─ C02 Staging activation
│ ├─ C03 Root-action executor
│ ├─ C04 Legacy attach
│ ├─ C05 Live failure/replay/concurrency E2E
│ └─ C06 Production convergence
├─ Track B ANNEX-2.3 R2
│ ├─ B1 Schedulability proof
│ ├─ B2 Discovery
│ ├─ B3 Claim-time recheck
│ ├─ B4 Claim/lease/fencing
│ ├─ B5 Execution/heartbeat
│ └─ B6 Evidence/readback A01-A23
└─ Track C Platform G0
├─ WP0a Version/Baseline
├─ WP0b Git writers
├─ WP0c Authorization
├─ WP0d Canonical/Digest
├─ WP0e caller_scan
├─ WP1 Environment Profile
└─ WP2 Retry/Toil Governor
A + B + C review GO
↓
U05 Governed unified commit/readback
↓
U06 Staging integrated E2E
↓
U07/U08 Production + Recovery
```
## Gate matrix
| Gate | Required evidence | Status in R1 |
|---|---|---|
| Baseline authority | main/runtime/prod current readback | PASS |
| Identity/Lineage candidate | frozen 27-file evidence + merge review | INHERITED/PENDING REBASE |
| Legacy reader | non-mutating authenticated read + terminal-only | LOCAL PASS |
| Planner dynamic readiness | CAS monotonic readiness, active/terminal freeze | CODED; repo-native rerun pending |
| R2 schedulability | blocker matrix + proof replay + claim recheck | LOCAL PASS |
| WP0a–d | read-only inventories | CODED; live scan pending |
| WP0e caller_scan | UNKNOWN safety/orphan rules | LOCAL PASS |
| WP1 env profile | host capability evidence | LOCAL PASS |
| WP2 retry governor | reconcile/no-op/block/execute | LOCAL PASS |
| Governed commit | writer receipt + branch readback | NOT RUN |
| Staging E2E | actual executor/readback | NOT RUN |
| Production | release acceptance/readback | NOT RUN |
## Deployment ordering
No deployment starts until all Review Gate rows required for R0 are PASS/GO.
+49
View File
@@ -12,6 +12,11 @@ components:
type: http
scheme: bearer
bearerFormat: opaque-token
taskPrincipalAuth:
type: http
scheme: bearer
bearerFormat: task-principal-jwt
description: Server-side TaskPrincipal resolved by the Bridge from the authoritative admitted+claimed Task; caller-supplied tokens are never accepted.
schemas:
PublicationState:
type: string
@@ -782,3 +787,47 @@ paths:
'200': {description: Safe GitOps plan}
'401': {description: Unauthorized}
x-openai-isConsequential: true
/reopenOwnerApproval:
post:
operationId: reopenOwnerApproval
summary: Reopen one expired Owner approval cycle without rebuilding release bytes
security:
- bearerAuth: []
x-openai-isConsequential: true
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- schema
- candidate_id
- expected_approval_fingerprint
- expected_source_commit
- expected_tree_oid
- expected_artifact_sha256
- expected_manifest_sha256
- expected_release_plan_sha256
- expected_policy_sha256
- reason
- idempotency_key
properties:
schema:
type: string
enum: [aios.owner-approval-reopen-request.v1]
candidate_id: {type: string}
expected_approval_fingerprint: {type: string, pattern: "^[0-9a-f]{64}$"}
expected_source_commit: {type: string, pattern: "^[0-9a-f]{40}$"}
expected_tree_oid: {type: string, pattern: "^[0-9a-f]{40}$"}
expected_artifact_sha256: {type: string, pattern: "^[0-9a-f]{64}$"}
expected_manifest_sha256: {type: string, pattern: "^[0-9a-f]{64}$"}
expected_release_plan_sha256: {type: string, pattern: "^[0-9a-f]{64}$"}
expected_policy_sha256: {type: string, pattern: "^[0-9a-f]{64}$"}
reason: {type: string, minLength: 1, maxLength: 512}
idempotency_key: {type: string, minLength: 8, maxLength: 128}
responses:
"200": {description: Reopened or idempotent replay}
"403": {description: Missing release:approve capability}
"409": {description: Governed reopen gate rejected}
@@ -0,0 +1,31 @@
# Review Gate V1
## Review disposition
This package is a **review candidate**, not a deployable release.
### Code review focus
- authority base = `beabd053961047f170e87e1110f2636dbd50f5fb`
- no direct DB mutation outside existing repository/service contracts
- no new control-plane database
- proof != authorization
- caller_scan cannot promote UNKNOWN to orphan
- retry governor cannot perform business effects
- legacy read path is non-mutating
- planner readiness is observation, not immutable identity
- existing mission/task/job IDs preserved
### Required test classes before GO
1. candidate syntax
2. new module focused unit tests
3. inherited Identity/Lineage regression suite
4. legacy core/runtime tests
5. ANNEX A01–A23 staging tests
6. root-action executor restart/response-loss tests
7. tenant isolation
8. governed Git apply/readback
### Current R1 decision rule
`ARCH_REVIEW_GO` may be issued if architecture/code review finds no blocker.
`DEPLOY_GO` is explicitly unavailable until governed commit + staging E2E pass.
+131
View File
@@ -0,0 +1,131 @@
# TaskPlane task identity and lineage
TaskPlane can bind admitted tasks to an authenticated owner principal and mission, assign stable root/parent lineage, and persist a root review after a child reaches a proof-bearing terminal state. The integration is opt-in through `TaskPlaneService(..., lineage_policy=...)`; existing services without a lineage policy retain their existing behavior.
## Authority boundary
`PrincipalLineageBinding` is service configuration. It maps the identity returned by the HTTP authenticator to `owner_principal_id`, `mission_id`, a safe display alias, and a registered root-acceptance policy reference. HTTP admission overwrites `issuer` with that authenticated identity. Direct in-process callers must pass an identity already verified by their boundary through `actor_identity`.
Client task metadata is never an identity or terminal-proof source. `aios_task_lineage`, the derived display name, and the lineage request digest are server-owned and rejected on new lineage admission. Similar-looking client metadata such as `owner_principal_id`, `mission_id`, or `terminal_evidence_proven` remains ordinary opaque metadata and cannot override the dedicated TaskPlane authority columns or the validated terminal-proof tables.
Root and child creation use the existing TaskPlane SQLite connection and transaction fence. The store adds authority columns and lineage sequence, terminal proof, root review, and root action tables to that database. It does not create a second TaskPlane database. The original task ID, claim ID, idempotency key, budget transaction, event journal, and revision CAS remain authoritative.
## Admission and delegation
A root admission requires:
- an authenticated identity with both TaskPlane capability and a `PrincipalLineageBinding`;
- a registered acceptance evaluator for the configured reference;
- no client-supplied reserved lineage metadata.
The request digest is computed before derived metadata is added. An identical idempotency replay returns the original task; a changed request, principal, mission, or parent conflicts. The persisted TaskPlane `created_at` is the same timestamp serialized into lineage metadata.
Task detail and list responses expose the server-bound name as `display_name`, including list responses that omit the full plan. Durable task and job identifiers remain unchanged.
Delegation currently creates only `CHILD` relations. It preserves the root owner, mission, acceptance reference, budget constraints, capability/risk monotonicity, and existing delegation transaction. The caller must resolve to the same server-owned root authority. Concurrent sibling admission uses the same SQLite write transaction to reserve unique sibling sequences.
`SUCCESSOR` and `RECOVERY` are intentionally not admitted by this binding because the current TaskPlane service does not expose a formal continuation/supersession authority contract. Adding those relations requires a separate source-of-authority design; callers must not encode them in metadata.
## Proven terminal review
The proof-bearing result path first applies the existing claim identity, lease, operation digest, evidence, result digest, event-chain, and evidence-chain checks. In the same transaction, a final child result records a terminal proof and triggers one `ROOT_REVIEW`. The review CAS advances the root revision and persists the exact trigger child and terminal revision.
The resulting next legal action is stored idempotently with status `PENDING`. `PENDING` means a durable action request exists. It does not mean a planner completed the root, created another child, recovered work, or crossed an owner boundary.
The optional `TaskPlaneService.consume_one_root_action()` consumes one persisted action through a root claim and a durable effect ledger. Mission configuration must provide an authoritative plan builder and acceptance verifier, and the service must receive a real `root_action_executor`. Missing execution wiring returns `BLOCKED`; the loopback Mission bootstrap adapter alone does not execute root actions. Plans survive process restart, and an unknown effect must be reconciled against its existing authority Job before execution can proceed. Worker metadata cannot supply that Job identity or certify a completed effect.
`PublicBridgeRootActionExecutor` is an adapter for authenticated public Bridge operations, not a deployed worker. Runtime integration must supply its trusted authentication and transport, then prove the actual Job result and repository readback. After an execution receipt, the consumer rechecks the stored original root acceptance and descendants. Root completion and child admission share the store's transaction boundary; a child result alone cannot close the root.
Direct worker completion of a lineage root is rejected. Root completion is legal only when the registered evaluator returns satisfied conditions backed by durable evidence references and the root review selects `ROOT_COMPLETE`. This integration persists that legal decision and request; it does not itself change the root task to `COMPLETED`.
After restart, `resume_terminal_review(child_task_id=...)` reads the durable terminal proof. If the trigger already has a review, it validates and returns that exact review and action receipt without running the evaluator again. This freezes the decision against evaluator drift and prevents a lost response from producing a second review. Tests close and reopen the SQLite database before treating this state as durable; visibility through the writing connection inside its transaction is only transaction-local.
The legacy `/fail` and `/cancel` paths do not construct a validated `ResultEnvelope` and therefore do not create this terminal proof or claim root-review completion. Use the proof-bearing result endpoint when terminal lineage review is required.
## Minimal service configuration
```python
from app.aios_task_lineage import AcceptanceCondition, RootAcceptanceSnapshot
from app.task_plane.api import TaskPlaneService
from app.task_plane.lineage_store import PrincipalLineageBinding, TaskLineagePolicy
def evaluate_root(root, descendants):
refs = tuple(
ref
for child in descendants
if child.terminal_evidence_proven
for ref in child.evidence_refs
)
return RootAcceptanceSnapshot(
(
AcceptanceCondition(
key="original-root-acceptance-not-yet-evaluated",
# Replace with the original mission's authoritative acceptance
# checks. A completed child alone cannot prove root completion.
satisfied=False,
evidence_refs=refs,
),
)
)
lineage_policy = TaskLineagePolicy(
principal_bindings=(
PrincipalLineageBinding(
authenticated_identity="spiffe://aios/owner/example",
owner_principal_id="principal-example",
mission_id="mission-example",
preferred_owner_alias="OWNER",
root_acceptance_ref="policy://root/example-v1",
),
),
acceptance_evaluators={"policy://root/example-v1": evaluate_root},
)
service = TaskPlaneService(store, policy=policy, lineage_policy=lineage_policy)
```
An evaluator that is missing, returns no conditions, or marks a condition satisfied without evidence fails closed. It cannot be supplied by task metadata.
Existing tasks receive empty authority-column defaults during additive schema
initialization. This does not infer ownership from their metadata or attach old
jobs to a new root. Migrating an actual legacy task requires an authoritative
owner/mission binding; this change does not fabricate one or alter a Job DB.
## Canonical Mission issuance
When discovery establishes that no root exists, the optional `MissionAuthority`
can allocate the first root. `POST /missions` accepts only `goal_slug` and
`original_acceptance`, with an `Idempotency-Key`. The authenticated principal
must have a server-configured `MissionPrincipalBinding`. Mission/root IDs,
owner binding, safe alias, UTC creation time and acceptance reference are
allocated and persisted by the authority in the existing TaskPlane database,
in the same transaction as root creation. They are never request identity
fields. An exact retry returns the original identity; a changed contract with
the same principal/idempotency key conflicts. `GET /missions/{mission_id}`
requires the same owner binding and returns the persisted acceptance and root.
Acceptance JSON is data. A service-owned verifier must independently establish
its conditions; absence of a registered verifier keeps acceptance unmet.
Creating the root or attaching an arbitrary evidence string cannot satisfy it.
The Mission authority and ordinary worker execution are separate: enabling
Mission admission does not grant worker claim or result permissions.
`app.aios_task_mission_authority_server` is a narrow loopback staging adapter
for environments with the existing registered Owner signature verifier. It
accepts `POST /v1/signed-request` containing exactly `request`, `signature` and
`key_id`. The request schema is `aios.signed-mission-request.v1`, audience is
`aios.task-mission-authority.staging.v1`, and the signed fields bind method,
path, body, idempotency key, issued/expiry timestamps and nonce. The maximum
validity is 300 seconds. The existing `aios-owner-approval-v1` verifier checks
the signature, and its registered signer identity is the service actor.
Neither a bearer belonging to Shadow nor an unsigned owner name establishes
that identity. The adapter does not load private keys or sign requests.
Allowed signed operations are Mission create/read and
`POST /tasks/{authority-returned-parent}/delegate`. Mission delegation issues
child IDs on the server and uses a durable, principal-scoped request mapping
for retries. The adapter exposes no Git writer, worker claim, terminal result,
legacy recovery, production deployment or arbitrary execution route. Deployment
of this bootstrap adapter is staging evidence, not a governed commit receipt
or proof that `PENDING` root actions execute.
+1
View File
@@ -0,0 +1 @@
{"baseline_mutation":false,"bootstrap_recovery_mode":false,"git_ls_tree_sha256":"58689cd365bec5795c19c2e1e03fc8ae774f768ed03a42d501b4430c95f71a8b","production_mutation":false,"schema":"aios.governed-runtime-package.v1","source_commit":"2852c2a40394195192c6c02a215ba31764e2cdda","source_file_count":965,"tag_mutation":false,"tree_oid":"ccd67c2357e7a4cd74e06be61ab81bba820d7671"}
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
import sqlite3, sys
dbs = [
("scoped-leases", "/var/lib/aios-gitops/bridge-v2-staging/scoped-leases.sqlite3"),
("task-admission", "/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3"),
]
for label, db in dbs:
print(f"=== {label}: {db} ===")
try:
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
tabs = [r[0] for r in c.execute("select name from sqlite_master where type='table'")]
print("tables:", tabs)
for t in tabs:
try:
n = c.execute(f"select count(*) from \"{t}\"").fetchone()[0]
print(f" {t}: rows={n}")
except Exception as e:
print(f" {t}: count-err {e}")
c.close()
except Exception as e:
print("ERR", e)
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
import sqlite3
# scoped leases detail
db = "/var/lib/aios-gitops/bridge-v2-staging/scoped-leases.sqlite3"
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
cols = [r[1] for r in c.execute("PRAGMA table_info(scoped_leases)")]
print("scoped_leases cols:", cols)
rows = c.execute("select * from scoped_leases").fetchall()
print("scoped_leases rows:", len(rows))
for r in rows[:12]:
print(" ", dict(zip(cols, r)))
c.close()
# task-admission envelopes status
db2 = "/var/lib/aios-gitops/bridge-v2-staging/task-admission.sqlite3"
c2 = sqlite3.connect(f"file:{db2}?mode=ro", uri=True)
for t in ["task_admission_envelopes","task_admission_grants","task_admission_receipts"]:
cols2 = [r[1] for r in c2.execute(f"PRAGMA table_info({t})")]
print(f"\n=== {t} cols: {cols2}")
rows2 = c2.execute(f"select * from {t}").fetchall()
for r in rows2[:6]:
print(" ", dict(zip(cols2, r)))
c2.close()
@@ -24,12 +24,14 @@ if run_gitea admin user list 2>/dev/null | awk '{print $2}' | grep -Fxq "$OWNER_
else
run_gitea admin user create --username "$OWNER_USERNAME" --password "$OWNER_PASSWORD" --email "$OWNER_EMAIL" --admin --must-change-password=false >/dev/null
fi
python3 - <<PY
import sqlite3, time
con=sqlite3.connect('$DB')
cur=con.cursor()
cur.execute("update user set is_admin=1,is_active=1,prohibit_login=0,must_change_password=0,updated_unix=? where lower_name=lower(?)", (int(time.time()), '$OWNER_USERNAME'))
cur.execute("update user set prohibit_login=1,is_admin=0,passwd='',passwd_hash_algo='disabled',must_change_password=1,updated_unix=? where lower_name in ('aios','admin','gitea') and lower_name<>lower(?)", (int(time.time()), '$OWNER_USERNAME'))
python3 - "$DB" "$OWNER_USERNAME" <<'PY'
import sqlite3, sys, time
db = sys.argv[1]
username = sys.argv[2]
con = sqlite3.connect(db)
cur = con.cursor()
cur.execute("update user set is_admin=1,is_active=1,prohibit_login=0,must_change_password=0,updated_unix=? where lower_name=lower(?)", (int(time.time()), username))
cur.execute("update user set prohibit_login=1,is_admin=0,passwd='',passwd_hash_algo='disabled',must_change_password=1,updated_unix=? where lower_name in ('aios','admin','gitea') and lower_name<>lower(?)", (int(time.time()), username))
con.commit()
PY
printf 'GITEA_OWNER_SYNC_OK user=%s default_local_login_disabled=1\n' "$OWNER_USERNAME"
@@ -219,6 +219,81 @@
</section>
</main>
</div>
<!-- SEC-P1 WEB CONTROL SURFACE hardening: CSRF token propagation + server-authoritative input pre-validation.
Runs before app.js so every state-change fetch issued by the control plane carries the CSRF token,
and invalid branch/worktree identifiers are blocked client-side (server remains authoritative). -->
<script>
(function () {
'use strict';
// Double-submit cookie pattern: server sets a signed `aios_csrf` cookie; the client echoes its
// value in X-CSRF-Token on every state-changing (non-safe) request. A server-issued
// <meta name="csrf-token"> takes precedence when present.
function csrfToken() {
var meta = document.querySelector('meta[name="csrf-token"]');
if (meta && meta.content) return meta.content;
var m = document.cookie.match(/(?:^|;\\s*)aios_csrf=([^;]+)/);
return m ? decodeURIComponent(m[1]) : '';
}
// Server-authoritative identifier rules for git refs / worktrees. Kept in sync with the backend
// validator so the client gives fast feedback, but the REAL enforcement happens server-side.
var ID_CHARS = /^[A-Za-z0-9._/-]+$/;
var ID_FORBIDDEN = ['..', '//', '@{', '.lock', ' ', '\\t', ';', '&', '|', '$', '(', ')', '<', '>', '"', "'", '`', '\\\\'];
function validateBranch(name) {
if (typeof name !== 'string' || !name.trim()) return 'branch name is required';
if (name.trim().length > 255) return 'branch name too long';
if (name !== name.trim()) return 'branch name must not have leading/trailing whitespace';
for (var i = 0; i < ID_FORBIDDEN.length; i++) {
if (name.indexOf(ID_FORBIDDEN[i]) !== -1) return 'branch name contains forbidden characters: ' + ID_FORBIDDEN[i];
}
if (name.indexOf('..') !== -1) return 'branch name must not contain ".."';
if (name.indexOf('@{') !== -1) return 'branch name must not contain "@{"';
if (!ID_CHARS.test(name)) return 'branch name contains invalid characters';
if (name.indexOf('/') === 0 || name[name.length - 1] === '/') return 'branch name must not start/end with "/"';
return null;
}
function validateWorktree(name) {
if (typeof name !== 'string' || !name.trim()) return 'worktree id is required';
var e = validateBranch(name);
return e ? 'worktree id invalid: ' + e : null;
}
function stateHeaders() {
return { 'X-CSRF-Token': csrfToken() };
}
// Wrap fetch: inject CSRF header on any non-safe method (state change).
var nativeFetch = window.fetch;
window.fetch = function (url, opts) {
opts = opts || {};
var method = String(opts.method || 'GET').toUpperCase();
var safe = (method === 'GET' || method === 'HEAD' || method === 'OPTIONS');
if (!safe) {
opts.headers = Object.assign({}, opts.headers);
opts.headers['X-CSRF-Token'] = csrfToken();
}
return nativeFetch.call(this, url, opts);
};
// Client-side pre-validation for state-change forms (server re-validates authoritatively).
document.addEventListener('submit', function (e) {
var form = e.target;
if (!form || (form.id !== 'branch-form' && form.id !== 'plan-form')) return;
var fd = new FormData(form);
var branch = String(fd.get('branch') || '').trim();
var err = validateBranch(branch);
if (err) {
e.preventDefault();
e.stopPropagation();
window.alert('Blocked: ' + err);
}
}, true);
window.__gitopsSurface = { csrfToken: csrfToken, validateBranch: validateBranch, validateWorktree: validateWorktree, stateHeaders: stateHeaders };
})();
</script>
<script src="./app.js"></script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import sys, importlib.util
# Load the installed migration planner runtime module
app = "/opt/aios/release-migration/current/app"
sys.path.insert(0, app)
import release_migration_plan as rm
VERSION = "V2R2C2-baseline11-principal-recovery"
CANDIDATE = "rc-v2r2c2-baseline11-principal-recovery-2852c2a40394"
SOURCE = "2852c2a40394195192c6c02a215ba31764e2cdda"
resolver = rm.FileMigrationPlanResolver(
"/var/lib/aios-release-migration",
profile_root="/etc/aios-gitops/release-migration-profiles",
expected_owner_uid=0,
)
try:
plan = resolver.resolve(VERSION, CANDIDATE, SOURCE)
if plan is None:
print("RESOLVE_RESULT = NONE (not required?)")
sys.exit(3)
print("RESOLVE_RESULT = MIGRATION_PLAN_READY")
d = plan.to_dict()
print("migration_id =", d.get("migration_id"))
print("migration_plan_sha =", d.get("sha256"))
print("source_commit =", d.get("source_commit"))
print("tree_oid =", d.get("tree_oid"))
# confirm the migration sub-contract binds snapshot
for sc in d.get("state_contracts", []):
print("state_contract snapshot_name =", sc.get("snapshot_name"))
print("state_contract snapshot_sha =", sc.get("snapshot_sha256"))
print("DURABLE_PREREQUISITE_STATE = SATISFIED")
sys.exit(0)
except rm.MigrationPlanRequired as e:
print("RESOLVE_RESULT =", e)
print("DURABLE_PREREQUISITE_STATE = NOT_SATISFIED")
sys.exit(1)
except Exception as e:
print("RESOLVE_ERROR =", type(e).__name__, e)
sys.exit(2)
+73
View File
@@ -0,0 +1,73 @@
--- a/active-runtime/app/aios_trusted_candidate_builder.py
+++ b/active-runtime/app/aios_trusted_candidate_builder.py
@@ -367,15 +367,15 @@
raise BuilderError(code) from exc
if not stat.S_ISDIR(metadata.st_mode) or path.is_symlink():
raise BuilderError(code)
- if os.name != "nt" and metadata.st_mode & 0o022:
- raise BuilderError("COLLECTOR_EVIDENCE_ROOT_MODE_DENIED")
- if (
- path == self._evidence_root
- and os.name != "nt"
- and self._expected_owner_uid is not None
- and metadata.st_uid != self._expected_owner_uid
- ):
- raise BuilderError("COLLECTOR_EVIDENCE_ROOT_OWNER_DENIED")
+ if path == self._evidence_root:
+ if os.name != "nt" and metadata.st_mode & 0o022:
+ raise BuilderError("COLLECTOR_EVIDENCE_ROOT_MODE_DENIED")
+ if (
+ os.name != "nt"
+ and self._expected_owner_uid is not None
+ and metadata.st_uid != self._expected_owner_uid
+ ):
+ raise BuilderError("COLLECTOR_EVIDENCE_ROOT_OWNER_DENIED")
def _digest(self, path: Path) -> str:
digest = hashlib.sha256()
--- a/tests/test_trusted_candidate_builder.py
+++ b/tests/test_trusted_candidate_builder.py
@@ -323,9 +323,11 @@
policy = load_builder_policy(POLICY_PATH)
config = TrustedCandidateBuilderConfig.from_policy(policy)
evidence_root = tmp_path / "evidence"
- evidence_root.mkdir(mode=0o700)
+ evidence_root.mkdir()
+ evidence_root.chmod(0o700)
authority_git = tmp_path / "repo.git"
- authority_git.mkdir(mode=0o700)
+ authority_git.mkdir()
+ authority_git.chmod(0o2775)
source = ServerOwnedEvidenceSource(
config,
@@ -356,6 +358,29 @@
)
with pytest.raises(BuilderError, match="COLLECTOR_EVIDENCE_UNAVAILABLE"):
server.handle_request(build, connection=object())
+
+
+def test_readiness_keeps_evidence_root_permissions_strict_with_group_shared_authority(tmp_path) -> None:
+ config = TrustedCandidateBuilderConfig.from_policy(load_builder_policy(POLICY_PATH))
+ evidence_root = tmp_path / "evidence"
+ evidence_root.mkdir()
+ evidence_root.chmod(0o720)
+ authority_git = tmp_path / "repo.git"
+ authority_git.mkdir()
+ authority_git.chmod(0o2775)
+
+ source = ServerOwnedEvidenceSource(
+ config,
+ authority_git,
+ evidence_root,
+ expected_owner_uid=None,
+ git_runner=lambda *_args, **_kwargs: SimpleNamespace(
+ stdout="a" * 40 + "\n", returncode=0
+ ),
+ )
+
+ with pytest.raises(BuilderError, match="COLLECTOR_EVIDENCE_ROOT_MODE_DENIED"):
+ source.assert_ready()
def test_unix_client_applies_bounded_timeout_and_fails_closed(monkeypatch) -> None:
+8 -1
View File
@@ -19,10 +19,17 @@ usage() {
[[ "$ACTION" =~ ^(create|remove)$ ]] || usage
[[ "$AGENT" =~ ^(openclaw|chatgpt|controller)$ ]] || usage
[[ "$TASK_ID" =~ ^[A-Za-z0-9._-]+$ ]] || {
[[ "$TASK_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
echo "ERROR: invalid task id" >&2
exit 65
}
# Reject path traversal: no dot-dot, no leading dot, no trailing dot.
case "$TASK_ID" in
*".."*|.*|*.)
echo "ERROR: invalid task id (path traversal rejected)" >&2
exit 65
;;
esac
[[ "$(id -un)" == "$RUNTIME_USER" ]] || {
echo "ERROR: must run as $RUNTIME_USER" >&2
exit 66
+10
View File
@@ -8,6 +8,16 @@ ZIP_PATH="$(find /data /opt /root /home -name "$ZIP_NAME" 2>/dev/null | head -1
if [ -z "$ZIP_PATH" ]; then echo "BLOCKED: missing $ZIP_NAME"; exit 10; fi
cp "$ZIP_PATH" "$ROOT/imports/"
cd "$WORKSPACE"
# zip-slip guard: reject any entry that would escape the workspace on extract.
command -v unzip >/dev/null 2>&1 || { echo "BLOCKED: unzip unavailable"; exit 11; }
while IFS= read -r entry; do
case "$entry" in
*/../*|../*|*/..|..|/*|*\\*|*:* )
echo "BLOCKED: unsafe zip entry: $entry"
exit 11
;;
esac
done < <(unzip -Z1 "$ROOT/imports/$ZIP_NAME" 2>/dev/null || true)
unzip -o "$ROOT/imports/$ZIP_NAME"
git init
git config user.name "AIOS GitOps Bot"
@@ -0,0 +1,52 @@
"""Read-only WP0c authorization inventory."""
from __future__ import annotations
from dataclasses import dataclass, asdict
from pathlib import Path
import argparse, json, re
TERMS={
"task_principal":re.compile(r"\bTaskPrincipal\b|\btask[_ -]?principal\b",re.I),
"owner_approval":re.compile(r"\bowner[_ -]?approval\b|\bOWNER_SIGNATURE_REQUIRED\b",re.I),
"capability":re.compile(r"\bcapabilit(?:y|ies)\b",re.I),
"signer":re.compile(r"\bsigner\b|\bsshsig\b",re.I),
"lease":re.compile(r"\b(?:ClaimLease|WriterLease|writer[_ -]?lease)\b",re.I),
"authorization":re.compile(r"\bauthori[sz]ation\b",re.I),
}
def scan(repo:Path)->dict:
repo=repo.resolve(); refs=[]
for p in sorted(repo.rglob("*")):
if not p.is_file() or p.suffix.lower() not in {".py",".json",".yaml",".yml",".md"}: continue
if any(x in p.parts for x in {".git",".venv","node_modules","__pycache__"}): continue
try: lines=p.read_text(encoding="utf-8",errors="strict").splitlines()
except (UnicodeDecodeError,OSError): continue
for i,line in enumerate(lines,1):
kinds=sorted(k for k,pat in TERMS.items() if pat.search(line))
if kinds:
refs.append({"path":p.relative_to(repo).as_posix(),"line":i,"kinds":kinds,"excerpt":line.strip()[:240]})
return {
"schema_version":"aios.authorization-inventory.v1",
"references":refs,
"reference_count":len(refs),
"classification":"EVIDENCE_ONLY_NOT_AUTHORITY",
"completeness":"PARTIAL_DISCOVERY",
"covered_scope":["lexical_authorization_references"],
"missing_scope":[
"owner_broker_chain",
"taskprincipal_admission_chain",
"claim_and_task_lease_chain",
"writerlease_chain",
"release_recovery_capability_chain",
"signer_credential_provisioning_chain",
"live_caller_and_revocation_evidence",
],
"g0_exit_eligible":False,
}
def main()->int:
ap=argparse.ArgumentParser(); ap.add_argument("--repo",type=Path,required=True); ap.add_argument("--out",type=Path)
ns=ap.parse_args(); result=scan(ns.repo); text=json.dumps(result,indent=2,sort_keys=True)+"\n"
if ns.out: ns.out.write_text(text,encoding="utf-8")
else: print(text,end="")
return 0
if __name__=="__main__": raise SystemExit(main())
+432
View File
@@ -0,0 +1,432 @@
"""Read-only caller scanner for AIOS platform convergence.
Key safety rules:
- static source references are evidence of source callers, not runtime liveness;
- runtime liveness requires active, artifact-bound readback evidence;
- PROVEN_ORPHAN requires a complete negative-evidence chain and independent GO;
- parse/runtime uncertainty is fail-closed to UNKNOWN;
- this module never imports scanned business modules and never mutates authority.
"""
from __future__ import annotations
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Iterable, Mapping, Any, Sequence
import argparse
import ast
import hashlib
import json
import keyword
@dataclass(frozen=True)
class Caller:
kind: str
identity: str
evidence_ref: str
@dataclass(frozen=True)
class ScanResult:
schema_version: str
subject: dict[str, str]
callers: tuple[Caller, ...]
runtime_loaded: tuple[dict[str, str], ...]
unknown_dynamic_refs: tuple[str, ...]
test_only_callers: tuple[Caller, ...]
caller_count: int
assessment: str
coverage: dict[str, object]
evidence_refs: tuple[str, ...]
def to_dict(self) -> dict[str, object]:
return {
"schema_version": self.schema_version,
"subject": self.subject,
"callers": [asdict(x) for x in self.callers],
"runtime_loaded": list(self.runtime_loaded),
"unknown_dynamic_refs": list(self.unknown_dynamic_refs),
"test_only_callers": [asdict(x) for x in self.test_only_callers],
"caller_count": self.caller_count,
"assessment": self.assessment,
"coverage": self.coverage,
"evidence_refs": list(self.evidence_refs),
}
def _sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _is_identifier_path(parts: Sequence[str]) -> bool:
return bool(parts) and all(p.isidentifier() and not keyword.iskeyword(p) for p in parts)
def _normalize_rel_path(value: object) -> str:
if not isinstance(value, str):
return ""
return value.replace("\\", "/").lstrip("./")
def _default_import_roots(repo: Path, profile: Mapping[str, Any]) -> tuple[Path, ...]:
roots: list[Path] = [repo]
explicit = profile.get("import_roots", ()) or ()
if isinstance(explicit, (list, tuple)):
for item in explicit:
if not isinstance(item, str) or not item.strip():
continue
path = (repo / item).resolve()
try:
path.relative_to(repo)
except ValueError:
continue
if path.exists():
roots.append(path)
for rel in ("active-runtime", "active-runtime/app", "src", "app"):
path = (repo / rel).resolve()
if path.exists():
roots.append(path)
unique: list[Path] = []
seen: set[str] = set()
for root in roots:
key = str(root)
if key not in seen:
seen.add(key)
unique.append(root)
return tuple(unique)
def _module_candidates(path: Path, roots: Sequence[Path]) -> tuple[str, ...]:
out: set[str] = set()
resolved = path.resolve()
for root in roots:
try:
rel = resolved.relative_to(root.resolve())
except ValueError:
continue
parts = list(rel.with_suffix("").parts)
if parts and parts[-1] == "__init__":
parts = parts[:-1]
if _is_identifier_path(parts):
out.add(".".join(parts))
return tuple(sorted(out))
def _is_test_path(path: Path) -> bool:
parts = {p.lower() for p in path.parts}
return "tests" in parts or "test" in parts or path.name.startswith("test_")
def _iter_python(repo: Path) -> Iterable[Path]:
for path in repo.rglob("*.py"):
if ".git" in path.parts:
continue
if path.is_file():
yield path
def _relative_imports(
*,
importer_modules: Sequence[str],
is_package_init: bool,
level: int,
module: str | None,
aliases: Sequence[str],
) -> set[str]:
resolved: set[str] = set()
for importer in importer_modules:
parts = importer.split(".") if importer else []
package = parts if is_package_init else parts[:-1]
ascend = max(level - 1, 0)
if ascend > len(package):
continue
base = package[: len(package) - ascend] if ascend else package
if module:
base = [*base, *module.split(".")]
if base:
resolved.add(".".join(base))
for alias in aliases:
if alias == "*":
continue
candidate = [*base, alias]
if candidate:
resolved.add(".".join(candidate))
return resolved
def _ast_imports(path: Path, importer_modules: Sequence[str]) -> tuple[set[str], bool]:
try:
source = path.read_text(encoding="utf-8", errors="strict")
tree = ast.parse(source)
except (SyntaxError, UnicodeError, OSError):
return set(), True
imports: set[str] = set()
dynamic = False
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom):
alias_names = [alias.name for alias in node.names]
if node.level:
imports.update(
_relative_imports(
importer_modules=importer_modules,
is_package_init=path.name == "__init__.py",
level=node.level,
module=node.module,
aliases=alias_names,
)
)
else:
if node.module:
imports.add(node.module)
imports.update(
f"{node.module}.{name}" for name in alias_names if name != "*"
)
elif alias_names:
# "from <unknown> import x" should not happen for absolute syntax;
# keep fail-closed if the AST is not informative.
dynamic = True
elif isinstance(node, ast.Call):
fn = node.func
name = None
if isinstance(fn, ast.Name):
name = fn.id
elif isinstance(fn, ast.Attribute) and isinstance(fn.value, ast.Name):
name = f"{fn.value.id}.{fn.attr}"
if name in {"__import__", "importlib.import_module"}:
if (
node.args
and isinstance(node.args[0], ast.Constant)
and isinstance(node.args[0].value, str)
and not node.args[0].value.startswith(".")
):
imports.add(node.args[0].value)
else:
dynamic = True
return imports, dynamic
def _match_module(imported: str, subject_modules: Sequence[str]) -> bool:
return any(
imported == subject
or imported.startswith(subject + ".")
or subject.startswith(imported + ".")
for subject in subject_modules
)
def _active(item: Mapping[str, Any]) -> bool:
if item.get("active") is True:
return True
state = str(item.get("state") or item.get("status") or "").upper()
return state in {"ACTIVE", "RUNNING", "LOADED", "READY"}
def _artifact_bound_match(
item: Mapping[str, Any],
*,
subject_rel: str,
subject_sha: str,
) -> bool:
if not _active(item):
return False
source_path = _normalize_rel_path(
item.get("source_path") or item.get("repo_path") or item.get("path")
)
artifact_sha = str(
item.get("source_sha256")
or item.get("artifact_sha256")
or item.get("sha256")
or ""
).lower()
return bool(source_path == subject_rel and artifact_sha == subject_sha)
def _probe_complete(profile: Mapping[str, Any]) -> tuple[bool, dict[str, object]]:
runtime_hosts = list(profile.get("runtime_hosts", ()) or ())
observation_seconds = profile.get("observation_window_seconds")
try:
observation_seconds = float(observation_seconds)
except (TypeError, ValueError):
observation_seconds = 0.0
coverage = {
"repo_static": True,
"systemd": bool(profile.get("systemd_scanned", False)),
"runtime_hosts": runtime_hosts,
"runtime_hosts_complete": bool(profile.get("runtime_hosts_complete", False)),
"loaded_bytes": bool(profile.get("loaded_bytes_scanned", False)),
"api_routes": bool(profile.get("api_routes_scanned", False)),
"scheduler": bool(profile.get("scheduler_scanned", False)),
"observation_window_complete": bool(profile.get("observation_window_complete", False)),
"observation_window_seconds": observation_seconds,
"rollback_ref": str(profile.get("rollback_ref") or ""),
"independent_acceptance": str(profile.get("independent_acceptance") or ""),
"probe_failures": list(profile.get("probe_failures", ()) or ()),
}
complete = all(
(
coverage["systemd"],
coverage["runtime_hosts_complete"],
bool(runtime_hosts), # empty relevant-host set never proves absence
coverage["loaded_bytes"],
coverage["api_routes"],
coverage["scheduler"],
coverage["observation_window_complete"],
observation_seconds > 0,
bool(coverage["rollback_ref"]),
coverage["independent_acceptance"] == "GO",
not coverage["probe_failures"],
)
)
return bool(complete), coverage
def scan(
*,
repo: Path,
subject: Path,
runtime_profile: Mapping[str, Any] | None = None,
authority_ref: str | None = None,
) -> ScanResult:
repo = repo.resolve()
profile = dict(runtime_profile or {})
subject = (repo / subject).resolve() if not subject.is_absolute() else subject.resolve()
if not subject.is_file():
raise FileNotFoundError(subject)
roots = _default_import_roots(repo, profile)
subject_modules = _module_candidates(subject, roots)
production_static: list[Caller] = []
runtime_callers: list[Caller] = []
tests: list[Caller] = []
unknown_dynamic: list[str] = []
subject_sha = _sha(subject)
subject_rel = subject.relative_to(repo).as_posix()
evidence = [f"sha256:{subject_sha}"]
for path in _iter_python(repo):
if path.resolve() == subject:
continue
importer_modules = _module_candidates(path, roots)
imports, dynamic = _ast_imports(path, importer_modules)
matched = bool(subject_modules and any(_match_module(x, subject_modules) for x in imports))
rel = path.relative_to(repo).as_posix()
if matched:
caller = Caller(
"STATIC_TEST_ONLY" if _is_test_path(path) else "STATIC_PRODUCTION",
rel,
f"repo:{rel}",
)
(tests if _is_test_path(path) else production_static).append(caller)
if dynamic:
unknown_dynamic.append(rel)
runtime_loaded: list[dict[str, str]] = []
for item in profile.get("runtime_loaded", ()) or ():
if not isinstance(item, Mapping):
continue
if _artifact_bound_match(item, subject_rel=subject_rel, subject_sha=subject_sha):
loaded_path = _normalize_rel_path(
item.get("source_path") or item.get("repo_path") or item.get("path")
)
runtime_loaded.append({"path": loaded_path, "sha256": subject_sha})
runtime_callers.append(
Caller("RUNTIME_LOADED", loaded_path, str(item.get("evidence_ref") or "runtime_profile"))
)
for key, kind, identity_key in (
("systemd", "SYSTEMD", "unit"),
("api_routes", "API_ROUTE", "route"),
("scheduler", "SCHEDULER", "identity"),
):
for item in profile.get(key, ()) or ():
if not isinstance(item, Mapping):
continue
if _artifact_bound_match(item, subject_rel=subject_rel, subject_sha=subject_sha):
runtime_callers.append(
Caller(
kind,
str(item.get(identity_key) or ""),
str(item.get("evidence_ref") or f"runtime_profile:{key}"),
)
)
def _dedup(items: Sequence[Caller]) -> list[Caller]:
unique = {(c.kind, c.identity, c.evidence_ref): c for c in items}
return sorted(unique.values(), key=lambda c: (c.kind, c.identity, c.evidence_ref))
production_static = _dedup(production_static)
runtime_callers = _dedup(runtime_callers)
tests = _dedup(tests)
production = _dedup([*production_static, *runtime_callers])
negative_complete, coverage = _probe_complete(profile)
opaque = bool(unknown_dynamic) or bool(coverage["probe_failures"])
if runtime_callers:
assessment = "LIVE"
elif production_static or opaque:
# Source caller != runtime liveness; opaque/probe failures are also unknown.
assessment = "UNKNOWN"
elif negative_complete:
assessment = "PROVEN_ORPHAN"
else:
assessment = "LIKELY_ORPHAN"
coverage["import_roots"] = [
root.relative_to(repo).as_posix() if root != repo else "."
for root in roots
]
coverage["subject_modules"] = list(subject_modules)
coverage["negative_evidence_complete"] = negative_complete
coverage["static_production_callers"] = len(production_static)
coverage["runtime_callers"] = len(runtime_callers)
return ScanResult(
schema_version="aios.caller-scan.v2",
subject={
"path": subject_rel,
"source_commit": authority_ref or "",
"sha256": subject_sha,
},
callers=tuple(production),
runtime_loaded=tuple(runtime_loaded),
unknown_dynamic_refs=tuple(sorted(set(unknown_dynamic))),
test_only_callers=tuple(tests),
caller_count=len(production),
assessment=assessment,
coverage=coverage,
evidence_refs=tuple(evidence),
)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--repo", type=Path, required=True)
ap.add_argument("--subject", type=Path, required=True)
ap.add_argument("--runtime-host-profile", type=Path)
ap.add_argument("--authority-ref")
ap.add_argument("--output", type=Path, required=True)
ns = ap.parse_args()
profile: dict[str, Any] = {}
if ns.runtime_host_profile:
profile = json.loads(ns.runtime_host_profile.read_text(encoding="utf-8"))
result = scan(
repo=ns.repo,
subject=ns.subject,
runtime_profile=profile,
authority_ref=ns.authority_ref,
)
ns.output.parent.mkdir(parents=True, exist_ok=True)
ns.output.write_text(
json.dumps(result.to_dict(), ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print(json.dumps({"assessment": result.assessment, "caller_count": result.caller_count}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,63 @@
"""Read-only WP0d canonical/digest inventory."""
from __future__ import annotations
from pathlib import Path
import argparse, ast, json
CANON_NAMES=("canonical","digest","sha","fingerprint","execution_digest","policy_digest")
def _functions(path:Path):
try: tree=ast.parse(path.read_text(encoding="utf-8"))
except (SyntaxError,UnicodeDecodeError,OSError): return []
out=[]
for node in ast.walk(tree):
if isinstance(node,(ast.FunctionDef,ast.AsyncFunctionDef)):
name=node.name.lower()
source=ast.get_source_segment(path.read_text(encoding="utf-8"),node) or ""
signals=[]
if any(k in name for k in CANON_NAMES): signals.append("NAME")
if "sort_keys=True" in source and "json.dumps" in source: signals.append("SORTED_JSON")
if "hashlib.sha256" in source or "sha256(" in source: signals.append("SHA256")
if signals:
out.append({"name":node.name,"line":node.lineno,"signals":sorted(set(signals))})
return out
def scan(repo:Path)->dict:
repo=repo.resolve(); defs=[]
for p in sorted(repo.rglob("*.py")):
if any(x in p.parts for x in {".git",".venv","node_modules","__pycache__"}): continue
funcs=_functions(p)
for item in funcs:
defs.append({"path":p.relative_to(repo).as_posix(),**item})
groups={}
for item in defs:
key="+".join(item["signals"])
groups.setdefault(key,[]).append(f'{item["path"]}:{item["line"]}:{item["name"]}')
return {
"schema_version":"aios.canonical-digest-inventory.v1",
"definitions":defs,
"duplicate_groups":groups,
"definition_count":len(defs),
"classification":"EVIDENCE_ONLY_NOT_CANONICAL_AUTHORITY",
"completeness":"PARTIAL_DISCOVERY",
"covered_scope":["function_level_name_and_syntax_signals"],
"missing_scope":[
"producer_consumer_mapping",
"input_schema",
"bytes_rule",
"digest_algorithm_contract",
"duplicate_semantics_review",
"live_caller_evidence",
],
"g0_exit_eligible":False,
}
def main()->int:
ap=argparse.ArgumentParser(); ap.add_argument("--repo",type=Path,required=True); ap.add_argument("--out",type=Path)
ns=ap.parse_args(); result=scan(ns.repo); text=json.dumps(result,indent=2,sort_keys=True)+"\n"
if ns.out: ns.out.write_text(text,encoding="utf-8")
else: print(text,end="")
return 0
if __name__=="__main__": raise SystemExit(main())
+60
View File
@@ -0,0 +1,60 @@
"""ChangeEnvelope v1 generator.
The envelope records a proposed change; it neither authorizes nor executes it.
"""
from __future__ import annotations
from pathlib import Path
from typing import Iterable, Mapping
import argparse, hashlib, json, time
def sha256(path:Path)->str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def build_change_envelope(
*,
change_id:str,
work_package:str,
source_commit:str,
candidate_root:Path,
target_paths:Iterable[str],
preimage_sha256:Mapping[str,str|None],
contract_versions:Mapping[str,str],
live_caller_evidence_ref:str|None,
tests:list[dict],
rollback_ref:str|None,
independent_review_ref:str|None,
)->dict:
paths=sorted(dict.fromkeys(str(p) for p in target_paths))
post={}
for rel in paths:
path=candidate_root/rel
if not path.is_file(): raise FileNotFoundError(rel)
post[rel]=sha256(path)
return {
"schema_version":"aios.change-envelope.v1",
"change_id":change_id,
"work_package":work_package,
"source_commit":source_commit,
"target_paths":paths,
"preimage_sha256":{p:preimage_sha256.get(p) for p in paths},
"postimage_sha256":post,
"contract_versions":dict(contract_versions),
"live_caller_evidence_ref":live_caller_evidence_ref,
"tests":tests,
"rollback_ref":rollback_ref,
"independent_review_ref":independent_review_ref,
"created_at_epoch":int(time.time()),
"authorization_effect":"NONE",
}
def main()->int:
ap=argparse.ArgumentParser()
ap.add_argument("--spec",type=Path,required=True); ap.add_argument("--candidate-root",type=Path,required=True); ap.add_argument("--out",type=Path,required=True)
ns=ap.parse_args(); spec=json.loads(ns.spec.read_text(encoding="utf-8"))
result=build_change_envelope(candidate_root=ns.candidate_root,**spec)
ns.out.write_text(json.dumps(result,indent=2,sort_keys=True)+"\n",encoding="utf-8")
return 0
if __name__=="__main__": raise SystemExit(main())
@@ -0,0 +1,129 @@
"""Environment capability profile with secret-safe, read-only probes.
The profile distinguishes "tool present" from "capability proven". Missing
requirements are computed from an explicit requirement set; an empty
requirement set never implies Generation-G0 completeness.
"""
from __future__ import annotations
from dataclasses import dataclass, asdict
from typing import Iterable
import argparse
import os
import platform
import shutil
import subprocess
from pathlib import Path
@dataclass(frozen=True)
class EnvironmentProfile:
schema_version: str
host_id: str
os_info: dict[str, str]
shells: tuple[str, ...]
tools: dict[str, str | None]
capabilities: dict[str, bool]
credential_mechanisms: tuple[str, ...]
filesystem: dict[str, object]
requirements: tuple[str, ...]
missing_requirements: tuple[str, ...]
completeness: str
g0_exit_eligible: bool
def to_dict(self): return asdict(self)
def _version(command: str) -> str | None:
exe = shutil.which(command)
if not exe:
return None
try:
cp = subprocess.run(
[exe, "--version"], text=True, capture_output=True, timeout=3
)
except Exception:
return None
first = (cp.stdout or cp.stderr).splitlines()
return first[0][:200] if first else None
def _systemd_running() -> bool:
if os.name != "posix" or not shutil.which("systemctl"):
return False
# Presence of systemctl alone is not evidence that systemd is PID1/runtime.
return Path("/run/systemd/system").is_dir()
def collect(host_id: str, requirements: Iterable[str] = ()) -> EnvironmentProfile:
requirements = tuple(dict.fromkeys(str(x) for x in requirements if str(x)))
shells = tuple(x for x in ("bash", "sh", "pwsh", "powershell", "cmd") if shutil.which(x))
tools = {name: _version(name) for name in ("git", "python3", "python", "node")}
posix = os.name == "posix"
systemd = _systemd_running()
sudo_noninteractive = False
if posix and shutil.which("sudo"):
try:
sudo_noninteractive = (
subprocess.run(
["sudo", "-n", "true"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
timeout=2,
).returncode
== 0
)
except Exception:
sudo_noninteractive = False
load_credential = bool(systemd and shutil.which("systemd-creds"))
capabilities = {
"posix": posix,
"systemd": systemd,
"systemd_load_credential": load_credential,
"sudo_noninteractive": sudo_noninteractive,
"git": bool(tools["git"]),
"python": bool(tools["python3"] or tools["python"]),
"node": bool(tools["node"]),
}
credential = ("systemd.LoadCredential",) if load_credential else ()
missing = tuple(sorted(req for req in requirements if not capabilities.get(req, False)))
completeness = "REQUIREMENTS_EVALUATED" if requirements else "CAPABILITY_DISCOVERY_ONLY"
return EnvironmentProfile(
schema_version="aios.environment-profile.v2",
host_id=host_id,
os_info={"name": platform.system(), "version": platform.version()},
shells=shells,
tools=tools,
capabilities=capabilities,
credential_mechanisms=credential,
filesystem={
"case_sensitive": os.path.normcase("A") != os.path.normcase("a"),
"line_ending_default": "LF" if os.linesep == "\n" else "CRLF",
},
requirements=requirements,
missing_requirements=missing,
completeness=completeness,
g0_exit_eligible=bool(requirements and not missing),
)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--host-id", required=True)
ap.add_argument("--require", action="append", default=[])
ap.add_argument("--out", type=Path)
ns = ap.parse_args()
result = collect(ns.host_id, ns.require)
import json
text = json.dumps(result.to_dict(), indent=2, sort_keys=True) + "\n"
if ns.out:
ns.out.write_text(text, encoding="utf-8")
else:
print(text, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,63 @@
"""Read-only WP0b Git writer inventory."""
from __future__ import annotations
from dataclasses import dataclass, asdict
from pathlib import Path
import argparse, json, re
PATTERNS={
"git_push":re.compile(r"\bgit\s+push\b"),
"git_commit":re.compile(r"\bgit\s+commit\b"),
"git_update_ref":re.compile(r"\bgit\s+update-ref\b"),
"governed_commit":re.compile(r"\b(?:commitFilesGoverned|commit_files_governed|commit-files)\b"),
"tag_publish":re.compile(r"\b(?:requestOrCreateTag|createReleaseCandidate|tag_publish)\b"),
"ref_mutation_api":re.compile(r"\b(?:update_ref|create_commit|push_branch|receive-pack)\b"),
}
SKIP_PARTS={".git",".venv","venv","node_modules","__pycache__"}
@dataclass(frozen=True)
class WriterEvidence:
path:str; line:int; kind:str; excerpt:str
def scan(repo:Path)->dict:
repo=repo.resolve()
evidence=[]
for p in sorted(repo.rglob("*")):
if not p.is_file() or any(x in SKIP_PARTS for x in p.parts):
continue
if p.suffix.lower() not in {".py",".sh",".bash",".service",".json",".yaml",".yml",".md"}:
continue
try: lines=p.read_text(encoding="utf-8",errors="strict").splitlines()
except (UnicodeDecodeError,OSError): continue
for number,line in enumerate(lines,1):
for kind,pattern in PATTERNS.items():
if pattern.search(line):
evidence.append(WriterEvidence(p.relative_to(repo).as_posix(),number,kind,line.strip()[:240]))
unique={(e.path,e.line,e.kind,e.excerpt):e for e in evidence}
ordered=sorted(unique.values(),key=lambda e:(e.path,e.line,e.kind))
return {
"schema_version":"aios.git-writer-inventory.v1",
"evidence":[asdict(e) for e in ordered],
"writer_candidate_count":len(ordered),
"classification":"EVIDENCE_ONLY_NOT_AUTHORITY",
"completeness":"PARTIAL_DISCOVERY",
"covered_scope":["lexical_writer_candidates"],
"missing_scope":[
"writer_to_authorization_mapping",
"repository_and_ref_resolution",
"cas_semantics",
"receipt_contract",
"live_caller_and_runtime_identity",
],
"g0_exit_eligible":False,
}
def main()->int:
ap=argparse.ArgumentParser(); ap.add_argument("--repo",type=Path,required=True); ap.add_argument("--out",type=Path)
ns=ap.parse_args(); result=scan(ns.repo); text=json.dumps(result,indent=2,sort_keys=True)+"\n"
if ns.out: ns.out.write_text(text,encoding="utf-8")
else: print(text,end="")
return 0
if __name__=="__main__": raise SystemExit(main())
@@ -0,0 +1,120 @@
"""Retry / Toil Governor V2.
Pure decision layer. It never performs the business operation.
The governor is deliberately conservative:
- unknown side effects reconcile;
- proven success is reused/suppressed, never executed again;
- unrecognized states block;
- only explicitly executable states may return EXECUTE.
Fingerprint bytes reuse the existing TaskPlane ``canonical_json`` contract.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
import hashlib
import sys
# Reuse the existing in-repo canonical JSON contract. The platform tool is
# intentionally not a second canonical implementation.
try:
from app.task_plane.models import canonical_json
except ModuleNotFoundError: # standalone in-repo CLI execution
_repo_root = Path(__file__).resolve().parents[2]
_runtime = _repo_root / "active-runtime"
if str(_runtime) not in sys.path:
sys.path.insert(0, str(_runtime))
from app.task_plane.models import canonical_json
RETRY_FINGERPRINT_SCHEMA = "aios.retry-toil-fingerprint.v1"
UNKNOWN_STATES = frozenset(
{"UNKNOWN", "RECONCILIATION_REQUIRED", "EXECUTION_OUTCOME_UNKNOWN", "REMOTE_SYNCING"}
)
BLOCKED_STATES = frozenset({"BLOCKED", "OWNER_BOUNDARY", "NO_GO"})
SUCCESS_STATES = frozenset(
{"SUCCEEDED", "REMOTE_VERIFIED", "COMPLETED", "APPLIED", "ACCEPTED"}
)
EXECUTABLE_STATES = frozenset(
{"NOT_APPLIED", "READY", "FAILED_RETRYABLE", "RETRYABLE"}
)
class Decision(StrEnum):
EXECUTE = "EXECUTE"
RECONCILE = "RECONCILE"
SUPPRESS_NOOP = "SUPPRESS_NOOP"
BLOCK = "BLOCK"
@dataclass(frozen=True)
class RetryObservation:
operation_fingerprint: str
blocker_fingerprint: str
evidence_revision: str
side_effect_state: str
attempt: int
@dataclass(frozen=True)
class RetryDecision:
decision: Decision
reason: str
def fingerprint(value: object) -> str:
payload = {
"schema": RETRY_FINGERPRINT_SCHEMA,
"value": value,
}
return hashlib.sha256(canonical_json(payload).encode("utf-8")).hexdigest()
def decide(previous: RetryObservation | None, current: RetryObservation) -> RetryDecision:
state = str(current.side_effect_state or "").upper()
if state in UNKNOWN_STATES:
return RetryDecision(
Decision.RECONCILE,
"unknown/indeterminate side effect must reconcile before retry",
)
if state in BLOCKED_STATES:
return RetryDecision(Decision.BLOCK, "governance or dependency boundary")
if state in SUCCESS_STATES:
return RetryDecision(
Decision.SUPPRESS_NOOP,
"current effect is already proven successful; reuse receipt/readback",
)
if state not in EXECUTABLE_STATES:
return RetryDecision(
Decision.BLOCK,
f"unrecognized or non-executable side effect state: {state or '<empty>'}",
)
if previous is not None:
prev_state = str(previous.side_effect_state or "").upper()
same_effect = previous.operation_fingerprint == current.operation_fingerprint
if same_effect and prev_state in SUCCESS_STATES:
return RetryDecision(
Decision.RECONCILE,
"current executable observation conflicts with prior proven success",
)
if (
same_effect
and previous.blocker_fingerprint == current.blocker_fingerprint
and previous.evidence_revision == current.evidence_revision
):
return RetryDecision(
Decision.SUPPRESS_NOOP,
"no state/evidence change since prior attempt",
)
return RetryDecision(
Decision.EXECUTE,
"explicitly executable state with new evidence/effect context",
)
@@ -0,0 +1,133 @@
"""Read-only WP0a version/baseline discovery.
This is a *partial discovery producer*, not a version authority. It compares
already-discovered Git/runtime/production OIDs and records which Generation-G0
inventory dimensions are still missing.
Generation G0 must not be declared complete from this result alone.
"""
from __future__ import annotations
from dataclasses import dataclass, asdict
from pathlib import Path
import argparse, json, re, subprocess
OID_RE = re.compile(r"^[0-9a-f]{40}$")
REQUIRED_SCOPE = (
"git_authority_ref",
"runtime_source_identity",
"production_release_identity",
"architecture_current_projection",
"baseline_current_projection",
"VERSION_projection",
"README_projection",
"versions_directory",
"release_metadata_readers_writers_callers",
)
@dataclass(frozen=True)
class VersionInventory:
schema_version: str
authority_oid: str
runtime_oid: str
production_oid: str
runtime_descends_from_authority: bool | None
production_descends_from_authority: bool | None
authority_runtime_equal: bool
authority_production_equal: bool
conflicts: tuple[str, ...]
classification: str
completeness: str
covered_scope: tuple[str, ...]
missing_scope: tuple[str, ...]
g0_exit_eligible: bool
def to_dict(self): return asdict(self)
def _oid(value: str, name: str) -> str:
value = str(value or "").strip().lower()
if not OID_RE.fullmatch(value):
raise ValueError(f"invalid {name} oid")
return value
def _ancestor(repo: Path, left: str, right: str) -> bool | None:
cp = subprocess.run(
["git", "-C", str(repo), "merge-base", "--is-ancestor", left, right],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
if cp.returncode == 0:
return True
if cp.returncode == 1:
return False
return None
def collect(*, repo: Path, authority_oid: str, runtime_oid: str, production_oid: str) -> VersionInventory:
repo = repo.resolve()
authority = _oid(authority_oid, "authority")
runtime = _oid(runtime_oid, "runtime")
production = _oid(production_oid, "production")
rd = _ancestor(repo, authority, runtime) if repo.exists() else None
pd = _ancestor(repo, authority, production) if repo.exists() else None
conflicts = []
if authority != runtime:
conflicts.append("AUTHORITY_RUNTIME_DIVERGENCE")
if authority != production:
conflicts.append("AUTHORITY_PRODUCTION_DIVERGENCE")
if rd is False:
conflicts.append("RUNTIME_NOT_DESCENDANT_OF_AUTHORITY")
if pd is False:
conflicts.append("PRODUCTION_NOT_DESCENDANT_OF_AUTHORITY")
covered = (
"git_authority_ref",
"runtime_source_identity",
"production_release_identity",
)
missing = tuple(item for item in REQUIRED_SCOPE if item not in covered)
return VersionInventory(
"aios.version-baseline-inventory.v2",
authority,
runtime,
production,
rd,
pd,
authority == runtime,
authority == production,
tuple(conflicts),
"EVIDENCE_ONLY_NOT_AUTHORITY",
"PARTIAL_DISCOVERY",
covered,
missing,
False,
)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--repo", type=Path, required=True)
ap.add_argument("--authority-oid", required=True)
ap.add_argument("--runtime-oid", required=True)
ap.add_argument("--production-oid", required=True)
ap.add_argument("--out", type=Path)
ns = ap.parse_args()
result = collect(
repo=ns.repo,
authority_oid=ns.authority_oid,
runtime_oid=ns.runtime_oid,
production_oid=ns.production_oid,
)
text = json.dumps(result.to_dict(), indent=2, sort_keys=True) + "\n"
if ns.out:
ns.out.write_text(text, encoding="utf-8")
else:
print(text, end="")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+6 -1
View File
@@ -1,7 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
bash "$ROOT/scripts/silent_install.sh"
SILENT_INSTALL="$ROOT/scripts/silent_install.sh"
if [ ! -f "$SILENT_INSTALL" ]; then
echo "[V5] ERROR: missing required installer: $SILENT_INSTALL" >&2
exit 1
fi
bash "$SILENT_INSTALL"
echo "V5_SILENT_INSTALL_OK"
echo "WebOps: https://ops.cnnetmkt.top"
echo "Digital Life: https://ops.cnnetmkt.top/life"
+19 -3
View File
@@ -9,11 +9,17 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
import json
import os
import pathlib
import re
import time
EVENT_DIR = pathlib.Path(os.environ.get("AIOS_EVENT_DIR", "runtime/gitops/events"))
EVENT_DIR = pathlib.Path(os.environ.get("AIOS_EVENT_DIR", "runtime/gitops/events")).resolve()
EVENT_DIR.mkdir(parents=True, exist_ok=True)
# Strictly-constrained event ID: a single safe path segment. Rejects absolute
# paths, separators, and any traversal (`..`, backslashes) before path join.
_SAFE_EVENT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]*$")
MAX_EVENT_FILE_BYTES = 8 * 1024 * 1024
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
@@ -26,8 +32,18 @@ class Handler(BaseHTTPRequestHandler):
return
event_id = event.get("id", f"evt-{int(time.time())}")
path = EVENT_DIR / f"{event_id}.json"
path.write_text(json.dumps(event, indent=2), encoding='utf-8')
if not isinstance(event_id, str) or not _SAFE_EVENT_ID.fullmatch(event_id):
self.send_response(400)
self.end_headers()
return
target = (EVENT_DIR / f"{event_id}.json").resolve()
if target.parent != EVENT_DIR:
# Defensive: resolved path must remain directly inside the event dir.
self.send_response(400)
self.end_headers()
return
target.write_text(json.dumps(event, indent=2), encoding='utf-8')
self.send_response(200)
self.end_headers()
+1 -1
View File
@@ -11,7 +11,7 @@ SERVICE_PACKAGE_ROOT = REPOSITORY_ROOT / "services" / "aios-autonomous-release-c
ACTIVE_RUNTIME_ROOT = REPOSITORY_ROOT / "active-runtime"
ACTIVE_RUNTIME_APP = ACTIVE_RUNTIME_ROOT / "app"
for path in (TEST_ROOT, SERVICE_PACKAGE_ROOT, ACTIVE_RUNTIME_APP):
for path in (ACTIVE_RUNTIME_ROOT, TEST_ROOT, SERVICE_PACKAGE_ROOT, ACTIVE_RUNTIME_APP):
value = str(path)
if value not in sys.path:
sys.path.insert(0, value)
+202
View File
@@ -0,0 +1,202 @@
from __future__ import annotations
from pathlib import Path
import importlib.util
import sys
def load(path: Path, name: str):
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
ROOT = Path(__file__).resolve().parents[2]
MOD = load(ROOT / "scripts/platform/aios_caller_scan.py", "aios_caller_scan")
def write(repo, rel, text):
p = repo / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(text, encoding="utf-8")
return p
def complete_negative_profile():
return {
"runtime_hosts_complete": True,
"runtime_hosts": ["h1"],
"loaded_bytes_scanned": True,
"systemd_scanned": True,
"api_routes_scanned": True,
"scheduler_scanned": True,
"observation_window_complete": True,
"observation_window_seconds": 3600,
"rollback_ref": "runbook:rollback-1",
"independent_acceptance": "GO",
"probe_failures": [],
}
def bound_runtime_item(subject, *, repo, identity_key, identity_value):
return {
identity_key: identity_value,
"source_path": subject.relative_to(repo).as_posix(),
"source_sha256": MOD._sha(subject),
"active": True,
"evidence_ref": "ev:runtime",
}
def test_static_production_import_is_source_evidence_not_runtime_live(tmp_path):
write(tmp_path, "pkg/target.py", "VALUE=1\n")
write(tmp_path, "pkg/caller.py", "from pkg import target\nprint(target.VALUE)\n")
result = MOD.scan(
repo=tmp_path,
subject=Path("pkg/target.py"),
runtime_profile={"runtime_hosts_complete": False},
)
assert result.assessment == "UNKNOWN"
assert any(c.kind == "STATIC_PRODUCTION" for c in result.callers)
def test_relative_import_is_resolved(tmp_path):
write(tmp_path, "app/__init__.py", "")
write(tmp_path, "app/foo.py", "VALUE=1\n")
write(tmp_path, "app/consumer.py", "from . import foo\nprint(foo.VALUE)\n")
result = MOD.scan(
repo=tmp_path,
subject=Path("app/foo.py"),
runtime_profile={"runtime_hosts_complete": False},
)
assert result.assessment == "UNKNOWN"
assert any(c.identity == "app/consumer.py" for c in result.callers)
def test_active_runtime_import_root_is_supported(tmp_path):
write(tmp_path, "active-runtime/app/foo.py", "VALUE=1\n")
write(tmp_path, "active-runtime/app/consumer.py", "import foo\nprint(foo.VALUE)\n")
result = MOD.scan(
repo=tmp_path,
subject=Path("active-runtime/app/foo.py"),
runtime_profile={"runtime_hosts_complete": False},
)
assert any(c.identity == "active-runtime/app/consumer.py" for c in result.callers)
def test_test_only_caller_does_not_make_live(tmp_path):
write(tmp_path, "pkg/target.py", "VALUE=1\n")
write(tmp_path, "tests/test_target.py", "import pkg.target\n")
result = MOD.scan(
repo=tmp_path,
subject=Path("pkg/target.py"),
runtime_profile={"runtime_hosts_complete": False},
)
assert result.caller_count == 0
assert result.test_only_callers
assert result.assessment == "LIKELY_ORPHAN"
def test_unknown_dynamic_reference_never_degrades_to_orphan(tmp_path):
write(tmp_path, "pkg/target.py", "VALUE=1\n")
write(
tmp_path,
"pkg/dynamic.py",
"import importlib\nname=input()\nimportlib.import_module(name)\n",
)
result = MOD.scan(
repo=tmp_path,
subject=Path("pkg/target.py"),
runtime_profile=complete_negative_profile(),
)
assert result.unknown_dynamic_refs
assert result.assessment == "UNKNOWN"
def test_runtime_hosts_complete_alone_cannot_prove_orphan(tmp_path):
write(tmp_path, "foo.py", "x=1\n")
result = MOD.scan(
repo=tmp_path,
subject=Path("foo.py"),
runtime_profile={"runtime_hosts_complete": True},
)
assert result.assessment == "LIKELY_ORPHAN"
assert result.coverage["negative_evidence_complete"] is False
def test_empty_runtime_host_set_cannot_prove_orphan(tmp_path):
write(tmp_path, "foo.py", "x=1\n")
profile = complete_negative_profile()
profile["runtime_hosts"] = []
result = MOD.scan(repo=tmp_path, subject=Path("foo.py"), runtime_profile=profile)
assert result.assessment == "LIKELY_ORPHAN"
def test_probe_failure_is_unknown(tmp_path):
write(tmp_path, "foo.py", "x=1\n")
profile = complete_negative_profile()
profile["probe_failures"] = ["host:h1 timeout"]
result = MOD.scan(repo=tmp_path, subject=Path("foo.py"), runtime_profile=profile)
assert result.assessment == "UNKNOWN"
def test_complete_negative_evidence_proves_orphan(tmp_path):
write(tmp_path, "pkg/target.py", "VALUE=1\n")
result = MOD.scan(
repo=tmp_path,
subject=Path("pkg/target.py"),
runtime_profile=complete_negative_profile(),
)
assert result.assessment == "PROVEN_ORPHAN"
assert result.coverage["negative_evidence_complete"] is True
def test_runtime_loaded_requires_exact_path_sha_and_active(tmp_path):
subject = write(tmp_path, "pkg/target.py", "VALUE=1\n")
digest = MOD._sha(subject)
good = {
"source_path": "pkg/target.py",
"source_sha256": digest,
"active": True,
"evidence_ref": "ev:loaded",
}
result = MOD.scan(
repo=tmp_path,
subject=Path("pkg/target.py"),
runtime_profile={"runtime_loaded": [good]},
)
assert result.assessment == "LIVE"
assert any(c.kind == "RUNTIME_LOADED" for c in result.callers)
wrong_path = dict(good, source_path="other/target.py")
result2 = MOD.scan(
repo=tmp_path,
subject=Path("pkg/target.py"),
runtime_profile={"runtime_loaded": [wrong_path]},
)
assert result2.assessment != "LIVE"
def test_systemd_api_scheduler_require_bound_active_artifact(tmp_path):
subject = write(tmp_path, "pkg/target.py", "VALUE=1\n")
profile = {
"systemd": [bound_runtime_item(subject, repo=tmp_path, identity_key="unit", identity_value="svc.service")],
"api_routes": [bound_runtime_item(subject, repo=tmp_path, identity_key="route", identity_value="/x")],
"scheduler": [bound_runtime_item(subject, repo=tmp_path, identity_key="identity", identity_value="sched")],
}
result = MOD.scan(repo=tmp_path, subject=Path("pkg/target.py"), runtime_profile=profile)
assert result.assessment == "LIVE"
assert {"SYSTEMD", "API_ROUTE", "SCHEDULER"} <= {c.kind for c in result.callers}
def test_scanner_does_not_import_business_subject(tmp_path):
marker = tmp_path / "EXECUTED"
subject = write(tmp_path, "pkg/target.py", f"open({str(marker)!r},'w').write('bad')\n")
result = MOD.scan(
repo=tmp_path,
subject=Path("pkg/target.py"),
runtime_profile={"runtime_hosts_complete": False},
)
assert not marker.exists()
assert result.subject["sha256"] == MOD._sha(subject)
@@ -0,0 +1,30 @@
from __future__ import annotations
from pathlib import Path
import importlib.util
import sys
ROOT=Path(__file__).resolve().parents[2]
spec=importlib.util.spec_from_file_location("envprof",ROOT/"scripts/platform/aios_environment_profile.py")
M=importlib.util.module_from_spec(spec); sys.modules[spec.name]=M; spec.loader.exec_module(M)
def test_environment_profile_schema_is_secret_safe_and_structured():
p=M.collect("test-host")
d=p.to_dict()
assert d["schema_version"]=="aios.environment-profile.v2"
assert d["host_id"]=="test-host"
assert set(d["capabilities"]) >= {
"posix","systemd","systemd_load_credential","sudo_noninteractive","git","python","node"
}
assert set(d["filesystem"]) == {"case_sensitive","line_ending_default"}
assert d["completeness"]=="CAPABILITY_DISCOVERY_ONLY"
assert d["g0_exit_eligible"] is False
assert all("token" not in x.lower() and "secret=" not in x.lower() for x in d["credential_mechanisms"])
def test_missing_requirements_are_computed(monkeypatch):
monkeypatch.setattr(M, "_systemd_running", lambda: False)
p=M.collect("h", requirements=("systemd","git"))
d=p.to_dict()
assert "systemd" in d["missing_requirements"]
assert d["completeness"]=="REQUIREMENTS_EVALUATED"
@@ -0,0 +1,84 @@
from __future__ import annotations
from pathlib import Path
import importlib.util, sys, subprocess
ROOT=Path(__file__).resolve().parents[2]
def load(rel,name):
spec=importlib.util.spec_from_file_location(name,ROOT/rel)
mod=importlib.util.module_from_spec(spec); sys.modules[name]=mod; spec.loader.exec_module(mod); return mod
V=load("scripts/platform/aios_version_baseline_inventory.py","wp0a")
G=load("scripts/platform/aios_git_writer_inventory.py","wp0b")
A=load("scripts/platform/aios_authorization_inventory.py","wp0c")
C=load("scripts/platform/aios_canonical_inventory.py","wp0d")
E=load("scripts/platform/aios_change_envelope.py","changeenv")
def test_version_inventory_detects_authority_runtime_split(tmp_path):
subprocess.run(["git","init","-q",str(tmp_path)],check=True)
subprocess.run(["git","-C",str(tmp_path),"config","user.email","t@example.invalid"],check=True)
subprocess.run(["git","-C",str(tmp_path),"config","user.name","t"],check=True)
(tmp_path/"a").write_text("1")
subprocess.run(["git","-C",str(tmp_path),"add","a"],check=True)
subprocess.run(["git","-C",str(tmp_path),"commit","-qm","a"],check=True)
a=subprocess.check_output(["git","-C",str(tmp_path),"rev-parse","HEAD"],text=True).strip()
(tmp_path/"a").write_text("2")
subprocess.run(["git","-C",str(tmp_path),"commit","-qam","b"],check=True)
b=subprocess.check_output(["git","-C",str(tmp_path),"rev-parse","HEAD"],text=True).strip()
result=V.collect(repo=tmp_path,authority_oid=a,runtime_oid=b,production_oid=a)
assert result.runtime_descends_from_authority is True
assert "AUTHORITY_RUNTIME_DIVERGENCE" in result.conflicts
assert result.completeness=="PARTIAL_DISCOVERY"
assert result.g0_exit_eligible is False
assert "VERSION_projection" in result.missing_scope
def test_git_writer_inventory_is_evidence_only(tmp_path):
(tmp_path/"x.py").write_text('subprocess.run(["git","push","origin","main"])\n# commitFilesGoverned\n')
result=G.scan(tmp_path)
assert result["classification"]=="EVIDENCE_ONLY_NOT_AUTHORITY"
assert result["completeness"]=="PARTIAL_DISCOVERY"
assert result["g0_exit_eligible"] is False
assert "cas_semantics" in result["missing_scope"]
assert {x["kind"] for x in result["evidence"]} >= {"governed_commit"}
def test_authorization_inventory_finds_terms_without_granting_authority(tmp_path):
(tmp_path/"x.py").write_text("TaskPrincipal = None\ncapabilities=['x']\n# owner approval\n")
result=A.scan(tmp_path)
assert result["classification"]=="EVIDENCE_ONLY_NOT_AUTHORITY"
assert result["completeness"]=="PARTIAL_DISCOVERY"
assert result["g0_exit_eligible"] is False
assert "owner_broker_chain" in result["missing_scope"]
kinds={k for row in result["references"] for k in row["kinds"]}
assert {"task_principal","capability","owner_approval"} <= kinds
def test_canonical_inventory_finds_sorted_json_and_sha(tmp_path):
(tmp_path/"x.py").write_text(
"import json,hashlib\n"
"def canonical(v): return json.dumps(v,sort_keys=True,separators=(',',':'))\n"
"def digest(v): return hashlib.sha256(canonical(v).encode()).hexdigest()\n"
)
result=C.scan(tmp_path)
assert result["classification"]=="EVIDENCE_ONLY_NOT_CANONICAL_AUTHORITY"
assert result["completeness"]=="PARTIAL_DISCOVERY"
assert result["g0_exit_eligible"] is False
assert "producer_consumer_mapping" in result["missing_scope"]
assert result["definition_count"]>=2
def test_change_envelope_records_postimage_but_has_no_authorization_effect(tmp_path):
(tmp_path/"x.txt").write_text("hello")
result=E.build_change_envelope(
change_id="chg-1",work_package="WP0e",source_commit="a"*40,
candidate_root=tmp_path,target_paths=["x.txt"],preimage_sha256={"x.txt":None},
contract_versions={"caller_scan":"v1"},live_caller_evidence_ref=None,
tests=[{"name":"t","status":"PASS"}],rollback_ref=None,independent_review_ref=None,
)
assert result["authorization_effect"]=="NONE"
assert len(result["postimage_sha256"]["x.txt"])==64
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
from pathlib import Path
import json
ROOT = Path(__file__).resolve().parents[2]
def test_gate_dag_is_acyclic_and_ordered():
doc = json.loads((ROOT / "docs/GATE_DAG_V2.json").read_text(encoding="utf-8"))
gates = [row["id"] for row in doc["gates"]]
assert gates == [
"REVIEW_READY",
"STAGING_ACTIVATION_READY",
"STAGING_ACCEPTED",
"PRODUCTION_ELIGIBLE",
"PRODUCTION_ACCEPTED",
"PROGRAM_COMPLETE",
]
index = {name: i for i, name in enumerate(gates)}
for row in doc["gates"]:
for dep in row["requires"]:
if dep in index:
assert index[dep] < index[row["id"]], (row["id"], dep)
def test_no_gate_requires_its_own_future_acceptance():
doc = json.loads((ROOT / "docs/GATE_DAG_V2.json").read_text(encoding="utf-8"))
prohibited = {
"REVIEW_READY": {"STAGING_ACCEPTED", "PRODUCTION_ACCEPTED", "PROGRAM_COMPLETE"},
"STAGING_ACTIVATION_READY": {"STAGING_ACCEPTED", "PRODUCTION_ACCEPTED", "PROGRAM_COMPLETE"},
"PRODUCTION_ELIGIBLE": {"PRODUCTION_ACCEPTED", "PROGRAM_COMPLETE"},
}
for row in doc["gates"]:
assert not (set(row["requires"]) & prohibited.get(row["id"], set()))
def test_schedulability_and_retry_governor_reuse_existing_canonical_contract():
sched = (ROOT / "active-runtime/app/task_plane/schedulability.py").read_text(encoding="utf-8")
retry = (ROOT / "scripts/platform/aios_retry_toil_governor.py").read_text(encoding="utf-8")
for text in (sched, retry):
assert "sort_keys=True" not in text
assert "separators=" not in text
assert "from .models import canonical_json" in sched
assert "from app.task_plane.models import canonical_json" in retry
def test_g0_partial_discovery_contract_is_documented():
text = (ROOT / "docs/G0_EVIDENCE_COVERAGE_V2.md").read_text(encoding="utf-8")
assert "PARTIAL_DISCOVERY" in text
assert "g0_exit_eligible=false" in text
assert "WP3–WP10" in text
@@ -0,0 +1,59 @@
from __future__ import annotations
from pathlib import Path
import importlib.util
import sys
ROOT = Path(__file__).resolve().parents[2]
ACTIVE_RUNTIME = ROOT / "active-runtime"
if str(ACTIVE_RUNTIME) not in sys.path:
sys.path.insert(0, str(ACTIVE_RUNTIME))
spec = importlib.util.spec_from_file_location(
"gov", ROOT / "scripts/platform/aios_retry_toil_governor.py"
)
G = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = G
spec.loader.exec_module(G)
def obs(side="NOT_APPLIED", op="op", block="b", rev="1", attempt=1):
return G.RetryObservation(op, block, rev, side, attempt)
def test_unknown_side_effect_reconciles():
for state in ("UNKNOWN", "RECONCILIATION_REQUIRED", "EXECUTION_OUTCOME_UNKNOWN"):
assert G.decide(None, obs(side=state)).decision == G.Decision.RECONCILE
def test_governance_boundary_blocks():
for state in ("BLOCKED", "OWNER_BOUNDARY", "NO_GO"):
assert G.decide(None, obs(side=state)).decision == G.Decision.BLOCK
def test_success_states_never_execute_even_without_previous():
for state in ("SUCCEEDED", "REMOTE_VERIFIED", "COMPLETED"):
assert G.decide(None, obs(side=state)).decision == G.Decision.SUPPRESS_NOOP
def test_unrecognized_state_blocks():
assert G.decide(None, obs(side="BOGUS")).decision == G.Decision.BLOCK
def test_same_evidence_suppresses_noop():
previous = obs()
current = obs(attempt=2)
assert G.decide(previous, current).decision == G.Decision.SUPPRESS_NOOP
def test_changed_evidence_executes_only_for_explicit_executable_state():
assert G.decide(obs(), obs(rev="2", attempt=2)).decision == G.Decision.EXECUTE
def test_prior_success_conflicting_with_not_applied_reconciles():
previous = obs(side="SUCCEEDED")
current = obs(side="NOT_APPLIED", rev="2", attempt=2)
assert G.decide(previous, current).decision == G.Decision.RECONCILE
def test_fingerprint_is_deterministic_and_schema_bound():
assert G.fingerprint({"b": 2, "a": 1}) == G.fingerprint({"a": 1, "b": 2})
assert G.RETRY_FINGERPRINT_SCHEMA in {"aios.retry-toil-fingerprint.v1"}
@@ -0,0 +1,581 @@
from __future__ import annotations
import importlib
import json
import sys
import threading
from dataclasses import replace
from datetime import datetime, timezone
from hashlib import sha256
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
REPO = ROOT / "repo"
CANDIDATE = ROOT / "s3" / "next-action-coordinator-v1" / "source" / "active-runtime"
sys.path.insert(0, str(REPO / "active-runtime"))
import app
import app.task_plane
app.__path__.append(str(CANDIDATE / "app"))
app.task_plane.__path__.append(str(CANDIDATE / "app" / "task_plane"))
mod = importlib.import_module("app.task_plane.autonomous_next_child")
from app.aios_task_lineage import ( # noqa: E402
AcceptanceCondition,
NextLegalAction,
RootAcceptanceSnapshot,
RootReviewRecord,
)
from app.aios_task_lineage_integration import ( # noqa: E402
LINEAGE_METADATA_KEY,
LINEAGE_REQUEST_DIGEST_KEY,
)
from app.task_plane.errors import ConflictError # noqa: E402
from app.task_plane.mission import MissionRecord # noqa: E402
from app.task_plane.models import TaskPlan, compute_sha256_hex # noqa: E402
from app.task_plane.root_action_consumer import AuthoritativeRootActionPlan # noqa: E402
from app.task_plane.store import TaskPlaneStore # noqa: E402
MISSION = "mission-autonomous-test"
ROOT_TASK = "task-autonomous-root"
OWNER = "owner-autonomous-test"
AUTH_ID = "authenticated-autonomous-test"
ACCEPTANCE_REF = f"mission-acceptance:{MISSION}:verifier-test"
NOW = datetime(2026, 9, 14, 12, 0, tzinfo=timezone.utc)
def canonical(value: object) -> bytes:
return json.dumps(
value, ensure_ascii=False, sort_keys=True, separators=(",", ":")
).encode("utf-8")
def make_task_plan(task_id: str, condition: str | None, sequence: int | None) -> TaskPlan:
relation = "ROOT" if condition is None else "CHILD"
parent = None if condition is None else ROOT_TASK
metadata = {
LINEAGE_METADATA_KEY: {
"schema": "aios.task-lineage.v1",
"task_id": task_id,
"root_task_id": ROOT_TASK,
"parent_task_id": parent,
"relation_type": relation,
"mission_id": MISSION,
"owner_principal_id": OWNER,
"root_acceptance_ref": ACCEPTANCE_REF,
**({} if sequence is None else {"sibling_sequence": sequence}),
}
}
if condition is not None:
metadata["acceptance_condition"] = condition
metadata[LINEAGE_REQUEST_DIGEST_KEY] = sha256(
f"request:{condition}".encode()
).hexdigest()
return TaskPlan.from_dict(
{
"schema_version": 1,
"task_id": task_id,
"task_type": "mission.acceptance",
"issuer": {"type": "authenticated", "id": AUTH_ID},
"intent": "ROOT" if condition is None else f"INTENT-{condition}",
"required_capabilities": ["code.read"],
"preferred_workers": [],
"risk": "medium",
"approval_policy": "patch-only",
"dependencies": [],
"max_delegation_depth": 5,
"budgets": {},
"evidence_requirements": ["acceptance-proof"],
"parent_task_id": parent,
"correlation_id": ROOT_TASK,
"metadata": metadata,
"data_classification": "internal",
"execution_zone": "isolated-development",
"network_policy": "controlled",
"logging_policy": "redacted",
"allowed_input_paths": [],
"allowed_output_paths": [],
"output_policy": "standard",
}
)
class Reader:
def __init__(self, context):
self.context = context
self.calls = 0
def read(self, root_task_id):
assert root_task_id == ROOT_TASK
self.calls += 1
return self.context
class Adapter:
def __init__(self, dispositions, outcomes):
self.dispositions = dispositions
self.outcomes = outcomes
self.classify_calls = []
self.prepare_calls = []
def classify(self, *, context, binding, task):
self.classify_calls.append(binding.acceptance_key)
return self.dispositions.get(binding.acceptance_key)
def prepare(self, request):
self.prepare_calls.append(request)
outcome = self.outcomes[request.binding.acceptance_key]
return outcome(request) if callable(outcome) else outcome
def make_world(tmp_path: Path, conditions=("C01",), *, action=None, db_name="task.sqlite3"):
store = TaskPlaneStore(str(tmp_path / db_name))
root = store.create_task(
make_task_plan(ROOT_TASK, None, None),
"root-key",
owner_principal_id=OWNER,
mission_id=MISSION,
)
children = []
bindings = []
original = []
for index, key in enumerate(conditions):
task_id = f"task-child-{key.lower()}"
plan = make_task_plan(task_id, key, index + 1)
child = store.create_task(
plan,
f"child-{key}",
owner_principal_id=OWNER,
mission_id=MISSION,
)
children.append(child)
route = (
mod.PreparationRoute.SEMANTIC_IMPORT
if key == "C02"
else mod.PreparationRoute.GOVERNED_GIT
)
bindings.append(
mod.TrustedChildBinding(
acceptance_key=key,
acceptance_position=index,
task_id=task_id,
mission_id=MISSION,
root_task_id=ROOT_TASK,
parent_task_id=ROOT_TASK,
owner_principal_id=OWNER,
root_acceptance_ref=ACCEPTANCE_REF,
sibling_sequence=index + 1,
lineage_request_digest=plan.metadata[LINEAGE_REQUEST_DIGEST_KEY],
task_plan_digest=compute_sha256_hex(plan.to_dict(redact=False)),
route=route,
)
)
original.append({"key": key})
mission = MissionRecord(
mission_id=MISSION,
root_task_id=ROOT_TASK,
authenticated_identity=AUTH_ID,
owner_principal_id=OWNER,
owner_alias="owner-alias",
goal_slug="AUTONOMOUS-TEST",
root_acceptance_ref=ACCEPTANCE_REF,
acceptance_verifier_id="verifier-test",
original_acceptance={"schema": "test.v1", "conditions": original},
request_digest="a" * 64,
idempotency_key="mission-key",
created_at=root.created_at,
)
snapshot = RootAcceptanceSnapshot(
tuple(AcceptanceCondition(key, False) for key in conditions)
)
next_action = action or NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD
review = RootReviewRecord(
review_id="RR-test-1",
root_task_id=ROOT_TASK,
reviewed_at=NOW,
trigger_child_task_id=children[0].task_id,
root_revision_before=root.revision,
root_revision_after=root.revision,
completed_children=(),
active_children=tuple(child.task_id for child in children),
blocked_children=(),
remaining_acceptance_conditions=tuple(conditions),
next_legal_action=next_action,
root_complete=False,
evidence_refs=(),
)
context = mod.CoordinationContext(
mission, review, root, tuple(children), snapshot
)
mod.PreparationLedger.install_schema(store)
ledger = mod.PreparationLedger(store)
policy = mod.AutonomousNextChildPolicy(bindings)
ready_dir = tmp_path / "ready"
ready_dir.mkdir(exist_ok=True)
authority = mod.ReadyEffectDirectoryAuthority(ready_dir.resolve())
return {
"store": store,
"root": root,
"children": children,
"bindings": bindings,
"mission": mission,
"review": review,
"snapshot": snapshot,
"context": context,
"reader": Reader(context),
"ledger": ledger,
"policy": policy,
"ready_dir": ready_dir,
"authority": authority,
}
def ready_document(world, key="C01", *, reconcile=False, external_key="effect-key"):
binding = next(item for item in world["bindings"] if item.acceptance_key == key)
job = "job-" + sha256(binding.task_id.encode()).hexdigest()[:32]
execution = sha256(f"execution:{key}".encode()).hexdigest()
base = {
"task_id": binding.task_id,
"job_id": job,
"repository": "aios/aios",
"base_oid": "1" * 40,
"expected_branch_oid": "1" * 40,
"target_branch": "agent/autonomous-test",
"generation": 1,
}
value = {
"schema": mod.READY_EFFECT_SCHEMA,
"mission_id": MISSION,
"root_task_id": ROOT_TASK,
"acceptance_key": key,
"target_task_id": binding.task_id,
"target_job_id": job,
"execution_digest": execution,
"admission_envelope_id": f"envelope-{key.lower()}",
"external_idempotency_key": external_key,
"effect_payload": {
"reconcile_request": {"task_id": binding.task_id, "job_id": job},
"writer_lease_request": {
**base,
"branch_mode": "CONTINUE_EXISTING",
"idempotency_key": external_key,
},
"commit_request": {
**base,
"idempotency_key": external_key,
"files": {"active-runtime/app/autonomous.txt": "YXV0b25vbW91cwo="},
"message": "autonomous exact effect",
},
},
"reconciliation_authority_digest": "b" * 64 if reconcile else None,
}
raw = canonical(value)
digest = sha256(raw).hexdigest()
path = world["ready_dir"] / f"{digest}.json"
path.write_bytes(raw)
return value, digest
def ready_outcome(value, digest):
return mod.PreparationOutcome(
state=mod.PreparationState.READY_EFFECT,
execution_digest=value["execution_digest"],
admission_envelope_id=value["admission_envelope_id"],
target_job_id=value["target_job_id"],
external_idempotency_key=value["external_idempotency_key"],
effect_binding_digest=digest,
)
def coordinator(world, adapter, *, identity="coordinator-a", clock=lambda: 100.0):
return mod.AutonomousNextChildCoordinator(
reader=world["reader"],
ledger=world["ledger"],
policy=world["policy"],
adapter=adapter,
ready_authority=world["authority"],
coordinator_id=identity,
clock=clock,
preparation_lease_seconds=10,
)
def test_ready_projection_is_read_only_and_stable_across_reviews(tmp_path):
world = make_world(tmp_path)
value, digest = ready_document(world)
disposition = mod.CandidateDisposition(
mod.ReadinessClass.FROZEN_GIT_PLAN,
mod.PreparationRoute.GOVERNED_GIT,
value["execution_digest"],
value["external_idempotency_key"],
)
adapter = Adapter({"C01": disposition}, {"C01": ready_outcome(value, digest)})
provider = mod.ReadyRootActionPlanProvider(
ledger=world["ledger"], policy=world["policy"], ready_authority=world["authority"]
)
before = world["store"].conn.total_changes
assert provider(world["mission"], world["review"], world["root"], world["children"]) is None
assert world["store"].conn.total_changes == before
result = coordinator(world, adapter).coordinate_once(ROOT_TASK)
assert result.status == mod.CoordinationStatus.READY_EFFECT
before = world["store"].conn.total_changes
first = provider(world["mission"], world["review"], world["root"], world["children"])
assert world["store"].conn.total_changes == before
assert first == {
"target_task_id": world["children"][0].task_id,
"target_job_id": value["target_job_id"],
"external_idempotency_key": value["external_idempotency_key"],
"effect_payload": value["effect_payload"],
}
old = AuthoritativeRootActionPlan.create(
review_id="RR-old", mission_id=MISSION, root_task_id=ROOT_TASK,
root_revision=1, action=NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD,
**first,
)
newer_review = replace(world["review"], review_id="RR-new")
second = provider(world["mission"], newer_review, world["root"], world["children"])
new = AuthoritativeRootActionPlan.create(
review_id="RR-new", mission_id=MISSION, root_task_id=ROOT_TASK,
root_revision=2, action=NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD,
**second,
)
assert (old.execution_key, old.target_job_id, old.external_idempotency_key) == (
new.execution_key, new.target_job_id, new.external_idempotency_key
)
def test_concurrent_coordinators_claim_once(tmp_path):
world = make_world(tmp_path)
value, digest = ready_document(world)
disposition = mod.CandidateDisposition(
mod.ReadinessClass.FROZEN_GIT_PLAN,
mod.PreparationRoute.GOVERNED_GIT,
value["execution_digest"], value["external_idempotency_key"],
)
entered, release = threading.Event(), threading.Event()
class BlockingAdapter(Adapter):
def prepare(self, request):
self.prepare_calls.append(request)
entered.set()
assert release.wait(5)
return ready_outcome(value, digest)
adapter = BlockingAdapter({"C01": disposition}, {})
first_result = []
thread = threading.Thread(
target=lambda: first_result.append(coordinator(world, adapter).coordinate_once(ROOT_TASK))
)
thread.start()
assert entered.wait(5)
second = coordinator(world, adapter, identity="coordinator-b").coordinate_once(ROOT_TASK)
assert second.status == mod.CoordinationStatus.IN_PROGRESS
assert len(adapter.prepare_calls) == 1
release.set()
thread.join(5)
assert first_result[0].status == mod.CoordinationStatus.READY_EFFECT
assert len(world["ledger"].list_root(ROOT_TASK)) == 1
def test_restart_after_unknown_outcome_reuses_exact_effect(tmp_path):
world = make_world(tmp_path)
value, digest = ready_document(world)
disposition = mod.CandidateDisposition(
mod.ReadinessClass.FROZEN_GIT_PLAN,
mod.PreparationRoute.GOVERNED_GIT,
value["execution_digest"], value["external_idempotency_key"],
)
calls = 0
def uncertain(_request):
nonlocal calls
calls += 1
if calls == 1:
raise TimeoutError("response lost after durable authority write")
return ready_outcome(value, digest)
adapter = Adapter({"C01": disposition}, {"C01": uncertain})
with pytest.raises(TimeoutError):
coordinator(world, adapter).coordinate_once(ROOT_TASK)
assert len(world["ledger"].list_root(ROOT_TASK)) == 1
world["store"].close()
restarted = TaskPlaneStore(str(tmp_path / "task.sqlite3"))
world["store"] = restarted
world["ledger"] = mod.PreparationLedger(restarted)
result = coordinator(world, adapter, identity="coordinator-restart", clock=lambda: 111.0).coordinate_once(ROOT_TASK)
assert result.status == mod.CoordinationStatus.READY_EFFECT
row = world["ledger"].list_root(ROOT_TASK)[0]
assert row.target_job_id == value["target_job_id"]
assert row.external_idempotency_key == value["external_idempotency_key"]
assert calls == 2
def test_unknown_effect_requires_reconcile_first_proof(tmp_path):
world = make_world(tmp_path, action=NextLegalAction.RECOVER_EXISTING_CHILD)
value, digest = ready_document(world, reconcile=True)
disposition = mod.CandidateDisposition(
mod.ReadinessClass.EXISTING_OR_UNKNOWN_GIT_EFFECT,
mod.PreparationRoute.GOVERNED_GIT,
value["execution_digest"], value["external_idempotency_key"],
)
def prepare(request):
assert request.require_reconcile_first is True
return ready_outcome(value, digest)
adapter = Adapter({"C01": disposition}, {"C01": prepare})
assert coordinator(world, adapter).coordinate_once(ROOT_TASK).status == mod.CoordinationStatus.READY_EFFECT
other = make_world(tmp_path / "negative", action=NextLegalAction.RECOVER_EXISTING_CHILD)
bad_value, bad_digest = ready_document(other, reconcile=False)
bad = Adapter(
{"C01": replace(disposition, execution_digest=bad_value["execution_digest"])},
{"C01": ready_outcome(bad_value, bad_digest)},
)
with pytest.raises(ConflictError, match="reconcile-first"):
coordinator(other, bad).coordinate_once(ROOT_TASK)
assert other["ledger"].list_root(ROOT_TASK)[0].state != mod.PreparationState.READY_EFFECT
def test_retryable_candidate_does_not_block_independent_ready_sibling(tmp_path):
world = make_world(tmp_path, conditions=("C01", "C03"))
value, digest = ready_document(world, key="C03")
first = mod.CandidateDisposition(
mod.ReadinessClass.FROZEN_GIT_PLAN,
mod.PreparationRoute.GOVERNED_GIT,
"1" * 64, "first-effect-key",
)
second = mod.CandidateDisposition(
mod.ReadinessClass.FROZEN_GIT_PLAN,
mod.PreparationRoute.GOVERNED_GIT,
value["execution_digest"], value["external_idempotency_key"],
)
adapter = Adapter(
{"C01": first, "C03": second},
{
"C01": mod.PreparationOutcome(
mod.PreparationState.RETRYABLE_BLOCKED,
retry_at=200.0,
error_code="RUNNER_TEMPORARILY_UNAVAILABLE",
execution_digest="1" * 64,
external_idempotency_key="first-effect-key",
),
"C03": ready_outcome(value, digest),
},
)
result = coordinator(world, adapter).coordinate_once(ROOT_TASK)
assert result.status == mod.CoordinationStatus.READY_EFFECT
assert [request.binding.acceptance_key for request in adapter.prepare_calls] == ["C01", "C03"]
def test_metadata_label_cannot_override_trusted_child_binding(tmp_path):
world = make_world(tmp_path)
world["children"][0].task_plan.metadata["acceptance_condition"] = "C99"
with pytest.raises(ConflictError, match="trusted child authority"):
world["policy"].eligible(world["context"])
assert world["ledger"].list_root(ROOT_TASK) == ()
def test_task_plan_or_ready_document_drift_fails_closed(tmp_path):
world = make_world(tmp_path)
value, digest = ready_document(world)
disposition = mod.CandidateDisposition(
mod.ReadinessClass.FROZEN_GIT_PLAN,
mod.PreparationRoute.GOVERNED_GIT,
value["execution_digest"], value["external_idempotency_key"],
)
adapter = Adapter({"C01": disposition}, {"C01": ready_outcome(value, digest)})
coordinator(world, adapter).coordinate_once(ROOT_TASK)
provider = mod.ReadyRootActionPlanProvider(
ledger=world["ledger"], policy=world["policy"], ready_authority=world["authority"]
)
path = world["ready_dir"] / f"{digest}.json"
path.write_bytes(path.read_bytes() + b" ")
with pytest.raises(ConflictError, match="authority changed"):
provider(world["mission"], world["review"], world["root"], world["children"])
def test_root_complete_and_not_ready_never_project_plan(tmp_path):
world = make_world(tmp_path)
world["reader"].context = replace(
world["context"],
review=replace(
world["review"],
root_complete=True,
next_legal_action=NextLegalAction.ROOT_COMPLETE,
remaining_acceptance_conditions=(),
),
acceptance=RootAcceptanceSnapshot((AcceptanceCondition("C01", True),)),
)
adapter = Adapter({}, {})
assert coordinator(world, adapter).coordinate_once(ROOT_TASK).status == mod.CoordinationStatus.NO_ACTION
assert adapter.classify_calls == []
assert world["ledger"].list_root(ROOT_TASK) == ()
def test_live_binding_file_matches_observed_children_exactly():
observed = json.loads((ROOT / "s3" / "live-mission-children.json").read_text(encoding="utf-8"))
configured = json.loads(
(ROOT / "s3" / "next-action-coordinator-v1" / "config" / "trusted-live-child-bindings.json").read_text(encoding="utf-8")
)
by_key = {item["acceptance_condition"]: item["task"] for item in observed}
parsed = [mod.TrustedChildBinding.from_mapping(item) for item in configured["bindings"]]
assert [item.acceptance_key for item in parsed] == ["C01", "C02", "C03", "C04", "C05", "C06"]
for binding in parsed:
task = by_key[binding.acceptance_key]
plan = task["plan"]
lineage = plan["metadata"][LINEAGE_METADATA_KEY]
assert binding.task_id == task["task_id"]
assert binding.sibling_sequence == lineage["sibling_sequence"]
assert binding.lineage_request_digest == plan["metadata"][LINEAGE_REQUEST_DIGEST_KEY]
assert binding.task_plan_digest == compute_sha256_hex(plan)
def test_same_child_readiness_advances_with_cas_without_identity_change(tmp_path):
world = make_world(tmp_path)
binding = world["bindings"][0]
task = world["children"][0]
policy = world["policy"]
ledger = world["ledger"]
context = world["reader"].read(world["root"].task_id)
first = mod.CandidateDisposition(
readiness_class=mod.ReadinessClass.NEEDS_AGENT_PREPARATION,
route=mod.PreparationRoute.GOVERNED_GIT,
execution_digest=None,
external_idempotency_key=None,
)
record1 = ledger.ensure_selection(
context=context, binding=binding, disposition=first, policy=policy, now=100.0
)
second = mod.CandidateDisposition(
readiness_class=mod.ReadinessClass.FROZEN_GIT_PLAN,
route=mod.PreparationRoute.GOVERNED_GIT,
execution_digest="a" * 64,
external_idempotency_key="stable-effect",
)
record2 = ledger.ensure_selection(
context=context, binding=binding, disposition=second, policy=policy, now=101.0
)
assert record2.selection_key == record1.selection_key
assert record2.row_version == record1.row_version + 1
assert record2.readiness_class == mod.ReadinessClass.FROZEN_GIT_PLAN
assert record2.execution_digest == "a" * 64
assert record2.external_idempotency_key == "stable-effect"
def test_malformed_acceptance_condition_fails_domain_error(tmp_path):
world = make_world(tmp_path)
context = world["reader"].read(world["root"].task_id)
broken = mod.CoordinationContext(
mission=context.mission,
review=context.review,
root=context.root,
descendants=context.descendants,
acceptance=RootAcceptanceSnapshot((object(),)),
)
with pytest.raises(ConflictError, match="fresh acceptance snapshot is invalid"):
world["policy"].validate_context(broken)
+629
View File
@@ -0,0 +1,629 @@
from __future__ import annotations
import concurrent.futures
import hashlib
import sys
import threading
import types
from copy import deepcopy
from dataclasses import replace
from pathlib import Path
import pytest
# Exercise the overlay exactly as deployment will see it: changed modules from
# this artifact plus unchanged dependencies from the checked-out repository.
_WORK = Path(__file__).resolve().parents[4]
_OVERLAY_APP = Path(__file__).resolve().parents[2] / "source" / "active-runtime" / "app"
_REPO_APP = _WORK / "repo" / "active-runtime" / "app"
_app = types.ModuleType("app")
_app.__path__ = [str(_OVERLAY_APP), str(_REPO_APP)]
_app.__package__ = "app"
sys.modules["app"] = _app
_task_plane = types.ModuleType("app.task_plane")
_task_plane.__path__ = [str(_OVERLAY_APP / "task_plane"), str(_REPO_APP / "task_plane")]
_task_plane.__package__ = "app.task_plane"
sys.modules["app.task_plane"] = _task_plane
from app.aios_task_mission_authority_server import (
AUDIENCE,
SignedMissionIngress,
canonical_bytes,
)
from app.task_plane.api import TaskPlaneService
from app.task_plane.errors import ConflictError, ForbiddenError
from app.task_plane.legacy_attachment import (
ATTACH_EVIDENCE_SCHEMA,
ATTACH_REQUEST_SCHEMA,
AuthorityReadbackRef,
VerifiedLegacyAttachmentEvidence,
)
from app.task_plane.mission import MissionAuthority, MissionPrincipalBinding
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
OWNER = "local-owner-authority"
OTHER = "other-owner-authority"
LEGACY_TASK = "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-INTEGRATE-20260913T184412Z-19b7"
EXTERNAL_JOB = "job-7457a322d5f9d6931c7cd99ce3ec6c18"
INTERNAL_JOB = "job-1eaae20deef71a85638f36926091392b"
EXECUTION_KEY = "1eaae20deef71a85638f36926091392b1032de9b302457912fca4ac14ec93b3b"
ACCEPTANCE = {
"schema": "identity-lineage-root-acceptance.v1",
"conditions": [
{
"key": "C04",
"purpose": "LEGACY-LINEAGE-MIGRATION",
"required_evidence": [
"verified legacy ownership and acceptance relevance",
"unchanged task/job/idempotency/reconciliation identity",
"authoritative attach readback",
],
}
],
"legacy_candidate": {"task_id": LEGACY_TASK, "job_id": EXTERNAL_JOB},
}
def evidence() -> VerifiedLegacyAttachmentEvidence:
refs = tuple(
AuthorityReadbackRef(
role=role,
authority=f"authority-{role}",
readback_ref=f"readonly:{role}:stable-ref",
canonical_sha256=hashlib.sha256(role.encode()).hexdigest(),
observed_at="2026-09-14T01:00:00Z",
revision=4 if role in {"agent_task", "bridge_job"} else 1,
)
for role in ("agent_task", "admission", "owner_registry", "bridge_job")
)
return VerifiedLegacyAttachmentEvidence(
schema=ATTACH_EVIDENCE_SCHEMA,
source_kind="agent-task-bridge-job",
source_task_id=LEGACY_TASK,
source_task_created_at="2026-09-13T18:44:12Z",
source_task_status="EXPIRED",
source_task_revision=4,
source_task_attempt=1,
source_task_max_attempts=1,
external_job_id=EXTERNAL_JOB,
internal_job_id=INTERNAL_JOB,
internal_idempotency_key=EXECUTION_KEY,
job_branch="refs/heads/task/legacy-recovery",
job_state="RECONCILIATION_REQUIRED",
job_revision=4,
side_effect_state="EXECUTION_OUTCOME_UNKNOWN",
grant_id="grant-historic",
grant_digest="a" * 64,
execution_digest="b" * 64,
epoch_id="epoch-historic",
epoch_digest="c" * 64,
historical_signer_id=OWNER,
historical_signer_fingerprint="owner-key-fingerprint",
registry_generation=7,
signature_verified=True,
epoch_currently_valid=False,
readbacks=refs,
)
class Provider:
def __init__(self, value=None):
self.value = value or evidence()
self.calls = []
def read_verified(self, **request):
self.calls.append(deepcopy(request))
return self.value
def service(path: Path, provider=...):
store = TaskPlaneStore(str(path))
bindings = [
MissionPrincipalBinding(
authenticated_identity=identity,
owner_principal_id=identity,
acceptance_verifier_id="identity-lineage-live-closure-v1",
preferred_owner_alias="LOCALOWNER" if identity == OWNER else "OTHEROWNER",
allowed_task_capabilities=("code.read",),
max_risk="low",
)
for identity in (OWNER, OTHER)
]
kwargs = {} if provider is ... else {"legacy_attachment_evidence_provider": provider}
authority = MissionAuthority(store, principal_bindings=bindings, **kwargs)
return TaskPlaneService(
store,
policy=PolicyEngine(
execution_enabled=False,
identity_capabilities={
OWNER: {"task.creator", "task.read", "task.delegate"},
OTHER: {"task.creator", "task.read", "task.delegate"},
},
),
mission_authority=authority,
)
def prepare(
svc,
mission_key="mission-create",
anchor_key="c04-anchor",
*,
canonical_contract=False,
):
mission = svc.create_mission(
{"goal_slug": "restore-canonical-task-lineage", "original_acceptance": ACCEPTANCE},
mission_key,
actor_identity=OWNER,
)
anchor = svc.delegate_mission(
mission["root_task_id"],
{
"task_type": "mission.acceptance",
"intent": (
"LEGACY-LINEAGE-MIGRATION"
if canonical_contract
else "attach verified legacy execution subject"
),
"risk": "low",
"required_capabilities": ["code.read"],
"metadata": (
{
"acceptance_condition": "C04",
"required_evidence": ACCEPTANCE["conditions"][0][
"required_evidence"
],
}
if canonical_contract
else {"acceptance_condition": "C04"}
),
},
anchor_key,
actor_identity=OWNER,
)
root = svc.store.get_task(mission["root_task_id"])
anchor_record = svc.store.get_task(anchor["task_id"])
request = {
"schema": ATTACH_REQUEST_SCHEMA,
"anchor_task_id": anchor["task_id"],
"source": {
"kind": "agent-task-bridge-job",
"task_id": LEGACY_TASK,
"external_job_id": EXTERNAL_JOB,
},
"expected": {
"root_revision": root.revision,
"anchor_revision": anchor_record.revision,
"agent_task_revision": 4,
"agent_task_attempt": 1,
"internal_job_id": INTERNAL_JOB,
"job_revision": 4,
"internal_idempotency_key": EXECUTION_KEY,
},
}
return mission, anchor, request
def test_attach_persists_recovery_display_and_preserves_unknown_source(tmp_path):
provider = Provider()
svc = service(tmp_path / "core.db", provider)
mission, anchor, request = prepare(svc)
tasks_before = {
row["task_id"]: row["task_json"]
for row in svc.store.conn.execute("SELECT task_id,task_json FROM tasks")
}
provider_before = deepcopy(provider.value.to_dict())
receipt = svc.attach_legacy_attachment(
mission["mission_id"], request, "attach-once", actor_identity=OWNER
)
assert receipt["attachment_kind"] == "LEGACY_EXECUTION_SUBJECT"
assert receipt["relation_type"] == "RECOVERY"
assert receipt["anchor_task_id"] == anchor["task_id"]
assert receipt["display_name"].startswith("TASK-LOCALOWNER-20260913T184412Z-R")
assert receipt["display_name"].endswith("-RECOVERY-412Z19B7")
assert receipt["source"]["source_task_id"] == LEGACY_TASK
assert receipt["source"]["job_state"] == "RECONCILIATION_REQUIRED"
assert receipt["source"]["side_effect_state"] == "EXECUTION_OUTCOME_UNKNOWN"
assert receipt["source"]["internal_job_id"] == INTERNAL_JOB
assert receipt["source"]["internal_idempotency_key"] == EXECUTION_KEY
assert receipt["mutation_scope"] == "MISSION_ATTACHMENT_ONLY"
assert receipt["source_mutated"] is False
assert receipt["reconciliation_executed"] is False
assert receipt["terminal_status_granted"] is False
assert provider.value.to_dict() == provider_before
assert tasks_before == {
row["task_id"]: row["task_json"]
for row in svc.store.conn.execute("SELECT task_id,task_json FROM tasks")
}
readback = svc.get_legacy_attachment(
mission["mission_id"], receipt["attachment_id"], actor_identity=OWNER
)
assert readback["receipt_digest"] == receipt["receipt_digest"]
assert readback["authoritative_readback"]["ledger_verified"] is True
assert readback["authoritative_readback"]["identity_match"] is True
svc.store.close()
def test_response_loss_restart_exact_replay_works_without_provider(tmp_path):
path = tmp_path / "restart.db"
provider = Provider()
first = service(path, provider)
mission, _, request = prepare(first)
written = first.attach_legacy_attachment(
mission["mission_id"], request, "lost-response", actor_identity=OWNER
)
first.store.close()
restarted = service(path, None)
replay = restarted.attach_legacy_attachment(
mission["mission_id"], request, "lost-response", actor_identity=OWNER
)
assert replay["idempotent"] is True
assert replay["attachment_id"] == written["attachment_id"]
assert replay["receipt_digest"] == written["receipt_digest"]
assert restarted.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_legacy_attachments"
).fetchone()[0] == 1
with pytest.raises(ForbiddenError, match="provider is not configured"):
restarted.get_legacy_attachment(
mission["mission_id"], written["attachment_id"], actor_identity=OWNER
)
restarted.store.close()
reader = service(path, Provider())
assert reader.get_legacy_attachment(
mission["mission_id"], written["attachment_id"], actor_identity=OWNER
)["source"]["job_state"] == "RECONCILIATION_REQUIRED"
reader.store.close()
def test_fail_closed_conflicts_closed_root_and_other_owner_do_not_write(tmp_path):
svc = service(tmp_path / "guards.db", None)
mission, _, request = prepare(svc)
with pytest.raises(ForbiddenError, match="provider is not configured"):
svc.attach_legacy_attachment(
mission["mission_id"], request, "no-provider", actor_identity=OWNER
)
with pytest.raises(ForbiddenError):
svc.attach_legacy_attachment(
mission["mission_id"], request, "other-owner", actor_identity=OTHER
)
svc.store.conn.execute(
"UPDATE tasks SET status='COMPLETED' WHERE task_id=?",
(mission["root_task_id"],),
)
svc.missions._legacy_attachment_provider = Provider()
with pytest.raises(ConflictError, match="completed root"):
svc.attach_legacy_attachment(
mission["mission_id"], request, "closed-root", actor_identity=OWNER
)
assert svc.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_legacy_attachments"
).fetchone()[0] == 0
svc.store.close()
def test_provider_revision_or_historical_owner_mismatch_never_writes(tmp_path):
path = tmp_path / "provider-mismatch.db"
mismatched = replace(evidence(), historical_signer_id=OTHER)
svc = service(path, Provider(mismatched))
mission, _, request = prepare(svc)
with pytest.raises(ForbiddenError, match="historical Owner"):
svc.attach_legacy_attachment(
mission["mission_id"], request, "wrong-owner", actor_identity=OWNER
)
svc.missions._legacy_attachment_provider = Provider(
replace(evidence(), job_revision=5)
)
with pytest.raises(ConflictError, match="provider evidence mismatch"):
svc.attach_legacy_attachment(
mission["mission_id"], request, "wrong-revision", actor_identity=OWNER
)
assert svc.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_legacy_attachments"
).fetchone()[0] == 0
svc.store.close()
def test_concurrent_exact_replay_and_source_uniqueness(tmp_path):
path = tmp_path / "concurrent.db"
first_provider, second_provider = Provider(), Provider()
left = service(path, first_provider)
mission, _, request = prepare(left)
right = service(path, second_provider)
def attach(svc):
return svc.attach_legacy_attachment(
mission["mission_id"], request, "concurrent-key", actor_identity=OWNER
)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
receipts = list(pool.map(attach, (left, right)))
assert {item["attachment_id"] for item in receipts}.__len__() == 1
assert {item["idempotent"] for item in receipts} == {False, True}
assert left.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_legacy_attachments"
).fetchone()[0] == 1
with pytest.raises(ConflictError, match="already attached"):
left.attach_legacy_attachment(
mission["mission_id"], request, "different-key", actor_identity=OWNER
)
other_mission, _, other_request = prepare(left, "mission-two", "c04-two")
with pytest.raises(ConflictError, match="already attached"):
left.attach_legacy_attachment(
other_mission["mission_id"],
other_request,
"different-mission",
actor_identity=OWNER,
)
with pytest.raises(ConflictError, match="replay conflict"):
left.attach_legacy_attachment(
mission["mission_id"],
{**request, "expected": {**request["expected"], "job_revision": 5}},
"concurrent-key",
actor_identity=OWNER,
)
left.store.close()
right.store.close()
def test_acceptance_child_binding_rejects_spoof_and_duplicate(tmp_path):
svc = service(tmp_path / "binding.db", Provider())
mission, anchor, _ = prepare(svc)
binding = svc.store.conn.execute(
"SELECT * FROM task_mission_acceptance_child_bindings "
"WHERE mission_id=? AND condition_key='C04'",
(mission["mission_id"],),
).fetchone()
assert binding["child_task_id"] == anchor["task_id"]
with pytest.raises(ConflictError, match="binding is ambiguous"):
svc.delegate_mission(
mission["root_task_id"],
{
"task_type": "mission.acceptance",
"intent": "unrelated-spoof-child",
"risk": "low",
"required_capabilities": ["code.read"],
"metadata": {
"acceptance_condition": "C04",
"required_evidence": ACCEPTANCE["conditions"][0][
"required_evidence"
],
},
},
"spoof-c04",
actor_identity=OWNER,
)
with pytest.raises(ConflictError, match="ambiguous"):
svc.delegate_mission(
mission["root_task_id"],
{
"task_type": "mission.acceptance",
"intent": "LEGACY-LINEAGE-MIGRATION",
"risk": "low",
"required_capabilities": ["code.read"],
"metadata": {
"acceptance_condition": "C04",
"required_evidence": ACCEPTANCE["conditions"][0][
"required_evidence"
],
},
},
"duplicate-c04",
actor_identity=OWNER,
)
assert svc.store.list_children(mission["root_task_id"]) == [anchor["task_id"]]
svc.store.close()
def test_existing_canonical_child_is_backfilled_without_recreation(tmp_path):
path = tmp_path / "binding-backfill.db"
first = service(path, Provider())
mission, anchor, request = prepare(first, canonical_contract=True)
first.store.conn.execute(
"DELETE FROM task_mission_acceptance_child_bindings WHERE mission_id=?",
(mission["mission_id"],),
)
task_ids = set(first.store.list_children(mission["root_task_id"]))
first.store.close()
restarted = service(path, Provider())
receipt = restarted.attach_legacy_attachment(
mission["mission_id"], request, "backfill-existing", actor_identity=OWNER
)
binding = restarted.store.conn.execute(
"SELECT * FROM task_mission_acceptance_child_bindings "
"WHERE mission_id=? AND condition_key='C04'",
(mission["mission_id"],),
).fetchone()
assert binding["child_task_id"] == anchor["task_id"]
assert receipt["anchor_task_id"] == anchor["task_id"]
assert set(restarted.store.list_children(mission["root_task_id"])) == task_ids
restarted.store.close()
def test_condition_binding_is_generic_not_c04_only(tmp_path):
svc = service(tmp_path / "generic-binding.db", Provider())
acceptance = deepcopy(ACCEPTANCE)
acceptance["conditions"].insert(
0,
{
"key": "C03",
"purpose": "ROOT-ACTION-EXECUTOR-BINDING",
"required_evidence": ["real executor receipt"],
},
)
mission = svc.create_mission(
{"goal_slug": "generic-condition-binding", "original_acceptance": acceptance},
"generic-binding-mission",
actor_identity=OWNER,
)
child = svc.delegate_mission(
mission["root_task_id"],
{
"task_type": "mission.acceptance",
"intent": "ROOT-ACTION-EXECUTOR-BINDING",
"risk": "low",
"required_capabilities": ["code.read"],
"metadata": {
"acceptance_condition": "C03",
"required_evidence": ["real executor receipt"],
},
},
"generic-c03-child",
actor_identity=OWNER,
)
binding = svc.store.conn.execute(
"SELECT * FROM task_mission_acceptance_child_bindings "
"WHERE mission_id=? AND condition_key='C03'",
(mission["mission_id"],),
).fetchone()
assert binding["child_task_id"] == child["task_id"]
svc.store.close()
def test_preexisting_ambiguous_c04_children_fail_closed_before_provider(tmp_path):
provider = Provider()
svc = service(tmp_path / "preexisting-ambiguous.db", provider)
mission, _, request = prepare(svc)
svc.store.conn.execute(
"DELETE FROM task_mission_acceptance_child_bindings WHERE mission_id=?",
(mission["mission_id"],),
)
binder = svc.missions._bind_declared_acceptance_child
svc.missions._bind_declared_acceptance_child = lambda *_args, **_kwargs: None
try:
svc.delegate_mission(
mission["root_task_id"],
{
"task_type": "mission.acceptance",
"intent": "unrelated-spoof-child",
"risk": "low",
"required_capabilities": ["code.read"],
"metadata": {
"acceptance_condition": "C04",
"required_evidence": ACCEPTANCE["conditions"][0][
"required_evidence"
],
},
},
"pre-v2-spoof-c04",
actor_identity=OWNER,
)
finally:
svc.missions._bind_declared_acceptance_child = binder
with pytest.raises(ConflictError, match="binding is ambiguous"):
svc.attach_legacy_attachment(
mission["mission_id"], request, "ambiguous-attach", actor_identity=OWNER
)
assert provider.calls == []
assert svc.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_legacy_attachments"
).fetchone()[0] == 0
svc.store.close()
def test_duplicate_acceptance_admission_racing_attach_rolls_back(tmp_path):
path = tmp_path / "binding-race.db"
entered = threading.Event()
release = threading.Event()
class BlockingProvider(Provider):
def read_verified(self, **request):
entered.set()
assert release.wait(timeout=5)
return super().read_verified(**request)
left = service(path, BlockingProvider())
mission, anchor, request = prepare(left)
right = service(path, Provider())
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
attached = pool.submit(
left.attach_legacy_attachment,
mission["mission_id"],
request,
"binding-race-attach",
actor_identity=OWNER,
)
assert entered.wait(timeout=5)
duplicate = pool.submit(
right.delegate_mission,
mission["root_task_id"],
{
"task_type": "mission.acceptance",
"intent": "LEGACY-LINEAGE-MIGRATION",
"risk": "low",
"required_capabilities": ["code.read"],
"metadata": {
"acceptance_condition": "C04",
"required_evidence": ACCEPTANCE["conditions"][0][
"required_evidence"
],
},
},
"binding-race-duplicate",
actor_identity=OWNER,
)
with pytest.raises(ConflictError, match="binding is ambiguous"):
duplicate.result(timeout=5)
release.set()
receipt = attached.result(timeout=5)
assert receipt["anchor_task_id"] == anchor["task_id"]
assert left.store.list_children(mission["root_task_id"]) == [anchor["task_id"]]
assert left.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_acceptance_child_bindings"
).fetchone()[0] == 1
assert left.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_legacy_attachments"
).fetchone()[0] == 1
left.store.close()
right.store.close()
class OwnerVerifier:
signer_id = OWNER
def verify(self, *, namespace, payload, signature, key_id):
return key_id == "owner-key" and signature == hashlib.sha256(payload).hexdigest()
def signed(method, path, body, key):
request = {
"schema": "aios.signed-mission-request.v1",
"audience": AUDIENCE,
"method": method,
"path": path,
"body": body,
"idempotency_key": key,
"issued_at": 100,
"expires_at": 200,
"nonce": "legacy_attach_nonce_1234",
}
return {
"request": request,
"key_id": "owner-key",
"signature": hashlib.sha256(canonical_bytes(request)).hexdigest(),
}
def test_real_signed_ingress_post_get_and_replay_status(tmp_path):
svc = service(tmp_path / "signed.db", Provider())
mission, _, request = prepare(svc)
ingress = SignedMissionIngress(svc, OwnerVerifier(), clock=lambda: 150)
path = f"/missions/{mission['mission_id']}/legacy-attachments"
status, first = ingress.handle(signed("POST", path, request, "signed-attach"))
assert status == 201
status, replay = ingress.handle(signed("POST", path, request, "signed-attach"))
assert status == 200 and replay["attachment_id"] == first["attachment_id"]
get_path = f"{path}/{first['attachment_id']}"
status, fetched = ingress.handle(signed("GET", get_path, None, None))
assert status == 200 and fetched["receipt_digest"] == first["receipt_digest"]
with pytest.raises(ForbiddenError):
ingress.handle(signed("GET", f"{path}/../bad", None, None))
svc.store.close()
@@ -0,0 +1,199 @@
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==[]
File diff suppressed because it is too large Load Diff
+405
View File
@@ -0,0 +1,405 @@
from __future__ import annotations
import pytest
from app.task_plane.schedulability import (
ClaimConditions,
MemoryProofRepository,
SchedulabilityChanged,
SchedulabilityService,
)
from app.task_plane.auto_claim import AutoClaimCoordinator
NOW = 2_000_000_000.0
ACTOR = "principal:owner"
WORKER = "worker-a"
TASK = "task-a"
class Authority:
def __init__(self):
self.task = {
"task_id": TASK,
"revision": 7,
"generation": 3,
"status": "QUEUED",
"required_capabilities": ["task.execute", "writer.lease"],
"priority": 9,
"created_at": "2026-09-14T00:00:00Z",
}
self.worker = {
"worker_id": WORKER,
"registration_revision": 11,
"identity": "worker-principal-a",
"status": "ACTIVE",
"disabled": False,
"last_heartbeat": NOW - 5,
"heartbeat_interval_seconds": 10,
"capabilities": ["task.execute", "writer.lease"],
"max_concurrency": 2,
}
self.auth = {
"allowed": True,
"authorization_ref": "authz-1",
"revision": "4",
}
self.scope_ok = True
self.scope_digest = "scope:" + "a" * 32
self.active_claims = 0
self.deps = True
self.freeze = False
self.lease = False
self.claim_calls = []
self.candidates = [self.task]
self.before_atomic_claim = None
self._fence = 40
def read_task(self, task_id):
if task_id != self.task["task_id"]:
raise KeyError(task_id)
return dict(self.task)
def list_candidate_tasks(self):
return [dict(v) for v in self.candidates]
def read_worker(self, worker_id):
if worker_id != self.worker["worker_id"]:
raise KeyError(worker_id)
return dict(self.worker)
def read_authorization(self, **_kwargs):
return dict(self.auth)
def active_claim_count(self, worker_id):
assert worker_id == WORKER
return self.active_claims
def dependencies_satisfied(self, task):
assert task.get("task_id")
return self.deps
def freeze_blocked(self, task):
assert task.get("task_id")
return self.freeze
def conflicting_lease(self, task_id):
assert task_id
return self.lease
def exact_scope_digest(self, **_kwargs):
return self.scope_ok, self.scope_digest
def claim_atomically(
self,
*,
task_id,
worker_id,
actor_identity,
expected: ClaimConditions,
lease_seconds,
schedulability_proof_digest,
):
if self.before_atomic_claim:
callback = self.before_atomic_claim
self.before_atomic_claim = None
callback(self)
# This fake models the *contract* required of the real TaskPlane claim
# transaction. It does not prove a production adapter is atomic.
task = self.read_task(task_id)
worker = self.read_worker(worker_id)
auth = self.read_authorization(task=task, worker=worker, actor_identity=actor_identity)
scope_ok, scope_digest = self.exact_scope_digest(
task=task, worker=worker, actor_identity=actor_identity
)
checks = [
(task["revision"] == expected.task_revision, "task revision"),
(task["generation"] == expected.task_generation, "task generation"),
(task["status"] == expected.task_status and task["status"] in {"CREATED","QUEUED","FAILED_RECOVERABLE"}, "task status"),
(worker["registration_revision"] == expected.worker_generation, "worker generation"),
(worker["identity"] == expected.worker_identity, "worker identity"),
(worker["status"] == "ACTIVE" and not worker["disabled"], "worker state"),
(auth["allowed"] is True, "authorization allowed"),
(auth["authorization_ref"] == expected.authorization_ref, "authorization ref"),
(str(auth["revision"]) == expected.authorization_revision, "authorization revision"),
(scope_ok and scope_digest == expected.scope_digest, "scope"),
(self.active_claims < expected.capacity_limit, "capacity"),
(self.deps, "dependencies"),
(not self.freeze, "freeze"),
(not self.lease, "lease"),
]
failed = [name for ok, name in checks if not ok]
if failed:
raise SchedulabilityChanged("atomic claim condition changed: " + ",".join(failed))
self.active_claims += 1
self._fence += 1
call = {
"task_id": task_id,
"worker_id": worker_id,
"actor_identity": actor_identity,
"expected": expected,
"lease_seconds": lease_seconds,
"schedulability_proof_digest": schedulability_proof_digest,
}
self.claim_calls.append(call)
return {
"status": "CLAIMED",
"claim_id": f"claim-{len(self.claim_calls)}",
"lease_id": f"lease-{len(self.claim_calls)}",
"fencing_generation": self._fence,
"receipt_revision": task["revision"] + 1,
}
@pytest.fixture()
def env():
authority = Authority()
proofs = MemoryProofRepository()
service = SchedulabilityService(authority, proofs, clock=lambda: NOW, proof_ttl_seconds=30)
return authority, proofs, service
def codes(proof):
return {b.code for b in proof.blockers}
def test_a01_preflight_aggregates_multiple_blockers(env):
authority, _, service = env
authority.worker["status"] = "INACTIVE"
authority.worker["capabilities"] = []
authority.scope_ok = False
authority.auth["allowed"] = False
authority.active_claims = 2
authority.deps = False
authority.freeze = True
authority.lease = True
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert codes(proof) == {
"STALE_OR_INACTIVE_WORKER", "MISSING_CAPABILITY", "EXACT_SCOPE_MISMATCH",
"AUTHORIZATION_BLOCKED", "CAPACITY_EXHAUSTED", "DEPENDENCY_BLOCKED",
"FREEZE_BLOCKED", "CONFLICTING_LEASE",
}
assert not proof.claimable
def test_a02_healthy_worker_is_claimable_and_proof_is_non_bearer(env):
_, proofs, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=False
)
assert proof.claimable
assert proofs.get(proof.proof_id) is None
def test_a03_explicit_zero_capacity_is_exhausted(env):
authority, _, service = env
authority.worker["max_concurrency"] = 0
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert proof.observation.capacity_limit == 0
assert "CAPACITY_EXHAUSTED" in codes(proof)
assert not proof.claimable
@pytest.mark.parametrize("value", [-1, True, "2", 1.5])
def test_invalid_capacity_is_structured_blocker(env, value):
authority, _, service = env
authority.worker["max_concurrency"] = value
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert "INVALID_AUTHORITY_VALUE" in codes(proof)
assert not proof.claimable
@pytest.mark.parametrize("status", ["COMPLETED", "SUCCEEDED", "FAILED_FINAL", "CANCELLED", "RUNNING"])
def test_terminal_or_running_task_is_not_claimable(env, status):
authority, _, service = env
authority.task["status"] = status
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert "TASK_STATE_NOT_CLAIMABLE" in codes(proof)
assert not proof.claimable
@pytest.mark.parametrize(
("mutation", "expected"),
[
(lambda a: a.worker.__setitem__("last_heartbeat", NOW - 999), "STALE_OR_INACTIVE_WORKER"),
(lambda a: a.worker.__setitem__("capabilities", ["task.execute"]), "MISSING_CAPABILITY"),
(lambda a: setattr(a, "scope_ok", False), "EXACT_SCOPE_MISMATCH"),
(lambda a: a.auth.__setitem__("allowed", False), "AUTHORIZATION_BLOCKED"),
(lambda a: setattr(a, "active_claims", 2), "CAPACITY_EXHAUSTED"),
(lambda a: setattr(a, "deps", False), "DEPENDENCY_BLOCKED"),
(lambda a: setattr(a, "freeze", True), "FREEZE_BLOCKED"),
(lambda a: setattr(a, "lease", True), "CONFLICTING_LEASE"),
],
)
def test_preflight_specific_blockers(env, mutation, expected):
authority, _, service = env
mutation(authority)
proof = service.preflight(task_id=TASK, worker_id=WORKER, actor_identity=ACTOR)
assert expected in codes(proof)
assert not proof.claimable
def test_cross_actor_proof_replay_is_rejected(env):
_, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
with pytest.raises(SchedulabilityChanged, match="identity mismatch"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity="other"
)
def test_cross_worker_proof_replay_is_rejected(env):
_, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
with pytest.raises(SchedulabilityChanged, match="identity mismatch"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id="worker-b", actor_identity=ACTOR
)
def test_expired_proof_is_rejected(env):
authority, proofs, _ = env
clock = [NOW]
service = SchedulabilityService(authority, proofs, clock=lambda: clock[0], proof_ttl_seconds=1)
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
clock[0] += 2
with pytest.raises(SchedulabilityChanged, match="expired"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
@pytest.mark.parametrize(
"mutation",
[
lambda a: a.worker.__setitem__("registration_revision", 12),
lambda a: a.task.__setitem__("revision", 8),
lambda a: a.task.__setitem__("generation", 4),
lambda a: a.auth.__setitem__("revision", "5"),
lambda a: setattr(a, "active_claims", 1),
lambda a: setattr(a, "scope_digest", "scope:" + "b" * 32),
lambda a: a.task.__setitem__("status", "COMPLETED"),
],
)
def test_claim_time_stable_authority_change_invalidates_proof(env, mutation):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
mutation(authority)
with pytest.raises(SchedulabilityChanged):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
def test_atomic_linearization_rechecks_capacity_after_read_recheck(env):
authority, _, service = env
authority.worker["max_concurrency"] = 1
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
authority.before_atomic_claim = lambda a: setattr(a, "active_claims", 1)
with pytest.raises(SchedulabilityChanged, match="capacity"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
assert authority.claim_calls == []
def test_atomic_linearization_rechecks_authorization_after_read_recheck(env):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
authority.before_atomic_claim = lambda a: a.auth.__setitem__("allowed", False)
with pytest.raises(SchedulabilityChanged, match="authorization"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
def test_atomic_linearization_rechecks_freeze_after_read_recheck(env):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
authority.before_atomic_claim = lambda a: setattr(a, "freeze", True)
with pytest.raises(SchedulabilityChanged, match="freeze"):
service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR
)
def test_normal_claim_requires_lease_and_fence_receipt(env):
authority, _, service = env
proof = service.preflight(
task_id=TASK, worker_id=WORKER, actor_identity=ACTOR, persist=True
)
out = service.claim_with_recheck(
proof_id=proof.proof_id, worker_id=WORKER, actor_identity=ACTOR, lease_seconds=45
)
assert out["status"] == "CLAIMED"
assert out["claim_id"].startswith("claim-")
assert out["lease_id"].startswith("lease-")
assert out["fencing_generation"] > 0
assert out["receipt_revision"] == 8
assert len(authority.claim_calls) == 1
def test_discovery_skips_blocked_high_priority_and_selects_next_claimable(env):
authority, proofs, service = env
blocked = dict(authority.task)
blocked["task_id"] = "blocked"
blocked["priority"] = 100
good = dict(authority.task)
good["task_id"] = TASK
authority.candidates = [blocked, good]
original_read = authority.read_task
def read_task(task_id):
if task_id == "blocked":
value = dict(blocked)
value["required_capabilities"] = ["missing-cap"]
return value
return original_read(task_id)
authority.read_task = read_task
proof = service.discover(worker_id=WORKER, actor_identity=ACTOR)
assert proof is not None
assert proof.task_id == TASK
assert proofs.get(proof.proof_id) is proof
def test_auto_claim_diagnostics_do_not_mask_authorization(env):
authority, _, service = env
coordinator = AutoClaimCoordinator(service)
authority.auth["allowed"] = False
waiting = coordinator.discover(worker_id=WORKER, actor_identity=ACTOR)
assert waiting.status == "WAITING_AUTHORIZATION"
assert "AUTHORIZATION_BLOCKED" in waiting.blocker_codes
def test_auto_claim_reports_no_candidates(env):
authority, _, service = env
authority.candidates = []
out = AutoClaimCoordinator(service).discover(worker_id=WORKER, actor_identity=ACTOR)
assert out.status == "NO_CANDIDATES"
assert out.candidate_count == 0
def test_auto_claim_capacity_wait_is_precise(env):
authority, _, service = env
authority.active_claims = 2
out = AutoClaimCoordinator(service).discover(worker_id=WORKER, actor_identity=ACTOR)
assert out.status == "WAITING_CAPACITY"
assert out.blocker_codes == ("CAPACITY_EXHAUSTED",)
@@ -0,0 +1,516 @@
from __future__ import annotations
from dataclasses import replace
from datetime import datetime, timezone
import threading
import pytest
from aios_task_lineage import (
AcceptanceCondition,
LineageError,
NextLegalAction,
RelationType,
RootAcceptanceSnapshot,
TaskRecord,
attach_legacy_lineage,
derive_safe_owner_alias,
make_child_lineage,
make_root_lineage,
lineage_from_metadata,
lineage_to_metadata,
rename_display_only,
review_root_after_child_terminal,
root_review_from_record,
root_review_to_record,
validate_lineage,
)
NOW = datetime(2026, 9, 14, 8, 30, tzinfo=timezone.utc)
def root_record(task_id="TASK-ROOT-00000001", owner="principal-user-a", alias="ALICE"):
lineage = make_root_lineage(
task_id=task_id,
owner_principal_id=owner,
owner_alias=alias,
created_at=NOW,
mission_id="MISSION-1",
goal_slug="goal-to-evidence-closure",
root_acceptance_ref="acceptance:v1",
)
return TaskRecord(task_id, 1, "RUNNING", lineage)
def child_record(
root,
*,
task_id="TASK-CHILD-00000001",
parent=None,
relation=RelationType.CHILD,
seq=1,
status="RUNNING",
proven=False,
evidence=(),
):
parent = parent or root
lineage = make_child_lineage(
task_id=task_id,
created_at=NOW,
parent=parent,
root=root,
relation_type=relation,
sibling_sequence=seq,
goal_slug="current-head-reconciliation",
)
return TaskRecord(task_id, 1, status, lineage, tuple(evidence), proven)
class InMemoryStore:
def __init__(self, *tasks):
self.tasks = {t.task_id: t for t in tasks}
self.reviews = {}
self.lock = threading.Lock()
self.force_one_cas_conflict = False
def get_task(self, task_id):
return self.tasks[task_id]
def list_descendants(self, root_task_id):
return [
t for t in self.tasks.values()
if t.lineage.root_task_id == root_task_id and t.task_id != root_task_id
]
def get_root_review(self, review_id):
return self.reviews.get(review_id)
def compare_and_swap_root_review(self, root_task_id, expected_root_revision, review):
with self.lock:
root = self.tasks[root_task_id]
if self.force_one_cas_conflict:
self.force_one_cas_conflict = False
self.tasks[root_task_id] = replace(root, revision=root.revision + 1)
return False
if root.revision != expected_root_revision:
return False
self.reviews[review.review_id] = review
self.tasks[root_task_id] = replace(root, revision=root.revision + 1)
return True
def acceptance(*conditions):
def evaluator(root, descendants):
return RootAcceptanceSnapshot(tuple(conditions))
return evaluator
def test_root_name_contains_user_time_root_and_purpose():
root = root_record()
name = root.lineage.display_name
assert "ALICE" in name
assert "20260914T083000Z" in name
assert "-ROOT-" in name
assert "GOAL-TO-EVIDENCE-CLOSURE" in name
def test_child_name_contains_user_time_root_parent_depth_sequence_and_purpose():
root = root_record()
child = child_record(root)
name = child.lineage.display_name
assert "ALICE" in name and "20260914T083000Z" in name
assert "-R" in name and "-P" in name and "-D01-C01-" in name
assert "CURRENT-HEAD-RECONCILIATION" in name
def test_grandchild_preserves_root_and_immediate_parent():
root = root_record()
child = child_record(root)
grand = child_record(
root,
task_id="TASK-GRAND-00000001",
parent=child,
seq=1,
)
assert grand.lineage.root_task_id == root.task_id
assert grand.lineage.parent_task_id == child.task_id
assert grand.lineage.lineage_depth == 2
def test_successor_preserves_root_and_predecessor_parent():
root = root_record()
child = child_record(root)
successor = child_record(
root,
task_id="TASK-SUCC-00000001",
parent=child,
relation=RelationType.SUCCESSOR,
seq=1,
)
assert successor.lineage.root_task_id == root.task_id
assert successor.lineage.parent_task_id == child.task_id
assert "-S01-" in successor.lineage.display_name
def test_cross_user_lineage_attachment_rejected():
root = root_record()
child = child_record(root)
other_root = root_record("TASK-ROOT-00000002", owner="principal-user-b", alias="BOB")
forged = replace(
child.lineage,
root_task_id=other_root.task_id,
parent_task_id=other_root.task_id,
parent_revision=other_root.revision,
owner_principal_id=root.lineage.owner_principal_id,
owner_alias=root.lineage.owner_alias,
mission_id=other_root.lineage.mission_id,
root_acceptance_ref=other_root.lineage.root_acceptance_ref,
)
with pytest.raises(LineageError, match="cross-owner"):
validate_lineage(forged, parent=other_root, root=other_root)
def test_non_root_without_parent_or_root_authority_rejected():
root = root_record()
child = child_record(root)
with pytest.raises(LineageError, match="requires parent and root"):
validate_lineage(child.lineage)
def test_stale_parent_revision_rejected():
root = root_record()
child = child_record(root)
stale = replace(child.lineage, parent_revision=0)
with pytest.raises(LineageError, match="parent revision"):
validate_lineage(stale, parent=root, root=root)
def test_duplicate_child_terminal_callback_is_idempotent():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True, evidence=("E1",))
store = InMemoryStore(root, child)
evaluator = acceptance(AcceptanceCondition("A", False))
first = review_root_after_child_terminal(
store=store, trigger_child_task_id=child.task_id,
acceptance_evaluator=evaluator, reviewed_at=NOW,
)
second = review_root_after_child_terminal(
store=store, trigger_child_task_id=child.task_id,
acceptance_evaluator=evaluator, reviewed_at=NOW,
)
assert second.review_id == first.review_id
assert len(store.reviews) == 1
def test_concurrent_sibling_style_cas_retry_preserves_review():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True, evidence=("E1",))
store = InMemoryStore(root, child)
store.force_one_cas_conflict = True
review = review_root_after_child_terminal(
store=store, trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("A", False)),
reviewed_at=NOW,
)
assert review.root_revision_before == 2
assert store.get_task(root.task_id).revision == 3
assert review.review_id in store.reviews
def test_child_complete_automatically_creates_root_review():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True, evidence=("E1",))
store = InMemoryStore(root, child)
review = review_root_after_child_terminal(
store=store, trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("A", False)),
reviewed_at=NOW,
)
assert review.trigger_child_task_id == child.task_id
assert review.root_task_id == root.task_id
def test_child_complete_does_not_close_root_with_acceptance_gap():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True)
store = InMemoryStore(root, child)
review = review_root_after_child_terminal(
store=store, trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("remaining", False)),
reviewed_at=NOW,
)
assert review.root_complete is False
assert review.next_legal_action == NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD
assert review.remaining_acceptance_conditions == ("remaining",)
def test_final_child_closes_root_only_when_acceptance_proven():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True, evidence=("CHILD-E",))
store = InMemoryStore(root, child)
review = review_root_after_child_terminal(
store=store, trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("goal", True, ("ROOT-E",))),
reviewed_at=NOW,
)
assert review.root_complete is True
assert review.next_legal_action == NextLegalAction.ROOT_COMPLETE
assert review.evidence_refs == ("CHILD-E", "ROOT-E")
def test_legacy_task_gets_lineage_without_task_id_change():
root = root_record()
legacy_id = "TASK-P0-CHATGPT-BRIDGE-V2-CONTROL-PLANE-CONVERGENCE-INTEGRATE-20260913T184412Z-19b7"
lineage = attach_legacy_lineage(
existing_task_id=legacy_id,
created_at=NOW,
parent=root,
root=root,
relation_type=RelationType.RECOVERY,
sibling_sequence=1,
goal_slug="current-head-reconciliation",
)
assert lineage.task_id == legacy_id
assert lineage.parent_task_id == root.task_id
assert "-X01-" in lineage.display_name
def test_display_rename_does_not_change_authoritative_lineage():
root = root_record()
child = child_record(root)
renamed = rename_display_only(child.lineage, "Readable Alias Only")
authoritative_before = (
child.lineage.task_id, child.lineage.root_task_id,
child.lineage.parent_task_id, child.lineage.mission_id,
child.lineage.owner_principal_id, child.lineage.lineage_depth,
child.lineage.relation_type, child.lineage.parent_revision,
)
authoritative_after = (
renamed.task_id, renamed.root_task_id,
renamed.parent_task_id, renamed.mission_id,
renamed.owner_principal_id, renamed.lineage_depth,
renamed.relation_type, renamed.parent_revision,
)
assert authoritative_after == authoritative_before
def test_owner_boundary_is_selected_when_acceptance_requires_human_only_action():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True)
store = InMemoryStore(root, child)
review = review_root_after_child_terminal(
store=store,
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("signature", False, owner_boundary=True)),
reviewed_at=NOW,
)
assert review.next_legal_action == NextLegalAction.OWNER_BOUNDARY
def test_recovery_child_is_preferred_over_new_child():
root = root_record()
done = child_record(root, status="COMPLETE", proven=True)
recovering = child_record(
root, task_id="TASK-REC-00000001", seq=2,
status="RECONCILIATION_REQUIRED", proven=False,
)
store = InMemoryStore(root, done, recovering)
review = review_root_after_child_terminal(
store=store,
trigger_child_task_id=done.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("goal", False)),
reviewed_at=NOW,
)
assert review.next_legal_action == NextLegalAction.RECOVER_EXISTING_CHILD
def test_terminal_without_proven_evidence_cannot_trigger_root_review():
root = root_record()
child = child_record(root, status="COMPLETE", proven=False)
store = InMemoryStore(root, child)
with pytest.raises(LineageError, match="not proven"):
review_root_after_child_terminal(
store=store,
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("goal", False)),
reviewed_at=NOW,
)
def test_safe_alias_never_embeds_email():
alias = derive_safe_owner_alias("alice@example.com")
assert "@" not in alias
assert alias.startswith("U")
def test_safe_preferred_alias_rejects_email_shaped_value():
alias = derive_safe_owner_alias("principal-1", "alice@example.com")
assert alias.startswith("U")
assert "ALICE" not in alias
def test_lineage_metadata_round_trip_preserves_authoritative_fields():
root = root_record()
metadata = lineage_to_metadata(root.lineage)
restored = lineage_from_metadata(metadata)
assert restored == root.lineage
def test_root_review_serializes_for_existing_task_metadata_or_journal():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True, evidence=("E1",))
store = InMemoryStore(root, child)
review = review_root_after_child_terminal(
store=store,
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("goal", False)),
reviewed_at=NOW,
)
record = root_review_to_record(review)
assert record["schema"] == "aios.root-review.v1"
assert record["root_task_id"] == root.task_id
assert record["trigger_child_task_id"] == child.task_id
def test_two_sibling_completions_keep_both_root_review_records():
root = root_record()
c1 = child_record(root, task_id="TASK-C1-00000001", seq=1, status="COMPLETE", proven=True, evidence=("E1",))
c2 = child_record(root, task_id="TASK-C2-00000002", seq=2, status="COMPLETE", proven=True, evidence=("E2",))
store = InMemoryStore(root, c1, c2)
evaluator = acceptance(AcceptanceCondition("goal", False))
r1 = review_root_after_child_terminal(
store=store, trigger_child_task_id=c1.task_id,
acceptance_evaluator=evaluator, reviewed_at=NOW,
)
r2 = review_root_after_child_terminal(
store=store, trigger_child_task_id=c2.task_id,
acceptance_evaluator=evaluator, reviewed_at=NOW,
)
assert r1.review_id != r2.review_id
assert set(store.reviews) == {r1.review_id, r2.review_id}
assert store.get_task(root.task_id).revision == 3
@pytest.mark.parametrize("field,value", [("lineage_depth", True), ("owner_principal_id", 7)])
def test_lineage_parser_rejects_type_coercion(field, value):
metadata = lineage_to_metadata(root_record().lineage)
metadata[field] = value
with pytest.raises(LineageError, match="invalid lineage metadata"):
lineage_from_metadata(metadata)
def test_lineage_parser_rejects_extra_fields_and_invalid_non_root_shape():
root = root_record()
child = child_record(root)
extra = lineage_to_metadata(root.lineage)
extra["tenant_hint"] = "untrusted"
with pytest.raises(LineageError, match="fields mismatch"):
lineage_from_metadata(extra)
malformed = lineage_to_metadata(child.lineage)
malformed["parent_task_id"] = None
with pytest.raises(LineageError, match="non-root"):
lineage_from_metadata(malformed)
def test_root_review_parser_rejects_wrong_lookup_id_and_string_boolean():
root = root_record()
child = child_record(root, status="COMPLETE", proven=True)
store = InMemoryStore(root, child)
review = review_root_after_child_terminal(
store=store,
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("done", True)),
reviewed_at=NOW,
)
record = root_review_to_record(review)
with pytest.raises(LineageError, match="lookup key"):
root_review_from_record(record, expected_review_id="RR-WRONG")
record["root_complete"] = "false"
with pytest.raises(LineageError, match="boolean"):
root_review_from_record(record, expected_review_id=review.review_id)
def test_root_review_rejects_cross_owner_root_binding():
root_a = root_record(owner="principal-a")
root_b = root_record("TASK-ROOT-00000002", owner="principal-b", alias="BOB")
child = child_record(root_a, status="COMPLETE", proven=True)
forged = replace(
child,
lineage=replace(child.lineage, root_task_id=root_b.task_id),
)
store = InMemoryStore(root_a, root_b, forged)
with pytest.raises(LineageError, match="owner does not match"):
review_root_after_child_terminal(
store=store,
trigger_child_task_id=forged.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("done", True)),
reviewed_at=NOW,
)
def test_root_review_identity_canonicalizes_acceptance_and_evidence_order():
root = root_record()
child = child_record(root, status="COMPLETED", proven=True, evidence=("Z", "A"))
store = InMemoryStore(root, child)
one = review_root_after_child_terminal(
store=store,
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(
AcceptanceCondition("b", True, ("2", "1")),
AcceptanceCondition("a", False),
),
reviewed_at=NOW,
)
two = review_root_after_child_terminal(
store=store,
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(
AcceptanceCondition("a", False),
AcceptanceCondition("b", True, ("1", "2")),
),
reviewed_at=NOW,
)
assert two.review_id == one.review_id
assert one.evidence_refs == ("1", "2", "A", "Z")
assert len(store.reviews) == 1
def test_failed_recoverable_is_not_accepted_as_terminal_trigger():
root = root_record()
child = child_record(root, status="FAILED_RECOVERABLE", proven=True)
store = InMemoryStore(root, child)
with pytest.raises(LineageError, match="terminal"):
review_root_after_child_terminal(
store=store,
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(AcceptanceCondition("done", True)),
reviewed_at=NOW,
)
def test_stable_review_id_rejects_stale_descendant_semantics():
root = root_record()
trigger = child_record(
root, task_id="TASK-TRIGGER-0001", status="COMPLETE", proven=True
)
sibling = child_record(
root, task_id="TASK-SIBLING-0002", seq=2, status="RUNNING"
)
store = InMemoryStore(root, trigger, sibling)
evaluator = acceptance(AcceptanceCondition("left", False))
review_root_after_child_terminal(
store=store,
trigger_child_task_id=trigger.task_id,
acceptance_evaluator=evaluator,
reviewed_at=NOW,
)
store.tasks[sibling.task_id] = replace(sibling, status="WAITING_DEPENDENCY")
with pytest.raises(LineageError, match="current semantics"):
review_root_after_child_terminal(
store=store,
trigger_child_task_id=trigger.task_id,
acceptance_evaluator=evaluator,
reviewed_at=NOW,
)
@@ -0,0 +1,767 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import replace
from datetime import datetime, timezone
import pytest
from aios_task_lineage import (
AcceptanceCondition,
NextLegalAction,
RelationType,
RootAcceptanceSnapshot,
TaskRecord,
lineage_to_metadata,
make_child_lineage,
make_root_lineage,
)
from aios_task_lineage_integration import (
IntegrationError,
LINEAGE_METADATA_KEY,
NewTaskIdentityRequest,
RootActionDispatchReceipt,
TaskAuthoritySnapshot,
TaskIdentityLineageService,
)
NOW = datetime(2026, 9, 14, 9, 30, tzinfo=timezone.utc)
def acceptance(*states: tuple[str, bool, bool]):
def evaluator(root, descendants):
return RootAcceptanceSnapshot(
tuple(
AcceptanceCondition(
key=key,
satisfied=satisfied,
evidence_refs=(f"ev:{key}",) if satisfied else (),
owner_boundary=owner_boundary,
)
for key, satisfied, owner_boundary in states
)
)
return evaluator
class MemoryPort:
def __init__(self):
self.tasks = {}
self.root_reviews = {}
self.dispatches = {}
self.sequence_by_key = {}
self.next_seq_by_parent = {}
self.fail_metadata_cas = 0
self.fail_root_review_cas = 0
self.dispatch_calls = 0
self.raise_after_review_once = False
def put(self, snapshot):
self.tasks[snapshot.task_id] = snapshot
def get_task_snapshot(self, task_id):
return self.tasks[task_id]
def list_descendant_snapshots(self, root_task_id):
return tuple(
s for s in self.tasks.values()
if isinstance(s.metadata.get(LINEAGE_METADATA_KEY), dict)
and s.metadata[LINEAGE_METADATA_KEY]["root_task_id"] == root_task_id
and s.task_id != root_task_id
)
def reserve_sibling_sequence(self, *, parent_task_id, relation_type, child_task_id):
key = (parent_task_id, relation_type.value, child_task_id)
if key in self.sequence_by_key:
return self.sequence_by_key[key]
seqkey = (parent_task_id, relation_type.value)
seq = self.next_seq_by_parent.get(seqkey, 0) + 1
self.next_seq_by_parent[seqkey] = seq
self.sequence_by_key[key] = seq
return seq
def compare_and_swap_task_metadata(self, *, task_id, expected_revision, metadata_patch):
snap = self.tasks[task_id]
if self.fail_metadata_cas:
self.fail_metadata_cas -= 1
self.tasks[task_id] = replace(snap, revision=snap.revision + 1)
return False
if snap.revision != expected_revision:
return False
merged = dict(snap.metadata)
merged.update(metadata_patch)
self.tasks[task_id] = replace(
snap, revision=snap.revision + 1, metadata=merged
)
return True
def get_root_review_record(self, review_id):
return deepcopy(self.root_reviews.get(review_id))
def compare_and_swap_root_review_record(
self, *, root_task_id, expected_root_revision, review_record
):
root = self.tasks[root_task_id]
if self.fail_root_review_cas:
self.fail_root_review_cas -= 1
self.tasks[root_task_id] = replace(root, revision=root.revision + 1)
return False
if root.revision != expected_root_revision:
return False
self.root_reviews[review_record["review_id"]] = deepcopy(review_record)
self.tasks[root_task_id] = replace(root, revision=root.revision + 1)
return True
def get_root_action_dispatch(self, review_id):
return self.dispatches.get(review_id)
def dispatch_root_action(self, *, review, idempotency_key):
self.dispatch_calls += 1
if review.review_id in self.dispatches:
return self.dispatches[review.review_id]
receipt = RootActionDispatchReceipt(
review_id=review.review_id,
root_task_id=review.root_task_id,
action=review.next_legal_action,
idempotency_key=idempotency_key,
status="DISPATCHED",
)
self.dispatches[review.review_id] = receipt
return receipt
def root_snapshot(task_id="ROOT-00000001", owner="principal-A", mission="M1"):
lineage = make_root_lineage(
task_id=task_id,
owner_principal_id=owner,
owner_alias="OWNER1",
created_at=NOW,
mission_id=mission,
goal_slug="GOAL-TO-EVIDENCE",
root_acceptance_ref="acceptance:v1",
)
return TaskAuthoritySnapshot(
task_id=task_id,
revision=1,
status="RUNNING",
owner_principal_id=owner,
mission_id=mission,
created_at=NOW,
metadata={LINEAGE_METADATA_KEY: lineage_to_metadata(lineage)},
)
def child_snapshot(
root,
*,
task_id="CHILD-00000001",
status="COMPLETE",
proven=True,
relation=RelationType.CHILD,
seq=1,
):
root_record = TaskRecord(
task_id=root.task_id,
revision=root.revision,
status=root.status,
lineage=make_root_lineage(
task_id=root.task_id,
owner_principal_id=root.owner_principal_id,
owner_alias="OWNER1",
created_at=root.created_at,
mission_id=root.mission_id,
goal_slug="GOAL-TO-EVIDENCE",
root_acceptance_ref="acceptance:v1",
),
)
lineage = make_child_lineage(
task_id=task_id,
created_at=NOW,
parent=root_record,
root=root_record,
relation_type=relation,
sibling_sequence=seq,
goal_slug="CHILD-WORK",
)
return TaskAuthoritySnapshot(
task_id=task_id,
revision=2,
status=status,
owner_principal_id=root.owner_principal_id,
mission_id=root.mission_id,
created_at=NOW,
metadata={LINEAGE_METADATA_KEY: lineage_to_metadata(lineage)},
evidence_refs=(f"evidence:{task_id}",),
terminal_evidence_proven=proven,
)
def test_prepare_root_identity_is_readable_and_safe():
port = MemoryPort()
svc = TaskIdentityLineageService(port)
out = svc.prepare_new_task_identity(NewTaskIdentityRequest(
task_id="TASK-ROOT-ABC123",
owner_principal_id="alice@example.com",
preferred_owner_alias="alice@example.com",
created_at=NOW,
mission_id="M1",
goal_slug="goal to evidence closure",
root_acceptance_ref="acceptance:v1",
))
assert "20260914T093000Z" in out.display_name
assert "-ROOT-GOAL-TO-EVIDENCE-CLOSURE-" in out.display_name
assert "alice@example.com" not in out.display_name.lower()
assert out.lineage.root_task_id == out.task_id
def test_prepare_child_loads_root_parent_and_reserves_sequence_idempotently():
port = MemoryPort()
root = root_snapshot()
port.put(root)
svc = TaskIdentityLineageService(port)
req = NewTaskIdentityRequest(
task_id="TASK-CHILD-A",
owner_principal_id=root.owner_principal_id,
preferred_owner_alias=None,
created_at=NOW,
mission_id=root.mission_id,
goal_slug="s1 governed commit readback",
root_acceptance_ref="acceptance:v1",
relation_type=RelationType.CHILD,
parent_task_id=root.task_id,
)
one = svc.prepare_new_task_identity(req)
two = svc.prepare_new_task_identity(req)
assert one.lineage.sibling_sequence == two.lineage.sibling_sequence == 1
assert one.lineage.root_task_id == root.task_id
assert one.lineage.parent_task_id == root.task_id
assert "-R" in one.display_name and "-P" in one.display_name and "-C01-" in one.display_name
def test_prepare_child_rejects_cross_owner():
port = MemoryPort()
root = root_snapshot()
port.put(root)
svc = TaskIdentityLineageService(port)
with pytest.raises(IntegrationError, match="cross-owner"):
svc.prepare_new_task_identity(NewTaskIdentityRequest(
task_id="TASK-CHILD-A",
owner_principal_id="principal-B",
preferred_owner_alias=None,
created_at=NOW,
mission_id=root.mission_id,
goal_slug="child work",
root_acceptance_ref="acceptance:v1",
relation_type=RelationType.CHILD,
parent_task_id=root.task_id,
))
def test_prepare_child_rejects_cross_mission():
port = MemoryPort()
root = root_snapshot()
port.put(root)
svc = TaskIdentityLineageService(port)
with pytest.raises(IntegrationError, match="cross-mission"):
svc.prepare_new_task_identity(NewTaskIdentityRequest(
task_id="TASK-CHILD-A",
owner_principal_id=root.owner_principal_id,
preferred_owner_alias=None,
created_at=NOW,
mission_id="OTHER",
goal_slug="child work",
root_acceptance_ref="acceptance:v1",
relation_type=RelationType.CHILD,
parent_task_id=root.task_id,
))
def test_attach_legacy_retries_cas_and_preserves_task_id():
port = MemoryPort()
root = root_snapshot()
port.put(root)
legacy = TaskAuthoritySnapshot(
task_id="LEGACY-JOB-19B7",
revision=4,
status="RECONCILIATION_REQUIRED",
owner_principal_id=root.owner_principal_id,
mission_id=root.mission_id,
created_at=NOW,
metadata={},
)
port.put(legacy)
port.fail_metadata_cas = 1
svc = TaskIdentityLineageService(port)
lineage = svc.attach_legacy_task(
task_id=legacy.task_id,
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="current head reconciliation",
)
assert lineage.task_id == legacy.task_id
assert lineage.relation_type == RelationType.RECOVERY
assert port.get_task_snapshot(legacy.task_id).revision == 6
def test_attach_legacy_is_idempotent_after_success():
port = MemoryPort()
root = root_snapshot()
port.put(root)
legacy = TaskAuthoritySnapshot(
task_id="LEGACY",
revision=1,
status="RUNNING",
owner_principal_id=root.owner_principal_id,
mission_id=root.mission_id,
created_at=NOW,
metadata={},
)
port.put(legacy)
svc = TaskIdentityLineageService(port)
first = svc.attach_legacy_task(
task_id="LEGACY",
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="recovery",
)
rev = port.get_task_snapshot("LEGACY").revision
second = svc.attach_legacy_task(
task_id="LEGACY",
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="recovery",
)
assert first == second
assert port.get_task_snapshot("LEGACY").revision == rev
@pytest.mark.parametrize(
"conditions,expected",
[
((("done", True, False),), NextLegalAction.ROOT_COMPLETE),
((("human", False, True),), NextLegalAction.OWNER_BOUNDARY),
((("left", False, False),), NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD),
],
)
def test_terminal_review_persists_and_dispatches_expected_action(conditions, expected):
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root)
port.put(child)
svc = TaskIdentityLineageService(port)
out = svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(*conditions),
reviewed_at=NOW,
)
assert out.review.next_legal_action == expected
assert out.dispatch.action == expected
assert port.dispatch_calls == 1
assert out.review.review_id in port.root_reviews
def test_duplicate_terminal_callback_is_review_and_dispatch_idempotent():
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root)
port.put(child)
svc = TaskIdentityLineageService(port)
one = svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
two = svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
assert one.review.review_id == two.review.review_id
assert one.dispatch == two.dispatch
assert port.dispatch_calls == 1
assert len(port.root_reviews) == 1
def test_crash_after_root_review_before_dispatch_can_resume_without_duplicate_review():
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root)
port.put(child)
svc = TaskIdentityLineageService(port)
# Simulate the crash boundary by invoking the domain review directly through
# the same adapter path, without dispatch.
from aios_task_lineage_integration import _RootReviewStoreAdapter
from aios_task_lineage import review_root_after_child_terminal
review = review_root_after_child_terminal(
store=_RootReviewStoreAdapter(port),
trigger_child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
assert review.review_id in port.root_reviews
assert not port.dispatches
resumed = svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
assert resumed.review.review_id == review.review_id
assert port.dispatch_calls == 1
assert len(port.root_reviews) == 1
def test_root_review_retries_after_sibling_revision_race():
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root)
port.put(child)
port.fail_root_review_cas = 1
svc = TaskIdentityLineageService(port)
out = svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
assert out.review.root_revision_before == 2
assert out.review.root_revision_after == 3
assert port.get_task_snapshot(root.task_id).revision == 3
def test_terminal_review_rejects_unproven_terminal_evidence():
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root, proven=False)
port.put(root)
port.put(child)
svc = TaskIdentityLineageService(port)
with pytest.raises(Exception, match="terminal evidence"):
svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("done", True, False)),
reviewed_at=NOW,
)
def test_recovery_descendant_prioritizes_recover_existing_child():
port = MemoryPort()
root = root_snapshot()
trigger = child_snapshot(root, task_id="CHILD-DONE")
recovery = child_snapshot(
root,
task_id="CHILD-RECOVERY",
status="RECONCILIATION_REQUIRED",
proven=False,
relation=RelationType.RECOVERY,
seq=2,
)
port.put(root); port.put(trigger); port.put(recovery)
svc = TaskIdentityLineageService(port)
out = svc.on_proven_terminal_child(
child_task_id=trigger.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
assert out.review.next_legal_action == NextLegalAction.RECOVER_EXISTING_CHILD
def test_blocked_descendant_prioritizes_wait_dependency():
port = MemoryPort()
root = root_snapshot()
trigger = child_snapshot(root, task_id="CHILD-DONE")
blocked = child_snapshot(
root,
task_id="CHILD-BLOCKED",
status="WAITING_DEPENDENCY",
proven=False,
seq=2,
)
port.put(root); port.put(trigger); port.put(blocked)
svc = TaskIdentityLineageService(port)
out = svc.on_proven_terminal_child(
child_task_id=trigger.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
assert out.review.next_legal_action == NextLegalAction.WAIT_DEPENDENCY
def test_conflicting_existing_dispatch_fails_closed():
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root); port.put(child)
svc = TaskIdentityLineageService(port)
first = svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
port.dispatches[first.review.review_id] = replace(
first.dispatch, action=NextLegalAction.ROOT_COMPLETE
)
with pytest.raises(IntegrationError, match="conflicts"):
svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
def test_dispatch_receipt_mismatch_fails_closed():
class BadPort(MemoryPort):
def dispatch_root_action(self, *, review, idempotency_key):
return RootActionDispatchReceipt(
review_id=review.review_id,
root_task_id="WRONG",
action=review.next_legal_action,
idempotency_key=idempotency_key,
status="DISPATCHED",
)
port = BadPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root); port.put(child)
svc = TaskIdentityLineageService(port)
with pytest.raises(IntegrationError, match="receipt"):
svc.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
def test_attach_legacy_repairs_missing_display_name_without_changing_task_id():
port = MemoryPort()
root = root_snapshot()
port.put(root)
legacy = TaskAuthoritySnapshot(
task_id="LEGACY",
revision=1,
status="RUNNING",
owner_principal_id=root.owner_principal_id,
mission_id=root.mission_id,
created_at=NOW,
metadata={},
)
port.put(legacy)
svc = TaskIdentityLineageService(port)
lineage = svc.attach_legacy_task(
task_id="LEGACY",
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="recovery",
)
snap = port.get_task_snapshot("LEGACY")
# Simulate an older persisted lineage record that predates display-name
# persistence.
md = dict(snap.metadata)
md.pop("aios_task_display_name", None)
port.tasks["LEGACY"] = replace(snap, revision=snap.revision + 1, metadata=md)
repaired = svc.attach_legacy_task(
task_id="LEGACY",
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="recovery",
)
out = port.get_task_snapshot("LEGACY")
assert repaired == lineage
assert out.task_id == "LEGACY"
assert out.metadata["aios_task_display_name"] == lineage.display_name
def test_terminal_review_rejects_child_forged_to_cross_owner_root():
port = MemoryPort()
root_a = root_snapshot(owner="principal-A")
root_b = root_snapshot("ROOT-00000002", owner="principal-B")
child = child_snapshot(root_a)
metadata = deepcopy(child.metadata)
metadata[LINEAGE_METADATA_KEY]["root_task_id"] = root_b.task_id
forged = replace(child, metadata=metadata)
port.put(root_a)
port.put(root_b)
port.put(forged)
service = TaskIdentityLineageService(port)
with pytest.raises(IntegrationError, match="owner does not match"):
service.on_proven_terminal_child(
child_task_id=forged.task_id,
acceptance_evaluator=acceptance(("done", True, False)),
reviewed_at=NOW,
)
@pytest.mark.parametrize(
"parent_id,relation,goal",
[
("ROOT-00000001", RelationType.CHILD, "recovery"),
("ROOT-00000001", RelationType.RECOVERY, "different goal"),
("ROOT-00000002", RelationType.RECOVERY, "recovery"),
],
)
def test_legacy_replay_rejects_changed_binding(parent_id, relation, goal):
port = MemoryPort()
root = root_snapshot()
other_root = root_snapshot("ROOT-00000002")
port.put(root)
port.put(other_root)
port.put(TaskAuthoritySnapshot(
task_id="LEGACY-BINDING",
revision=1,
status="RECONCILIATION_REQUIRED",
owner_principal_id=root.owner_principal_id,
mission_id=root.mission_id,
created_at=NOW,
metadata={},
))
service = TaskIdentityLineageService(port)
service.attach_legacy_task(
task_id="LEGACY-BINDING",
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="recovery",
)
with pytest.raises(IntegrationError, match="requested binding"):
service.attach_legacy_task(
task_id="LEGACY-BINDING",
parent_task_id=parent_id,
relation_type=relation,
goal_slug=goal,
)
def test_legacy_replay_rejects_lineage_identity_drift_from_authority():
port = MemoryPort()
root = root_snapshot()
legacy = child_snapshot(root, task_id="LEGACY-DRIFT", relation=RelationType.RECOVERY)
metadata = deepcopy(legacy.metadata)
metadata[LINEAGE_METADATA_KEY]["owner_principal_id"] = "principal-B"
port.put(root)
port.put(replace(legacy, metadata=metadata))
with pytest.raises(IntegrationError, match="lineage owner"):
TaskIdentityLineageService(port).attach_legacy_task(
task_id=legacy.task_id,
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="child work",
)
def test_corrupt_persisted_root_review_is_not_replayed():
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root)
port.put(child)
service = TaskIdentityLineageService(port)
first = service.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
port.root_reviews[first.review.review_id]["root_complete"] = "false"
with pytest.raises(IntegrationError, match="root_complete must be a boolean"):
service.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
@pytest.mark.parametrize(
"change",
[
{"review_id": "RR-WRONG"},
{"idempotency_key": "wrong-key"},
{"status": "FAILED"},
],
)
def test_existing_dispatch_replay_requires_full_persisted_receipt(change):
port = MemoryPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root)
port.put(child)
service = TaskIdentityLineageService(port)
first = service.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
port.dispatches[first.review.review_id] = replace(first.dispatch, **change)
with pytest.raises(IntegrationError, match="conflicts"):
service.on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
def test_pending_dispatch_receipt_is_a_valid_durable_outcome():
class PendingPort(MemoryPort):
def dispatch_root_action(self, *, review, idempotency_key):
receipt = RootActionDispatchReceipt(
review_id=review.review_id,
root_task_id=review.root_task_id,
action=review.next_legal_action,
idempotency_key=idempotency_key,
status="PENDING",
)
self.dispatches[review.review_id] = receipt
return receipt
port = PendingPort()
root = root_snapshot()
child = child_snapshot(root)
port.put(root)
port.put(child)
result = TaskIdentityLineageService(port).on_proven_terminal_child(
child_task_id=child.task_id,
acceptance_evaluator=acceptance(("left", False, False)),
reviewed_at=NOW,
)
assert result.dispatch.status == "PENDING"
def test_legacy_attach_rechecks_authorized_owner_after_cas_race():
class OwnershipRacePort(MemoryPort):
def compare_and_swap_task_metadata(
self, *, task_id, expected_revision, metadata_patch
):
snapshot = self.tasks[task_id]
self.tasks[task_id] = replace(
snapshot,
revision=snapshot.revision + 1,
owner_principal_id="principal-B",
mission_id="M2",
)
return False
port = OwnershipRacePort()
root = root_snapshot()
port.put(root)
port.put(TaskAuthoritySnapshot(
task_id="LEGACY-RACE",
revision=1,
status="RECONCILIATION_REQUIRED",
owner_principal_id=root.owner_principal_id,
mission_id=root.mission_id,
created_at=NOW,
metadata={},
))
with pytest.raises(IntegrationError, match="owner changed after authorization"):
TaskIdentityLineageService(port).attach_legacy_task(
task_id="LEGACY-RACE",
parent_task_id=root.task_id,
relation_type=RelationType.RECOVERY,
goal_slug="recovery",
expected_owner_principal_id="principal-A",
expected_mission_id="M1",
)
+488
View File
@@ -0,0 +1,488 @@
from __future__ import annotations
from copy import deepcopy
from dataclasses import replace
from datetime import datetime, timezone
import pytest
from aios_task_lineage import (
AcceptanceCondition,
NextLegalAction,
RelationType,
RootAcceptanceSnapshot,
)
from aios_task_lineage_hooks import LifecycleHookError, TaskLineageLifecycleHooks
from aios_task_lineage_integration import (
DISPLAY_NAME_METADATA_KEY,
LINEAGE_METADATA_KEY,
NewTaskIdentityRequest,
RootActionDispatchReceipt,
TaskAuthoritySnapshot,
)
from aios_task_lineage_taskplane_adapter import (
AuthenticatedTaskContext,
CallableTaskPlaneReadback,
InProcessTaskPlaneLineageAdapter,
TaskPlaneServiceBindings,
)
NOW = datetime(2026, 9, 14, 10, 20, tzinfo=timezone.utc)
def acceptance(*states):
def evaluator(root, descendants):
return RootAcceptanceSnapshot(tuple(
AcceptanceCondition(
key=key,
satisfied=satisfied,
evidence_refs=(f"ev:{key}",) if satisfied else (),
owner_boundary=owner_boundary,
)
for key, satisfied, owner_boundary in states
))
return evaluator
class Backend:
def __init__(self):
self.tasks = {}
self.root_reviews = {}
self.dispatches = {}
self.seq = {}
self.next_seq = {}
self.fail_task_cas = 0
self.fail_review_cas = 0
self.dispatch_calls = 0
def put(self, snap):
self.tasks[snap.task_id] = snap
def get_task(self, task_id):
return self.tasks[task_id]
def list_desc(self, root_id):
out = []
for snap in self.tasks.values():
raw = snap.metadata.get(LINEAGE_METADATA_KEY)
if isinstance(raw, dict) and raw.get("root_task_id") == root_id and snap.task_id != root_id:
out.append(snap)
return tuple(out)
def reserve(self, parent_id, relation_type, child_id):
key = (parent_id, relation_type.value, child_id)
if key in self.seq:
return self.seq[key]
group = (parent_id, relation_type.value)
n = self.next_seq.get(group, 0) + 1
self.next_seq[group] = n
self.seq[key] = n
return n
def cas_metadata(self, task_id, expected_revision, patch):
snap = self.tasks[task_id]
if self.fail_task_cas:
self.fail_task_cas -= 1
self.tasks[task_id] = replace(snap, revision=snap.revision + 1)
return False
if snap.revision != expected_revision:
return False
md = dict(snap.metadata); md.update(deepcopy(dict(patch)))
self.tasks[task_id] = replace(snap, revision=snap.revision + 1, metadata=md)
return True
def get_review(self, review_id):
return deepcopy(self.root_reviews.get(review_id))
def cas_review(self, root_id, expected_revision, record):
root = self.tasks[root_id]
if self.fail_review_cas:
self.fail_review_cas -= 1
self.tasks[root_id] = replace(root, revision=root.revision + 1)
return False
if root.revision != expected_revision:
return False
self.root_reviews[record["review_id"]] = deepcopy(dict(record))
self.tasks[root_id] = replace(root, revision=root.revision + 1)
return True
def get_dispatch(self, review_id):
return self.dispatches.get(review_id)
def dispatch(self, review, key):
self.dispatch_calls += 1
if review.review_id in self.dispatches:
return self.dispatches[review.review_id]
receipt = RootActionDispatchReceipt(
review_id=review.review_id,
root_task_id=review.root_task_id,
action=review.next_legal_action,
idempotency_key=key,
status="DISPATCHED",
)
self.dispatches[review.review_id] = receipt
return receipt
def adapter_for(backend, *, with_readback=True):
bindings = TaskPlaneServiceBindings(
get_task_snapshot=backend.get_task,
list_descendant_snapshots=backend.list_desc,
reserve_sibling_sequence=backend.reserve,
compare_and_swap_task_metadata=backend.cas_metadata,
get_root_review_record=backend.get_review,
compare_and_swap_root_review_record=backend.cas_review,
get_root_action_dispatch=backend.get_dispatch,
dispatch_root_action=backend.dispatch,
)
readback = None
if with_readback:
readback = CallableTaskPlaneReadback(
get_task=backend.get_task,
list_descendants=backend.list_desc,
get_review=backend.get_review,
get_dispatch=backend.get_dispatch,
)
return InProcessTaskPlaneLineageAdapter(bindings, readback=readback)
def bare(task_id, *, owner="principal-A", mission="M1", revision=1, status="ADMITTED", proven=False):
return TaskAuthoritySnapshot(
task_id=task_id,
revision=revision,
status=status,
owner_principal_id=owner,
mission_id=mission,
created_at=NOW,
metadata={},
evidence_refs=(f"ev:{task_id}",) if proven else (),
terminal_evidence_proven=proven,
)
def root_request(task_id="ROOT-A", *, owner="principal-A", mission="M1"):
return NewTaskIdentityRequest(
task_id=task_id,
owner_principal_id=owner,
preferred_owner_alias="ops-owner",
created_at=NOW,
mission_id=mission,
goal_slug="goal to evidence closure",
root_acceptance_ref="acceptance:v1",
)
def child_request(task_id, parent_id, *, owner="principal-A", mission="M1", relation=RelationType.CHILD):
return NewTaskIdentityRequest(
task_id=task_id,
owner_principal_id=owner,
preferred_owner_alias=None,
created_at=NOW,
mission_id=mission,
goal_slug="child work",
root_acceptance_ref="acceptance:v1",
relation_type=relation,
parent_task_id=parent_id,
)
def ctx(owner="principal-A", mission="M1"):
return AuthenticatedTaskContext(owner, "ops-owner", mission)
@pytest.mark.parametrize("field,value", [
("created_at", datetime(2025, 1, 1, tzinfo=timezone.utc)),
("preferred_owner_alias", "different-owner"),
])
def test_identity_hook_rejects_untrusted_identity_provenance(field, value):
b = Backend(); b.put(bare("ROOT-A"))
hooks = TaskLineageLifecycleHooks(adapter_for(b))
with pytest.raises(LifecycleHookError):
hooks.persist_new_task_identity(
request=replace(root_request(), **{field: value}), context=ctx())
assert b.get_task("ROOT-A").revision == 1
def test_child_identity_replay_preserves_original_parent_revision():
b = Backend(); hooks = TaskLineageLifecycleHooks(adapter_for(b))
_, original = persist_root_and_child(b, hooks)
b.tasks["ROOT-A"] = replace(b.tasks["ROOT-A"], revision=10)
replay = hooks.persist_new_task_identity(
request=child_request("CHILD-A", "ROOT-A"), context=ctx())
assert replay.prepared.lineage == original.prepared.lineage
@pytest.mark.parametrize("fault", ["missing_trigger", "missing_lineage", "wrong_owner",
"stale_root", "wrong_review", "wrong_dispatch"])
def test_terminal_hook_independent_readback_fails_closed(fault):
b = Backend(); hooks = TaskLineageLifecycleHooks(adapter_for(b))
persist_root_and_child(b, hooks)
def read_task(task_id):
snap = b.get_task(task_id)
return replace(snap, revision=1) if fault == "stale_root" else snap
def descendants(root_id):
if fault == "missing_trigger":
return ()
child = b.get_task("CHILD-A")
if fault == "missing_lineage":
child = replace(child, metadata={})
if fault == "wrong_owner":
child = replace(child, owner_principal_id="other")
return (child,)
adapter = adapter_for(b)
adapter._readback = CallableTaskPlaneReadback(
get_task=read_task, list_descendants=descendants,
get_review=lambda key: None if fault == "wrong_review" else b.get_review(key),
get_dispatch=lambda key: None if fault == "wrong_dispatch" else b.get_dispatch(key))
with pytest.raises(Exception, match="readback|lineage|wrong root"):
TaskLineageLifecycleHooks(adapter).on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("done", True, False)), reviewed_at=NOW)
def test_callable_task_readback_rejects_wrong_task_identity():
rb = CallableTaskPlaneReadback(get_task=lambda _: bare("WRONG"),
list_descendants=lambda _: ())
with pytest.raises(Exception, match="wrong task identity"):
rb.get_task_snapshot("ROOT-A")
def persist_root_and_child(backend, hooks, *, child_status="COMPLETE", proven=True):
backend.put(bare("ROOT-A"))
root_out = hooks.persist_new_task_identity(
request=root_request(), context=ctx()
)
backend.put(bare("CHILD-A", revision=1, status=child_status, proven=proven))
child_out = hooks.persist_new_task_identity(
request=child_request("CHILD-A", "ROOT-A"), context=ctx()
)
# Simulate execution evidence becoming proven after identity persistence.
current = backend.get_task("CHILD-A")
backend.tasks["CHILD-A"] = replace(
current,
status=child_status,
terminal_evidence_proven=proven,
evidence_refs=("ev:child",),
)
return root_out, child_out
def test_root_identity_hook_persists_readable_name_and_lineage_with_readback():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
out=hooks.persist_new_task_identity(request=root_request(), context=ctx())
snap=b.get_task("ROOT-A")
assert out.independently_verified is True
assert snap.revision == 2
assert snap.metadata[DISPLAY_NAME_METADATA_KEY] == out.prepared.display_name
assert snap.metadata[LINEAGE_METADATA_KEY]["root_task_id"] == "ROOT-A"
assert "20260914T102000Z" in out.prepared.display_name
assert "-ROOT-GOAL-TO-EVIDENCE-CLOSURE-" in out.prepared.display_name
def test_child_identity_hook_persists_parent_root_relation_and_readback():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
hooks.persist_new_task_identity(request=root_request(), context=ctx())
b.put(bare("CHILD-A"))
out=hooks.persist_new_task_identity(
request=child_request("CHILD-A","ROOT-A"), context=ctx()
)
md=b.get_task("CHILD-A").metadata[LINEAGE_METADATA_KEY]
assert out.independently_verified
assert md["root_task_id"]=="ROOT-A"
assert md["parent_task_id"]=="ROOT-A"
assert "-R" in out.prepared.display_name and "-P" in out.prepared.display_name
def test_identity_hook_retries_task_metadata_cas():
b=Backend(); b.put(bare("ROOT-A")); b.fail_task_cas=1
hooks=TaskLineageLifecycleHooks(adapter_for(b))
out=hooks.persist_new_task_identity(request=root_request(), context=ctx())
assert out.independently_verified
assert b.get_task("ROOT-A").revision == 3
def test_identity_hook_is_idempotent_after_success():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
one=hooks.persist_new_task_identity(request=root_request(), context=ctx())
rev=b.get_task("ROOT-A").revision
two=hooks.persist_new_task_identity(request=root_request(), context=ctx())
assert one.prepared.lineage == two.prepared.lineage
assert b.get_task("ROOT-A").revision == rev
def test_identity_hook_rejects_cross_principal_context():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
with pytest.raises(LifecycleHookError, match="principal"):
hooks.persist_new_task_identity(
request=root_request(), context=ctx(owner="principal-B")
)
def test_identity_hook_requires_independent_readback_when_configured_as_required():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b, with_readback=False))
with pytest.raises(Exception, match="readback"):
hooks.persist_new_task_identity(request=root_request(), context=ctx())
assert b.get_task("ROOT-A").revision == 1
assert b.get_task("ROOT-A").metadata == {}
def test_identity_hook_can_operate_without_independent_readback_only_when_explicit():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b, with_readback=False))
out=hooks.persist_new_task_identity(
request=root_request(), context=ctx(), require_independent_readback=False
)
assert out.independently_verified is False
def test_callable_readback_rejects_wrong_snapshot_type():
rb=CallableTaskPlaneReadback(get_task=lambda _: {"bad":1}, list_descendants=lambda _: ())
with pytest.raises(Exception, match="invalid task snapshot"):
rb.get_task_snapshot("X")
def test_callable_readback_accepts_official_query_surface_without_sqlite():
b=Backend(); b.put(bare("ROOT-A"))
rb=CallableTaskPlaneReadback(get_task=b.get_task, list_descendants=b.list_desc)
assert rb.get_task_snapshot("ROOT-A").task_id=="ROOT-A"
assert rb.list_descendant_snapshots("ROOT-A")==()
def test_terminal_hook_requires_proven_evidence():
b=Backend()
hooks=TaskLineageLifecycleHooks(adapter_for(b))
persist_root_and_child(b,hooks,proven=False)
with pytest.raises(LifecycleHookError, match="proven terminal evidence"):
hooks.on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("done",True,False)), reviewed_at=NOW
)
def test_terminal_hook_persists_review_dispatch_and_readback():
b=Backend()
hooks=TaskLineageLifecycleHooks(adapter_for(b))
persist_root_and_child(b,hooks)
out=hooks.on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("left",False,False)), reviewed_at=NOW
)
assert out.root_review_persisted and out.dispatch_persisted and out.independently_verified
assert out.outcome.review.review_id in b.root_reviews
assert b.get_dispatch(out.outcome.review.review_id)==out.outcome.dispatch
assert out.outcome.review.next_legal_action==NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD
def test_terminal_hook_replay_is_exactly_once_dispatch():
b=Backend()
hooks=TaskLineageLifecycleHooks(adapter_for(b))
persist_root_and_child(b,hooks)
one=hooks.on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("left",False,False)), reviewed_at=NOW
)
two=hooks.on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("left",False,False)), reviewed_at=NOW
)
assert one.outcome.dispatch==two.outcome.dispatch
assert b.dispatch_calls==1
assert len(b.root_reviews)==1
def test_terminal_hook_recovers_root_review_cas_race():
b=Backend()
hooks=TaskLineageLifecycleHooks(adapter_for(b))
persist_root_and_child(b,hooks)
b.fail_review_cas=1
out=hooks.on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("left",False,False)), reviewed_at=NOW
)
assert out.root_review_persisted
assert b.get_task("ROOT-A").revision >= 4
def test_terminal_hook_root_complete_only_when_acceptance_proven():
b=Backend()
hooks=TaskLineageLifecycleHooks(adapter_for(b))
persist_root_and_child(b,hooks)
out=hooks.on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("done",True,False)), reviewed_at=NOW
)
assert out.outcome.review.root_complete is True
assert out.outcome.review.next_legal_action==NextLegalAction.ROOT_COMPLETE
def test_terminal_hook_owner_boundary_is_preserved():
b=Backend()
hooks=TaskLineageLifecycleHooks(adapter_for(b))
persist_root_and_child(b,hooks)
out=hooks.on_proven_terminal_child(
child_task_id="CHILD-A", context=ctx(),
acceptance_evaluator=acceptance(("sign",False,True)), reviewed_at=NOW
)
assert out.outcome.review.next_legal_action==NextLegalAction.OWNER_BOUNDARY
def test_legacy_attach_preserves_task_id_and_writes_readable_display_name():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
hooks.persist_new_task_identity(request=root_request(), context=ctx())
b.put(bare("LEGACY-X",status="RECONCILIATION_REQUIRED"))
out=hooks.attach_legacy_identity(
task_id="LEGACY-X", parent_task_id="ROOT-A",
relation_type=RelationType.RECOVERY, goal_slug="current head reconciliation",
context=ctx(),
)
snap=b.get_task("LEGACY-X")
assert out.lineage.task_id=="LEGACY-X"
assert snap.task_id=="LEGACY-X"
assert snap.metadata[DISPLAY_NAME_METADATA_KEY]==out.lineage.display_name
assert out.independently_verified
def test_conflicting_existing_identity_fails_closed_on_replay():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
hooks.persist_new_task_identity(request=root_request(), context=ctx())
snap=b.get_task("ROOT-A")
md=dict(snap.metadata); md[DISPLAY_NAME_METADATA_KEY]="tampered"
b.tasks["ROOT-A"]=replace(snap, metadata=md)
with pytest.raises(LifecycleHookError, match="display name conflicts"):
hooks.persist_new_task_identity(request=root_request(), context=ctx())
def test_identity_hook_repairs_missing_display_for_matching_existing_lineage():
b=Backend(); b.put(bare("ROOT-A"))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
first=hooks.persist_new_task_identity(request=root_request(), context=ctx())
snap=b.get_task("ROOT-A")
md=dict(snap.metadata); md.pop(DISPLAY_NAME_METADATA_KEY)
b.tasks["ROOT-A"]=replace(snap, revision=snap.revision+1, metadata=md)
repaired=hooks.persist_new_task_identity(request=root_request(), context=ctx())
out=b.get_task("ROOT-A")
assert repaired.prepared.lineage == first.prepared.lineage
assert out.metadata[DISPLAY_NAME_METADATA_KEY] == first.prepared.display_name
assert repaired.independently_verified
def test_identity_hook_rejects_display_without_authoritative_lineage():
b=Backend()
snap=bare("ROOT-A")
b.put(replace(snap, metadata={DISPLAY_NAME_METADATA_KEY:"orphan-display"}))
hooks=TaskLineageLifecycleHooks(adapter_for(b))
with pytest.raises(LifecycleHookError, match="without authoritative lineage"):
hooks.persist_new_task_identity(request=root_request(), context=ctx())
@@ -0,0 +1,441 @@
from __future__ import annotations
import concurrent.futures
from datetime import datetime
import os
from pathlib import Path
import subprocess
import sys
import pytest
from app.aios_task_lineage import (
AcceptanceCondition,
NextLegalAction,
RootAcceptanceSnapshot,
)
from app.aios_task_lineage_integration import (
DISPLAY_NAME_METADATA_KEY,
LINEAGE_METADATA_KEY,
)
from app.task_plane.api import TaskPlaneService
from app.task_plane.errors import ConflictError, ForbiddenError, ValidationError
from app.task_plane.lineage_store import (
PrincipalLineageBinding,
TaskLineagePolicy,
TaskPlaneLineageStoreBinding,
)
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
OWNER_IDENTITY = "spiffe://aios/owner/live-store"
OTHER_IDENTITY = "spiffe://aios/owner/other"
WORKER_IDENTITY = "spiffe://aios/agent/live-store"
WORKER_ID = "lineage-worker-01"
ACCEPTANCE_REF = "policy://root/evidence-v1"
def _complete_when_proven(_root, descendants):
refs = tuple(
ref
for task in descendants
if task.terminal_evidence_proven
for ref in task.evidence_refs
)
return RootAcceptanceSnapshot(
(
AcceptanceCondition(
key="terminal-child-evidence",
satisfied=bool(refs),
evidence_refs=refs,
),
)
)
def _more_work_required(_root, _descendants):
return RootAcceptanceSnapshot(
(AcceptanceCondition(key="more-work-required", satisfied=False),)
)
def _lineage_policy(evaluator=_complete_when_proven) -> TaskLineagePolicy:
return TaskLineagePolicy(
principal_bindings=(
PrincipalLineageBinding(
authenticated_identity=OWNER_IDENTITY,
owner_principal_id="principal-owner-live",
mission_id="mission-live-store",
preferred_owner_alias="OWNER",
root_acceptance_ref=ACCEPTANCE_REF,
),
PrincipalLineageBinding(
authenticated_identity=OTHER_IDENTITY,
owner_principal_id="principal-owner-other",
mission_id="mission-other",
preferred_owner_alias="OTHER",
root_acceptance_ref=ACCEPTANCE_REF,
),
),
acceptance_evaluators={ACCEPTANCE_REF: evaluator},
)
def _service(db_path: Path, evaluator=_complete_when_proven) -> TaskPlaneService:
grants = {
OWNER_IDENTITY: {
"task.creator",
"task.read",
"task.audit",
"task.delegate",
},
OTHER_IDENTITY: {
"task.creator",
"task.read",
"task.audit",
"task.delegate",
},
WORKER_IDENTITY: {
"task.claim",
"worker.self_register",
},
}
return TaskPlaneService(
TaskPlaneStore(str(db_path)),
policy=PolicyEngine(
execution_enabled=True,
identity_capabilities=grants,
),
lineage_policy=_lineage_policy(evaluator),
)
def _root_body(task_id: str = "root-live") -> dict:
return {
"task_id": task_id,
"task_type": "code.review",
"intent": "verify durable lineage",
"required_capabilities": ["code.read"],
"risk": "low",
"max_delegation_depth": 3,
"budgets": {"attempts": 8},
# These client keys deliberately resemble authority. They remain ordinary
# metadata; the server-owned task columns and lineage record decide identity.
"metadata": {
"owner_principal_id": "forged-client-owner",
"mission_id": "forged-client-mission",
"terminal_evidence_proven": True,
},
}
def _child_body(task_id: str = "child-live") -> dict:
return {
"task_id": task_id,
"task_type": "code.review",
"intent": f"produce evidence for {task_id}",
"required_capabilities": ["code.read"],
"evidence_requirements": ["test-report"],
"risk": "low",
"budgets": {"attempts": 1},
}
def _register_worker(service: TaskPlaneService) -> None:
service.register_worker(
{
"worker_id": WORKER_ID,
"worker_type": "codex",
"identity": WORKER_IDENTITY,
"owner_identity": WORKER_IDENTITY,
"capabilities": ["code.read"],
"trust_zone": "isolated-development",
"max_concurrency": 1,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
"registration_policy": "self",
"actor_identity": WORKER_IDENTITY,
}
)
def _finish_task(service: TaskPlaneService, task_id: str) -> dict:
_register_worker(service)
claim = service.claim(
task_id,
{"worker_id": WORKER_ID, "actor_identity": WORKER_IDENTITY},
)
service.progress(
task_id,
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"progress": 100,
"stage": "verified",
},
)
return service.complete(
task_id,
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"outputs": [{"kind": "summary", "status": "passed"}],
"evidence": [{"kind": "test-report", "status": "passed"}],
},
)
def test_package_imports_in_clean_process_with_only_runtime_on_pythonpath(tmp_path):
repo = Path(__file__).resolve().parents[2]
runtime = repo / "active-runtime"
env = os.environ.copy()
env["PYTHONPATH"] = str(runtime)
result = subprocess.run(
[
sys.executable,
"-c",
"from app.task_plane.api import TaskPlaneService; "
"from app.task_plane.store import TaskPlaneStore; "
"s=TaskPlaneStore(':memory:'); TaskPlaneService(s); s.close()",
],
cwd=tmp_path,
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 0, result.stderr
def test_actual_store_lifecycle_is_durable_and_restart_replays_frozen_review(tmp_path):
db_path = tmp_path / "task-plane.db"
service = _service(db_path)
root_body = _root_body()
root = service.create_task(
root_body,
idempotency_key="root-admission-live",
actor_identity=OWNER_IDENTITY,
)
replay = service.create_task(
root_body,
idempotency_key="root-admission-live",
actor_identity=OWNER_IDENTITY,
)
assert replay == root
root_row = service.store.get_task(root["task_id"])
root_lineage = root["plan"]["metadata"][LINEAGE_METADATA_KEY]
assert root["display_name"] == root_lineage["display_name"]
assert service.list_tasks()[0]["display_name"] == root["display_name"]
assert root_row.owner_principal_id == "principal-owner-live"
assert root_row.mission_id == "mission-live-store"
assert root_lineage["owner_principal_id"] == root_row.owner_principal_id
assert root_lineage["mission_id"] == root_row.mission_id
assert datetime.fromisoformat(
root_lineage["created_at"].replace("Z", "+00:00")
).timestamp() == root_row.created_at
root_snapshot = service.lineage.binding.get_task_snapshot(root["task_id"])
assert root_snapshot.terminal_evidence_proven is False
child = service.delegate(
root["task_id"],
{"actor_identity": OWNER_IDENTITY, "task": _child_body()},
)
child_row = service.store.get_task(child["task_id"])
child_lineage = child["plan"]["metadata"][LINEAGE_METADATA_KEY]
assert datetime.fromisoformat(
child_lineage["created_at"].replace("Z", "+00:00")
).timestamp() == child_row.created_at
assert child_row.owner_principal_id == root_row.owner_principal_id
assert child_row.mission_id == root_row.mission_id
result = _finish_task(service, child["task_id"])
review = result["root_review"]
assert review["root_complete"] is True
assert review["next_legal_action"] == NextLegalAction.ROOT_COMPLETE.value
assert review["dispatch_status"] == "PENDING"
review_id = review["review_id"]
root_revision_after = service.store.get_task(root["task_id"]).revision
service.store.close()
# A new connection and a changed evaluator must reuse the exact committed
# review and dispatch for this terminal trigger instead of re-evaluating it.
restarted = _service(db_path, _more_work_required)
outcome = restarted.lineage.resume_terminal_review(child_task_id=child["task_id"])
assert outcome.review.review_id == review_id
assert outcome.review.root_complete is True
assert outcome.dispatch.status == "PENDING"
assert restarted.store.get_task(root["task_id"]).revision == root_revision_after
reopened_snapshot = restarted.lineage.binding.get_task_snapshot(child["task_id"])
assert reopened_snapshot.terminal_evidence_proven is True
assert reopened_snapshot.evidence_refs
assert restarted.store.verify_event_chain(root["task_id"])
assert restarted.store.verify_event_chain(child["task_id"])
restarted.store.close()
def test_resume_rejects_review_record_with_wrong_trigger(tmp_path):
import json
service = _service(tmp_path / "wrong-trigger.db")
root = service.create_task(_root_body(), actor_identity=OWNER_IDENTITY)
child = service.delegate(root["task_id"], {
"actor_identity": OWNER_IDENTITY, "task": _child_body()})
result = _finish_task(service, child["task_id"])
review_id = result["root_review"]["review_id"]
record = dict(service.lineage.binding.get_root_review_record(review_id))
record["trigger_child_task_id"] = "different-child"
# Corrupt a test fixture to ensure restart does not dispatch a misbound row.
with service.store._transaction():
service.store.conn.execute(
"UPDATE task_root_reviews SET record_json=? WHERE review_id=?",
(json.dumps(record), review_id))
with pytest.raises(ConflictError, match="authority mismatch"):
service.lineage.resume_terminal_review(child_task_id=child["task_id"])
service.store.close()
def test_lineage_authority_and_stale_cas_fail_without_partial_delegation(tmp_path):
service = _service(tmp_path / "authority.db")
with pytest.raises(ValidationError, match="server-owned"):
service.create_task(
{
**_root_body("root-forged-lineage"),
"metadata": {LINEAGE_METADATA_KEY: {"owner_principal_id": "forged"}},
},
actor_identity=OWNER_IDENTITY,
)
root = service.create_task(_root_body(), actor_identity=OWNER_IDENTITY)
budget_before = service.store.get_task_budget(root["task_id"]).attempts
sequence_count_before = service.store.conn.execute(
"SELECT COUNT(*) FROM task_lineage_sequences"
).fetchone()[0]
with pytest.raises(ForbiddenError, match="root authority"):
service.delegate(
root["task_id"],
{"actor_identity": OTHER_IDENTITY, "task": _child_body("child-other")},
)
assert service.store.get_task_budget(root["task_id"]).attempts == budget_before
assert service.store.conn.execute(
"SELECT COUNT(*) FROM task_lineage_sequences"
).fetchone()[0] == sequence_count_before
assert service.store.conn.execute(
"SELECT COUNT(*) FROM tasks WHERE task_id='child-other'"
).fetchone()[0] == 0
binding = TaskPlaneLineageStoreBinding(service.store)
current = service.store.get_task(root["task_id"])
assert binding.compare_and_swap_task_metadata(
root["task_id"],
current.revision - 1,
{
DISPLAY_NAME_METADATA_KEY:
current.task_plan.metadata[DISPLAY_NAME_METADATA_KEY]
},
) is False
service.store.close()
def test_lineage_root_cannot_be_worker_completed_directly(tmp_path):
service = _service(tmp_path / "root-completion.db")
root = service.create_task(_root_body(), actor_identity=OWNER_IDENTITY)
_register_worker(service)
claim = service.claim(
root["task_id"],
{"worker_id": WORKER_ID, "actor_identity": WORKER_IDENTITY},
)
service.progress(
root["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
"progress": 100,
"stage": "done",
},
)
with pytest.raises(ForbiddenError, match="root acceptance review"):
service.complete(
root["task_id"],
{
"claim_id": claim["claim_id"],
"actor_identity": WORKER_IDENTITY,
},
)
assert service.store.get_task(root["task_id"]).status == "RUNNING"
assert service.store.conn.execute(
"SELECT COUNT(*) FROM task_terminal_proofs"
).fetchone()[0] == 0
assert service.store.conn.execute(
"SELECT COUNT(*) FROM task_root_reviews"
).fetchone()[0] == 0
service.store.close()
def test_unbound_planner_persists_pending_next_action_without_creating_task(tmp_path):
service = _service(tmp_path / "pending.db", _more_work_required)
root = service.create_task(_root_body(), actor_identity=OWNER_IDENTITY)
child = service.delegate(
root["task_id"],
{"actor_identity": OWNER_IDENTITY, "task": _child_body()},
)
result = _finish_task(service, child["task_id"])
assert result["root_review"]["root_complete"] is False
assert (
result["root_review"]["next_legal_action"]
== NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD.value
)
assert result["root_review"]["dispatch_status"] == "PENDING"
assert service.store.conn.execute(
"SELECT COUNT(*) FROM tasks WHERE root_task_id=? AND task_id<>?",
(root["task_id"], root["task_id"]),
).fetchone()[0] == 1
pending = service.store.conn.execute(
"SELECT action,status FROM task_root_action_dispatches"
).fetchone()
assert tuple(pending) == (NextLegalAction.CREATE_OR_RESUME_NEXT_CHILD.value, "PENDING")
service.store.close()
def test_concurrent_store_connections_reserve_distinct_sibling_sequences(tmp_path):
db_path = tmp_path / "concurrent.db"
creator = _service(db_path)
root = creator.create_task(_root_body(), actor_identity=OWNER_IDENTITY)
creator.store.close()
left = _service(db_path)
right = _service(db_path)
def delegate(service: TaskPlaneService, task_id: str):
return service.delegate(
root["task_id"],
{"actor_identity": OWNER_IDENTITY, "task": _child_body(task_id)},
)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
futures = (
executor.submit(delegate, left, "child-left"),
executor.submit(delegate, right, "child-right"),
)
children = [future.result(timeout=15) for future in futures]
assert {child["task_id"] for child in children} == {"child-left", "child-right"}
sequences = {
child["plan"]["metadata"][LINEAGE_METADATA_KEY]["sibling_sequence"]
for child in children
}
assert sequences == {1, 2}
rows = left.store.conn.execute(
"SELECT child_task_id,sibling_sequence FROM task_lineage_sequences "
"ORDER BY sibling_sequence"
).fetchall()
assert [(row["child_task_id"], row["sibling_sequence"]) for row in rows] in (
[("child-left", 1), ("child-right", 2)],
[("child-right", 1), ("child-left", 2)],
)
assert left.store.get_task_budget(root["task_id"]).attempts == 6
left.store.close()
right.store.close()
@@ -0,0 +1,294 @@
from __future__ import annotations
from dataclasses import replace
from datetime import datetime, timezone
import json
from pathlib import Path
import sqlite3
import pytest
from aios_task_lineage import (
NextLegalAction,
RelationType,
lineage_to_metadata,
make_root_lineage,
)
from aios_task_lineage_integration import (
LINEAGE_METADATA_KEY,
RootActionDispatchReceipt,
TaskAuthoritySnapshot,
)
from aios_task_lineage_taskplane_adapter import (
AuthenticatedTaskContext,
AuthorityAdapterError,
InProcessTaskPlaneLineageAdapter,
ReadOnlySQLiteTaskPlane,
TaskPlaneServiceBindings,
require_context_matches_task,
)
NOW = datetime(2026, 9, 14, 10, 0, tzinfo=timezone.utc)
def create_db(path: Path):
con = sqlite3.connect(path)
con.execute(
"""CREATE TABLE tasks(
task_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
revision INTEGER NOT NULL,
metadata_json TEXT NOT NULL,
created_at TEXT NOT NULL,
claimed_by TEXT
)"""
)
con.commit()
return con
def metadata(task_id="ROOT-1", owner="principal-A", mission="M1"):
lineage = make_root_lineage(
task_id=task_id,
owner_principal_id=owner,
owner_alias="OWNER1",
created_at=NOW,
mission_id=mission,
goal_slug="GOAL-TO-EVIDENCE",
root_acceptance_ref="acceptance:v1",
)
return {
LINEAGE_METADATA_KEY: lineage_to_metadata(lineage),
"owner_principal_id": owner,
"mission_id": mission,
"evidence_refs": ["ev:1"],
"terminal_evidence_proven": True,
}
def insert(con, task_id, meta, revision=3, status="RUNNING"):
con.execute(
"INSERT INTO tasks(task_id,status,revision,metadata_json,created_at) VALUES(?,?,?,?,?)",
(task_id, status, revision, json.dumps(meta), NOW.isoformat()),
)
con.commit()
def snapshot(task_id="T", revision=1, owner="principal-A", mission="M1", metadata=None):
return TaskAuthoritySnapshot(
task_id=task_id,
revision=revision,
status="RUNNING",
owner_principal_id=owner,
mission_id=mission,
created_at=NOW,
metadata=metadata or {"owner_principal_id": owner, "mission_id": mission},
)
class Backend:
def __init__(self):
self.tasks = {"T": snapshot()}
self.seq = 0
self.reviews = {}
self.dispatches = {}
def get_task(self, task_id): return self.tasks[task_id]
def descendants(self, root_id): return tuple(self.tasks.values())
def reserve(self, parent, relation, child):
self.seq += 1
return self.seq
def cas_meta(self, task_id, expected, patch):
cur = self.tasks[task_id]
if cur.revision != expected:
return False
merged = dict(cur.metadata); merged.update(patch)
self.tasks[task_id] = replace(cur, revision=cur.revision + 1, metadata=merged)
return True
def get_review(self, review_id): return self.reviews.get(review_id)
def cas_review(self, root_id, expected, record):
self.reviews[record["review_id"]] = dict(record)
return True
def get_dispatch(self, review_id): return self.dispatches.get(review_id)
def dispatch(self, review, idem):
receipt = RootActionDispatchReceipt(
review_id=review.review_id, root_task_id=review.root_task_id,
action=review.next_legal_action, idempotency_key=idem, status="DISPATCHED"
)
self.dispatches[review.review_id] = receipt
return receipt
def bindings(b: Backend):
return TaskPlaneServiceBindings(
get_task_snapshot=b.get_task,
list_descendant_snapshots=b.descendants,
reserve_sibling_sequence=b.reserve,
compare_and_swap_task_metadata=b.cas_meta,
get_root_review_record=b.get_review,
compare_and_swap_root_review_record=b.cas_review,
get_root_action_dispatch=b.get_dispatch,
dispatch_root_action=b.dispatch,
)
def test_readonly_sqlite_schema_matches_authority_shape(tmp_path):
db = tmp_path/"state.sqlite3"
con=create_db(db); insert(con, "ROOT-1", metadata()); con.close()
ro=ReadOnlySQLiteTaskPlane(db)
report=ro.inspect_schema()
assert report.required_columns_present
assert report.task_count == 1
def test_readonly_sqlite_reads_lineage_snapshot(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db); insert(con,"ROOT-1",metadata()); con.close()
snap=ReadOnlySQLiteTaskPlane(db).get_task_snapshot("ROOT-1")
assert snap.owner_principal_id=="principal-A"
assert snap.mission_id=="M1"
assert snap.revision==3
assert snap.terminal_evidence_proven is False
assert snap.evidence_refs==("ev:1",)
def test_readonly_sqlite_accepts_legacy_identity_metadata(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db)
insert(con,"LEGACY",{"owner_principal_id":"p","mission_id":"M"})
con.close()
snap=ReadOnlySQLiteTaskPlane(db).get_task_snapshot("LEGACY")
assert snap.owner_principal_id=="p" and snap.mission_id=="M"
@pytest.mark.parametrize("meta", [
{"mission_id":"M"},
{"owner_principal_id":"p"},
{},
])
def test_readonly_sqlite_fails_closed_when_legacy_identity_missing(tmp_path, meta):
db=tmp_path/"state.sqlite3"; con=create_db(db); insert(con,"T",meta); con.close()
with pytest.raises(AuthorityAdapterError):
ReadOnlySQLiteTaskPlane(db).get_task_snapshot("T")
def test_readonly_sqlite_never_exposes_write_api(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db); insert(con,"ROOT-1",metadata()); con.close()
ro=ReadOnlySQLiteTaskPlane(db)
assert not hasattr(ro, "compare_and_swap_task_metadata")
assert not hasattr(ro, "execute_write")
def test_readonly_sqlite_reads_committed_wal_and_uri_metacharacters(tmp_path):
db = tmp_path / "state #1.sqlite3"
con = create_db(db)
con.execute("PRAGMA journal_mode=WAL")
con.execute("PRAGMA wal_autocheckpoint=0")
insert(con, "ROOT-1", metadata(), revision=4)
try:
snap = ReadOnlySQLiteTaskPlane(db).get_task_snapshot("ROOT-1")
assert snap.revision == 4
assert snap.terminal_evidence_proven is False
finally:
con.close()
def test_descendant_readback_uses_authoritative_lineage_only(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db)
root_meta=metadata()
insert(con,"ROOT-1",root_meta)
child_meta=json.loads(json.dumps(root_meta))
child_meta[LINEAGE_METADATA_KEY]["task_id"]="CHILD-1"
child_meta[LINEAGE_METADATA_KEY]["parent_task_id"]="ROOT-1"
child_meta[LINEAGE_METADATA_KEY]["root_task_id"]="ROOT-1"
child_meta[LINEAGE_METADATA_KEY]["lineage_depth"]=1
child_meta[LINEAGE_METADATA_KEY]["relation_type"]="CHILD"
child_meta[LINEAGE_METADATA_KEY]["display_name"]="TASK-OWNER1-20260914T100000Z-RROOT0001-PROOT0001-D01-C01-CHILD"
insert(con,"CHILD-1",child_meta)
insert(con,"UNRELATED",{"owner_principal_id":"p","mission_id":"M"})
con.close()
desc=ReadOnlySQLiteTaskPlane(db).list_descendant_snapshots("ROOT-1")
assert [d.task_id for d in desc]==["CHILD-1"]
def test_inprocess_adapter_delegates_cas_to_backend_not_sqlite():
b=Backend(); a=InProcessTaskPlaneLineageAdapter(bindings(b))
assert a.compare_and_swap_task_metadata(
task_id="T", expected_revision=1, metadata_patch={"x":1}
)
assert b.tasks["T"].revision==2
assert b.tasks["T"].metadata["x"]==1
def test_inprocess_adapter_rejects_invalid_sequence():
b=Backend()
bad=TaskPlaneServiceBindings(
**{**bindings(b).__dict__, "reserve_sibling_sequence": lambda p,r,c: 0}
)
a=InProcessTaskPlaneLineageAdapter(bad)
with pytest.raises(AuthorityAdapterError):
a.reserve_sibling_sequence(
parent_task_id="T", relation_type=RelationType.CHILD, child_task_id="C"
)
def test_independent_readback_verifies_revision_and_metadata(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db)
insert(con,"ROOT-1",metadata(),revision=4); con.close()
b=Backend()
a=InProcessTaskPlaneLineageAdapter(bindings(b), readback=ReadOnlySQLiteTaskPlane(db))
snap=a.verify_task_readback(
task_id="ROOT-1", expected_revision_at_least=4,
expected_metadata={"mission_id":"M1"}
)
assert snap.revision==4
def test_independent_readback_fails_on_stale_revision(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db)
insert(con,"ROOT-1",metadata(),revision=2); con.close()
b=Backend()
a=InProcessTaskPlaneLineageAdapter(bindings(b), readback=ReadOnlySQLiteTaskPlane(db))
with pytest.raises(AuthorityAdapterError, match="revision"):
a.verify_task_readback(
task_id="ROOT-1", expected_revision_at_least=3,
expected_metadata={"mission_id":"M1"}
)
def test_independent_readback_fails_on_metadata_mismatch(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db)
insert(con,"ROOT-1",metadata(),revision=4); con.close()
b=Backend()
a=InProcessTaskPlaneLineageAdapter(bindings(b), readback=ReadOnlySQLiteTaskPlane(db))
with pytest.raises(AuthorityAdapterError, match="metadata mismatch"):
a.verify_task_readback(
task_id="ROOT-1", expected_revision_at_least=4,
expected_metadata={"mission_id":"OTHER"}
)
def test_authenticated_context_matches_owner_and_mission():
require_context_matches_task(
AuthenticatedTaskContext("principal-A", "OWNER1", "M1"),
snapshot(owner="principal-A", mission="M1")
)
@pytest.mark.parametrize("ctx", [
AuthenticatedTaskContext("principal-B", None, "M1"),
AuthenticatedTaskContext("principal-A", None, "M2"),
])
def test_authenticated_context_mismatch_rejected(ctx):
with pytest.raises(AuthorityAdapterError):
require_context_matches_task(ctx, snapshot())
def test_malformed_metadata_json_rejected(tmp_path):
db=tmp_path/"state.sqlite3"; con=create_db(db)
con.execute(
"INSERT INTO tasks(task_id,status,revision,metadata_json,created_at) VALUES(?,?,?,?,?)",
("T","RUNNING",1,"{broken",NOW.isoformat())
); con.commit(); con.close()
with pytest.raises(AuthorityAdapterError, match="invalid JSON"):
ReadOnlySQLiteTaskPlane(db).get_task_snapshot("T")
@@ -0,0 +1,570 @@
from __future__ import annotations
import concurrent.futures
import http.client
import json
import threading
from http.server import ThreadingHTTPServer
from pathlib import Path
from types import SimpleNamespace
import pytest
from app.aios_task_lineage_integration import LINEAGE_METADATA_KEY
from app.task_plane.api import TaskPlaneRequestHandler, TaskPlaneService
from app.task_plane.errors import (
ConflictError,
ForbiddenError,
LockedError,
ValidationError,
)
from app.task_plane.mission import MissionAuthority, MissionPrincipalBinding
from app.task_plane.models import TaskPlan
from app.task_plane.policy import PolicyEngine
from app.task_plane.store import TaskPlaneStore
OWNER = "local-owner-authority"
OTHER = "other-owner-authority"
VERIFIER = "identity-lineage-live-closure-v1"
BODY = {
"goal_slug": "restore-canonical-task-lineage",
"original_acceptance": {
"schema": "original-acceptance-v1",
"conditions": ["durable-child-evidence", "owner-reviewed-result"],
},
}
def _service(
path: Path,
*,
include_other: bool = True,
execution_enabled: bool = True,
allowed_task_capabilities: tuple[str, ...] = (),
max_risk: str = "low",
) -> TaskPlaneService:
store = TaskPlaneStore(str(path))
bindings = [
MissionPrincipalBinding(
authenticated_identity=OWNER,
owner_principal_id=OWNER,
acceptance_verifier_id=VERIFIER,
allowed_task_capabilities=allowed_task_capabilities,
max_risk=max_risk,
)
]
if include_other:
bindings.append(
MissionPrincipalBinding(
authenticated_identity=OTHER,
owner_principal_id=OTHER,
acceptance_verifier_id=VERIFIER,
allowed_task_capabilities=allowed_task_capabilities,
max_risk=max_risk,
)
)
authority = MissionAuthority(store, principal_bindings=bindings)
grants = {
OWNER: {"task.creator", "task.read", "task.delegate"},
OTHER: {"task.creator", "task.read", "task.delegate"},
}
return TaskPlaneService(
store,
policy=PolicyEngine(
execution_enabled=execution_enabled,
identity_capabilities=grants,
),
mission_authority=authority,
)
def test_mission_authority_is_default_off(tmp_path):
service = TaskPlaneService(TaskPlaneStore(str(tmp_path / "off.db")))
with pytest.raises(ForbiddenError, match="not configured"):
service.create_mission(BODY, "mission-off", actor_identity="anon")
service.store.close()
def test_shared_store_legacy_service_cannot_read_mission_tasks(tmp_path):
path = tmp_path / "shared-authority.db"
owner = _service(path)
legacy_store = TaskPlaneStore(str(path))
legacy = TaskPlaneService(
legacy_store,
policy=PolicyEngine(identity_capabilities={
"shadow-controller": {"task.creator", "task.read", "task.audit"}
}),
)
try:
original = legacy.create_task(
{"task_id": "legacy-shadow", "task_type": "platform.shadow"},
"legacy-before-mission", actor_identity="shadow-controller",
)
mission = owner.create_mission(BODY, "shared-mission", actor_identity=OWNER)
child = owner.delegate_mission(
mission["root_task_id"], {"task_type": "mission.acceptance", "risk": "low"},
"shared-child", actor_identity=OWNER,
)
for actor in ("shadow-controller", None):
assert [item["task_id"] for item in legacy.list_tasks(actor)] == [original["task_id"]]
for task_id in (mission["root_task_id"], child["task_id"]):
for method in (legacy.get_task, legacy.get_events, legacy.get_evidence, legacy.get_children):
with pytest.raises(ForbiddenError, match="configured owner"):
method(task_id, actor)
assert legacy.get_task(original["task_id"], "shadow-controller")["task_id"] == original["task_id"]
assert owner.get_task(child["task_id"], OWNER)["task_id"] == child["task_id"]
finally:
legacy_store.close()
owner.store.close()
def test_canonical_mission_issues_and_persists_all_authority(tmp_path):
service = _service(
tmp_path / "canonical.db",
allowed_task_capabilities=("code.read", "test.execute"),
)
created = service.create_mission(
BODY, "mission-canonical", actor_identity=OWNER
)
assert created["mission_id"].startswith("mission-")
assert created["root_task_id"].startswith("task-")
assert created["owner_principal_id"] == OWNER
assert created["owner_alias"].startswith("U")
assert created["goal_slug"] == "RESTORE-CANONICAL-TASK-LINEAGE"
assert created["root_acceptance_ref"].startswith(
f"mission-acceptance:{created['mission_id']}:"
)
assert created["original_acceptance"] == BODY["original_acceptance"]
root = service.store.get_task(created["root_task_id"])
lineage = root.task_plan.metadata[LINEAGE_METADATA_KEY]
assert root.task_id == created["root_task_id"]
assert root.root_task_id == root.task_id
assert root.mission_id == created["mission_id"]
assert root.owner_principal_id == OWNER
assert root.task_plan.required_capabilities == ["code.read", "test.execute"]
assert root.task_plan.risk == "low"
assert lineage["mission_id"] == created["mission_id"]
assert lineage["root_acceptance_ref"] == created["root_acceptance_ref"]
assert lineage["owner_alias"] == created["owner_alias"]
assert lineage["created_at"] == created["created_at"]
assert service.get_mission(
created["mission_id"], actor_identity=OWNER
) == created
row = service.store.conn.execute(
"SELECT original_acceptance_json FROM task_missions WHERE mission_id=?",
(created["mission_id"],),
).fetchone()
assert json.loads(row["original_acceptance_json"]) == BODY["original_acceptance"]
service.store.close()
@pytest.mark.parametrize(
"forged",
[
{"mission_id": "mission-forged"},
{"root_task_id": "task-forged"},
{"owner_principal_id": "forged"},
{"owner_alias": "FORGED"},
{"root_acceptance_ref": "policy://forged"},
{"actor_identity": OWNER},
{"issuer": {"id": OWNER}},
],
)
def test_mission_body_rejects_client_authority_fields(tmp_path, forged):
service = _service(tmp_path / "forged.db")
with pytest.raises(ValidationError, match="requires only"):
service.create_mission(
{**BODY, **forged}, "mission-forged", actor_identity=OWNER
)
assert service.store.conn.execute(
"SELECT COUNT(*) FROM task_missions"
).fetchone()[0] == 0
assert service.store.conn.execute(
"SELECT COUNT(*) FROM tasks"
).fetchone()[0] == 0
service.store.close()
def test_response_loss_reopen_and_exact_principal_scoped_replay(tmp_path):
db_path = tmp_path / "replay.db"
first_service = _service(db_path)
first = first_service.create_mission(
BODY, "same-key", actor_identity=OWNER
)
first_service.store.close()
restarted = _service(db_path)
readback = restarted.get_mission(
first["mission_id"], actor_identity=OWNER
)
replay = restarted.create_mission(
BODY, "same-key", actor_identity=OWNER
)
assert readback == first
assert replay == first
assert restarted.store.conn.execute(
"SELECT COUNT(*) FROM task_missions"
).fetchone()[0] == 1
assert restarted.store.conn.execute(
"SELECT COUNT(*) FROM tasks"
).fetchone()[0] == 1
with pytest.raises(ConflictError, match="replay conflict"):
restarted.create_mission(
{
**BODY,
"original_acceptance": {"conditions": ["changed"]},
},
"same-key",
actor_identity=OWNER,
)
other = restarted.create_mission(BODY, "same-key", actor_identity=OTHER)
assert other["mission_id"] != first["mission_id"]
with pytest.raises(ForbiddenError):
restarted.get_mission(first["mission_id"], actor_identity=OTHER)
restarted.store.close()
def test_binding_capability_or_risk_drift_invalidates_readback(tmp_path):
db_path = tmp_path / "binding-drift.db"
created_by = _service(
db_path,
allowed_task_capabilities=("code.read",),
max_risk="low",
)
mission = created_by.create_mission(BODY, "drift", actor_identity=OWNER)
created_by.store.close()
changed = _service(
db_path,
allowed_task_capabilities=("code.read", "test.execute"),
max_risk="medium",
)
with pytest.raises(ConflictError, match="root authority mismatch"):
changed.get_mission(mission["mission_id"], actor_identity=OWNER)
with pytest.raises(ConflictError, match="root authority mismatch"):
changed.create_mission(BODY, "drift", actor_identity=OWNER)
changed.store.close()
def test_concurrent_same_request_creates_one_mission_and_root(tmp_path):
db_path = tmp_path / "concurrent.db"
left = _service(db_path)
right = _service(db_path)
def create(service):
return service.create_mission(
BODY, "concurrent-key", actor_identity=OWNER
)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
results = list(executor.map(create, (left, right)))
assert results[0] == results[1]
assert left.store.conn.execute(
"SELECT COUNT(*) FROM task_missions"
).fetchone()[0] == 1
assert left.store.conn.execute(
"SELECT COUNT(*) FROM tasks"
).fetchone()[0] == 1
left.store.close()
right.store.close()
def test_unregistered_acceptance_verifier_remains_unmet(tmp_path):
service = _service(tmp_path / "unregistered.db")
created = service.create_mission(
BODY, "unregistered-verifier", actor_identity=OWNER
)
evaluator = service.missions.resolve_acceptance_evaluator(
created["root_acceptance_ref"]
)
root = SimpleNamespace(
lineage=SimpleNamespace(
root_task_id=created["root_task_id"],
mission_id=created["mission_id"],
owner_principal_id=OWNER,
root_acceptance_ref=created["root_acceptance_ref"],
)
)
snapshot = evaluator(root, ())
assert snapshot.complete is False
assert snapshot.owner_boundary_required is False
assert snapshot.conditions[0].key == "mission-acceptance-verifier-unregistered"
service.store.close()
def test_mission_child_is_scoped_and_replays_after_restart(tmp_path):
db_path = tmp_path / "child.db"
service = _service(
db_path,
execution_enabled=False,
allowed_task_capabilities=("code.read", "test.execute"),
)
mission = service.create_mission(BODY, "mission-child", actor_identity=OWNER)
child_body = {
"task_type": "code.review",
"intent": "collect durable evidence",
"risk": "low",
"required_capabilities": ["code.read"],
}
child = service.delegate_mission(
mission["root_task_id"],
child_body,
"child-exact",
actor_identity=OWNER,
)
assert child["task_id"].startswith("task-")
assert child["task_id"] != mission["root_task_id"]
assert child["parent_task_id"] == mission["root_task_id"]
assert child["root_task_id"] == mission["root_task_id"]
assert child["plan"]["metadata"][LINEAGE_METADATA_KEY]["mission_id"] == mission[
"mission_id"
]
assert service.delegate_mission(
mission["root_task_id"],
child_body,
"child-exact",
actor_identity=OWNER,
) == child
assert service.store.conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 2
service.store.close()
restarted = _service(
db_path,
execution_enabled=False,
allowed_task_capabilities=("code.read", "test.execute"),
)
assert restarted.delegate_mission(
mission["root_task_id"],
child_body,
"child-exact",
actor_identity=OWNER,
) == child
with pytest.raises(ConflictError, match="replay conflict"):
restarted.delegate_mission(
mission["root_task_id"],
{**child_body, "intent": "changed"},
"child-exact",
actor_identity=OWNER,
)
second = restarted.delegate_mission(
mission["root_task_id"],
{"task_type": "code.review", "risk": "low"},
"second-child",
actor_identity=OWNER,
)
with pytest.raises(ConflictError, match="replay conflict"):
restarted.delegate_mission(
second["task_id"],
child_body,
"child-exact",
actor_identity=OWNER,
)
for forged in (
{"task_id": "task-forged"},
{"parent_task_id": mission["root_task_id"]},
{"issuer": {"id": OWNER}},
{"actor_identity": OWNER},
):
with pytest.raises(ForbiddenError, match="server-owned"):
restarted.delegate_mission(
mission["root_task_id"],
{**child_body, **forged},
f"forged-{next(iter(forged))}",
actor_identity=OWNER,
)
with pytest.raises(ForbiddenError, match="exceed parent"):
restarted.delegate_mission(
mission["root_task_id"],
{
"task_type": "code.change",
"risk": "low",
"required_capabilities": ["code.write"],
},
"forbidden-capability",
actor_identity=OWNER,
)
with pytest.raises(ForbiddenError):
restarted.delegate_mission(
mission["root_task_id"],
child_body,
"cross-owner",
actor_identity=OTHER,
)
restarted.store.close()
def test_concurrent_same_child_request_creates_one_task(tmp_path):
db_path = tmp_path / "concurrent-child.db"
left = _service(
db_path,
execution_enabled=False,
allowed_task_capabilities=("code.read",),
)
mission = left.create_mission(BODY, "concurrent-parent", actor_identity=OWNER)
right = _service(
db_path,
execution_enabled=False,
allowed_task_capabilities=("code.read",),
)
child_body = {
"task_type": "code.review",
"risk": "low",
"required_capabilities": ["code.read"],
}
def delegate(service):
return service.delegate_mission(
mission["root_task_id"],
child_body,
"concurrent-child",
actor_identity=OWNER,
)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
children = list(executor.map(delegate, (left, right)))
assert children[0] == children[1]
assert left.store.conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 2
assert left.store.conn.execute(
"SELECT COUNT(*) FROM task_mission_delegations"
).fetchone()[0] == 1
left.store.close()
right.store.close()
def test_mission_admission_does_not_enable_legacy_execution(tmp_path):
policy = PolicyEngine(
execution_enabled=False,
identity_capabilities={OWNER: {"task.creator", "task.delegate"}},
)
legacy = TaskPlaneService(TaskPlaneStore(str(tmp_path / "legacy-gate.db")), policy)
parent = legacy.create_task(
{
"task_id": "legacy-parent",
"task_type": "code.review",
"issuer": {"type": "authenticated", "id": OWNER},
"risk": "low",
},
actor_identity=OWNER,
)
with pytest.raises(LockedError, match="execution disabled"):
legacy.delegate(
parent["task_id"],
{
"actor_identity": OWNER,
"task_id": "legacy-child",
"task_type": "code.review",
"risk": "low",
},
)
legacy.store.close()
@pytest.mark.parametrize(
"mutation",
[
"UPDATE tasks SET mission_id='mission-forged' WHERE task_id=?",
"UPDATE tasks SET owner_principal_id='owner-forged' WHERE task_id=?",
"UPDATE tasks SET root_task_id='task-forged' WHERE task_id=?",
"UPDATE tasks SET task_json=replace(task_json, 'mission-acceptance:', "
"'mission-acceptance-forged:') WHERE task_id=?",
],
)
def test_mission_readback_rejects_inconsistent_root_authority(tmp_path, mutation):
service = _service(tmp_path / "tamper.db")
mission = service.create_mission(BODY, "tamper", actor_identity=OWNER)
service.store.conn.execute(mutation, (mission["root_task_id"],))
with pytest.raises(ConflictError, match="root authority mismatch"):
service.get_mission(mission["mission_id"], actor_identity=OWNER)
service.store.close()
def test_mission_schema_upgrade_is_additive_for_existing_database(tmp_path):
db_path = tmp_path / "upgrade.db"
old = TaskPlaneStore(str(db_path))
old.create_task_with_event(
TaskPlan(task_id="pre-mission-task", task_type="code.review", risk="low")
)
old.conn.execute("DROP TABLE task_mission_delegations")
old.conn.execute("DROP TABLE task_missions")
old.close()
upgraded = TaskPlaneStore(str(db_path))
assert upgraded.get_task("pre-mission-task").task_id == "pre-mission-task"
tables = {
row[0]
for row in upgraded.conn.execute(
"SELECT name FROM sqlite_master WHERE type='table'"
)
}
assert {"task_missions", "task_mission_delegations"}.issubset(tables)
assert upgraded.integrity_check() == "ok"
upgraded.close()
def test_http_mission_create_get_and_cross_owner_denial(tmp_path):
service = _service(tmp_path / "http.db")
class Handler(TaskPlaneRequestHandler):
pass
Handler.service = service
Handler.authenticator = lambda token: {"owner-token": OWNER, "other-token": OTHER}.get(
token
)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
host, port = server.server_address
def request(method, path, *, token, body=None, key=None):
connection = http.client.HTTPConnection(host, port, timeout=3)
headers = {"Authorization": f"Bearer {token}"}
payload = None
if body is not None:
payload = json.dumps(body)
headers["Content-Type"] = "application/json"
if key is not None:
headers["Idempotency-Key"] = key
connection.request(method, path, body=payload, headers=headers)
response = connection.getresponse()
result = response.status, json.loads(response.read())
connection.close()
return result
try:
status, created = request(
"POST", "/missions", token="owner-token", body=BODY, key="http-key"
)
assert status == 201
status, fetched = request(
"GET", f"/missions/{created['mission_id']}", token="owner-token"
)
assert status == 200
assert fetched == created
status, denied = request(
"GET", f"/missions/{created['mission_id']}", token="other-token"
)
assert status == 403
assert denied["error"] == "forbidden"
status, missing_key = request(
"POST", "/missions", token="owner-token", body=BODY
)
assert status == 400
assert missing_key["error"] == "validation_error"
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
service.store.close()
@@ -0,0 +1,141 @@
from copy import deepcopy
import hashlib
import json
import threading
import urllib.error
import urllib.request
from http.server import HTTPServer
import pytest
from app.aios_task_mission_authority_server import (
AUDIENCE, SignedMissionIngress, canonical_bytes, make_handler,
)
from app.task_plane.errors import ForbiddenError, UnauthorizedError, ValidationError
class Verifier:
signer_id = "registered-owner"
def verify(self, *, namespace, payload, signature, key_id):
return (namespace == "aios-owner-approval-v1" and key_id == "test-key"
and signature == hashlib.sha256(payload).hexdigest())
class Service:
def __init__(self):
self.calls = []
def create_mission(self, body, key, *, actor_identity):
self.calls.append((body, key, actor_identity))
return {"mission_id": "mission-authority-generated"}
def get_mission(self, mission_id, *, actor_identity):
self.calls.append((mission_id, actor_identity))
return {"mission_id": mission_id}
def delegate_mission(self, parent_id, body, key, *, actor_identity):
self.calls.append((parent_id, body, key, actor_identity))
return {"task_id": "authority-issued-child", "parent_task_id": parent_id}
def envelope(**overrides):
request = {
"schema": "aios.signed-mission-request.v1", "audience": AUDIENCE,
"method": "POST", "path": "/missions",
"body": {"goal_slug": "closure", "original_acceptance": {"goal": "prove"}},
"idempotency_key": "root-create-exact", "issued_at": 100,
"expires_at": 200, "nonce": "test_nonce_123456789",
}
request.update(overrides)
return {"request": request, "key_id": "test-key",
"signature": hashlib.sha256(canonical_bytes(request)).hexdigest()}
def test_verified_registry_identity_is_only_actor():
service = Service()
ingress = SignedMissionIngress(service, Verifier(), clock=lambda: 150)
assert ingress.handle(envelope())[0] == 201
assert service.calls[0][2] == "registered-owner"
get = envelope(method="GET", path="/missions/mission-one", body=None,
idempotency_key=None)
assert ingress.handle(get) == (200, {"mission_id": "mission-one"})
child = envelope(path="/tasks/root-from-authority/delegate",
body={"task_type": "code.review"})
assert ingress.handle(child) == (201, {
"task_id": "authority-issued-child", "parent_task_id": "root-from-authority",
})
assert service.calls[-1][-1] == "registered-owner"
@pytest.mark.parametrize("field,value", [
("body", {"goal_slug": "other"}), ("idempotency_key", "another-root"),
("path", "/missions/another"), ("method", "GET"),
])
def test_every_effect_coordinate_is_signed(field, value):
service = Service()
ingress = SignedMissionIngress(service, Verifier(), clock=lambda: 150)
altered = deepcopy(envelope())
altered["request"][field] = value
with pytest.raises(UnauthorizedError):
ingress.handle(altered)
assert service.calls == []
@pytest.mark.parametrize("changes,error", [
({"issued_at": True}, UnauthorizedError),
({"issued_at": 151}, UnauthorizedError),
({"expires_at": 150}, UnauthorizedError),
({"expires_at": 401}, UnauthorizedError),
({"audience": "other-service"}, ForbiddenError),
({"nonce": "short"}, ValidationError),
({"path": "/v2/commit-files"}, ForbiddenError),
({"path": "/missions/a/../b", "method": "GET", "body": None,
"idempotency_key": None}, ForbiddenError),
({"idempotency_key": ""}, ValidationError),
])
def test_reject_expired_malformed_and_out_of_scope(changes, error):
service = Service()
ingress = SignedMissionIngress(service, Verifier(), clock=lambda: 150)
with pytest.raises(error):
ingress.handle(envelope(**changes))
assert not service.calls
def test_unknown_identity_fields_and_untrusted_signature_rejected():
service = Service()
ingress = SignedMissionIngress(service, Verifier(), clock=lambda: 150)
extra = envelope()
extra["owner_principal_id"] = "owner"
with pytest.raises(ValidationError):
ingress.handle(extra)
for field in ("key_id", "signature"):
bad = envelope()
bad[field] = "untrusted"
with pytest.raises(UnauthorizedError):
ingress.handle(bad)
assert not service.calls
def test_http_signed_route_and_duplicate_json_rejection():
service = Service()
server = HTTPServer(("127.0.0.1", 0), make_handler(
SignedMissionIngress(service, Verifier(), clock=lambda: 150)))
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
url = f"http://127.0.0.1:{server.server_port}/v1/signed-request"
req = urllib.request.Request(url, data=canonical_bytes(envelope()),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as response:
assert response.status == 201
assert json.load(response)["mission_id"] == "mission-authority-generated"
bad = urllib.request.Request(url, data=b'{"request":{},"request":{}}')
with pytest.raises(urllib.error.HTTPError) as exc:
urllib.request.urlopen(bad)
assert exc.value.code == 400
assert len(service.calls) == 1
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
@@ -0,0 +1,411 @@
from __future__ import annotations
import concurrent.futures
from copy import deepcopy
import hashlib
import json
import pytest
from app import aios_task_terminal_proof as proof_mod
from app import aios_task_mission_authority_server as server_mod
from app.task_plane.api import TaskPlaneService # noqa: E402
from app.task_plane.errors import ( # noqa: E402
ConflictError,
ForbiddenError,
UnauthorizedError,
ValidationError,
)
from app.task_plane.mission import MissionAuthority, MissionPrincipalBinding # noqa: E402
from app.task_plane.policy import IdentityVerifier, PolicyEngine # noqa: E402
from app.task_plane.store import TaskPlaneStore # noqa: E402
AuthenticatedTerminalProofProvider = proof_mod.AuthenticatedTerminalProofProvider
TerminalProofEvidenceVerifier = proof_mod.TerminalProofEvidenceVerifier
TerminalProofImporter = proof_mod.TerminalProofImporter
SignedMissionIngress = server_mod.SignedMissionIngress
canonical_bytes = server_mod.canonical_bytes
OWNER = "local-owner-authority"
OTHER = "other-owner-authority"
WORKER = "terminal-proof-worker-test"
WORKER_ID = "terminal-proof-worker-id-test"
CAPABILITIES = (
"code.read",
"code.write",
"test.execute",
"governed.write.file",
"governed.write.branch",
)
class PinnedAuthority:
"""Test-only stand-in for an existing configured authority verifier."""
def __init__(self, document):
self.document = document
self.repin()
self.reads = 0
@staticmethod
def digest(value):
return hashlib.sha256(
json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
def repin(self):
self.pinned = self.digest(self.document)
def read(self, task_id):
self.reads += 1
assert task_id == self.document["task_id"]
return deepcopy(self.document)
def verify(self, document):
return self.digest(document) == self.pinned
def _document(mission, child):
digest = "a" * 64
oid = "b" * 40
return {
"schema": proof_mod.PROOF_SCHEMA,
"task_id": child["task_id"],
"mission_id": mission["mission_id"],
"root_task_id": mission["root_task_id"],
"parent_task_id": mission["root_task_id"],
"acceptance_condition": "C01",
"agent_task_status": "SUCCEEDED",
"admission_status": "ADMITTED",
"execution_outcome": "APPLIED_VERIFIED",
"external_job_id": "job-authority-reserved-test",
"internal_job_id": "job-authority-internal-test",
"repository": "aios/aios",
"base_revision": "c" * 40,
"target_branch": "agent/task-identity-lineage/root/c01",
"generation": 1,
"execution_plan_digest": digest,
"task_principal_ref": "task-principal:test",
"task_lease_ref": "task-lease:test",
"writer_lease_ref": "writer-lease:test",
"governed_commit_oid": oid,
"authority_ref_oid": oid,
"postimage_hashes": {"active-runtime/app/example.py": "d" * 64},
"source_receipt_digests": {
source: hashlib.sha256(source.encode()).hexdigest()
for source in proof_mod.REQUIRED_SOURCES
},
"attestation_ref": "bridge-receipt:test",
"started_at": "2026-09-14T01:00:00Z",
"completed_at": "2026-09-14T01:02:00Z",
}
def _worker_payload(worker_id=WORKER_ID, identity=WORKER):
return {
"worker_id": worker_id,
"worker_type": "authority-proof-importer",
"identity": identity,
"owner_identity": identity,
"capabilities": list(CAPABILITIES),
"trust_zone": "isolated-development",
"max_concurrency": 4,
"heartbeat_interval_seconds": 15,
"supported_protocols": ["aios-task-v1"],
"actor_identity": identity,
}
def _fixture(tmp_path):
store = TaskPlaneStore(str(tmp_path / "terminal-proof.db"), high_assurance=True)
bindings = [
MissionPrincipalBinding(
authenticated_identity=identity,
owner_principal_id=identity,
acceptance_verifier_id="identity-lineage-live-closure-v1",
allowed_task_capabilities=CAPABILITIES,
max_risk="medium",
)
for identity in (OWNER, OTHER)
]
authority = MissionAuthority(store, principal_bindings=bindings)
policy = PolicyEngine(
identity_verifier=IdentityVerifier({OWNER, OTHER, WORKER}),
execution_enabled=True,
identity_capabilities={
OWNER: {"task.creator", "task.read", "task.delegate"},
OTHER: {"task.creator", "task.read", "task.delegate"},
WORKER: {"worker.self_register", "task.claim"},
},
)
service = TaskPlaneService(store, policy=policy, mission_authority=authority)
mission = service.create_mission(
{
"goal_slug": "terminal-proof-test",
"original_acceptance": {
"schema": "acceptance.v1",
"conditions": [{"key": "C01"}, {"key": "C06"}],
},
},
"mission-test",
actor_identity=OWNER,
)
child = service.delegate_mission(
mission["root_task_id"],
{
"task_type": "mission.acceptance",
"intent": "GOVERNED-CANDIDATE-COMMIT-READBACK",
"required_capabilities": list(CAPABILITIES),
"evidence_requirements": ["acceptance-proof"],
"risk": "medium",
"metadata": {
"acceptance_condition": "C01",
"production_side_effects": False,
},
},
"child-c01",
actor_identity=OWNER,
)
service.register_worker(_worker_payload())
pinned = PinnedAuthority(_document(mission, child))
provider = AuthenticatedTerminalProofProvider(
reader=pinned.read,
verifier=pinned.verify,
task_reader=store.get_task,
worker_identity=WORKER,
)
policy.evidence_verifier = TerminalProofEvidenceVerifier(provider)
policy.result_signature_verifier = provider.verify_result
importer = TerminalProofImporter(
service,
provider=provider,
worker_id=WORKER_ID,
worker_identity=WORKER,
)
return service, mission, child, pinned, importer
class OwnerVerifier:
signer_id = OWNER
def verify(self, *, namespace, payload, signature, key_id):
return (
namespace == server_mod.NAMESPACE
and key_id == "registered-test-key"
and signature == hashlib.sha256(payload).hexdigest()
)
class RecordingImporter:
def __init__(self):
self.calls = []
def reconcile(self, task_id, key, *, actor_identity):
self.calls.append((task_id, key, actor_identity))
return {"task_id": task_id}
def _envelope(task_id="task-existing-child", body=None, key="fixed-key"):
request = {
"schema": "aios.signed-mission-request.v1",
"audience": server_mod.AUDIENCE,
"method": "POST",
"path": f"/tasks/{task_id}/reconcile-terminal-proof",
"body": {} if body is None else body,
"idempotency_key": key,
"issued_at": 100,
"expires_at": 200,
"nonce": "terminal_nonce_123456789",
}
return {
"request": request,
"key_id": "registered-test-key",
"signature": hashlib.sha256(canonical_bytes(request)).hexdigest(),
}
def _key(importer, service, child):
return importer.expected_idempotency_key(service.store.get_task(child["task_id"]))
def test_signed_trigger_rejects_caller_proof_fields_and_bad_signature():
importer = RecordingImporter()
ingress = SignedMissionIngress(
object(), OwnerVerifier(), terminal_proof_importer=importer, clock=lambda: 150
)
forged = _envelope(body={"external_job_id": "caller-job", "status": "COMPLETED"})
with pytest.raises(ValidationError, match="empty body"):
ingress.handle(forged)
bad = _envelope()
bad["signature"] = "forged"
with pytest.raises(UnauthorizedError):
ingress.handle(bad)
assert importer.calls == []
def test_signed_trigger_injects_registered_owner_and_missing_provider_fails_closed():
importer = RecordingImporter()
ingress = SignedMissionIngress(
object(), OwnerVerifier(), terminal_proof_importer=importer, clock=lambda: 150
)
assert ingress.handle(_envelope()) == (200, {"task_id": "task-existing-child"})
assert importer.calls == [("task-existing-child", "fixed-key", OWNER)]
disabled = SignedMissionIngress(object(), OwnerVerifier(), clock=lambda: 150)
with pytest.raises(ForbiddenError, match="not configured"):
disabled.handle(_envelope())
@pytest.mark.parametrize(
"field,value,error",
[
("mission_id", "mission-other", "TaskPlane binding"),
("external_job_id", "job-mutated", "authentication"),
("agent_task_status", "VERIFYING", "not authoritatively successful"),
("authority_ref_oid", "e" * 40, "does not read back"),
],
)
def test_invalid_or_mutated_authority_proof_fails_before_claim(
tmp_path, field, value, error
):
service, _, child, pinned, importer = _fixture(tmp_path)
pinned.document[field] = value
if field != "external_job_id":
pinned.repin() # authenticated but semantically invalid document
with pytest.raises(ForbiddenError, match=error):
importer.reconcile(child["task_id"], _key(importer, service, child), actor_identity=OWNER)
assert service.store.active_claim(child["task_id"]) is None
assert service.store.get_task(child["task_id"]).status == "CREATED"
service.store.close()
@pytest.mark.parametrize(
"started,completed",
[
("2026-09-14T00:00:00.100Z", "2026-09-14T00:00:00Z"),
("2026-09-14T08:00:00.100+08:00", "2026-09-14T00:00:00Z"),
],
)
def test_fractional_proof_time_cannot_complete_before_start(tmp_path, started, completed):
service, _, child, pinned, importer = _fixture(tmp_path)
pinned.document.update(started_at=started, completed_at=completed)
pinned.repin()
with pytest.raises(ForbiddenError, match="time order"):
importer.reconcile(child["task_id"], _key(importer, service, child), actor_identity=OWNER)
assert service.store.active_claim(child["task_id"]) is None
service.store.close()
def test_cross_mission_and_changed_idempotency_are_rejected_without_claim(tmp_path):
service, _, child, _, importer = _fixture(tmp_path)
with pytest.raises(ForbiddenError):
importer.reconcile(child["task_id"], _key(importer, service, child), actor_identity=OTHER)
with pytest.raises(ValidationError, match="idempotency key mismatch"):
importer.reconcile(child["task_id"], "caller-selected-key", actor_identity=OWNER)
assert service.store.active_claim(child["task_id"]) is None
service.store.close()
def test_success_and_exact_replay_preserve_one_claim_review_and_receipt(tmp_path):
service, _, child, pinned, importer = _fixture(tmp_path)
key = _key(importer, service, child)
first = importer.reconcile(child["task_id"], key, actor_identity=OWNER)
reads_after_first = pinned.reads
replay = importer.reconcile(child["task_id"], key, actor_identity=OWNER)
assert replay == first
assert pinned.reads == reads_after_first + 1 # replay reauthenticates authority
assert service.store.get_task(child["task_id"]).status == "COMPLETED"
assert service.store.conn.execute(
"SELECT COUNT(*) FROM claims WHERE task_id=?", (child["task_id"],)
).fetchone()[0] == 1
assert service.store.conn.execute(
"SELECT COUNT(*) FROM task_terminal_proofs WHERE task_id=?", (child["task_id"],)
).fetchone()[0] == 1
assert service.store.conn.execute(
"SELECT COUNT(*) FROM task_root_reviews WHERE trigger_child_task_id=?",
(child["task_id"],),
).fetchone()[0] == 1
service.store.close()
def test_completed_replay_rejects_authenticated_authority_tuple_drift(tmp_path):
service, _, child, pinned, importer = _fixture(tmp_path)
key = _key(importer, service, child)
importer.reconcile(child["task_id"], key, actor_identity=OWNER)
pinned.document["internal_job_id"] = "job-different-internal-test"
pinned.repin()
with pytest.raises(ConflictError, match="receipt integrity"):
importer.reconcile(child["task_id"], key, actor_identity=OWNER)
assert service.store.conn.execute(
"SELECT COUNT(*) FROM claims WHERE task_id=?", (child["task_id"],)
).fetchone()[0] == 1
service.store.close()
@pytest.mark.parametrize("lost_call", ["claim", "complete"])
def test_response_loss_reconciles_same_claim_and_terminal_receipt(tmp_path, lost_call):
service, _, child, _, importer = _fixture(tmp_path)
original = getattr(service, lost_call)
lost = {"raised": False}
def response_lost(*args, **kwargs):
result = original(*args, **kwargs)
if not lost["raised"]:
lost["raised"] = True
raise ConnectionError(f"{lost_call} response lost")
return result
setattr(service, lost_call, response_lost)
receipt = importer.reconcile(
child["task_id"], _key(importer, service, child), actor_identity=OWNER
)
assert receipt["task_status"] == "COMPLETED"
assert lost["raised"] is True
assert service.store.conn.execute(
"SELECT COUNT(*) FROM claims WHERE task_id=?", (child["task_id"],)
).fetchone()[0] == 1
service.store.close()
def test_foreign_active_claim_is_never_adopted(tmp_path):
service, _, child, _, importer = _fixture(tmp_path)
foreign_id = "foreign-proof-worker-id"
foreign_identity = "foreign-proof-worker"
service.policy.identity_verifier.trusted_identities.add(foreign_identity)
service.policy.identity_capabilities[foreign_identity] = {
"worker.self_register", "task.claim"
}
service.register_worker(_worker_payload(foreign_id, foreign_identity))
service.claim(
child["task_id"],
{"worker_id": foreign_id, "actor_identity": foreign_identity, "lease_seconds": 300},
)
with pytest.raises(ConflictError, match="another execution binding"):
importer.reconcile(
child["task_id"], _key(importer, service, child), actor_identity=OWNER
)
assert service.store.get_task(child["task_id"]).status == "CLAIMED"
service.store.close()
def test_concurrent_replay_produces_one_claim_and_one_review(tmp_path):
service, _, child, _, importer = _fixture(tmp_path)
key = _key(importer, service, child)
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
results = list(
pool.map(
lambda _: importer.reconcile(child["task_id"], key, actor_identity=OWNER),
range(2),
)
)
assert results[0] == results[1]
assert service.store.conn.execute(
"SELECT COUNT(*) FROM claims WHERE task_id=?", (child["task_id"],)
).fetchone()[0] == 1
assert service.store.conn.execute(
"SELECT COUNT(*) FROM task_root_reviews WHERE trigger_child_task_id=?",
(child["task_id"],),
).fetchone()[0] == 1
service.store.close()
+51
View File
@@ -15,6 +15,12 @@ from aios_gitops_job_identity import ( # noqa: E402
legacy_task_storage_key,
task_storage_key,
)
from aios_gitops_job_controller import ( # noqa: E402
GitOpsJobController,
JobError,
JobStore,
sha256_json,
)
def test_collision_resistant_key_separates_historical_lossy_collision() -> None:
@@ -31,3 +37,48 @@ def test_collision_resistant_key_separates_historical_lossy_collision() -> None:
def test_task_storage_key_rejects_noncanonical_task_identity(value: object) -> None:
with pytest.raises(JobIdentityError, match="TASK_ID_INVALID"):
task_storage_key(value) # type: ignore[arg-type]
def test_internal_job_id_matches_persisted_submit_and_replay(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
request = {
"idempotency_key": "task-lineage-v4:bootstrap-1",
"campaign_id": "task-lineage-v4",
"task_id": "task-1",
"repository": "aios/aios",
"operation": "GOVERNED_TASK_CREATE",
"branch": "agent/task-1",
"expected_branch_oid": "a" * 40,
"payload_digest": "a" * 64,
}
controller = GitOpsJobController(
JobStore(tmp_path / "jobs", payload_root=tmp_path / "payloads")
)
monkeypatch.setattr(controller.store, "_mirror", lambda job: None)
submitted = controller.submit(request)
expected = "job-" + sha256_json({
"client_idempotency_key": request["idempotency_key"],
})[:32]
reopened = GitOpsJobController(JobStore.for_existing_append(tmp_path / "jobs"))
replayed = reopened.submit(request)
assert controller.internal_job_id_for_idempotency_key(request["idempotency_key"]) == expected
assert submitted.job_id == expected
assert replayed.job_id == expected
assert replayed.idempotency_key == submitted.idempotency_key
assert replayed.state is submitted.state
assert replayed.side_effect_state == "SAME_JOB"
assert replayed.http_status == submitted.http_status
assert reopened.store.read(expected)["idempotency_key"] == submitted.idempotency_key
@pytest.mark.parametrize(
"value",
[None, 123, "", " leading", "trailing ", "contains/slash", "x" * 129],
)
def test_internal_job_id_rejects_invalid_client_idempotency_key(value: object) -> None:
with pytest.raises(JobError, match="IDEMPOTENCY_KEY_INVALID"):
GitOpsJobController.internal_job_id_for_idempotency_key(value) # type: ignore[arg-type]
+184
View File
@@ -0,0 +1,184 @@
"""G7 security gate S1 — negative tests for auth/authorization fixes.
Each test asserts that a client-controlled header/body/query value CANNOT
elevate role or impersonate another user, and that owner-only actions are
denied for non-owner principals.
"""
import importlib.util
import os
import sys
import tempfile
from pathlib import Path
import pytest
# Modules under test (active-runtime package uses relative imports, so we
# import through the active_runtime package like the rest of the suite).
from active_runtime.app import agent_observability_api as obs_api
from active_runtime.app import task_action_api as task_api
# ---------------------------------------------------------------------------
# (B) task_action_api: body role must be ignored
# ---------------------------------------------------------------------------
def test_json_role_owner_is_ignored_for_anonymous():
task_api.set_authenticator(lambda payload: None) # anonymous
task = {"task_id": "t1", "status": "waiting_approval"}
# request body claims role=owner, but the resolver returns "user"
assert task_api.effective_role({"role": "owner"}) == "user"
def test_owner_action_denied_when_body_claims_owner():
task_api.set_authenticator(lambda payload: None) # anonymous -> user
task = {"task_id": "t1", "status": "waiting_approval"}
# body sneaks role=owner; approve requires owner -> PermissionError
role = task_api.effective_role({"role": "owner"})
assert role == "user"
with pytest.raises(PermissionError):
task_api.execute_task_action(task, "approve", role, "please")
def test_authenticator_overrides_body_role():
# Server-side principal says user; body claims owner -> still user.
task_api.set_authenticator(lambda payload: {"role": "user"})
assert task_api.effective_role({"role": "owner"}) == "user"
with pytest.raises(PermissionError):
task_api.execute_task_action(
{"task_id": "t1", "status": "waiting_approval"}, "approve",
task_api.effective_role({"role": "owner"}), "x",
)
# ---------------------------------------------------------------------------
# (C) agent_observability_api: owner role / foreign user-group must not leak
# ---------------------------------------------------------------------------
def test_observability_role_owner_does_not_escalate_for_anonymous():
obs_api.set_authenticator(lambda: None) # anonymous
with tempfile.TemporaryDirectory() as d:
# plant a foreign record
from active_runtime.app.agent_observability import AgentExecutionRecord
rec = AgentExecutionRecord(
execution_id="e-foreign", user_id="victim", visible_scope="grp-a",
entry_agent="openclaw", path=[], used_hermes=False, closed_by="victim",
)
# direct store injection to isolate this store instance
old = obs_api.store
obs_api.store = type("S", (), {
"query": lambda self, u, g, r: [] if r != "owner" else [rec.to_dict()],
"load": lambda self: type("L", (), {
"records": [rec], "summary": lambda self, u=None: {"total": 1}
})(),
"append": lambda self, x: x.to_dict(),
})()
try:
out = obs_api.get_agent_observability(user_id="victim", group_id="grp-a", role="owner")
# anonymous -> no scope -> query runs with role "user"/no scope -> 0 records
assert out["records"] == []
finally:
obs_api.store = old
def test_observability_non_owner_cannot_read_another_user():
obs_api.set_authenticator(lambda: {"user_id": "alice", "role": "user"})
with tempfile.TemporaryDirectory():
old = obs_api.store
captured = {}
class FakeLoad:
def __init__(self, recs): self.records = recs
def summary(self, user_id=None):
captured["summary_scope"] = user_id
return {}
rec = type("R", (), {"to_dict": lambda self: {
"user_id": "victim", "visible_scope": "grp-a", "execution_id": "e1"}})()
obs_api.store = type("S", (), {
"query": lambda self, u, g, r: captured.update({"q_user": u, "q_group": g, "q_role": r}) or [],
"load": lambda self: FakeLoad([rec]),
"append": lambda self, x: x.to_dict(),
})()
try:
out = obs_api.get_agent_observability(user_id="victim", group_id="grp-a", role="owner")
# client demanded role=owner + victim scope -> must be scoped to alice
assert captured["q_user"] == "alice"
assert captured["q_role"] == "user"
assert captured["summary_scope"] == "alice"
finally:
obs_api.store = old
obs_api.set_authenticator(None)
# ---------------------------------------------------------------------------
# (A) gitops model api: X-AIOS-Role / X-AIOS-User spoofing
# ---------------------------------------------------------------------------
def _load_gitops_api(path):
spec = importlib.util.spec_from_file_location(
"gitops_api_neg_" + str(abs(hash(path))), path)
mod = importlib.util.module_from_spec(spec)
sys.modules[mod.__name__] = mod
parent = str(Path(path).parent)
try:
if parent not in sys.path:
sys.path.insert(0, parent)
spec.loader.exec_module(mod)
finally:
if parent in sys.path:
sys.path.remove(parent)
return mod
@pytest.mark.parametrize("path", [
"adapters/gitops/aios_gitops_model_api.py",
"active-runtime/app/aios_gitops_model_api.py",
])
def test_x_aios_role_spoof_does_not_elevate(path):
base = Path(__file__).resolve().parents[1]
mod = _load_gitops_api(str(base / path))
with tempfile.TemporaryDirectory() as d:
mod.TOKEN_FILE = Path(d) / "tokens"
mod.TOKEN_FILE.write_text("plain-token\n", encoding="utf-8")
mod.DEV_ALLOW = False
h = type("H", (), {
"headers": {
"Authorization": "Bearer plain-token",
"X-AIOS-Role": "owner", # client spoof
"X-AIOS-User": "root", # client spoof
}
})()
a = mod.Handler.actor(h)
assert a["token_ok"] is True
# spoofed role never applied; plain token stays role 'user'
assert a["role"] != "owner"
assert a["username"] != "root"
assert a["role"] == "user"
def test_require_write_owner_returns_403_for_non_owner_token():
base = Path(__file__).resolve().parents[1]
mod = _load_gitops_api(str(base / "adapters/gitops/aios_gitops_model_api.py"))
with tempfile.TemporaryDirectory() as d:
mod.TOKEN_FILE = Path(d) / "tokens"
mod.TOKEN_FILE.write_text("plain-token\n", encoding="utf-8")
mod.DEV_ALLOW = False
responses = []
class H(mod.Handler):
def send_json(self, data, code=200):
responses.append((data, code))
h = H.__new__(H)
h.headers = {"Authorization": "Bearer plain-token", "X-AIOS-Role": "owner"}
a = mod.Handler.require_write(h, ["owner"])
assert a is None
assert responses and responses[-1][1] == 403
def test_owner_token_mapping_allows_owner():
base = Path(__file__).resolve().parents[1]
mod = _load_gitops_api(str(base / "adapters/gitops/aios_gitops_model_api.py"))
with tempfile.TemporaryDirectory() as d:
mod.TOKEN_FILE = Path(d) / "tokens"
mod.TOKEN_FILE.write_text("admin-token:ops:owner\n", encoding="utf-8")
mod.DEV_ALLOW = False
h = type("H", (), {"headers": {"Authorization": "Bearer admin-token",
"X-AIOS-Role": "user"}})()
a = mod.Handler.actor(h)
# server-side mapping is authoritative, not the client header
assert a["role"] == "owner"
assert a["username"] == "ops"
+104
View File
@@ -0,0 +1,104 @@
"""G7 安全关卡 S2 · SEC-P0-BOOTSTRAP-CREDENTIAL 负面测试。
覆盖:
- 已知 bootstrap 口令 `bootstrap-change-me` 拒绝登录(空种子 + 明文种子都不接受)
- 密码哈希不等于明文 / 非确定性(随机盐,两次哈希不同)
- 生产模式缺 SECRET 拒绝启动(fail closed)
- 会话令牌过期后失效(parse_session 校验过期时间戳)
"""
import hashlib
import hmac
import os
import subprocess
import sys
import time
APP_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
'active-runtime', 'app',
)
if APP_DIR not in sys.path:
sys.path.insert(0, APP_DIR)
# 必须先设置,否则模块 import 即 fail-closed(SystemExit)。
os.environ.setdefault('AIOS_WEBOPS_SECRET', 'g7-test-secret-not-for-production')
os.environ.setdefault('AIOS_WEBOPS_DB', ':memory:')
import aios_webops_app as m
import aios_v5_webops_app as m5
def test_bootstrap_password_rejected():
# 空(强制重置)种子绝不接受已知 bootstrap 口令
assert m.check_pw('', 'bootstrap-change-me') is False
assert m5.check_pw('', 'bootstrap-change-me') is False
# 历史明文种子原样入库时,绝不作为可用凭据
assert m.check_pw('bootstrap-change-me', 'bootstrap-change-me') is False
assert m5.check_pw('bootstrap-change-me', 'bootstrap-change-me') is False
# 留着旧哈希的 legacy 也能过 bootstrap 校验 -> 必须拒绝
legacy = hashlib.sha256((m.SECRET + '|bootstrap-change-me').encode()).hexdigest()
assert m.check_pw(legacy, 'bootstrap-change-me') is False
assert m5.check_pw(legacy, 'bootstrap-change-me') is False
def test_hash_not_plaintext_and_random_salt():
h1 = m.hash_pw('s3cret!pw')
assert h1 != 's3cret!pw'
assert h1.startswith('pbkdf2$')
# 同一口令两次哈希因随机盐不同 -> 非确定性无盐快哈希
h2 = m.hash_pw('s3cret!pw')
assert h1 != h2
# 校验通过,且错误口令 / 明文直存均被拒
assert m.check_pw(h1, 's3cret!pw') is True
assert m.check_pw(h2, 's3cret!pw') is True
assert m.check_pw(h1, 'wrong') is False
assert m.check_pw('s3cret!pw', 's3cret!pw') is False # stored 明文 -> 拒绝
# v5 一致
vh = m5.hash_pw('s3cret!pw')
assert vh != 's3cret!pw'
assert m5.check_pw(vh, 's3cret!pw') is True
assert m5.check_pw('s3cret!pw', 's3cret!pw') is False
def test_missing_secret_fails_closed():
env = dict(os.environ)
env.pop('AIOS_WEBOPS_SECRET', None)
for mod in ('aios_webops_app', 'aios_v5_webops_app'):
code = (
"import os,sys;"
"os.environ.pop('AIOS_WEBOPS_SECRET',None);"
"sys.path.insert(0, r'%s');"
"import %s" % (APP_DIR, mod)
)
r = subprocess.run([sys.executable, '-c', code],
capture_output=True, text=True, env=env)
assert r.returncode != 0, f'{mod} 缺 SECRET 竟启动成功'
assert 'AIOS_WEBOPS_SECRET' in (r.stderr + r.stdout), \
f'{mod} fail-closed 未指明 SECRET'
def _session_header(mod, token):
return f'aios_session={token}'
def test_session_token_expiry():
fake = {'username': 'owner', 'role': 'owner', 'status': 'approved'}
orig = m.one
m.one = lambda sql, args=(): fake
try:
# 过期但签名正确的令牌 -> 失效
exp_past = int(time.time()) - 100
data = f'owner|owner|{exp_past}'
sig = hmac.new(m.SECRET.encode(), data.encode(), hashlib.sha256).hexdigest()
assert m.parse_session(_session_header(m, data + '|' + sig)) is None
# 新鲜令牌 -> 生效
fresh = m.token('owner', 'owner')
assert m.parse_session(_session_header(m, fresh)) == fake
# 篡改签名 -> 失效
parts = fresh.split('|')
bad = parts[0] + '|' + parts[1] + '|' + parts[2] + '|deadbeef'
assert m.parse_session(_session_header(m, bad)) is None
finally:
m.one = orig

Some files were not shown because too many files have changed in this diff Show More