Skip to content

Commit 162d646

Browse files
Pigbibicodex
andcommitted
Add automation authority and ledger endpoints
Co-Authored-By: Codex <noreply@openai.com>
1 parent 09fc747 commit 162d646

11 files changed

Lines changed: 721 additions & 1 deletion

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ When a source issue contains a `codex-pr-feedback` marker from a failed CI run o
3333

3434
This avoids hard-coding Codex CLI setup in every source repository and avoids depending on a repository outside the `QuantStrategyLab` organization.
3535

36+
Automation authority is intentionally split from model confidence. The service treats routine low-risk maintenance as auto-merge eligible, allows already-live same-family strategy optimization to auto-merge only when the caller provides complete evidence flags, and keeps new/reconstructed strategies, live-candidate promotion, plugin position-control changes, broker/order execution changes, workflow permissions, and secrets behind human review. Use `POST /v1/ai/automation/authority` to evaluate that machine-readable policy before requesting live-impacting automation.
37+
38+
Automation run state is persisted through the service ledger. Async Codex jobs are recorded automatically; external workflows can also call `POST /v1/ai/automation/runs`. Operators and dashboards can read `GET /v1/ai/automation/runs`, `GET /v1/ai/automation/runs/{run_id}`, and `GET /v1/ai/automation/control` to decide whether to continue, pause auto-fix, switch to review-only, or escalate.
39+
3640
## Compatibility governance role
3741

3842
`QuantStrategyLab/AIAuditBridge` is an ops/control-plane consumer only:

README.zh-CN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ Codex 执行现在只走 service backend:workflow 从 GitHub-hosted runner 调
3333

3434
这样可以避免每个 source repository 都硬编码 Codex CLI,也不会依赖 `QuantStrategyLab` 组织外的仓库。
3535

36+
自动化授权层和模型置信度分开处理。service 将低风险日常维护视为可自动合并;已 live 的同族策略优化只有在调用方提供完整证据标记时才允许自动合并;新策略/重构策略、live-candidate 提升、插件 position-control、券商/订单执行、workflow 权限和 secrets 相关变更都保留人工复核。live 影响自动化前可调用 `POST /v1/ai/automation/authority` 评估这套机器可读 policy。
37+
38+
自动化运行状态通过 service ledger 持久化。异步 Codex job 会自动记录;外部 workflow 也可调用 `POST /v1/ai/automation/runs` 写入状态。Dashboard 或运维脚本可读取 `GET /v1/ai/automation/runs``GET /v1/ai/automation/runs/{run_id}``GET /v1/ai/automation/control`,判断继续执行、暂停 auto-fix、切到 review-only 或升级人工处理。
39+
3640
## 兼容性治理定位
3741

3842
`QuantStrategyLab/AIAuditBridge` 只作为 ops/control-plane 的消费侧参与兼容治理:

docs/architecture.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ operations: **Analyze**, **Execute**, and **Review**.
4343
| `health.py` | Online endpoint metrics, error rates, latency tracking |
4444
| `quota.py` | Per-repo budget enforcement, model cost estimation |
4545
| `autonomy.py` | Confidence scoring, change risk classification |
46+
| `automation_authority.py` | Machine-readable live-impact authority policy |
47+
| `automation_run_ledger.py` | Persistent automation run state and runtime control suggestions |
4648
| `feedback.py` | Closed-loop change tracking and evaluation |
4749

4850
### Adapters (`service/adapters/`)
@@ -70,6 +72,10 @@ operations: **Analyze**, **Execute**, and **Review**.
7072
| `/v1/ai/health` | GET | Online service health snapshot | No |
7173
| `/healthz` | GET | Liveness check | No |
7274
| `/v1/ai/quota` | GET | Provider usage, Codex account limits, and internal estimates | No |
75+
| `/v1/ai/automation/authority` | POST | Evaluate auto/human-review authority for live-impacting changes | No |
76+
| `/v1/ai/automation/control` | GET | Convert health/quota/org-health into continue/review-only/pause/escalate | No |
77+
| `/v1/ai/automation/runs` | GET/POST | Read or record automation run ledger entries | No |
78+
| `/v1/ai/automation/runs/{id}` | GET | Read one automation run ledger entry | No |
7379
| `/v1/ai/feedback/*` | GET/POST | Change tracking and evaluation | No |
7480

