Compare commits

..
Author SHA1 Message Date
openclaw b90b0b9d26 fix: update baseline git hash to latest and fix docs/releases ownership to aiman 2026-07-11 02:33:16 +08:00
openclaw a32c76a3c1 V1R4C5 baseline promotion: V1R4C3 -> V1R4C5
promoted_by: openclaw/admin

promotion_reason: V1R4C5 reliability, watcher cleanup, user data isolation gate, and admin workbench read-only preparation; memory search remains disabled

constraints: memory_search_enabled=false, force_push_allowed=false
2026-07-11 02:19:22 +08:00
openclaw c128d6b01d release: complete V1R4C5 metadata on final approval branch 2026-07-11 02:10:39 +08:00
openclaw d457ac3961 release: record final owner approval for V1R4C5 2026-07-11 01:58:14 +08:00
openclaw 9fa7511bb5 release: add V1R4C5 acceptance evidence 2026-07-11 01:56:45 +08:00
337 changed files with 3642 additions and 5254 deletions
@@ -1,38 +0,0 @@
status: PASS
integration_path: tests/integration/test_runtime_ingress_flow.py
tests: pytest tests/v2r1 = 29 passed in 0.98s; pytest tests/integration = 3 passed in 0.18s
runtime_flow: PASS - signed public ingress -> IngressAuthenticator -> RuntimeIdentityResolver -> RuntimeWiring.ingest -> ChannelAdapter -> MessageLedger.append -> SessionPool
commit: a6878d30b8690e55f1bf9819da5aac189dabf89e
gate1: BLOCKED
risk: high
# Result
## Changed Files
- `tests/integration/test_runtime_ingress_flow.py`
- `.agent/results/codex/RESULT-V2R1-202607132000-CODEX.md`
## RED Evidence
The baseline integration entrypoint from `f11513e9d7326caa827e59f4f10f07681633dfc0` was inadequate: its successful flow replaced `MessageLedger.remember()` and its alleged duplicate conflict changed the message ID. The quality checks failed before this update because neither test proved the required runtime contract.
## Verification
- The success path sends a real signed public ingress request and uses a `sys.setprofile` observer against the real `MessageLedger.remember` code object. It preserves the production object and all behavior, then asserts zero legacy calls.
- The duplicate path sends two authenticated requests under the same principal, tenant, cell, channel, account, peer, direction, session, and `message_id`; only `event_id` and body differ. The second response must be `CONFLICT`, with exactly one ledger record and one session lease remaining.
## Commands
```text
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/v2r1 -q
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/integration -q
```
## Gate 1
`BLOCKED`. This task makes the runtime integration entrypoint meaningful, but does not close the independent Gate 1 matrix, authenticated production OpenClaw registration, secret scanning, second clean-room review, or staging rollback rehearsal.
## Recommendation
Keep this isolated evidence commit for independent review. Do not merge, tag, or deploy until all Gate 1 requirements have independent evidence.
@@ -1,95 +0,0 @@
status: PASS
commit: 7b82ae11f685b77628308ba46b9cfdf5bcdf78d3
tests:
v2r1: "29 passed, exit code 0"
integration: "3 passed, exit code 0"
cleanup_scan:
status: PASS
findings: "No remaining conflict markers, unexplained worktree changes, unapproved temporary/cache/build artifacts, or scanner-detected secrets. Runtime SQLite was removed and its sidecars are ignored with exact root-anchored rules; tracked dist/ZIP artifacts are documented exceptions."
risk: MEDIUM
recommendation: "Push the controlled work branch, then request independent OpenClaw Gate Review; do not merge or release from this result alone."
# V2R1 Gate 1 Codex Cleanup Result
## Scope and Git Baseline
- Task: `V2R1-G1-CODEX-CLEANUP-20260713`
- Branch: `work/v2r1-g1-cleanup-remediation-20260713`
- Cleanup implementation commit: `7b82ae11f685b77628308ba46b9cfdf5bcdf78d3`
- Integration commit: `a6878d30b8690e55f1bf9819da5aac189dabf89e`
- Locked integration result baseline: `d6ba7e8fa859407ec55acf49b5468bba5eac96da`
- `git merge-base --is-ancestor a6878d30b8690e55f1bf9819da5aac189dabf89e HEAD`: exit code `0`
- `git merge-base --is-ancestor d6ba7e8fa859407ec55acf49b5468bba5eac96da HEAD`: exit code `0`
- Tree before this result-only commit: `94146e05f700921b12ef023eb4d8be771bae4047`
The locked integration artifacts were not modified by cleanup:
- `tests/integration/test_runtime_ingress_flow.py`: blob `6b6732a4c73bbb7fb5273a05b897adce9da39a9a`
- `.agent/results/codex/RESULT-V2R1-202607132000-CODEX.md`: blob `e2d6ce3b037339385831f51cb0db42c86b2ae827`
## Cleanup Changes Verified
Commit `7b82ae11f685b77628308ba46b9cfdf5bcdf78d3`:
- removed the tracked runtime database `data/webops.sqlite3`;
- added exact root-anchored ignore rules for `/data/webops.sqlite3`, `/data/webops.sqlite3-wal`, `/data/webops.sqlite3-shm`, and `/data/webops.sqlite3-journal`;
- resolved tracked merge-conflict residue in `agents/registry.yaml` and `docs/architecture/AIOS_AGENT_AUDIT_PLANE.md`;
- left the locked integration test and prior Codex result blobs unchanged.
`git grep -n -E '^(<<<<<<<|=======|>>>>>>>)' -- . ':!source-baseline/*.zip'` returned no matches (exit code `1`, the expected no-match result).
The repository scan found no unapproved `__pycache__`, `*.pyc`, `*.pyo`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `*.tmp`, `*.swp`, backup-suffix, or `.DS_Store` artifacts after verification cleanup. The following tracked artifacts are expected baseline exceptions, not unexplained generated files:
- `openclaw/plugins/aios-tenant-boundary/dist/` (2 tracked plugin distribution files);
- `source-baseline/current_actual_deployment_source.zip`;
- `source-baseline/previous_current_actual_deployment_source_from_webops_delta.zip`.
Before creating this result file, `git status --porcelain=v1 --untracked-files=all` was empty with exit code `0`.
## Secret Scan
Scanner: Gitleaks `8.30.1`, official Linux x64 release asset.
- Asset: `/tmp/gitleaks_8.30.1_linux_x64.tar.gz.codex`
- SHA-256: `551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb`
- `/tmp/gitleaks-codex-8.30.1/gitleaks git --no-banner --no-color --redact --report-format json --report-path /tmp/v2r1-cleanup-audit-git.json --log-opts=7b82ae11f685b77628308ba46b9cfdf5bcdf78d3 /data/aios-gitops/worktrees/v2r1-g1-cleanup-remediation-20260713`: exit code `0`; Gitleaks reported `312 commits scanned`, approximately `1,384,427 bytes (1.38 MB)` scanned, and `no leaks found` (`0` findings in the JSON report).
- `git rev-list --count 7b82ae11f685b77628308ba46b9cfdf5bcdf78d3`: exit code `0`, `327` reachable commits. This Git reachability count is recorded separately and is not the Gitleaks scanned-commit count.
- `/tmp/gitleaks-codex-8.30.1/gitleaks dir --no-banner --no-color --redact --report-format json --report-path /tmp/v2r1-cleanup-audit-tree.json /data/aios-gitops/worktrees/v2r1-g1-cleanup-remediation-20260713`: exit code `0`; Gitleaks reported approximately `1,214,737 bytes (1.21 MB)` scanned and `no leaks found` (`0` findings in the JSON report).
No secret content is reproduced in this report.
## Tests
Command:
```bash
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/v2r1
```
Output summary:
```text
collected 29 items
29 passed in 0.97s
exit code: 0
```
Command:
```bash
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/integration
```
Output summary:
```text
collected 3 items
3 passed in 0.18s
exit code: 0
```
Environment: Linux, Python `3.12.3`, pytest `9.1.1`, pluggy `1.6.0`.
## Disposition
The cleanup and requested test verification pass at the stated implementation commit. Risk remains `MEDIUM` until the controlled branch is pushed and independently fetched and reviewed by OpenClaw. This result does not authorize merge, tag creation, baseline update, staging, or production deployment.
@@ -1,124 +0,0 @@
---
status: PASS
commit: 502ef30642597fb0b70b90ae2e8a0b6af217d80e
task_id: TASK-CODEX-V2R1-G1-FINAL-VERIFY-RETRY-20260715T010807UTC8
executor: CODEX_CANDIDATE_BUILDER
recovery_iteration: r2
rejected_candidate: 78c22cbfb95cb252b671c5ffe3b34bf4944aee2d
source_commit: 502ef30642597fb0b70b90ae2e8a0b6af217d80e
source_branch: work/v2r1-integration-quality-2000
candidate_ref: work/codex/v2r1-g1-recovery-candidate-20260715T010807UTC8-r2
tests:
status: PASS
v2r1:
command: "PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPTS='-p no:cacheprovider' PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/v2r1 -q"
exit_code: 0
passed: 29
summary: 29 passed in 0.95s
integration:
command: "PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPTS='-p no:cacheprovider' PYTHONPATH=active-runtime/app /usr/bin/python3 -m pytest tests/integration -q"
exit_code: 0
passed: 3
summary: 3 passed in 0.19s
commands:
- id: v2r1_tests
exit_code: 0
- id: integration_tests
exit_code: 0
- id: source_object_conflict_marker_scan
exit_code: 1
accepted_exit_code: 1
- id: tracked_sqlite_object_scan
command: "git ls-tree -r --name-only 502ef30642597fb0b70b90ae2e8a0b6af217d80e -- data/webops.sqlite3 data/webops.sqlite3-wal data/webops.sqlite3-shm data/webops.sqlite3-journal"
exit_code: 0
- id: ordered_webops_ignore_rule_scan
exit_code: 0
- id: gitleaks_version
binary: /tmp/gitleaks-codex-8.30.1/gitleaks
exit_code: 0
- id: gitleaks_git_source_object_scan
exit_code: 0
- id: gitleaks_directory_scan
exit_code: 0
- id: python_ast_key_length_scan
exit_code: 0
conflict_scan:
status: PASS
command_name: source_object_conflict_marker_scan
source_object: 502ef30642597fb0b70b90ae2e8a0b6af217d80e
exit_code: 1
stdout_lines: 0
conflict_count: 0
tracked_sqlite:
status: PASS
exit_code: 0
count: 0
checked_paths:
- data/webops.sqlite3
- data/webops.sqlite3-wal
- data/webops.sqlite3-shm
- data/webops.sqlite3-journal
webops_ignore_rules:
status: PASS
command_name: ordered_webops_ignore_rule_scan
count: 4
ordered:
- /data/webops.sqlite3
- /data/webops.sqlite3-wal
- /data/webops.sqlite3-shm
- /data/webops.sqlite3-journal
gitleaks:
status: PASS
version: "8.30.1"
git:
command_name: gitleaks_git_source_object_scan
source_object: 502ef30642597fb0b70b90ae2e8a0b6af217d80e
exit_code: 0
findings: 0
report_json: []
dir:
command_name: gitleaks_directory_scan
exit_code: 0
findings: 0
report_json: []
hmac:
status: PASS
method: AST assignment inspection
target: tests/v2r1/test_runtime_wiring.py
command_name: python_ast_key_length_scan
exit_code: 0
internal_key_length: 39
ingress_key_length: 39
placeholder_scan:
status: PASS
token_count: 0
angle_count: 0
total_count: 0
placeholder_count: 0
mandatory_fields:
status: PASS
present:
- status
- commit
- tests
- cleanup_scan
- risk
- recommendation
cleanup_scan:
status: PASS
details:
temporary_reports_outside_worktree: true
temporary_reports_removed: true
unexpected_workspace_artifacts: 0
secret_scan: PASS
rollback:
status: PASS
required: false
deployment: false
merge: false
tag: false
baseline: false
workspace_clean_at_test_completion: true
risk: LOW
recommendation: Advance this r2 candidate commit to independent verification only; do not deploy, merge, tag, or alter baselines.
---
-4
View File
@@ -11,7 +11,3 @@ runtime/tmp/
runtime/cache/
logs/
config/test_write_perms
/data/webops.sqlite3
/data/webops.sqlite3-wal
/data/webops.sqlite3-shm
/data/webops.sqlite3-journal
+1 -1
View File
@@ -1 +1 @@
V1R4C6
V1R4C3
-89
View File
@@ -1,89 +0,0 @@
"""Append-only audit chain for AIOS V2R1."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import sqlite3
from typing import Any
from .tenant_context import canonical_json, isoformat, require_ident
class AuditLog:
def __init__(self, database: str | Path) -> None:
self.database = str(database)
with sqlite3.connect(self.database) as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute(
"""
CREATE TABLE IF NOT EXISTS audit_events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
tenant_id TEXT NOT NULL,
trace_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
previous_hash TEXT NOT NULL,
event_hash TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL
)
"""
)
def append(self, *, tenant_id: str, trace_id: str, event_type: str, payload: dict[str, Any]) -> str:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
trace_id = require_ident("trace_id", trace_id)
event_type = require_ident("event_type", event_type)
with sqlite3.connect(self.database) as db:
previous = db.execute(
"SELECT event_hash FROM audit_events ORDER BY seq DESC LIMIT 1"
).fetchone()
previous_hash = previous[0] if previous else "GENESIS"
body = {
"tenant_id": tenant_id,
"trace_id": trace_id,
"event_type": event_type,
"payload": payload,
"previous_hash": previous_hash,
"created_at": isoformat(),
}
digest = hashlib.sha256(
canonical_json(body)
).hexdigest()
db.execute(
"INSERT INTO audit_events VALUES(NULL,?,?,?,?,?,?,?)",
(
tenant_id,
trace_id,
event_type,
json.dumps(payload, ensure_ascii=False),
previous_hash,
digest,
body["created_at"],
),
)
return digest
def verify(self) -> bool:
previous = "GENESIS"
with sqlite3.connect(self.database) as db:
rows = db.execute(
"SELECT tenant_id,trace_id,event_type,payload,previous_hash,event_hash,created_at "
"FROM audit_events ORDER BY seq"
)
for row in rows:
body = {
"tenant_id": row[0],
"trace_id": row[1],
"event_type": row[2],
"payload": json.loads(row[3]),
"previous_hash": row[4],
"created_at": row[6],
}
if row[4] != previous:
return False
digest = hashlib.sha256(canonical_json(body)).hexdigest()
if digest != row[5]:
return False
previous = digest
return True
@@ -1,73 +0,0 @@
"""AIOS V2R1 Tenant Cell lifecycle CLI primitives."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import json
from pathlib import Path
from .tenant_context import ValidationError, require_ident
@dataclass(frozen=True)
class CellRecord:
tenant_id: str
cell_id: str
version: str
state: str
config_digest: str
created_at: str
class CellManager:
def __init__(self, registry: str | Path) -> None:
self.registry = Path(registry)
self.registry.parent.mkdir(parents=True, exist_ok=True)
if not self.registry.exists():
self.registry.write_text("{\"cells\":{}}\n", encoding="utf-8")
def _load(self) -> dict:
return json.loads(self.registry.read_text(encoding="utf-8"))
def _save(self, value: dict) -> None:
self.registry.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
def create(self, tenant_id: str, cell_id: str, version: str, digest: str) -> CellRecord:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
cell_id = require_ident("cell_id", cell_id)
data = self._load()
if tenant_id in data["cells"]:
raise ValidationError("tenant already has a cell")
record = CellRecord(
tenant_id,
cell_id,
version,
"created",
digest,
datetime.now(timezone.utc).isoformat(),
)
data["cells"][tenant_id] = record.__dict__
self._save(data)
return record
def status(self, tenant_id: str) -> CellRecord:
data = self._load()
item = data["cells"].get(tenant_id)
if not item:
raise ValidationError("unknown tenant")
return CellRecord(**item)
def transition(self, tenant_id: str, state: str) -> CellRecord:
allowed = {"created", "starting", "healthy", "upgrading", "rollback", "failed"}
if state not in allowed:
raise ValidationError("invalid cell state")
data = self._load()
item = data["cells"].get(tenant_id)
if not item:
raise ValidationError("unknown tenant")
item["state"] = state
self._save(data)
return CellRecord(**item)
@@ -1,64 +0,0 @@
"""The only V2R1 ingress adapter: verified context to routed ledger record."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from threading import RLock
from typing import Callable, Mapping, Any
from .message_ledger import LedgerRecord, MessageLedger
from .router import Route, TenantRouter
from .session_pool import SessionLease, SessionPool
from .tenant_context import CanonicalMessage, TenantContext, utc_now
@dataclass(frozen=True)
class IngressResult:
record: LedgerRecord
route: Route
session: SessionLease
class ChannelAdapter:
"""Fail-closed runtime ingress composition.
The adapter intentionally accepts no tenant or Cell authority outside the
signed context and TenantRouter. A process lock keeps rollback handling
deterministic for a single runtime while SQLite protects cross-process
ledger writes.
"""
def __init__(
self,
*,
router: TenantRouter,
ledger: MessageLedger,
session_pool: SessionPool,
signing_key: bytes,
clock: Callable[[], datetime] = utc_now,
) -> None:
self._router = router
self._ledger = ledger
self._session_pool = session_pool
self._signing_key = signing_key
self._clock = clock
self._lock = RLock()
def ingest(self, *, token: str, message: Mapping[str, Any]) -> IngressResult:
with self._lock:
context = TenantContext.verify(token, self._signing_key, now=self._clock())
canonical = CanonicalMessage.from_mapping(message)
canonical.assert_context(context)
route = self._router.assert_context_route(context)
self._router.assert_message_route(canonical)
self._session_pool.assert_available(context, route)
existed = self._ledger.matches(canonical)
record = self._ledger.append(canonical)
try:
lease = self._session_pool.acquire(context, route)
except Exception:
if not existed:
self._ledger.remove(record)
raise
return IngressResult(record=record, route=route, session=lease)
-48
View File
@@ -1,48 +0,0 @@
"""aios-cell command implementation for V2R1 Cell lifecycle."""
from __future__ import annotations
import argparse
import json
import sys
from .cell_manager import CellManager
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="aios-cell")
parser.add_argument("registry")
sub = parser.add_subparsers(dest="command", required=True)
create = sub.add_parser("create")
create.add_argument("tenant_id")
create.add_argument("cell_id")
create.add_argument("version")
create.add_argument("digest")
status = sub.add_parser("status")
status.add_argument("tenant_id")
transition = sub.add_parser("transition")
transition.add_argument("tenant_id")
transition.add_argument("state")
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
manager = CellManager(args.registry)
if args.command == "create":
result = manager.create(args.tenant_id, args.cell_id, args.version, args.digest)
elif args.command == "status":
result = manager.status(args.tenant_id)
elif args.command == "transition":
result = manager.transition(args.tenant_id, args.state)
else:
return 2
print(json.dumps(result.__dict__, ensure_ascii=False, sort_keys=True))
return 0
if __name__ == "__main__":
sys.exit(main())
-106
View File
@@ -1,106 +0,0 @@
"""OpenClaw Cell lifecycle adapter for AIOS V2R1."""
from __future__ import annotations
from dataclasses import asdict, dataclass, field
import json
from pathlib import Path
import subprocess
from typing import Any, Mapping
from .tenant_context import ValidationError, V2R1Error, require_ident
@dataclass(frozen=True)
class CellSpec:
tenant_id: str
image: str
runtime: str = "docker"
port: int | None = None
memory: str = "2g"
cpus: str = "2"
pids_limit: int = 512
network: str = "bridge"
env: tuple[str, ...] = field(default_factory=tuple)
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
if self.runtime not in {"docker", "podman"}:
raise ValidationError("unsupported runtime")
if not self.image:
raise ValidationError("image required")
class OpenClawFleet:
def __init__(self, binary: str = "openclaw") -> None:
self.binary = binary
def _run(self, args: list[str]) -> dict[str, Any]:
proc = subprocess.run(
[self.binary, *args],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if proc.returncode:
raise V2R1Error(proc.stderr.strip() or "openclaw command failed")
data = json.loads(proc.stdout or "{}")
if isinstance(data, dict):
return data
return {"result": data}
def create(self, spec: CellSpec) -> dict[str, Any]:
return self._run([
"fleet", "create", spec.tenant_id,
"--image", spec.image,
"--runtime", spec.runtime,
"--json",
])
def status(self, tenant_id: str) -> dict[str, Any]:
require_ident("tenant_id", tenant_id, tenant=True)
return self._run(["fleet", "status", tenant_id, "--json"])
def doctor(self, tenant_id: str | None = None) -> dict[str, Any]:
args = ["fleet", "doctor"]
if tenant_id:
require_ident("tenant_id", tenant_id, tenant=True)
args.append(tenant_id)
args.append("--json")
return self._run(args)
def backup(self, tenant_id: str, output: str | Path) -> dict[str, Any]:
return self._run(["fleet", "backup", tenant_id, "--out", str(output), "--json"])
def restore(self, tenant_id: str, archive: str | Path) -> dict[str, Any]:
return self._run(["fleet", "restore", tenant_id, "--from", str(archive), "--json"])
def upgrade(self, tenant_id: str, image: str) -> dict[str, Any]:
return self._run(["fleet", "upgrade", tenant_id, "--image", image, "--json"])
def load_inventory(path: str | Path) -> list[CellSpec]:
raw = json.loads(Path(path).read_text(encoding="utf-8"))
if raw.get("schema_version") != "aios.cells.v1":
raise ValidationError("invalid inventory schema")
result: list[CellSpec] = []
seen: set[str] = set()
for item in raw.get("cells", []):
spec = CellSpec(
tenant_id=item["tenant_id"],
image=item["image"],
runtime=item.get("runtime", "docker"),
port=item.get("port"),
env=tuple(item.get("env", [])),
)
if spec.tenant_id in seen:
raise ValidationError("duplicate tenant")
seen.add(spec.tenant_id)
result.append(spec)
if not result:
raise ValidationError("no cells")
return result
def inventory_as_dict(items: list[CellSpec]) -> dict[str, Any]:
return {"schema_version": "aios.cells.v1", "cells": [asdict(x) for x in items]}
@@ -1,143 +0,0 @@
"""Tenant-partitioned Hermes shared agent fabric primitives."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from contextlib import closing
from pathlib import Path
import sqlite3
from typing import Any
from .tenant_context import (
CanonicalMessage,
ConflictError,
ScopeError,
ValidationError,
isoformat,
require_ident,
)
@dataclass(frozen=True)
class FabricJob:
job_id: str
tenant_id: str
cell_id: str
profile_id: str
session_id: str
event_id: str
status: str = "queued"
def __post_init__(self) -> None:
for name in (
"job_id", "cell_id", "profile_id", "session_id", "event_id"
):
require_ident(name, getattr(self, name))
require_ident("tenant_id", self.tenant_id, tenant=True)
if self.status not in {"queued", "running", "completed", "failed", "cancelled"}:
raise ValidationError("invalid job status")
class HermesFabric:
"""Shared scheduler storage with mandatory tenant partition keys."""
def __init__(self, database: str | Path) -> None:
self.database = str(database)
with closing(sqlite3.connect(self.database)) as connection, connection as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute(
"""
CREATE TABLE IF NOT EXISTS fabric_jobs (
job_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
profile_id TEXT NOT NULL,
session_id TEXT NOT NULL,
event_id TEXT NOT NULL UNIQUE,
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
"""
)
db.execute(
"CREATE INDEX IF NOT EXISTS idx_fabric_tenant_queue "
"ON fabric_jobs(tenant_id, cell_id, status)"
)
def enqueue(
self,
*,
message: CanonicalMessage,
profile_id: str,
) -> FabricJob:
profile_id = require_ident("profile_id", profile_id)
if not message.command_capable:
raise ScopeError("non-command events cannot create execution jobs")
if not message.is_authenticated_human_command:
raise ScopeError("only authenticated human commands can create jobs")
job = FabricJob(
job_id=f"job-{message.event_id}",
tenant_id=message.tenant_id,
cell_id=message.cell_id,
profile_id=profile_id,
session_id=message.session_id,
event_id=message.event_id,
)
now = isoformat()
conflict: ConflictError | None = None
try:
with closing(sqlite3.connect(self.database)) as connection, connection as db:
db.execute("PRAGMA journal_mode=WAL")
db.execute(
"INSERT INTO fabric_jobs VALUES(?,?,?,?,?,?,?,?,?)",
(
job.job_id,
job.tenant_id,
job.cell_id,
job.profile_id,
job.session_id,
job.event_id,
job.status,
now,
now,
),
)
except sqlite3.IntegrityError:
conflict = ConflictError(
"fabric event already exists or violates queue uniqueness"
)
if conflict is not None:
raise conflict
return job
def claim(self, tenant_id: str, cell_id: str, profile_id: str) -> FabricJob | None:
require_ident("tenant_id", tenant_id, tenant=True)
require_ident("cell_id", cell_id)
require_ident("profile_id", profile_id)
with closing(sqlite3.connect(self.database)) as connection, connection as db:
db.execute("BEGIN IMMEDIATE")
row = db.execute(
"SELECT * FROM fabric_jobs WHERE tenant_id=? AND cell_id=? "
"AND profile_id=? AND status='queued' ORDER BY created_at LIMIT 1",
(tenant_id, cell_id, profile_id),
).fetchone()
if not row:
db.commit()
return None
db.execute(
"UPDATE fabric_jobs SET status='running', updated_at=? WHERE job_id=?",
(isoformat(), row[0]),
)
db.commit()
return FabricJob(*row[:7])
def health(self) -> dict[str, Any]:
with closing(sqlite3.connect(self.database)) as connection, connection as db:
count = db.execute("SELECT count(*) FROM fabric_jobs").fetchone()[0]
return {
"component": "hermes_fabric",
"jobs": count,
"checked_at": datetime.now(timezone.utc).isoformat(),
}
@@ -1,47 +0,0 @@
"""Hermes shared fabric profile isolation primitives."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
import os
from .tenant_context import ValidationError, require_ident
@dataclass(frozen=True)
class HermesProfile:
tenant_id: str
cell_id: str
profile_id: str
hermes_home: Path
workspace: Path
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in ("cell_id", "profile_id"):
require_ident(name, getattr(self, name))
if not self.hermes_home.is_absolute() or not self.workspace.is_absolute():
raise ValidationError("profile paths must be absolute")
@property
def environment(self) -> dict[str, str]:
return {
"HERMES_HOME": str(self.hermes_home),
"HOME": str(self.hermes_home),
"AIOS_TENANT_ID": self.tenant_id,
"AIOS_CELL_ID": self.cell_id,
"AIOS_PROFILE_ID": self.profile_id,
}
def prepare(self) -> None:
self.hermes_home.mkdir(parents=True, exist_ok=True)
self.workspace.mkdir(parents=True, exist_ok=True)
os.chmod(self.hermes_home, 0o700)
os.chmod(self.workspace, 0o700)
def assert_path_allowed(self, path: str | Path) -> None:
target = Path(path).resolve()
for root in (self.hermes_home.resolve(), self.workspace.resolve()):
if target == root or root in target.parents:
return
raise ValidationError("path escapes profile isolation boundary")
-158
View File
@@ -1,158 +0,0 @@
"""Fail-closed identity binding for authenticated external ingress.
External payload claims are not authority. An HMAC-authenticated envelope is
the sole factory for :class:`AuthenticatedPrincipal`; the resolver then checks
every claim against the authoritative ``TenantRouter`` before the runtime signs
an internal ``TenantContext``.
"""
from __future__ import annotations
from dataclasses import dataclass
import hashlib
import hmac
import re
from typing import Any, Mapping
from .router import Route, TenantRouter
from .tenant_context import ScopeError, ValidationError, canonical_json, require_ident
_PROVENANCE = object()
_SIGNATURE_RE = re.compile(r"^[0-9a-f]{64}$")
_SCOPE_FIELDS = (
"principal_id", "tenant_id", "cell_id", "channel",
"channel_account_id", "session_id", "message",
)
class AuthenticationError(ValidationError):
"""Authentication or identity resolution failed without disclosing scope."""
@dataclass(frozen=True, init=False)
class AuthenticatedPrincipal:
"""A principal which only the verified envelope factory can create."""
principal_id: str
def __init__(self, principal_id: str, *, _provenance: object) -> None:
if _provenance is not _PROVENANCE:
raise TypeError("AuthenticatedPrincipal is created by authenticated ingress only")
object.__setattr__(self, "principal_id", require_ident("principal_id", principal_id))
@classmethod
def _verified(cls, principal_id: str) -> "AuthenticatedPrincipal":
return cls(principal_id, _provenance=_PROVENANCE)
@dataclass(frozen=True)
class IdentityClaims:
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
session_id: str
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for field in ("cell_id", "channel", "channel_account_id", "session_id"):
require_ident(field, getattr(self, field))
@dataclass(frozen=True)
class AuthenticatedIngress:
principal: AuthenticatedPrincipal
claims: IdentityClaims
message: Mapping[str, Any]
@dataclass(frozen=True)
class RuntimeIdentity:
tenant_id: str
cell_id: str
principal_id: str
channel: str
channel_account_id: str
session_id: str
class IngressAuthenticator:
"""Verify a canonical HMAC envelope before any identity lookup occurs."""
def __init__(self, hmac_key: bytes) -> None:
if not isinstance(hmac_key, bytes) or len(hmac_key) < 32:
raise ValidationError("ingress HMAC key must be at least 32 bytes")
self._hmac_key = hmac_key
@staticmethod
def _signed_payload(request: Mapping[str, Any]) -> dict[str, Any]:
if not isinstance(request, Mapping):
raise ValidationError("ingress request must be an object")
if set(request) != {*_SCOPE_FIELDS, "signature"}:
raise ValidationError("ingress request fields are invalid")
values = {field: request[field] for field in _SCOPE_FIELDS}
if not isinstance(values["message"], Mapping):
raise ValidationError("ingress message must be an object")
return values
def authenticate(self, request: Mapping[str, Any]) -> AuthenticatedIngress:
values = self._signed_payload(request)
signature = request["signature"]
if not isinstance(signature, str) or not _SIGNATURE_RE.fullmatch(signature):
raise AuthenticationError("invalid ingress signature")
expected = hmac.new(
self._hmac_key, canonical_json(values), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise AuthenticationError("invalid ingress signature")
return AuthenticatedIngress(
principal=AuthenticatedPrincipal._verified(values["principal_id"]),
claims=IdentityClaims(
tenant_id=values["tenant_id"],
cell_id=values["cell_id"],
channel=values["channel"],
channel_account_id=values["channel_account_id"],
session_id=values["session_id"],
),
message=dict(values["message"]),
)
class RuntimeIdentityResolver:
"""Resolve authenticated ingress identity through ``TenantRouter`` only."""
def __init__(self, router: TenantRouter) -> None:
self._router = router
def resolve(
self, principal: AuthenticatedPrincipal, claims: IdentityClaims
) -> RuntimeIdentity:
if not isinstance(principal, AuthenticatedPrincipal):
raise AuthenticationError("untrusted principal type")
try:
route: Route = self._router.resolve(
channel=claims.channel,
channel_account_id=claims.channel_account_id,
principal_id=principal.principal_id,
session_id=claims.session_id,
)
except (ScopeError, ValidationError) as exc:
raise AuthenticationError("identity is not registered") from None
expected = (
claims.tenant_id, claims.cell_id, claims.channel,
claims.channel_account_id, principal.principal_id, claims.session_id,
)
actual = (
route.tenant_id, route.cell_id, route.channel,
route.channel_account_id, route.principal_id, route.session_id,
)
if actual != expected:
raise AuthenticationError("identity claims do not match authoritative route")
return RuntimeIdentity(
tenant_id=route.tenant_id,
cell_id=route.cell_id,
principal_id=route.principal_id,
channel=route.channel,
channel_account_id=route.channel_account_id,
session_id=route.session_id or "",
)
@@ -1,60 +0,0 @@
"""Tenant-scoped memory namespace and access policy for AIOS V2R1."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import PurePosixPath
from typing import Literal
from .tenant_context import ScopeError, ValidationError, require_ident
MemoryKind = Literal["session", "profile", "cell", "tenant", "shared_artifact"]
@dataclass(frozen=True)
class MemoryScope:
tenant_id: str
cell_id: str
profile_id: str
session_id: str | None
kind: MemoryKind
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in ("cell_id", "profile_id"):
require_ident(name, getattr(self, name))
if self.session_id is not None:
require_ident("session_id", self.session_id)
if self.kind not in {"session", "profile", "cell", "tenant", "shared_artifact"}:
raise ValidationError("invalid memory kind")
if self.kind == "session" and self.session_id is None:
raise ValidationError("session memory requires session_id")
@property
def namespace(self) -> str:
parts = ["tenant", self.tenant_id, "cell", self.cell_id]
if self.kind in {"profile", "session"}:
parts.extend(["profile", self.profile_id])
if self.kind == "session":
parts.extend(["session", str(self.session_id)])
parts.extend(["memory", self.kind])
return str(PurePosixPath(*parts))
def assert_access(self, requested: "MemoryScope", *, write: bool = False) -> None:
if self.tenant_id != requested.tenant_id:
raise ScopeError("cross-tenant memory access denied")
if self.cell_id != requested.cell_id:
raise ScopeError("cross-cell memory access denied")
if requested.kind == "shared_artifact":
if write:
raise ScopeError("shared artifacts are immutable")
return
if self.profile_id != requested.profile_id and requested.kind in {"profile", "session"}:
raise ScopeError("cross-profile memory access denied")
if requested.kind == "session" and self.session_id != requested.session_id:
raise ScopeError("cross-session memory access denied")
def qualify_memory_key(scope: MemoryScope, key: str) -> str:
key = require_ident("memory_key", key)
return f"{scope.namespace}/{key}"
@@ -1,224 +0,0 @@
"""Tenant-scoped append-only message ledger and exact quote resolver."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from contextlib import closing
import os
from pathlib import Path
import sqlite3
from typing import Iterable
from .tenant_context import (
CanonicalMessage,
ConflictError,
ScopeError,
ValidationError,
isoformat,
normalize_message_id,
parse_time,
utc_now,
)
@dataclass(frozen=True)
class LedgerRecord:
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
peer_id: str
direction: str
message_id: str
event_id: str
body: str
occurred_at: str
expires_at: str
class MessageLedger:
"""Exact-ID ledger.
The resolver never uses time windows, prefixes, similarity, or nearest-message
fallbacks. Ambiguity and absence fail closed.
"""
def __init__(self, database: str | Path, *, ttl_days: int = 7) -> None:
if not 1 <= ttl_days <= 365:
raise ValidationError("ttl_days must be between 1 and 365")
self.database = Path(database)
self.ttl_days = ttl_days
self.database.parent.mkdir(parents=True, exist_ok=True)
self._initialize()
try:
os.chmod(self.database, 0o600)
except OSError:
pass
def _connect(self) -> sqlite3.Connection:
db = sqlite3.connect(str(self.database), timeout=30)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA synchronous=FULL")
db.execute("PRAGMA busy_timeout=30000")
return db
def _initialize(self) -> None:
with closing(self._connect()) as connection, connection as db:
db.executescript(
"""
CREATE TABLE IF NOT EXISTS message_ledger (
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
channel TEXT NOT NULL,
channel_account_id TEXT NOT NULL,
peer_id TEXT NOT NULL,
direction TEXT NOT NULL CHECK(direction IN ('inbound','outbound','internal')),
message_id TEXT NOT NULL,
event_id TEXT NOT NULL,
body TEXT NOT NULL,
occurred_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(
tenant_id, cell_id, channel, channel_account_id,
peer_id, direction, message_id
),
UNIQUE(tenant_id, cell_id, event_id)
);
CREATE INDEX IF NOT EXISTS idx_message_reply_lookup ON message_ledger(
tenant_id, cell_id, channel, channel_account_id,
peer_id, message_id, expires_at
);
"""
)
def append(self, message: CanonicalMessage) -> LedgerRecord:
"""Append one exact message through the runtime write path.
This is intentionally the owner of validation, idempotency, and the
immediate SQLite transaction. Runtime callers must not depend on the
legacy ``remember`` compatibility name.
"""
message_id = normalize_message_id(message.message_id)
now = utc_now()
expires = isoformat(now + timedelta(days=self.ttl_days))
key = (
message.tenant_id,
message.cell_id,
message.channel,
message.channel_account_id,
message.peer_id,
message.direction,
message_id,
)
storage_error: ConflictError | None = None
try:
with closing(self._connect()) as connection, connection as db:
db.execute("BEGIN IMMEDIATE")
existing = db.execute(
"SELECT event_id, body, occurred_at, expires_at FROM message_ledger "
"WHERE tenant_id=? AND cell_id=? AND channel=? AND channel_account_id=? "
"AND peer_id=? AND direction=? AND message_id=?",
key,
).fetchone()
if existing:
if existing["body"] != message.body:
db.rollback()
raise ConflictError("message ID collision with different content")
db.commit()
return LedgerRecord(
*key, existing["event_id"], existing["body"],
existing["occurred_at"], existing["expires_at"],
)
db.execute(
"INSERT INTO message_ledger VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
(*key, message.event_id, message.body, message.occurred_at, expires, isoformat(now)),
)
db.commit()
except sqlite3.Error:
storage_error = ConflictError("ledger storage unavailable")
if storage_error is not None:
# Raise outside the sqlite exception handler so callers cannot inspect
# a driver exception through __cause__ or __context__.
raise storage_error
return LedgerRecord(*key, message.event_id, message.body, message.occurred_at, expires)
def remember(self, message: CanonicalMessage) -> LedgerRecord:
"""Backward-compatible API wrapper for callers outside the runtime path."""
return self.append(message)
def matches(self, message: CanonicalMessage) -> bool:
"""Whether this exact message is already durably represented."""
message_id = normalize_message_id(message.message_id)
with closing(self._connect()) as connection, connection as db:
row = db.execute(
"SELECT event_id, body FROM message_ledger WHERE tenant_id=? AND "
"cell_id=? AND channel=? AND channel_account_id=? AND peer_id=? "
"AND direction=? AND message_id=?",
(
message.tenant_id, message.cell_id, message.channel,
message.channel_account_id, message.peer_id, message.direction,
message_id,
),
).fetchone()
return bool(row and row["event_id"] == message.event_id and row["body"] == message.body)
def remove(self, record: LedgerRecord) -> None:
"""Compensate a newly appended record if a later ingress step fails."""
with closing(self._connect()) as connection, connection as db:
db.execute(
"DELETE FROM message_ledger WHERE tenant_id=? AND cell_id=? AND "
"channel=? AND channel_account_id=? AND peer_id=? AND direction=? "
"AND message_id=? AND event_id=?",
(
record.tenant_id, record.cell_id, record.channel,
record.channel_account_id, record.peer_id, record.direction,
record.message_id, record.event_id,
),
)
def resolve_quote(self, message: CanonicalMessage) -> str:
if message.quoted_body is not None:
return message.quoted_body
if message.reply_to_message_id is None:
raise ScopeError("quoted_text_unavailable")
reply_id = normalize_message_id(
message.reply_to_message_id,
field_name="reply_to_message_id",
)
with closing(self._connect()) as connection, connection as db:
rows = db.execute(
"SELECT direction, body, event_id FROM message_ledger "
"WHERE tenant_id=? AND cell_id=? AND channel=? AND channel_account_id=? "
"AND peer_id=? AND message_id=? AND expires_at>?",
(
message.tenant_id,
message.cell_id,
message.channel,
message.channel_account_id,
message.peer_id,
reply_id,
isoformat(),
),
).fetchall()
if len(rows) == 1:
return str(rows[0]["body"])
if len(rows) > 1:
raise ConflictError("ambiguous reply ID across message directions")
raise ScopeError("quoted_text_unavailable")
def purge_expired(self) -> int:
with closing(self._connect()) as connection, connection as db:
cursor = db.execute(
"DELETE FROM message_ledger WHERE expires_at<=?", (isoformat(),)
)
return cursor.rowcount
def integrity_check(self) -> str:
with closing(self._connect()) as connection, connection as db:
return str(db.execute("PRAGMA integrity_check").fetchone()[0])
def count(self) -> int:
with closing(self._connect()) as connection, connection as db:
return int(db.execute("SELECT count(*) FROM message_ledger").fetchone()[0])
-336
View File
@@ -1,336 +0,0 @@
"""Authoritative tenant registry and fail-closed message router."""
from __future__ import annotations
from contextlib import contextmanager
from contextlib import closing
from dataclasses import dataclass
from datetime import datetime, timedelta
import sqlite3
from pathlib import Path
from typing import Callable, Iterator
from .tenant_context import (
CanonicalMessage,
ConflictError,
ScopeError,
TenantContext,
ValidationError,
isoformat,
parse_time,
require_ident,
utc_now,
)
@dataclass(frozen=True)
class Route:
tenant_id: str
cell_id: str
channel: str
channel_account_id: str
principal_id: str
session_id: str | None
class TenantRouter:
"""SQLite-backed routing registry.
Channel accounts are authoritative tenant ownership boundaries. Principal and
session bindings may narrow a route, but can never move traffic to another
tenant or Cell.
"""
def __init__(
self, database: str | Path, *, clock: Callable[[], datetime] = utc_now
) -> None:
self.database = str(database)
self._clock = clock
self._initialize()
def _connect(self) -> sqlite3.Connection:
connection = sqlite3.connect(self.database, timeout=30, isolation_level=None)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys=ON")
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA synchronous=FULL")
connection.execute("PRAGMA busy_timeout=30000")
return connection
@contextmanager
def _transaction(self) -> Iterator[sqlite3.Connection]:
connection = self._connect()
try:
connection.execute("BEGIN IMMEDIATE")
yield connection
connection.execute("COMMIT")
except Exception:
connection.execute("ROLLBACK")
raise
finally:
connection.close()
def _initialize(self) -> None:
with closing(self._connect()) as connection, connection as db:
db.executescript(
"""
CREATE TABLE IF NOT EXISTS tenants (
tenant_id TEXT PRIMARY KEY,
cell_id TEXT NOT NULL UNIQUE,
status TEXT NOT NULL CHECK(status IN ('active','suspended','retired')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS channel_routes (
channel TEXT NOT NULL,
channel_account_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(channel, channel_account_id),
FOREIGN KEY(tenant_id) REFERENCES tenants(tenant_id)
);
CREATE TABLE IF NOT EXISTS principal_routes (
channel TEXT NOT NULL,
channel_account_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(channel, channel_account_id, principal_id),
FOREIGN KEY(channel, channel_account_id)
REFERENCES channel_routes(channel, channel_account_id)
);
CREATE TABLE IF NOT EXISTS session_bindings (
tenant_id TEXT NOT NULL,
session_id TEXT NOT NULL,
cell_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY(tenant_id, session_id),
FOREIGN KEY(tenant_id) REFERENCES tenants(tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_session_expiry
ON session_bindings(expires_at);
"""
)
def register_tenant(self, tenant_id: str, cell_id: str) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
cell_id = require_ident("cell_id", cell_id)
now = isoformat(self._clock())
with self._transaction() as db:
row = db.execute(
"SELECT cell_id, status FROM tenants WHERE tenant_id=?", (tenant_id,)
).fetchone()
if row:
if row["cell_id"] != cell_id:
raise ConflictError("tenant already bound to another cell")
db.execute(
"UPDATE tenants SET status='active', updated_at=? WHERE tenant_id=?",
(now, tenant_id),
)
return
conflict = db.execute(
"SELECT tenant_id FROM tenants WHERE cell_id=?", (cell_id,)
).fetchone()
if conflict:
raise ConflictError("cell already belongs to another tenant")
db.execute(
"INSERT INTO tenants VALUES(?,?,?,?,?)",
(tenant_id, cell_id, "active", now, now),
)
def set_tenant_status(self, tenant_id: str, status: str) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
if status not in {"active", "suspended", "retired"}:
raise ValidationError("invalid tenant status")
with self._transaction() as db:
cursor = db.execute(
"UPDATE tenants SET status=?, updated_at=? WHERE tenant_id=?",
(status, isoformat(self._clock()), tenant_id),
)
if cursor.rowcount != 1:
raise ValidationError("unknown tenant")
def bind_channel(
self,
*,
tenant_id: str,
channel: str,
channel_account_id: str,
) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
channel = require_ident("channel", channel)
channel_account_id = require_ident("channel_account_id", channel_account_id)
with self._transaction() as db:
tenant = db.execute(
"SELECT cell_id, status FROM tenants WHERE tenant_id=?", (tenant_id,)
).fetchone()
if not tenant or tenant["status"] != "active":
raise ValidationError("tenant is not active")
existing = db.execute(
"SELECT tenant_id, cell_id FROM channel_routes "
"WHERE channel=? AND channel_account_id=?",
(channel, channel_account_id),
).fetchone()
if existing:
if existing["tenant_id"] != tenant_id or existing["cell_id"] != tenant["cell_id"]:
raise ConflictError("channel account already belongs to another tenant")
return
db.execute(
"INSERT INTO channel_routes VALUES(?,?,?,?,?)",
(channel, channel_account_id, tenant_id, tenant["cell_id"], isoformat(self._clock())),
)
def bind_principal(
self,
*,
tenant_id: str,
channel: str,
channel_account_id: str,
principal_id: str,
) -> None:
principal_id = require_ident("principal_id", principal_id)
route = self.resolve_channel(channel, channel_account_id)
if route.tenant_id != tenant_id:
raise ScopeError("principal cannot cross tenant boundary")
with self._transaction() as db:
existing = db.execute(
"SELECT tenant_id, cell_id FROM principal_routes WHERE "
"channel=? AND channel_account_id=? AND principal_id=?",
(channel, channel_account_id, principal_id),
).fetchone()
if existing:
if existing["tenant_id"] != tenant_id or existing["cell_id"] != route.cell_id:
raise ConflictError("principal route conflict")
return
db.execute(
"INSERT INTO principal_routes VALUES(?,?,?,?,?,?)",
(
channel,
channel_account_id,
principal_id,
tenant_id,
route.cell_id,
isoformat(self._clock()),
),
)
def bind_session(
self,
*,
tenant_id: str,
session_id: str,
cell_id: str,
principal_id: str,
ttl_seconds: int = 86400,
) -> None:
tenant_id = require_ident("tenant_id", tenant_id, tenant=True)
session_id = require_ident("session_id", session_id)
cell_id = require_ident("cell_id", cell_id)
principal_id = require_ident("principal_id", principal_id)
if not 60 <= ttl_seconds <= 31 * 86400:
raise ValidationError("invalid session TTL")
now = self._clock()
expires = isoformat(now + timedelta(seconds=ttl_seconds))
with self._transaction() as db:
tenant = db.execute(
"SELECT cell_id, status FROM tenants WHERE tenant_id=?", (tenant_id,)
).fetchone()
if not tenant or tenant["status"] != "active" or tenant["cell_id"] != cell_id:
raise ScopeError("session cell does not match tenant")
existing = db.execute(
"SELECT cell_id, principal_id FROM session_bindings "
"WHERE tenant_id=? AND session_id=?",
(tenant_id, session_id),
).fetchone()
if existing and (
existing["cell_id"] != cell_id or existing["principal_id"] != principal_id
):
raise ConflictError("session is already bound to another principal or cell")
db.execute(
"INSERT INTO session_bindings VALUES(?,?,?,?,?,?) "
"ON CONFLICT(tenant_id,session_id) DO UPDATE SET expires_at=excluded.expires_at",
(tenant_id, session_id, cell_id, principal_id, expires, isoformat(now)),
)
def resolve_channel(self, channel: str, channel_account_id: str) -> Route:
channel = require_ident("channel", channel)
channel_account_id = require_ident("channel_account_id", channel_account_id)
with closing(self._connect()) as connection, connection as db:
row = db.execute(
"SELECT r.tenant_id, r.cell_id, t.status FROM channel_routes r "
"JOIN tenants t ON t.tenant_id=r.tenant_id "
"WHERE r.channel=? AND r.channel_account_id=?",
(channel, channel_account_id),
).fetchone()
if not row:
raise ScopeError("unknown channel account")
if row["status"] != "active":
raise ScopeError("tenant is not active")
return Route(row["tenant_id"], row["cell_id"], channel, channel_account_id, "", None)
def resolve(
self,
*,
channel: str,
channel_account_id: str,
principal_id: str,
session_id: str | None = None,
) -> Route:
principal_id = require_ident("principal_id", principal_id)
base = self.resolve_channel(channel, channel_account_id)
with closing(self._connect()) as connection, connection as db:
principal = db.execute(
"SELECT tenant_id, cell_id FROM principal_routes WHERE "
"channel=? AND channel_account_id=? AND principal_id=?",
(channel, channel_account_id, principal_id),
).fetchone()
if principal and (
principal["tenant_id"] != base.tenant_id or principal["cell_id"] != base.cell_id
):
raise ScopeError("principal route violates channel ownership")
if session_id is not None:
session_id = require_ident("session_id", session_id)
session = db.execute(
"SELECT cell_id, principal_id, expires_at FROM session_bindings "
"WHERE tenant_id=? AND session_id=?",
(base.tenant_id, session_id),
).fetchone()
if not session:
raise ScopeError("unknown session")
if parse_time(session["expires_at"]) <= self._clock():
raise ScopeError("session expired")
if session["cell_id"] != base.cell_id or session["principal_id"] != principal_id:
raise ScopeError("session scope mismatch")
return Route(
base.tenant_id,
base.cell_id,
channel,
channel_account_id,
principal_id,
session_id,
)
def assert_message_route(self, message: CanonicalMessage) -> Route:
route = self.resolve(
channel=message.channel,
channel_account_id=message.channel_account_id,
principal_id=message.principal_id,
session_id=message.session_id,
)
if message.tenant_id != route.tenant_id or message.cell_id != route.cell_id:
raise ScopeError("message claims do not match authoritative route")
return route
def assert_context_route(self, context: TenantContext) -> Route:
route = self.resolve(
channel=context.channel,
channel_account_id=context.channel_account_id,
principal_id=context.principal_id,
session_id=context.session_id,
)
if context.tenant_id != route.tenant_id or context.cell_id != route.cell_id:
raise ScopeError("context claims do not match authoritative route")
return route
-136
View File
@@ -1,136 +0,0 @@
"""V2R1 runtime composition root for authenticated channel ingress."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, Mapping
from .channel_adapter import ChannelAdapter, IngressResult
from .identity import (
AuthenticationError,
IngressAuthenticator,
RuntimeIdentity,
RuntimeIdentityResolver,
)
from .message_ledger import MessageLedger
from .router import TenantRouter
from .session_pool import SessionPool
from .tenant_context import ConflictError, ScopeError, TenantContext, ValidationError, utc_now
@dataclass(frozen=True)
class RuntimeIngressResponse:
"""Sanitised boundary response; no exception text crosses external ingress."""
status: str
result: IngressResult | None = None
class RuntimeWiring:
"""Own the only external-to-runtime identity binding and message path.
The request accepted by :meth:`ingest` is an HMAC-authenticated envelope;
tenant, cell, channel, account, session, principal, and message claims are
signed together. The envelope is then resolved against ``TenantRouter``.
Only that resolved identity can mint the short-lived internal context passed
to ``ChannelAdapter``.
"""
def __init__(
self,
*,
router: TenantRouter,
ledger: MessageLedger,
session_pool: SessionPool,
channel_adapter: ChannelAdapter,
identity_resolver: RuntimeIdentityResolver,
ingress_authenticator: IngressAuthenticator,
internal_signing_key: bytes,
clock: Callable[[], datetime],
) -> None:
self.router = router
self.ledger = ledger
self.session_pool = session_pool
self.channel_adapter = channel_adapter
self.identity_resolver = identity_resolver
self._ingress_authenticator = ingress_authenticator
self._internal_signing_key = internal_signing_key
self._clock = clock
@classmethod
def create(
cls,
root: str | Path,
*,
signing_key: bytes,
ingress_hmac_key: bytes | None = None,
clock: Callable[[], datetime] = utc_now,
) -> "RuntimeWiring":
if not isinstance(signing_key, bytes) or len(signing_key) < 32:
raise ValidationError("internal signing key must be at least 32 bytes")
# Kept optional for one controlled migration release; the external
# request path still cannot supply a TenantContext token.
hmac_key = ingress_hmac_key if ingress_hmac_key is not None else signing_key
root_path = Path(root)
root_path.mkdir(parents=True, exist_ok=True)
router = TenantRouter(root_path / "router.sqlite3", clock=clock)
ledger = MessageLedger(root_path / "message_ledger.sqlite3")
session_pool = SessionPool(router)
adapter = ChannelAdapter(
router=router,
ledger=ledger,
session_pool=session_pool,
signing_key=signing_key,
clock=clock,
)
return cls(
router=router,
ledger=ledger,
session_pool=session_pool,
channel_adapter=adapter,
identity_resolver=RuntimeIdentityResolver(router),
ingress_authenticator=IngressAuthenticator(hmac_key),
internal_signing_key=signing_key,
clock=clock,
)
def _issue_internal_context(self, identity: RuntimeIdentity) -> TenantContext:
"""Mint internal scope only after all claims matched the router route."""
return TenantContext.issue(
lifetime_seconds=300,
now=self._clock(),
tenant_id=identity.tenant_id,
cell_id=identity.cell_id,
principal_id=identity.principal_id,
principal_type="authenticated_user",
session_id=identity.session_id,
task_id=None,
channel=identity.channel,
channel_account_id=identity.channel_account_id,
)
def ingest(self, *, request: Mapping[str, Any]) -> RuntimeIngressResponse:
"""Authenticate, resolve, issue internal context, then append and lease.
The only successful route is ``IngressAuthenticator`` ->
``RuntimeIdentityResolver`` -> ``ChannelAdapter`` ->
``MessageLedger.append`` + ``SessionPool``. The legacy caller-supplied
TenantContext token interface is intentionally not accepted.
"""
try:
ingress = self._ingress_authenticator.authenticate(request)
identity = self.identity_resolver.resolve(ingress.principal, ingress.claims)
context = self._issue_internal_context(identity)
result = self.channel_adapter.ingest(
token=context.sign(self._internal_signing_key), message=ingress.message
)
return RuntimeIngressResponse(status="ACCEPTED", result=result)
except (AuthenticationError, ScopeError):
return RuntimeIngressResponse(status="AUTH_FAILED")
except ValidationError:
return RuntimeIngressResponse(status="VALIDATION_FAILED")
except ConflictError:
return RuntimeIngressResponse(status="CONFLICT")
except Exception:
return RuntimeIngressResponse(status="INTERNAL_ERROR")
@@ -1,33 +0,0 @@
"""Tenant and Cell scoped secret reference model.
This module never stores secret values. It stores ownership and retrieval scope
metadata for integration with a real secret broker.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from .tenant_context import ValidationError, require_ident
@dataclass(frozen=True)
class SecretRef:
secret_id: str
tenant_id: str
cell_id: str
kind: Literal["channel", "model", "tool", "runtime"]
version: str
broker_path: str
def __post_init__(self) -> None:
for name in ("secret_id", "cell_id", "version"):
require_ident(name, getattr(self, name))
require_ident("tenant_id", self.tenant_id, tenant=True)
if self.kind not in {"channel", "model", "tool", "runtime"}:
raise ValidationError("invalid secret kind")
if not self.broker_path.startswith("secret://"):
raise ValidationError("secret broker path required")
def can_be_used_by(self, *, tenant_id: str, cell_id: str) -> bool:
return self.tenant_id == tenant_id and self.cell_id == cell_id
@@ -1,59 +0,0 @@
"""Process-local session lease pool backed by TenantRouter authority."""
from __future__ import annotations
from dataclasses import dataclass
from threading import RLock
from .router import Route, TenantRouter
from .tenant_context import ScopeError, TenantContext
@dataclass(frozen=True)
class SessionLease:
tenant_id: str
cell_id: str
principal_id: str
session_id: str
class SessionPool:
"""Tracks active leases without creating a second session authority store."""
def __init__(self, router: TenantRouter) -> None:
self._router = router
self._leases: dict[tuple[str, str], SessionLease] = {}
self._lock = RLock()
@staticmethod
def _assert_route(context: TenantContext, route: Route) -> None:
if (
route.tenant_id != context.tenant_id
or route.cell_id != context.cell_id
or route.principal_id != context.principal_id
or route.session_id != context.session_id
):
raise ScopeError("session lease does not match authoritative route")
def assert_available(self, context: TenantContext, route: Route) -> None:
"""Re-resolve the context from the router before a ledger mutation."""
self._assert_route(context, route)
self._assert_route(context, self._router.assert_context_route(context))
def acquire(self, context: TenantContext, route: Route) -> SessionLease:
self._assert_route(context, route)
lease = SessionLease(
tenant_id=context.tenant_id,
cell_id=context.cell_id,
principal_id=context.principal_id,
session_id=context.session_id,
)
with self._lock:
return self._leases.setdefault((lease.tenant_id, lease.session_id), lease)
def release(self, lease: SessionLease) -> None:
with self._lock:
self._leases.pop((lease.tenant_id, lease.session_id), None)
def active_count(self) -> int:
with self._lock:
return len(self._leases)
@@ -1,316 +0,0 @@
"""Signed tenant context and canonical message contract for AIOS V2R1."""
from __future__ import annotations
import base64
from dataclasses import asdict, dataclass
from datetime import datetime, timedelta, timezone
import hashlib
import hmac
import json
import re
import secrets
from typing import Any, Mapping
VERSION = "V2R1C0"
MAX_SAFE_INTEGER = 9_007_199_254_740_991
TENANT_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$")
IDENT_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9._:@-]{0,126}[A-Za-z0-9])?$")
class V2R1Error(RuntimeError):
pass
class ValidationError(V2R1Error):
pass
class ScopeError(V2R1Error):
pass
class ConflictError(V2R1Error):
pass
def utc_now() -> datetime:
return datetime.now(timezone.utc)
def isoformat(value: datetime | None = None) -> str:
return (value or utc_now()).astimezone(timezone.utc).isoformat()
def parse_time(value: str) -> datetime:
if not isinstance(value, str) or not value.strip():
raise ValidationError("timestamp must be a non-empty string")
text = value.strip()
if text.endswith("Z"):
text = text[:-1] + "+00:00"
try:
parsed = datetime.fromisoformat(text)
except ValueError as exc:
raise ValidationError(f"invalid timestamp: {value!r}") from exc
if parsed.tzinfo is None:
raise ValidationError("timestamp must include timezone")
return parsed.astimezone(timezone.utc)
def require_ident(name: str, value: Any, *, tenant: bool = False) -> str:
if not isinstance(value, str):
raise ValidationError(f"{name} must be a string")
value = value.strip()
pattern = TENANT_RE if tenant else IDENT_RE
if not pattern.fullmatch(value):
raise ValidationError(f"invalid {name}: {value!r}")
return value
def normalize_message_id(value: Any, *, field_name: str = "message_id") -> str:
if isinstance(value, bool):
raise ValidationError(f"{field_name} cannot be boolean")
if isinstance(value, str):
value = value.strip()
if not value or len(value) > 256 or any(ord(ch) < 32 for ch in value):
raise ValidationError(f"invalid {field_name}")
return value
if isinstance(value, int):
if abs(value) > MAX_SAFE_INTEGER:
raise ValidationError(f"{field_name} is an unsafe numeric ID")
return str(value)
raise ValidationError(f"{field_name} must be a lossless string or safe integer")
def canonical_json(value: Any) -> bytes:
return json.dumps(
value, ensure_ascii=False, separators=(",", ":"), sort_keys=True
).encode("utf-8")
def _b64(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def _unb64(value: str) -> bytes:
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
@dataclass(frozen=True)
class TenantContext:
tenant_id: str
cell_id: str
principal_id: str
principal_type: str
session_id: str
task_id: str | None
channel: str
channel_account_id: str
issued_at: str
expires_at: str
nonce: str
def __post_init__(self) -> None:
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in (
"cell_id", "principal_id", "principal_type", "session_id",
"channel", "channel_account_id", "nonce",
):
require_ident(name, getattr(self, name))
if self.task_id is not None:
require_ident("task_id", self.task_id)
issued = parse_time(self.issued_at)
expires = parse_time(self.expires_at)
if expires <= issued or expires - issued > timedelta(hours=1):
raise ValidationError("invalid tenant context lifetime")
@classmethod
def issue(cls, *, lifetime_seconds: int = 300, now: datetime | None = None, **scope: Any) -> "TenantContext":
if not 1 <= lifetime_seconds <= 3600:
raise ValidationError("lifetime_seconds must be between 1 and 3600")
current = (now or utc_now()).astimezone(timezone.utc)
return cls(
**scope,
issued_at=isoformat(current),
expires_at=isoformat(current + timedelta(seconds=lifetime_seconds)),
nonce=secrets.token_hex(16),
)
def sign(self, key: bytes) -> str:
if len(key) < 32:
raise ValidationError("signing key must be at least 32 bytes")
payload = _b64(canonical_json(asdict(self)))
signature = _b64(hmac.new(key, payload.encode("ascii"), hashlib.sha256).digest())
return f"{payload}.{signature}"
@classmethod
def verify(
cls,
token: str,
key: bytes,
*,
now: datetime | None = None,
expected_tenant_id: str | None = None,
expected_cell_id: str | None = None,
) -> "TenantContext":
if len(key) < 32:
raise ValidationError("signing key must be at least 32 bytes")
try:
payload, encoded_signature = token.split(".", 1)
actual = _unb64(encoded_signature)
if encoded_signature != _b64(actual):
raise ValidationError("non-canonical tenant context signature")
data = json.loads(_unb64(payload))
except Exception as exc:
raise ValidationError("malformed tenant context token") from exc
expected = hmac.new(key, payload.encode("ascii"), hashlib.sha256).digest()
if not hmac.compare_digest(actual, expected):
raise ValidationError("tenant context signature mismatch")
context = cls(**data)
current = (now or utc_now()).astimezone(timezone.utc)
if current < parse_time(context.issued_at) - timedelta(seconds=60):
raise ValidationError("tenant context issued in future")
if current >= parse_time(context.expires_at):
raise ValidationError("tenant context expired")
if expected_tenant_id and context.tenant_id != expected_tenant_id:
raise ScopeError("tenant mismatch")
if expected_cell_id and context.cell_id != expected_cell_id:
raise ScopeError("cell mismatch")
return context
@dataclass(frozen=True)
class CanonicalMessage:
tenant_id: str
cell_id: str
principal_id: str
principal_type: str
origin_type: str
channel: str
channel_account_id: str
peer_id: str
conversation_id: str
session_id: str
task_id: str | None
event_id: str
message_id: str
reply_to_message_id: str | None
direction: str
command_capable: bool
occurred_at: str
received_at: str
body: str
quoted_body: str | None
adapter_name: str
adapter_version: str
schema_version: str = "aios.message.v1"
def __post_init__(self) -> None:
if self.schema_version != "aios.message.v1":
raise ValidationError("unsupported message schema version")
require_ident("tenant_id", self.tenant_id, tenant=True)
for name in (
"cell_id", "principal_id", "principal_type", "origin_type", "channel",
"channel_account_id", "peer_id", "conversation_id", "session_id",
"event_id", "adapter_name", "adapter_version",
):
require_ident(name, getattr(self, name))
if self.task_id is not None:
require_ident("task_id", self.task_id)
normalize_message_id(self.message_id)
if self.reply_to_message_id is not None:
normalize_message_id(self.reply_to_message_id, field_name="reply_to_message_id")
if self.direction not in {"inbound", "outbound", "internal"}:
raise ValidationError("invalid direction")
if not isinstance(self.command_capable, bool):
raise ValidationError("command_capable must be boolean")
parse_time(self.occurred_at)
parse_time(self.received_at)
if not isinstance(self.body, str):
raise ValidationError("body must be a string")
if self.quoted_body is not None and not isinstance(self.quoted_body, str):
raise ValidationError("quoted_body must be a string or null")
@classmethod
def from_mapping(cls, data: Mapping[str, Any]) -> "CanonicalMessage":
content = data.get("content") or {}
adapter = data.get("adapter") or {}
if not isinstance(content, Mapping) or not isinstance(adapter, Mapping):
raise ValidationError("content and adapter must be objects")
values = dict(data)
return cls(
schema_version=values.get("schema_version", "aios.message.v1"),
tenant_id=values["tenant_id"],
cell_id=values["cell_id"],
principal_id=values["principal_id"],
principal_type=values["principal_type"],
origin_type=values["origin_type"],
channel=values["channel"],
channel_account_id=values["channel_account_id"],
peer_id=values["peer_id"],
conversation_id=values["conversation_id"],
session_id=values["session_id"],
task_id=values.get("task_id"),
event_id=values["event_id"],
message_id=normalize_message_id(values["message_id"]),
reply_to_message_id=(
normalize_message_id(values["reply_to_message_id"], field_name="reply_to_message_id")
if values.get("reply_to_message_id") is not None else None
),
direction=values["direction"],
command_capable=values["command_capable"],
occurred_at=values["occurred_at"],
received_at=values["received_at"],
body=content.get("body", ""),
quoted_body=content.get("quoted_body"),
adapter_name=adapter["name"],
adapter_version=adapter["version"],
)
def assert_context(self, context: TenantContext) -> None:
fields = (
"tenant_id", "cell_id", "principal_id", "session_id", "task_id",
"channel", "channel_account_id",
)
mismatches = [name for name in fields if getattr(self, name) != getattr(context, name)]
if mismatches:
raise ScopeError("message/context mismatch: " + ", ".join(mismatches))
@property
def is_authenticated_human_command(self) -> bool:
return (
self.direction == "inbound"
and self.command_capable
and self.principal_type == "authenticated_user"
and self.origin_type == "channel_user"
)
@dataclass(frozen=True)
class PolicyDecision:
allowed: bool
approval_required: bool
code: str
reason: str
READ_ONLY_TOOLS = frozenset({
"read", "grep", "glob", "find", "ls", "web_search", "web_fetch",
"memory_search", "sessions_list", "sessions_history", "session_status",
})
def evaluate_tool_call(tool_name: str, message: CanonicalMessage) -> PolicyDecision:
normalized = tool_name.strip().lower() if isinstance(tool_name, str) else ""
if not normalized:
return PolicyDecision(False, False, "invalid_tool", "tool name missing")
if normalized in READ_ONLY_TOOLS:
return PolicyDecision(True, False, "read_only", "read-only operation")
if not message.is_authenticated_human_command:
return PolicyDecision(
False, False, "origin_denied",
"side effects require an authenticated current human command",
)
return PolicyDecision(
True, True, "approval_required",
"authenticated human side effect requires explicit approval",
)
+1
View File
@@ -0,0 +1 @@
/data/aios-gitops/workspace/active-runtime
+1
View File
@@ -0,0 +1 @@
{"ts": 1783642009.9384289, "name": "another_cmd.json"}
+1
View File
@@ -0,0 +1 @@
{"ts": 1783642009.9384289, "name": "stuck_cmd.json"}
@@ -0,0 +1,19 @@
# CMD-20260710-sr01-gitea-branch-sync
## Objective
Fix Gitea DB branch index desync: Gitea DB 与 bare repo 分支列表不一致(Gitea API 返回 50 条 vs bare repo 55 条)。
## Analysis
- Root cause: `h_branch_sync()` 之前依赖 Gitea API `POST /api/v1/repos/aios/aios/branches` 分页查询
- Token scope 不足 `read:admin` 导致只能获取 50 条(默认 limit),但 total 是 55
- 临时修复: 改为 `git ls-remote --heads` 直接枚举 bare repo,绕过了 Gitea API 分页问题
- 但根本问题:Gitea DB 索引没有完全同步 bare repo,需要调查 Gitea 后台同步机制
## Plan
1. 诊断 Gitea DB 与 bare repo 实际的差异详情
2. 决定修复方案:full resync Gitea, or fix the sync mechanism
3. 实现/验证
4. 更新 baseline
## Status
pending
+1
View File
@@ -0,0 +1 @@
{"ts": 1783642009.9169936, "name": "cmd1.json"}
+1
View File
@@ -0,0 +1 @@
{"ts": 1783642009.9169936, "name": "cmd2.json"}
+1
View File
@@ -0,0 +1 @@
{"ts": 1783643569.9295633, "name": "recent"}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783583895",
"title": "⚠️ Issue Detected: stuck",
"body": "1 commands stuck in running/ for >30m",
"urgency": "warning",
"ts": "2026-07-09T15:58:15+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783583993",
"title": "⚠️ Issue Detected: stuck",
"body": "1 commands stuck in running/ for >30m",
"urgency": "warning",
"ts": "2026-07-09T15:59:53+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783584042",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T16:00:42+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783597695",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T19:48:15+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783597737",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T19:48:57+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783598337",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T19:58:57+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783598938",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T20:08:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783599538",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T20:18:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783600138",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T20:28:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783600738",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T20:38:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783601338",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T20:48:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783601938",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T20:58:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783602538",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T21:08:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783603138",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T21:18:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783603738",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T21:28:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783604338",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T21:38:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783604938",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T21:48:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783605538",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T21:58:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783606138",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T22:08:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783606738",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T22:18:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783607338",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T22:28:58+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783607939",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T22:38:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783608539",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T22:48:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783609139",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T22:58:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783609739",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T23:08:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783610339",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T23:18:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783610939",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T23:28:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783611539",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T23:38:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783612139",
"title": "⚠️ Issue Detected: branch_sync",
"body": "5 branches missing from Gitea DB",
"urgency": "warning",
"ts": "2026-07-09T23:48:59+0800"
}
+7
View File
@@ -0,0 +1,7 @@
{
"nid": "n-1783643869",
"title": "Stuck Commands Cleaned",
"body": "Moved 2 stuck commands to failed/",
"urgency": "info",
"ts": "2026-07-10T08:37:49+0800"
}
+1
View File
@@ -0,0 +1 @@
{"check": "stuck", "severity": "medium", "diagnosis": "stuck commands", "stuck": [{"n": "stuck_cmd.json", "ts": 1783642009.9384289}, {"n": "another_cmd.json", "ts": 1783642009.9384289}], "fix": "move_to_failed"}
+1
View File
@@ -0,0 +1 @@
{"check": "branch_sync", "severity": "high", "diagnosis": "missing branches", "branches": ["br1"], "fix": "sync_branches"}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783584042",
"created": "2026-07-09T16:00:42+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/group-owner-agent-context-isolation",
"patch/agent-gpt-message-plane",
"patch/agent-event-bus-unified",
"patch/user-profile-isolation",
"patch/gitops-change-archive-audit"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783597695",
"created": "2026-07-09T19:48:15+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/gitops-change-archive-audit",
"patch/agent-gpt-message-plane",
"patch/agent-event-bus-unified",
"patch/user-profile-isolation",
"patch/group-owner-agent-context-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783597737",
"created": "2026-07-09T19:48:57+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783598337",
"created": "2026-07-09T19:58:57+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783598938",
"created": "2026-07-09T20:08:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783599538",
"created": "2026-07-09T20:18:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783600138",
"created": "2026-07-09T20:28:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783600738",
"created": "2026-07-09T20:38:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783601338",
"created": "2026-07-09T20:48:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783601938",
"created": "2026-07-09T20:58:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783602538",
"created": "2026-07-09T21:08:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783603138",
"created": "2026-07-09T21:18:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783603738",
"created": "2026-07-09T21:28:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783604338",
"created": "2026-07-09T21:38:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783604938",
"created": "2026-07-09T21:48:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783605538",
"created": "2026-07-09T21:58:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783606138",
"created": "2026-07-09T22:08:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783606738",
"created": "2026-07-09T22:18:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783607338",
"created": "2026-07-09T22:28:58+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783607939",
"created": "2026-07-09T22:38:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783608539",
"created": "2026-07-09T22:48:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783609139",
"created": "2026-07-09T22:58:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783609739",
"created": "2026-07-09T23:08:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783610339",
"created": "2026-07-09T23:18:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783610939",
"created": "2026-07-09T23:28:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783611539",
"created": "2026-07-09T23:38:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,16 @@
{
"patch_id": "patch-1783612139",
"created": "2026-07-09T23:48:59+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "5 branches missing from Gitea DB",
"branches": [
"patch/agent-event-bus-unified",
"patch/agent-gpt-message-plane",
"patch/group-owner-agent-context-isolation",
"patch/gitops-change-archive-audit",
"patch/user-profile-isolation"
],
"stuck": [],
"fix": "sync_branches"
}
@@ -0,0 +1,13 @@
{
"patch_id": "patch-1783643869",
"created": "2026-07-10T08:37:49+0800",
"check": "branch_sync",
"severity": "high",
"diagnosis": "2 branches missing from Gitea DB",
"branches": [
"br1",
"br2"
],
"stuck": [],
"fix": "sync_branches"
}
+1
View File
@@ -0,0 +1 @@
this is not json
+29 -18
View File
@@ -1,7 +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"
created_at: 2026-07-08T15:20:00+08:00
baseline: V1R3C1
commands:
@@ -11,22 +12,6 @@ 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"
lifecycle:
new: active/
running: running/
success: completed/
failure: failed/
# Command lifecycle state machine:
# active/ → locked & moved to running/ → execute → success → completed/
# → failure → failed/
policies:
path: "agents/policies/"
source: "git"
@@ -53,4 +38,30 @@ audit:
agent_runs: "audit/agent-runs/"
version_history: "audit/version-history/"
command_history: "audit/command-history/"
interaction_history: "audit/interaction-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)
+15 -10
View File
@@ -76,20 +76,25 @@ def h_api():
except Exception as e: return {"ok": False, "err": str(e)}
def h_branch_sync():
bare = Path("/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios-v23.git")
bare = Path("/data/aios-gitops/runtime/gitea-bin/data/repositories/aios/aios.git")
try:
# Use git command directly instead of Gitea API to avoid auth/permission issues
r = subprocess.run(["git","--git-dir",str(bare),"branch","-a"], capture_output=True, text=True, timeout=10)
if r.returncode != 0: return {"ok": False, "err": r.stderr[:200]}
bb = set(l.strip().replace("remotes/origin/","").replace("* ","") for l in r.stdout.strip().split("\n") if l.strip())
gb = []; pg = 1
while True:
r2 = requests.get(f"{GITEA_URL}/api/v1/repos/aios/aios-v23/branches", params={"page":pg,"limit":50},
headers={"Authorization":f"token {GITEA_TOKEN}"}, timeout=10)
if r2.status_code != 200: break
d = r2.json()
if not d: break
gb.extend(b["name"] for b in d); pg += 1
gs = set(gb); miss = bb - gs
# Use git ls-remote to get remote branches instead of Gitea API
r2 = subprocess.run(["git","ls-remote","--heads",GITEA_URL.split("//")[0]+"//"+GITEA_TOKEN+"@"+GITEA_URL.split("//")[1]+"/aios/aios.git"],
capture_output=True, text=True, timeout=15)
if r2.returncode != 0: return {"ok": False, "bare": len(bb), "gitea": 0, "err": "ls-remote failed: "+r2.stderr[:200]}
gb = []
for line in r2.stdout.strip().split("\n"):
if line.strip():
parts = line.strip().split(None, 1)
if len(parts) == 2:
ref = parts[1].replace("refs/heads/","")
gb.append(ref)
gs = set(gb)
miss = bb - gs
return {"ok": len(miss)==0, "bare": len(bb), "gitea": len(gs), "missing": list(miss)[:10]}
except Exception as e: return {"ok": False, "err": str(e)}
+10 -9
View File
@@ -1,11 +1,12 @@
{
"ts": "2026-07-09T00:00:00+09:00",
"ok": true,
"checks": {
"git_object_permission": true,
"api": true,
"branch_sync": true,
"stale_runtime_state_cleaned": true
},
"note": "Updated during PR #2 cleanup before merge"
"ts": "2026-07-11T02:23:29+0800",
"ok": false,
"checks": {
"gitea": false,
"api": true,
"branch_sync": true,
"stuck": true
},
"plans": 0,
"applied": 0
}
+1 -1
View File
@@ -11,5 +11,5 @@
"discovered_at": "2026-07-08T19:01:45+0800"
}
},
"last_scan": "2026-07-09T10:42:52+0800"
"last_scan": "2026-07-11T02:28:28+0800"
}
+15
View File
@@ -0,0 +1,15 @@
{
"run_id": "c-1783583895",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T15:58:15+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 292.9
}
]
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"run_id": "c-1783583993",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T15:59:53+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 294.6
}
]
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"run_id": "c-1783584042",
"check": "stuck",
"status": "degraded",
"ts": "2026-07-09T16:00:42+0800",
"details": {
"ok": false,
"stuck": [
{
"n": "CMD-20260709-enable-autonomous-controller.md",
"age": 295.4
}
]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783597602",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T19:46:42+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783597695",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T19:48:15+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783597737",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T19:48:57+0800",
"details": {
"ok": true,
"stuck": []
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"run_id": "c-1783598337",
"check": "stuck",
"status": "ok",
"ts": "2026-07-09T19:58:57+0800",
"details": {
"ok": true,
"stuck": []
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"run_id": "c-1783598937",
"check": "api",
"status": "ok",
"ts": "2026-07-09T20:08:57+0800",
"details": {
"ok": true,
"baseline": "V1R4C3",
"versions": 13
}
}

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