7581
Health status vocabulary is defined in [`health_taxonomy.md`](health_taxonomy.md). Do not use `/v1/ai/health` as a substitute for monthly audit results, artifact freshness, or strategy-level health evidence.
@@ -114,6 +120,7 @@ POST /v1/ai/review
114120
→ optional Codex verification
115121
→ extract confidence scores
116122
→ compute consensus + recommended action
123+
→ cap or promote action through automation authority policy
117124
→ return {results, consensus, recommended_action}
118125
```
119126

service/ai_gateway_service.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,15 @@
4444
from service.adapters.llm_adapter import LlmAdapter
4545
from service.adapters.codex_adapter import CodexAdapter
4646
from service.autonomy import (
47+
ACTION_AUTO_PR,
4748
load_autonomy_policy,
4849
recommended_action as compute_recommended_action,
4950
)
51+
from service.automation_authority import evaluate_automation_authority
52+
from service.automation_run_ledger import (
53+
get_automation_run_ledger,
54+
suggest_control_action,
55+
)
5056
from service.feedback import (
5157
write_change,
5258
read_change,
@@ -394,6 +400,7 @@ def _mark_stale_job_failed(job: dict[str, Any]) -> dict[str, Any]:
394400
job["updated_at"] = _now()
395401
job["error"] = "codex audit job became stale before completion"
396402
_write_job(job)
403+
_record_job_automation_run(job)
397404
return job
398405

399406

@@ -425,6 +432,41 @@ def _public_job_payload(job: dict[str, Any]) -> dict[str, object]:
425432
return payload
426433

427434

435+
def _automation_control_snapshot(repo: str) -> dict[str, Any]:
436+
try:
437+
org_health = read_org_health()
438+
except Exception:
439+
org_health = {"status": "unavailable"}
440+
try:
441+
quota_status = get_quota_manager().runtime_status(repo or "unknown")
442+
except Exception:
443+
quota_status = {"status": "unavailable"}
444+
return suggest_control_action(get_health_monitor().status, quota_status, org_health)
445+
446+
447+
def _record_job_automation_run(job: dict[str, Any]) -> None:
448+
try:
449+
repo = str(job.get("source_repository") or job.get("repository") or "unknown")
450+
control = _automation_control_snapshot(repo)
451+
get_automation_run_ledger().record(
452+
str(job.get("job_id") or ""),
453+
job_task_state(job),
454+
task_name=str(job.get("task") or ""),
455+
suggested_action=str(control.get("action") or ""),
456+
service_health=str(control.get("service_health") or ""),
457+
quota_status=str(control.get("quota_status") or ""),
458+
org_health_status=str(control.get("org_health_status") or ""),
459+
metadata={
460+
"repository": str(job.get("repository") or ""),
461+
"source_repository": str(job.get("source_repository") or ""),
462+
"source_ref": str(job.get("source_ref") or ""),
463+
"mode": str(job.get("mode") or ""),
464+
},
465+
)
466+
except Exception as exc:
467+
_audit_log("automation_ledger_record_failed", job_id=job.get("job_id"), error=type(exc).__name__)
468+
469+
428470
def _job_dedupe_key(payload: dict[str, Any]) -> str:
429471
issue_number = str(payload.get("issue_number") or "").strip()
430472
prompt_hash = hashlib.sha256(str(payload.get("prompt") or "").encode("utf-8")).hexdigest()
@@ -495,6 +537,7 @@ def _run_job(job_id: str, payload: dict[str, Any]) -> None:
495537
job["status"] = "running"
496538
job["updated_at"] = _now()
497539
_write_job(job)
540+
_record_job_automation_run(job)
498541
adapter = CodexAdapter()
499542
sandbox = _validate_sandbox(str(payload.get("sandbox") or ""))
500543
reasoning_effort = _resolve_codex_reasoning_effort(payload, str(payload.get("task") or TASK_EXECUTE))
@@ -516,6 +559,7 @@ def _run_job(job_id: str, payload: dict[str, Any]) -> None:
516559
job["failure_category"] = _classify_failure(result.error)
517560
job["updated_at"] = _now()
518561
_write_job(job)
562+
_record_job_automation_run(job)
519563
get_health_monitor().record(
520564
"/v1/ai/execute/jobs/run",
521565
time.time() - started,
@@ -534,6 +578,7 @@ def _run_job(job_id: str, payload: dict[str, Any]) -> None:
534578
job["error"] = str(exc)[-4000:]
535579
job["failure_category"] = _classify_failure(str(exc))
536580
_write_job(job)
581+
_record_job_automation_run(job)
537582
get_health_monitor().record("/v1/ai/execute/jobs/run", time.time() - started, False, type(exc).__name__)
538583
_audit_log("job_failed", job_id=job_id, error=type(exc).__name__,
539584
repository=job.get("repository"))
@@ -575,6 +620,7 @@ def _submit_job(claims: dict[str, Any], payload: dict[str, Any]) -> dict[str, ob
575620
"dedupe_key": dedupe_key,
576621
}
577622
_write_job(job)
623+
_record_job_automation_run(job)
578624
_audit_log("job_submitted", job_id=job_id, repository=job["repository"],
579625
task=job["task"], source_repository=job["source_repository"])
580626
thread = threading.Thread(target=_run_job, args=(job_id, payload), name=f"ai-gateway-job-{job_id}", daemon=True)
@@ -626,6 +672,16 @@ def do_GET(self) -> None:
626672
if request_path == "/v1/ai/quota":
627673
self._handle_quota_status()
628674
return
675+
if request_path == "/v1/ai/automation/control":
676+
self._handle_automation_control()
677+
return
678+
if request_path == "/v1/ai/automation/runs":
679+
self._handle_list_automation_runs()
680+
return
681+
if request_path.startswith("/v1/ai/automation/runs/"):
682+
run_id = request_path[len("/v1/ai/automation/runs/"):]
683+
self._handle_get_automation_run(run_id)
684+
return
629685

630686
# Feedback: list changes or get effectiveness report
631687
if request_path == "/v1/ai/changes/effectiveness":
@@ -698,6 +754,12 @@ def do_POST(self) -> None:
698754
elif self.path in {"/v1/ai/execute/jobs", "/v1/codex-audit/jobs"}:
699755
_assert_write_authz(claims, self.path)
700756
self._handle_execute_async(claims, payload)
757+
elif self.path in {"/v1/ai/automation/runs"}:
758+
_assert_write_authz(claims, self.path)
759+
self._handle_record_automation_run(claims, payload)
760+
elif self.path in {"/v1/ai/automation/authority"}:
761+
_assert_write_authz(claims, self.path)
762+
self._handle_automation_authority(claims, payload)
701763
elif self.path in {"/v1/ai/execute", "/v1/codex-audit"}:
702764
_assert_write_authz(claims, self.path)
703765
self._handle_execute_sync(claims, payload)
@@ -913,6 +975,7 @@ def _handle_review(self, payload: dict[str, Any]) -> None:
913975
changed_paths,
914976
repo=repo if repo else None,
915977
policy=load_autonomy_policy(),
978+
automation_metadata=payload.get("automation_metadata") if isinstance(payload.get("automation_metadata"), dict) else {},
916979
health_status=get_health_monitor().status,
917980
quota_status=str(quota_status),
918981
)
@@ -926,6 +989,77 @@ def _handle_review(self, payload: dict[str, Any]) -> None:
926989
"recommended_action": action,
927990
})
928991

992+
# -- automation control handlers --
993+
994+
def _handle_automation_control(self) -> None:
995+
from urllib.parse import parse_qs, urlparse
996+
try:
997+
authenticate(self.headers, audience=DEFAULT_AUDIENCE)
998+
except PermissionError as exc:
999+
_json_response(self, HTTPStatus.UNAUTHORIZED, {"status": "error", "error": str(exc)})
1000+
return
1001+
parsed = urlparse(self.path)
1002+
params = parse_qs(parsed.query)
1003+
repo = str(params.get("repo", ["unknown"])[0] or "unknown")
1004+
_json_response(self, HTTPStatus.OK, {"status": "ok", "control": _automation_control_snapshot(repo)})
1005+
1006+
def _handle_list_automation_runs(self) -> None:
1007+
from urllib.parse import parse_qs, urlparse
1008+
try:
1009+
authenticate(self.headers, audience=DEFAULT_AUDIENCE)
1010+
except PermissionError as exc:
1011+
_json_response(self, HTTPStatus.UNAUTHORIZED, {"status": "error", "error": str(exc)})
1012+
return
1013+
parsed = urlparse(self.path)
1014+
params = parse_qs(parsed.query)
1015+
limit = int(params.get("limit", ["100"])[0])
1016+
include_events = str(params.get("include_events", ["false"])[0]).lower() in {"1", "true", "yes", "on"}
1017+
snapshot = get_automation_run_ledger().snapshot(limit=limit, include_events=include_events)
1018+
_json_response(self, HTTPStatus.OK, {"status": "ok", "ledger": snapshot})
1019+
1020+
def _handle_get_automation_run(self, run_id: str) -> None:
1021+
try:
1022+
authenticate(self.headers, audience=DEFAULT_AUDIENCE)
1023+
except PermissionError as exc:
1024+
_json_response(self, HTTPStatus.UNAUTHORIZED, {"status": "error", "error": str(exc)})
1025+
return
1026+
record = get_automation_run_ledger().get(run_id)
1027+
if record is None:
1028+
_json_response(self, HTTPStatus.NOT_FOUND, {"status": "error", "error": "automation run not found"})
1029+
return
1030+
_json_response(self, HTTPStatus.OK, {"status": "ok", "run": record})
1031+
1032+
def _handle_record_automation_run(self, claims: dict[str, Any], payload: dict[str, Any]) -> None:
1033+
source_repo = str(payload.get("source_repository") or "")
1034+
if source_repo:
1035+
_validate_source_repo(source_repo)
1036+
_validate_source_repo_org(claims, source_repo)
1037+
repo = source_repo or str(claims.get("repository") or "unknown")
1038+
control = _automation_control_snapshot(repo)
1039+
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
1040+
record = get_automation_run_ledger().record(
1041+
str(payload.get("run_id") or payload.get("job_id") or ""),
1042+
str(payload.get("task_state") or payload.get("state") or "running"),
1043+
task_name=str(payload.get("task") or payload.get("task_name") or ""),
1044+
suggested_action=str(payload.get("suggested_action") or control.get("action") or ""),
1045+
service_health=str(payload.get("service_health") or control.get("service_health") or ""),
1046+
quota_status=payload.get("quota_status") or control.get("quota_status") or "",
1047+
org_health_status=str(payload.get("org_health_status") or control.get("org_health_status") or ""),
1048+
metadata={**metadata, "source_repository": source_repo},
1049+
)
1050+
_json_response(self, HTTPStatus.OK, {"status": "ok", "run": record, "control": control})
1051+
1052+
def _handle_automation_authority(self, claims: dict[str, Any], payload: dict[str, Any]) -> None:
1053+
source_repo = str(payload.get("source_repository") or "")
1054+
if source_repo:
1055+
_validate_source_repo(source_repo)
1056+
_validate_source_repo_org(claims, source_repo)
1057+
changed_paths = [str(path) for path in payload.get("changed_paths", []) if isinstance(path, str)]
1058+
metadata = payload.get("automation_metadata") if isinstance(payload.get("automation_metadata"), dict) else {}
1059+
proposed_action = str(payload.get("proposed_action") or ACTION_AUTO_PR)
1060+
authority = evaluate_automation_authority(changed_paths, metadata=metadata, proposed_action=proposed_action)
1061+
_json_response(self, HTTPStatus.OK, {"status": "ok", "automation_authority": authority})
1062+
9291063
# -- feedback handlers (Phase 3: closed-loop change tracking) --
9301064

9311065
def _handle_feedback_register(self, claims: dict[str, Any], payload: dict[str, Any]) -> None:

0 commit comments

Comments
 (0)