diff --git a/.github/workflows/monthly-orchestrator.yml b/.github/workflows/monthly-orchestrator.yml
index 731a6a4b..736b1cf5 100644
--- a/.github/workflows/monthly-orchestrator.yml
+++ b/.github/workflows/monthly-orchestrator.yml
@@ -117,7 +117,7 @@ jobs:
...targets.map(t => `- ${t}`),
'',
'Only snapshot artifact repositories are in scope for monthly report audits.',
- 'Each source repository owns its monthly review issue and dispatches CodexAuditBridge with source_repo and issue_number.',
+ 'Each source repository owns its monthly review issue and dispatches AIAuditBridge with source_repo and issue_number.',
'',
'Source of truth for policy and scheduling stays in GitHub Actions.',
'',
@@ -175,6 +175,6 @@ jobs:
echo "- Month: ${{ steps.resolve-month.outputs.month }}"
echo "- Tracking issue: ${{ steps.publish-monthly-issue.outputs.issue_url }}"
echo
- echo "CodexAuditBridge execution requires a source repository issue number."
+ echo "AIAuditBridge execution requires a source repository issue number."
echo "The source repositories create those issues in their own monthly workflows, then dispatch codex_audit.yml with source_repo and issue_number."
} >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/vps_codex_service_ops.yml b/.github/workflows/vps_codex_service_ops.yml
index ba25b433..567a7cd8 100644
--- a/.github/workflows/vps_codex_service_ops.yml
+++ b/.github/workflows/vps_codex_service_ops.yml
@@ -39,6 +39,11 @@ jobs:
- name: Run VPS service operation
env:
CODEX_AUDIT_SSH_UNBAN_IP: ${{ inputs.ssh_unban_ip }}
+ CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES: QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge
+ CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS: QuantStrategyLab/AIAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main,QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml@refs/heads/main,QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml@refs/pull/*/merge,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_pr_review.yml@refs/heads/main,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_pr_review.yml@refs/pull/*/merge
+ CODEX_AUDIT_SERVICE_ALLOWED_REFS: refs/heads/main,refs/pull/*/merge
+ CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES: QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge,QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines
CODEX_AUDIT_SERVICE_MODEL: ${{ vars.CODEX_AUDIT_SERVICE_MODEL }}
CODEX_AUDIT_SERVICE_REASONING_EFFORT: ${{ vars.CODEX_AUDIT_SERVICE_REASONING_EFFORT }}
+ CODEX_AUDIT_SERVICE_TOKEN: ${{ secrets.CODEX_AUDIT_SERVICE_TOKEN }}
run: bash scripts/deploy_codex_audit_service.sh "${{ inputs.mode }}"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ad509f0e..fb45d05b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,13 +2,13 @@
## 中文摘要
-- 用途:本文档说明如何向 `CodexAuditBridge` 提交低风险、可审阅的变更。
+- 用途:本文档说明如何向 `AIAuditBridge` 提交低风险、可审阅的变更。
- 主要覆盖:`Ground Rules`、`Documentation Standards`、`Branching and Pull Requests`、`Local Verification`。
- 阅读顺序:先确认仓库边界和变更范围,再运行适合本仓库的本地校验。
- 风险提示:涉及策略、artifact、自动化、密钥、云资源、券商或交易所行为的变更,必须先用测试环境、dry-run 或只读证据验证;不要只凭示例修改生产。
- 英文正文保留更完整的命令、字段名和配置键;如果摘要和正文不一致,以正文中的实际命令和配置为准。
-Thanks for contributing to `CodexAuditBridge`.
+Thanks for contributing to `AIAuditBridge`.
## Ground Rules
diff --git a/README.md b/README.md
index e7d4db25..4acf5cb6 100644
--- a/README.md
+++ b/README.md
@@ -33,11 +33,11 @@ This avoids hard-coding Codex CLI setup in every source repository and avoids de
## Compatibility governance role
-Compatibility governance metadata in this repository is ops/control-plane only:
+`QuantStrategyLab/AIAuditBridge` is an ops/control-plane consumer only:
-- It aligns audit/review execution contracts across QuantStrategyLab repositories.
+- It consumes compatibility governance metadata to align audit/review execution.
- It must **not** participate in trading runtime dependency graphs or strategy/runtime upgrade flows.
-- All governance references from this repo should be interpreted as control-plane/tooling compatibility, not runtime coupling.
+- All governance references here are for control-plane operation and should not be interpreted as runtime coupling.
## Supported source repositories
@@ -99,12 +99,12 @@ Configure these values in `QuantStrategyLab/AIAuditBridge`:
Run the service host with:
```bash
-CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/AIAuditBridge \
-CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
+CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge \
+CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge,QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
CODEX_AUDIT_SERVICE_MODEL=gpt-5.4 \
CODEX_AUDIT_SERVICE_REASONING_EFFORT=auto \
-python3 scripts/codex_audit_service.py
+python3 -m service.ai_gateway_service
```
Terminate TLS on 443 with the platform load balancer or a reverse proxy and forward `/v1/codex-audit` to the service port. Do not pass GitHub write tokens to this service.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 96653c19..748a0720 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -33,12 +33,13 @@ Codex 执行现在只走 service backend:workflow 从 GitHub-hosted runner 调
## 兼容性治理定位
-本仓库中的兼容性治理元数据只用于 ops/control-plane:
+`QuantStrategyLab/AIAuditBridge` 只作为 ops/control-plane 的消费侧参与兼容治理:
-- 对齐 QuantStrategyLab 各仓库的审计/评审执行契约;
+- 仅消费兼容矩阵和治理元数据,确保审计/评审边界行为一致;
- 不参与策略/交易运行时的依赖图、升级决策或 runtime 级联;
- 本仓库中的兼容关系只用于审计与 review 运营(control-plane),不应被源仓库当作交易策略运行时依赖。
+
## 支持的 source repository
| Source repository | 允许的 task |
@@ -75,11 +76,11 @@ AIAuditBridge 只使用 service backend。workflow 运行在 `ubuntu-latest`,
service host 启动示例:
```bash
-CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/AIAuditBridge \
-CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
+CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge \
+CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge,QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
CODEX_AUDIT_SERVICE_MODEL=gpt-5.4 \
-python3 scripts/codex_audit_service.py
+python3 -m service.ai_gateway_service
```
443/TLS 建议由平台负载均衡或反向代理负责,并把 `/v1/codex-audit` 转发到 service 端口。不要把 GitHub 写 token 传给这个 service。
diff --git a/SECURITY.md b/SECURITY.md
index 77e6fd31..5f2be942 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,13 +2,13 @@
## 中文摘要
-- 用途:本文档说明如何报告 `CodexAuditBridge` 的安全问题,以及密钥或凭证暴露时的处理顺序。
+- 用途:本文档说明如何报告 `AIAuditBridge` 的安全问题,以及密钥或凭证暴露时的处理顺序。
- 主要覆盖:`Reporting a Vulnerability`、`Secret and Credential Exposure`、`Scope Notes`。
- 阅读顺序:发现问题后先避免公开泄露,再通过私密渠道提供最小复现信息。
- 风险提示:涉及实盘、密钥、权限、Cloud Run、GitHub Actions、交易所或券商 API 的问题,不要开公开 issue 或贴出敏感日志。
- 英文正文保留更完整的命令、字段名和配置键;如果摘要和正文不一致,以正文中的实际命令和配置为准。
-Thanks for helping keep `CodexAuditBridge` safe.
+Thanks for helping keep `AIAuditBridge` safe.
This repository is part of the QuantStrategyLab automation, research, or trading-support surface. Please do **not** open a public issue for vulnerabilities involving credentials, broker or exchange access, cloud resources, workflow tokens, private market data, account identifiers, order execution, or secret material.
diff --git a/cloudflare/ai-gateway-dash/README.md b/cloudflare/ai-gateway-dash/README.md
index 2b2b1b81..d58a127d 100644
--- a/cloudflare/ai-gateway-dash/README.md
+++ b/cloudflare/ai-gateway-dash/README.md
@@ -14,6 +14,9 @@ Cloudflare Worker that serves an operations dashboard for the AiGateway service.
| `AI_GATEWAY_ORIGIN_URL` | VPS origin URL (e.g. `https://43.156.238.238.sslip.io`) |
| `DASHBOARD_API_TOKEN` | Static token for read-only API access |
+`DASHBOARD_API_TOKEN` must match the VPS service `CODEX_AUDIT_SERVICE_TOKEN`
+so the dashboard can read `/v1/ai/*` endpoints.
+
## Deploy
```bash
diff --git a/cloudflare/ai-gateway-dash/src/index.mjs b/cloudflare/ai-gateway-dash/src/index.mjs
index 8fff9ea6..02c900df 100644
--- a/cloudflare/ai-gateway-dash/src/index.mjs
+++ b/cloudflare/ai-gateway-dash/src/index.mjs
@@ -30,38 +30,65 @@ const DASHBOARD_API_ROUTES = new Set([
const LOGIN_HTML = `
-
-AiGateway · 登录
+
+
+
+AiGateway · QuantStrategyLab
+ *{margin:0;padding:0;box-sizing:border-box}
+ body{min-height:100vh;padding:28px;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:var(--text);background:radial-gradient(circle at 18% 80%,rgba(59,130,246,.18),transparent 28%),radial-gradient(circle at 74% 18%,rgba(139,92,246,.16),transparent 24%),linear-gradient(145deg,#050914,#07111d 52%,#04070f);-webkit-font-smoothing:antialiased}
+ body:before{content:"";position:fixed;inset:0;pointer-events:none;background-image:linear-gradient(rgba(148,163,184,.028) 1px,transparent 1px),linear-gradient(90deg,rgba(148,163,184,.028) 1px,transparent 1px);background-size:46px 46px;mask-image:linear-gradient(to bottom,rgba(0,0,0,.72),transparent)}
+ .page{position:relative;min-height:calc(100vh - 56px);display:grid;grid-template-rows:auto 1fr auto;max-width:1120px;margin:0 auto;border:1px solid rgba(148,163,184,.10);border-radius:28px;background:linear-gradient(180deg,rgba(7,15,28,.72),rgba(5,9,20,.86));box-shadow:var(--shadow);overflow:hidden}
+ .page:after{content:"";position:absolute;left:-120px;bottom:-120px;width:440px;height:260px;background:radial-gradient(ellipse at center,rgba(34,211,238,.18),transparent 68%);filter:blur(10px);pointer-events:none}
+ header{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:24px 28px;border-bottom:1px solid rgba(148,163,184,.09)}
+ .brand{display:flex;align-items:center;gap:12px;font-weight:780;font-size:20px;letter-spacing:-.04em}.brand span{color:#8aa0bd;font-size:13px;font-weight:500;letter-spacing:0}.mark{width:34px;height:34px;border-radius:12px;display:grid;place-items:center;color:#60a5fa;background:linear-gradient(135deg,rgba(59,130,246,.22),rgba(139,92,246,.18));border:1px solid rgba(96,165,250,.34)}.mark svg{width:20px;height:20px}.top-login{color:#cbd5e1;text-decoration:none;font-size:13px;border:1px solid rgba(148,163,184,.18);border-radius:12px;padding:9px 13px;background:rgba(15,28,47,.62)}
+ main{display:grid;place-items:center;padding:64px 28px 50px;text-align:center}.hero{width:min(680px,100%)}
+ h1{font-size:clamp(48px,7vw,76px);line-height:.95;letter-spacing:-.075em;color:#fff;margin-bottom:16px}.org{font-size:clamp(28px,4vw,42px);font-weight:760;letter-spacing:-.055em;background:linear-gradient(90deg,#38bdf8,#8b5cf6);-webkit-background-clip:text;background-clip:text;color:transparent;margin-bottom:16px}.tagline{font-size:18px;color:#d7e2f1;margin-bottom:26px}.desc{max-width:590px;margin:0 auto 38px;color:var(--muted);font-size:15px;line-height:1.9}
+ .login-title{font-size:28px;font-weight:760;letter-spacing:-.04em;margin-bottom:10px}.login-note{color:var(--muted);font-size:14px;margin-bottom:24px}.btn{display:inline-flex;align-items:center;justify-content:center;gap:11px;min-width:282px;padding:14px 22px;border-radius:13px;background:linear-gradient(135deg,var(--blue),var(--violet));color:#fff;text-decoration:none;font-size:16px;font-weight:760;box-shadow:0 18px 44px rgba(59,130,246,.24);transition:transform .18s ease,box-shadow .18s ease}.btn:hover{transform:translateY(-1px);box-shadow:0 22px 52px rgba(59,130,246,.32)}.btn svg{width:21px;height:21px;fill:currentColor}.limited{margin-top:14px;color:var(--muted2);font-size:13px}.err{display:none;max-width:560px;margin:0 auto 22px;padding:12px 14px;border-radius:13px;background:rgba(248,113,113,.11);border:1px solid rgba(248,113,113,.35);color:var(--danger);font-size:13px;text-align:left}
+ .capabilities{width:min(560px,100%);margin:42px auto 0;padding:22px;border:1px solid rgba(148,163,184,.16);border-radius:18px;background:linear-gradient(180deg,rgba(15,28,47,.70),rgba(8,16,30,.64));text-align:left}.capabilities h2{font-size:16px;margin-bottom:14px;letter-spacing:-.02em}.capabilities ul{display:grid;gap:11px;list-style:none;color:var(--muted);font-size:14px}.capabilities li{display:flex;gap:10px;align-items:flex-start}.capabilities li:before{content:"";width:16px;height:16px;flex:0 0 16px;margin-top:2px;border-radius:50%;border:1px solid rgba(147,197,253,.55);background:radial-gradient(circle at center,rgba(59,130,246,.65) 0 3px,transparent 4px)}
+ footer{position:relative;z-index:1;display:flex;justify-content:center;gap:12px;padding:24px 28px;color:var(--muted2);font-size:12px;border-top:1px solid rgba(148,163,184,.08)}
+ @media(max-width:720px){body{padding:14px}.page{min-height:calc(100vh - 28px);border-radius:22px}header{padding:18px;align-items:flex-start}.brand{align-items:flex-start;flex-direction:column;gap:6px}.top-login{display:none}main{padding:46px 20px 36px}.btn{width:100%;min-width:0}.capabilities{margin-top:32px;padding:18px}footer{flex-direction:column;align-items:center}}
+
+
-
-
⚡
-
AiGateway
-
QuantStrategyLab · 运维面板
GitHub 组织成员登录
-
-
-
- 使用 GitHub 登录
-
-
+
+
+ AiGateway
QuantStrategyLab
+ 组织成员登录
+
+
+
+ AiGateway
+ QuantStrategyLab
+ AI audit gateway operations
+ AiGateway 是 QuantStrategyLab 内部的 AI 审计与网关边界,统一承载 API 使用、配额控制、变更效果评估与影子审计,让 AI 能力在组织边界内安全、可观测、可追溯地运行。
+
+ 组织成员登录
+ 使用 GitHub 账号登录以查看内部运维数据。
+
+
+ 使用 GitHub 登录
+
+ 仅限 QuantStrategyLab 组织成员访问
+
+
登录后,您可以查看
+
+ - 服务整体健康状态与关键接口指标摘要
+ - 各仓库 / 团队的配额消耗与预算概览
+ - 变更有效性评估(90 天)
+ - 影子审计分歧统计与裁决结果
+ - 最近变更记录(7 天)
+
+
+
+
+
+
`;
// ── Helpers ────────────────────────────────────────────────────────────
@@ -274,6 +270,10 @@ function allowedDashboardApiPath(pathname) {
return DASHBOARD_API_ROUTES.has(clean) ? clean : "";
}
+function shouldIgnoreLegacyEndpointBase(basePath, pathname) {
+ return basePath === "/v1/codex-audit" && pathname.startsWith("/v1/ai/");
+}
+
export function buildDashboardApiUrl(rawOrigin, pathname, search = "") {
if (!rawOrigin || !rawOrigin.trim()) throw new Error("AI_GATEWAY_ORIGIN_URL not configured");
const clean = allowedDashboardApiPath(pathname);
@@ -281,7 +281,7 @@ export function buildDashboardApiUrl(rawOrigin, pathname, search = "") {
const origin = new URL(rawOrigin.trim());
if (origin.protocol !== "https:") throw new Error("AI_GATEWAY_ORIGIN_URL must use HTTPS");
const basePath = withoutTrailingSlash(origin.pathname);
- origin.pathname = !basePath || basePath === "/" ? clean : basePath + clean;
+ origin.pathname = !basePath || basePath === "/" || shouldIgnoreLegacyEndpointBase(basePath, clean) ? clean : basePath + clean;
origin.search = search;
origin.hash = "";
return origin.toString();
diff --git a/cloudflare/ai-gateway-dash/tests/index.test.mjs b/cloudflare/ai-gateway-dash/tests/index.test.mjs
index 00fb9571..76a160ab 100644
--- a/cloudflare/ai-gateway-dash/tests/index.test.mjs
+++ b/cloudflare/ai-gateway-dash/tests/index.test.mjs
@@ -17,6 +17,13 @@ test("buildDashboardApiUrl allows effectiveness route with query", () => {
);
});
+test("buildDashboardApiUrl maps AiGateway routes at root when origin is legacy audit endpoint", () => {
+ assert.equal(
+ buildDashboardApiUrl("https://origin.example/v1/codex-audit", "/v1/ai/health", ""),
+ "https://origin.example/v1/ai/health",
+ );
+});
+
test("buildDashboardApiUrl rejects unsupported origin paths", () => {
assert.throws(
() => buildDashboardApiUrl("https://origin.example", "/v1/ai/execute/jobs", ""),
diff --git a/cloudflare/codex-audit-proxy/README.md b/cloudflare/codex-audit-proxy/README.md
index 36885c9e..ccf7e5b1 100644
--- a/cloudflare/codex-audit-proxy/README.md
+++ b/cloudflare/codex-audit-proxy/README.md
@@ -1,6 +1,6 @@
-# Cloudflare Worker Proxy for CodexAuditBridge
+# Cloudflare Worker Proxy for AIAuditBridge
-This Worker provides a free `workers.dev` HTTPS entry point for CodexAuditBridge when no custom domain is available.
+This Worker provides a free `workers.dev` HTTPS entry point for AIAuditBridge when no custom domain is available.
This Worker should stay separate from the Pigbibi CodexGateway Worker so origin URLs, repository allowlists, and logs remain isolated.
@@ -28,7 +28,7 @@ The production shape is:
quantstrategylab-codex-audit-proxy -> VPS HTTPS origin -> codex-audit-service
```
-After deploy, set the `CodexAuditBridge` GitHub secret `CODEX_AUDIT_SERVICE_URL` to the Worker URL, for example:
+After deploy, set the `AIAuditBridge` GitHub secret `CODEX_AUDIT_SERVICE_URL` to the Worker URL, for example:
```text
https://quantstrategylab-codex-audit-proxy.
.workers.dev
diff --git a/cloudflare/codex-audit-proxy/src/index.mjs b/cloudflare/codex-audit-proxy/src/index.mjs
index e5d2a0aa..370e7343 100644
--- a/cloudflare/codex-audit-proxy/src/index.mjs
+++ b/cloudflare/codex-audit-proxy/src/index.mjs
@@ -1,4 +1,4 @@
-/** CodexAuditBridge Cloudflare proxy — forwards authenticated requests to VPS origin.
+/** AIAuditBridge Cloudflare proxy — forwards authenticated requests to VPS origin.
Allowed paths (expanded for AiGateway v2):
GET /healthz
@@ -82,6 +82,10 @@ function withoutTrailingSlash(pathname) {
return pathname.replace(/\/+$/, "");
}
+function shouldIgnoreLegacyEndpointBase(basePath, pathname) {
+ return basePath === "/v1/codex-audit" && pathname.startsWith("/v1/ai/");
+}
+
/** Check if pathname matches any allowed route or is a valid sub-path (jobs/{id}, changes/{id}). */
function matchRoute(pathname) {
const clean = withoutTrailingSlash(pathname);
@@ -136,7 +140,7 @@ export function buildOriginUrl(rawOriginUrl, pathname, search = "") {
throw new Error("CODEX_AUDIT_ORIGIN_URL must use HTTPS");
}
const basePath = withoutTrailingSlash(origin.pathname);
- if (!basePath || basePath === "/") {
+ if (!basePath || basePath === "/" || shouldIgnoreLegacyEndpointBase(basePath, pathname)) {
origin.pathname = pathname;
} else if (pathname === basePath || pathname.startsWith(basePath + "/")) {
origin.pathname = pathname;
diff --git a/cloudflare/codex-audit-proxy/tests/index.test.mjs b/cloudflare/codex-audit-proxy/tests/index.test.mjs
index df160839..8c8b7b5f 100644
--- a/cloudflare/codex-audit-proxy/tests/index.test.mjs
+++ b/cloudflare/codex-audit-proxy/tests/index.test.mjs
@@ -33,6 +33,13 @@ test("buildOriginUrl maps async status route next to audit endpoint", () => {
);
});
+test("buildOriginUrl maps AiGateway routes at root when origin is legacy audit endpoint", () => {
+ assert.equal(
+ buildOriginUrl("https://origin.example/v1/codex-audit", "/v1/ai/health"),
+ "https://origin.example/v1/ai/health",
+ );
+});
+
test("buildOriginUrl preserves nested base path", () => {
assert.equal(
buildOriginUrl("https://origin.example/codex", "/v1/codex-audit"),
diff --git a/docs/async_service_deployment.md b/docs/async_service_deployment.md
index 3693949a..cdd045d8 100644
--- a/docs/async_service_deployment.md
+++ b/docs/async_service_deployment.md
@@ -15,7 +15,7 @@ The synchronous `POST /v1/codex-audit` endpoint remains available for local diag
## Boundary with Pigbibi CodexGateway
-`AIAuditBridge` intentionally stays separate from `Pigbibi/AIGateway`.
+`AIAuditBridge` intentionally stays separate from `Pigbibi/CodexGateway`.
- `CodexGateway` is a generic Codex invocation facade for prompt/context/image/schema calls.
- `AIAuditBridge` owns QuantStrategyLab monthly audit semantics: source issue context, bounded repository snapshots, service patch contracts, source repository allowlists, GitHub App writeback, and generated remediation PRs.
@@ -29,11 +29,11 @@ The historical self-hosted direct-Codex workflows in `SelfHostedCodexAuditBridge
- Source repositories, including public QuantStrategyLab repositories, must not store provider keys or the Codex service URL.
- Source repositories should only dispatch the bridge workflow and provide issue/source context. Avoid running Codex directly in public workflows.
- `QuantStrategyLab/AIAuditBridge` is public, so it may contain only client/orchestration code. Its service URL, provider fallback keys, GitHub App private key, and Cloudflare origin stay in GitHub or Cloudflare secrets.
-- The VPS service should allow only `QuantStrategyLab/AIAuditBridge` in `CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES`; this OIDC allowlist is required because the bridge repository is public.
-- The VPS service should require `CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS=QuantStrategyLab/AIAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main`.
-- The VPS service should require `CODEX_AUDIT_SERVICE_ALLOWED_REFS=refs/heads/main`.
+- The VPS service should allow `QuantStrategyLab/AIAuditBridge` in `CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES`; keep `QuantStrategyLab/CodexAuditBridge` only as temporary migration compatibility.
+- The VPS service should require explicit `CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS` for the canonical `AIAuditBridge` workflows and any temporary legacy workflow refs.
+- The VPS service should keep `CODEX_AUDIT_SERVICE_ALLOWED_REFS` as narrow as the enabled workflows allow, normally `refs/heads/main` plus `refs/pull/*/merge` for PR review smoke tests.
- Keep `CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORY_VISIBILITIES=public` unless the bridge repository is intentionally private.
-- The VPS service should keep `CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES` limited to the current source repositories.
+- The VPS service should keep `CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES` limited to current audit source repositories and PR review targets.
- The Cloudflare Worker stores only `CODEX_AUDIT_ORIGIN_URL` as a Worker secret. Do not commit the origin URL if it exposes infrastructure details.
- Job IDs are random and status reads still require service authentication. Job responses never include the original prompt.
- Static service bearer tokens are no longer supported; production calls must use GitHub Actions OIDC.
@@ -55,11 +55,11 @@ For public source repositories:
After merging the async service code, run the manual `VPS Codex Service Ops` workflow with deploy mode, or run on the VPS:
```bash
-CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/AIAuditBridge \
-CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS='QuantStrategyLab/AIAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main' \
-CODEX_AUDIT_SERVICE_ALLOWED_REFS='refs/heads/main' \
+CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge \
+CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS='QuantStrategyLab/AIAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main,QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml@refs/heads/main,QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml@refs/pull/*/merge,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_pr_review.yml@refs/heads/main,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_pr_review.yml@refs/pull/*/merge' \
+CODEX_AUDIT_SERVICE_ALLOWED_REFS='refs/heads/main,refs/pull/*/merge' \
CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORY_VISIBILITIES='public' \
-CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
+CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge,QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
CODEX_AUDIT_SERVICE_MODEL=gpt-5.4 \
CODEX_AUDIT_SERVICE_REASONING_EFFORT=auto \
diff --git a/scripts/deploy_codex_audit_service.sh b/scripts/deploy_codex_audit_service.sh
index 8247dd2e..0a97ff58 100644
--- a/scripts/deploy_codex_audit_service.sh
+++ b/scripts/deploy_codex_audit_service.sh
@@ -8,10 +8,10 @@ DEPLOY_DIR="${CODEX_AUDIT_SERVICE_DEPLOY_DIR:-/opt/codex-audit-bridge}"
AUDIT_PORT="${CODEX_AUDIT_SERVICE_PORT:-8797}"
AUDIENCE="${CODEX_AUDIT_SERVICE_AUDIENCE:-quant-codex-audit}"
ALLOWED_REPOSITORIES="${CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES:-QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge}"
-ALLOWED_WORKFLOW_REFS="${CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS:-QuantStrategyLab/AIAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main}"
-ALLOWED_REFS="${CODEX_AUDIT_SERVICE_ALLOWED_REFS:-refs/heads/main}"
+ALLOWED_WORKFLOW_REFS="${CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS:-QuantStrategyLab/AIAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main,QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml@refs/heads/main,QuantStrategyLab/AIAuditBridge/.github/workflows/codex_pr_review.yml@refs/pull/*/merge,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_audit.yml@refs/heads/main,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_pr_review.yml@refs/heads/main,QuantStrategyLab/CodexAuditBridge/.github/workflows/codex_pr_review.yml@refs/pull/*/merge}"
+ALLOWED_REFS="${CODEX_AUDIT_SERVICE_ALLOWED_REFS:-refs/heads/main,refs/pull/*/merge}"
ALLOWED_REPOSITORY_VISIBILITIES="${CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORY_VISIBILITIES:-public}"
-ALLOWED_SOURCE_REPOSITORIES="${CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES:-QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines}"
+ALLOWED_SOURCE_REPOSITORIES="${CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES:-QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge,QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines}"
JOB_DIR="${CODEX_AUDIT_SERVICE_JOB_DIR:-/var/lib/codex-audit-bridge/jobs}"
AUDIT_MODEL="${CODEX_AUDIT_SERVICE_MODEL:-}"
AUDIT_REASONING_EFFORT="${CODEX_AUDIT_SERVICE_REASONING_EFFORT:-}"
@@ -48,6 +48,19 @@ systemctl_status_brief() {
fi
}
+systemctl_environment_brief() {
+ local service="$1"
+ if systemctl list-unit-files "${service}.service" >/dev/null 2>&1; then
+ echo "### ${service}.service environment"
+ systemctl show "$service" --property=Environment --no-pager 2>/dev/null \
+ | sed 's/^Environment=//' \
+ | tr ' ' '\n' \
+ | sed -E "s/^[\"']//; s/[\"']$//" \
+ | grep -E '^CODEX_AUDIT_SERVICE_(ALLOWED_|AUDIENCE=|HOST=|PORT=|JOB_DIR=|QUOTA_STORE=|SANDBOX=|MODEL=|REASONING_EFFORT=)' \
+ | mask_infra || true
+ fi
+}
+
sshd_bin() {
if command -v sshd >/dev/null 2>&1; then
command -v sshd
@@ -125,6 +138,10 @@ inspect() {
systemctl_status_brief caddy
echo
+ echo "## Codex audit service environment"
+ systemctl_environment_brief "$AUDIT_SERVICE_NAME"
+ echo
+
echo "## SSH access hints"
local sshd
sshd="$(sshd_bin || true)"
@@ -162,6 +179,14 @@ install_file() {
sudo install -D -m "$mode" "$source" "$target"
}
+install_service_package() {
+ sudo rm -rf "${DEPLOY_DIR}/service"
+ sudo install -d -m 0755 "${DEPLOY_DIR}/service"
+ sudo cp -R service/. "${DEPLOY_DIR}/service/"
+ sudo find "${DEPLOY_DIR}/service" -type d -exec chmod 0755 {} +
+ sudo find "${DEPLOY_DIR}/service" -type f -exec chmod 0644 {} +
+}
+
write_audit_service_unit() {
local runner_user runner_home
runner_user="$(id -un)"
@@ -174,6 +199,10 @@ write_audit_service_unit() {
if [ -n "$AUDIT_REASONING_EFFORT" ]; then
audit_reasoning_effort_line="Environment=CODEX_AUDIT_SERVICE_REASONING_EFFORT=${AUDIT_REASONING_EFFORT}"
fi
+ audit_token_line=""
+ if [ -n "${CODEX_AUDIT_SERVICE_TOKEN:-}" ]; then
+ audit_token_line="Environment=CODEX_AUDIT_SERVICE_TOKEN=${CODEX_AUDIT_SERVICE_TOKEN}"
+ fi
sudo tee "/etc/systemd/system/${AUDIT_SERVICE_NAME}.service" >/dev/null </dev/null < str:
[
"## Codex Audit",
"",
- "CodexAuditBridge stopped before making repository changes because the service backend failed outside the source PR/test surface.",
+ "AIAuditBridge stopped before making repository changes because the service backend failed outside the source PR/test surface.",
"",
f"- Failure category: `{category}`",
f"- Detail: `{sanitize_inline_code(reason, 600)}`",
@@ -741,7 +741,7 @@ def build_service_repository_context(
"## Repository context snapshot",
"",
"The service backend cannot access the source checkout directly. The following bounded text snapshot is provided "
- "by CodexAuditBridge after path filtering.",
+ "by AIAuditBridge after path filtering.",
"",
]
total = len("\n".join(parts).encode("utf-8"))
@@ -776,7 +776,7 @@ def service_patch_contract_instructions() -> str:
[
"## Service patch contract",
"",
- "You are running behind CodexAuditBridge's service backend. You cannot edit the checkout directly.",
+ "You are running behind AIAuditBridge's service backend. You cannot edit the checkout directly.",
"For review_and_fix mode, return exactly one JSON object and no surrounding prose:",
"",
"```json",
@@ -840,6 +840,16 @@ def codex_service_job_url(service_url: str, job_id: str) -> str:
return service_url.rstrip("/") + f"/jobs/{job_id}"
+def codex_service_api_url(service_url: str, api_path: str) -> str:
+ if not api_path.startswith("/v1/ai/"):
+ raise BridgeError("Codex audit service API path must start with /v1/ai/")
+ suffix = "/v1/codex-audit"
+ base = service_url.rstrip("/")
+ if base.endswith(suffix):
+ base = base[: -len(suffix)]
+ return base.rstrip("/") + api_path
+
+
def request_github_oidc_token(audience: str) -> str:
request_url = env_value("ACTIONS_ID_TOKEN_REQUEST_URL")
request_token = env_value("ACTIONS_ID_TOKEN_REQUEST_TOKEN")
@@ -966,6 +976,47 @@ def request_codex_service(
raise BridgeError("Codex audit service job timed out before completion")
+def register_gateway_change(
+ *,
+ source_repo: str,
+ task: str,
+ action: str,
+ risk: str,
+ changed_paths: list[str],
+ issue_number: int,
+ pr_number: int,
+ pr_url: str,
+) -> str:
+ """Best-effort write to AiGateway closed-loop change registry."""
+ raw_url = env_value("CODEX_AUDIT_SERVICE_URL")
+ if not raw_url:
+ return ""
+ try:
+ service_url = normalize_codex_service_url(raw_url)
+ audience = env_value("CODEX_AUDIT_SERVICE_AUDIENCE", DEFAULT_SERVICE_AUDIENCE)
+ request_codex_service_json(
+ method="POST",
+ url=codex_service_api_url(service_url, "/v1/ai/feedback/register"),
+ audience=audience,
+ payload={
+ "source_repository": source_repo,
+ "task": task,
+ "action": action,
+ "risk": risk,
+ "confidence": 0.0,
+ "changed_paths": changed_paths,
+ "before_metrics": {},
+ "issue_number": issue_number,
+ "pr_number": pr_number,
+ "external_url": pr_url,
+ },
+ timeout_seconds=30,
+ )
+ except BridgeError as exc:
+ return str(exc)
+ return ""
+
+
def extract_json_object(text: str) -> dict[str, Any]:
stripped = text.strip()
fence = re.fullmatch(r"```(?:json)?\s*(.*?)\s*```", stripped, flags=re.DOTALL | re.IGNORECASE)
@@ -1652,6 +1703,7 @@ def publish_remediation(
policy=workspace.baseline_auto_merge_policy,
diff_stats=diff_stats,
)
+ gateway_action = "auto_pr"
stale_auto_merge_label_removed = workspace.stale_auto_merge_label_removed
stale_auto_merge_label_error = ""
if (
@@ -1749,6 +1801,7 @@ def publish_remediation(
f"after Bridge classified the change set as `{guard['risk_level']}` risk. "
"CI and the source guard own the final merge decision."
)
+ gateway_action = "auto_merge"
except BridgeError as exc:
body_lines.append("")
body_lines.append(f"Auto-merge was requested but the guarded label could not be added: `{exc}`")
@@ -1767,6 +1820,18 @@ def publish_remediation(
elif workspace.feedback_retry_pr and workspace.stale_auto_merge_label_skip_reason:
body_lines.append("")
body_lines.append(stale_auto_merge_label_skip_message(workspace.stale_auto_merge_label_skip_reason))
+ gateway_warning = register_gateway_change(
+ source_repo=source_repo,
+ task=task,
+ action=gateway_action,
+ risk=str(guard_risk.get("risk_level") or ""),
+ changed_paths=paths,
+ issue_number=issue_number,
+ pr_number=int(pr["number"]),
+ pr_url=str(pr_url),
+ )
+ if gateway_warning:
+ print(f"warning: AiGateway change registration failed: {gateway_warning[:300]}", file=sys.stderr)
post_issue_comment(token, source_repo, issue_number, "\n".join(body_lines))
return 0
@@ -2411,7 +2476,7 @@ def blocked_paths(paths: list[str], *, task: str = DEFAULT_TASK) -> list[str]:
def truncate_markdown(text: str, limit: int = 12000) -> str:
if len(text) <= limit:
return text
- return text[:limit] + "\n\n...[truncated by CodexAuditBridge]"
+ return text[:limit] + "\n\n...[truncated by AIAuditBridge]"
def strip_audit_heading(text: str) -> str:
diff --git a/service/ai_gateway_service.py b/service/ai_gateway_service.py
index 0529ab34..faa1a323 100644
--- a/service/ai_gateway_service.py
+++ b/service/ai_gateway_service.py
@@ -485,6 +485,7 @@ def _find_active_job_by_dedupe_key(dedupe_key: str) -> dict[str, Any] | None:
def _run_job(job_id: str, payload: dict[str, Any]) -> None:
+ started = time.time()
try:
job = _read_job(job_id)
job["status"] = "running"
@@ -511,6 +512,12 @@ def _run_job(job_id: str, payload: dict[str, Any]) -> None:
job["failure_category"] = _classify_failure(result.error)
job["updated_at"] = _now()
_write_job(job)
+ get_health_monitor().record(
+ "/v1/ai/execute/jobs/run",
+ time.time() - started,
+ job["status"] == "succeeded",
+ str(job.get("failure_category") or job.get("error") or ""),
+ )
_audit_log("job_completed", job_id=job_id, status=job["status"],
repository=job.get("repository"), task=job.get("task"))
except Exception as exc:
@@ -523,6 +530,7 @@ def _run_job(job_id: str, payload: dict[str, Any]) -> None:
job["error"] = str(exc)[-4000:]
job["failure_category"] = _classify_failure(str(exc))
_write_job(job)
+ get_health_monitor().record("/v1/ai/execute/jobs/run", time.time() - started, False, type(exc).__name__)
_audit_log("job_failed", job_id=job_id, error=type(exc).__name__,
repository=job.get("repository"))
@@ -756,6 +764,7 @@ def _handle_analyze(self, payload: dict[str, Any]) -> None:
def _handle_execute_async(self, claims: dict[str, Any], payload: dict[str, Any]) -> None:
"""POST /v1/ai/execute/jobs — async Codex execution."""
+ started = time.time()
req = parse_execute_request(payload)
# Security: validate source_repository against allowlist
@@ -763,24 +772,27 @@ def _handle_execute_async(self, claims: dict[str, Any], payload: dict[str, Any])
if source_repo:
_validate_source_repo(source_repo)
_validate_source_repo_org(claims, source_repo)
+ quota_repo = source_repo or str(claims.get("repository") or "unknown")
# Quota check
quota = get_quota_manager()
- qr = quota.check(source_repo, "codex-cli", req.prompt)
+ qr = quota.check(quota_repo, "codex-cli", req.prompt)
if not qr["allowed"]:
_json_response(self, HTTPStatus.TOO_MANY_REQUESTS, {
"status": "error", "error": qr["reason"],
"remaining_usd": qr.get("remaining_usd", 0),
})
return
- quota.record_execute(source_repo)
+ quota.record_execute(quota_repo)
payload.setdefault("task", TASK_EXECUTE)
job = _submit_job(claims, payload)
+ get_health_monitor().record("/v1/ai/execute/jobs", time.time() - started, True)
_json_response(self, HTTPStatus.ACCEPTED, job)
def _handle_execute_sync(self, claims: dict[str, Any], payload: dict[str, Any]) -> None:
"""POST /v1/ai/execute — sync Codex execution (backward compat)."""
+ started = time.time()
req = parse_execute_request(payload)
source_repo = str(payload.get("source_repository") or "")
if source_repo:
@@ -806,6 +818,7 @@ def _handle_execute_sync(self, claims: dict[str, Any], payload: dict[str, Any])
reasoning_effort=reasoning_effort,
timeout=req.timeout_seconds,
)
+ get_health_monitor().record("/v1/ai/execute", time.time() - started, result.success, result.error if not result.success else "")
if result.success:
_json_response(self, HTTPStatus.OK, {"status": "ok", "output": result.output})
else:
@@ -899,16 +912,23 @@ def _handle_review(self, payload: dict[str, Any]) -> None:
def _handle_feedback_register(self, claims: dict[str, Any], payload: dict[str, Any]) -> None:
"""POST /v1/ai/feedback/register — register an autonomous change with pre-metrics."""
change_id = _new_change_id()
+ source_repo = str(payload.get("source_repository") or "")
+ if source_repo:
+ _validate_source_repo(source_repo)
+ _validate_source_repo_org(claims, source_repo)
record = ChangeRecord(
change_id=change_id,
- repo=str(claims.get("repository", "")),
+ repo=source_repo or str(claims.get("repository", "")),
task=str(payload.get("task", "")),
action=str(payload.get("action", "")),
confidence=float(payload.get("confidence", 0.0)),
risk=str(payload.get("risk", "")),
changed_paths=[str(p) for p in payload.get("changed_paths", []) if isinstance(p, str)],
before_metrics={str(k): float(v) for k, v in payload.get("before_metrics", {}).items()},
- source_repo=str(payload.get("source_repository", "")),
+ source_repo=source_repo,
+ external_url=str(payload.get("external_url", "")),
+ issue_number=int(payload["issue_number"]) if payload.get("issue_number") is not None else None,
+ pr_number=int(payload["pr_number"]) if payload.get("pr_number") is not None else None,
)
write_change(record)
_audit_log("change_registered", change_id=change_id, repo=record.repo,
diff --git a/service/feedback.py b/service/feedback.py
index 07b96143..1c6b2f5e 100644
--- a/service/feedback.py
+++ b/service/feedback.py
@@ -108,6 +108,9 @@ class ChangeRecord:
effect: str = EFFECT_PENDING
effect_detail: str = ""
rollback_issue_url: str = ""
+ external_url: str = ""
+ issue_number: int | None = None
+ pr_number: int | None = None
created_at: float = field(default_factory=_now)
evaluated_at: float | None = None
source_repo: str = ""
@@ -126,6 +129,9 @@ def to_dict(self) -> dict[str, Any]:
"effect": self.effect,
"effect_detail": self.effect_detail,
"rollback_issue_url": self.rollback_issue_url,
+ "external_url": self.external_url,
+ "issue_number": self.issue_number,
+ "pr_number": self.pr_number,
"created_at": self.created_at,
"evaluated_at": self.evaluated_at,
"source_repo": self.source_repo,
@@ -146,6 +152,9 @@ def from_dict(cls, d: dict[str, Any]) -> "ChangeRecord":
effect=str(d.get("effect", EFFECT_PENDING)),
effect_detail=str(d.get("effect_detail", "")),
rollback_issue_url=str(d.get("rollback_issue_url", "")),
+ external_url=str(d.get("external_url", "")),
+ issue_number=int(d["issue_number"]) if d.get("issue_number") is not None else None,
+ pr_number=int(d["pr_number"]) if d.get("pr_number") is not None else None,
created_at=float(d.get("created_at", _now())),
evaluated_at=float(d.get("evaluated_at", 0)) if d.get("evaluated_at") else None,
source_repo=str(d.get("source_repo", "")),
diff --git a/service/quota.py b/service/quota.py
index b7fb90ab..8089d724 100644
--- a/service/quota.py
+++ b/service/quota.py
@@ -141,6 +141,16 @@ def __init__(self):
self._weekly_budget = DEFAULT_WEEKLY_BUDGET_USD
self._repo_budgets: dict[str, dict[str, float]] = {}
self._load_config()
+ self._load_records()
+
+ def _store_path(self) -> Path | None:
+ path = os.environ.get("CODEX_AUDIT_SERVICE_QUOTA_STORE", "").strip()
+ if path:
+ return Path(path)
+ job_dir = os.environ.get("CODEX_AUDIT_SERVICE_JOB_DIR", "").strip()
+ if job_dir:
+ return Path(job_dir) / "quota.json"
+ return None
def _load_config(self) -> None:
config_path = os.environ.get("CODEX_AUDIT_SERVICE_QUOTA_CONFIG", "").strip()
@@ -156,6 +166,41 @@ def _load_config(self) -> None:
if isinstance(raw.get("repo_budgets"), dict):
self._repo_budgets = raw["repo_budgets"]
+ def _load_records(self) -> None:
+ path = self._store_path()
+ if path is None:
+ return
+ if not path.exists():
+ return
+ try:
+ raw = json.loads(path.read_text(encoding="utf-8"))
+ except (json.JSONDecodeError, OSError):
+ return
+ records = raw.get("records") if isinstance(raw, dict) else None
+ if not isinstance(records, dict):
+ return
+ self._records = {
+ repo: QuotaRecord.from_dict(item)
+ for repo, item in records.items()
+ if isinstance(repo, str) and isinstance(item, dict)
+ }
+
+ def _save_records_locked(self) -> None:
+ path = self._store_path()
+ if path is None:
+ return
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
+ payload = json.dumps(
+ {"records": {repo: record.to_dict() for repo, record in self._records.items()}},
+ ensure_ascii=False,
+ sort_keys=True,
+ ).encode("utf-8")
+ tmp = path.with_suffix(path.suffix + ".tmp")
+ with open(tmp, "wb") as handle:
+ handle.write(payload)
+ os.chmod(tmp, 0o600)
+ os.replace(tmp, path)
+
def _reset_if_needed(self, record: QuotaRecord) -> QuotaRecord:
now = time.time()
if now - record.last_reset_daily > 86400:
@@ -227,6 +272,7 @@ def record(self, repo: str, model: str, prompt: str, output: str = "") -> None:
record.codex_calls += 1
record.total_cost_usd += cost
self._records[repo] = record
+ self._save_records_locked()
def record_execute(self, repo: str) -> None:
"""Record a codex exec call (flat cost)."""
@@ -239,6 +285,7 @@ def record_execute(self, repo: str) -> None:
record.codex_calls += 1
record.total_cost_usd += cost
self._records[repo] = record
+ self._save_records_locked()
def status(self, repo: str = "") -> dict[str, Any]:
"""Get quota status for a repo or all repos."""
diff --git a/tests/test_ai_gateway_service_get_routes.py b/tests/test_ai_gateway_service_get_routes.py
new file mode 100644
index 00000000..cbbbb6b4
--- /dev/null
+++ b/tests/test_ai_gateway_service_get_routes.py
@@ -0,0 +1,83 @@
+import json
+import os
+import tempfile
+import threading
+import unittest
+import urllib.request
+from http.server import ThreadingHTTPServer
+from unittest.mock import patch
+
+from service.ai_gateway_service import AiGatewayRequestHandler
+
+
+class AiGatewayGetRoutesTest(unittest.TestCase):
+ def test_read_routes_accept_query_strings(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ env = {
+ "CODEX_AUDIT_SERVICE_AUTH": "none",
+ "CODEX_AUDIT_SERVICE_ALLOW_NO_AUTH_FOR_LOCAL_TESTS": "true",
+ "CODEX_AUDIT_SERVICE_JOB_DIR": tmp,
+ }
+ with patch.dict(os.environ, env, clear=False):
+ server = ThreadingHTTPServer(("127.0.0.1", 0), AiGatewayRequestHandler)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ base_url = f"http://127.0.0.1:{server.server_port}"
+ effectiveness_url = f"{base_url}/v1/ai/changes/effectiveness?days=90"
+ with urllib.request.urlopen(effectiveness_url, timeout=5) as response:
+ self.assertEqual(response.status, 200)
+ effectiveness = json.loads(response.read().decode("utf-8"))
+ self.assertEqual(effectiveness["status"], "ok")
+ self.assertIn("report", effectiveness)
+
+ quota_url = f"{base_url}/v1/ai/quota?repo=QuantStrategyLab/AIAuditBridge"
+ with urllib.request.urlopen(quota_url, timeout=5) as response:
+ self.assertEqual(response.status, 200)
+ quota = json.loads(response.read().decode("utf-8"))
+ self.assertEqual(quota["status"], "ok")
+ self.assertEqual(quota["quota"]["repo"], "QuantStrategyLab/AIAuditBridge")
+ finally:
+ server.shutdown()
+ server.server_close()
+
+ def test_feedback_register_uses_source_repository_for_change_feed(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ env = {
+ "CODEX_AUDIT_SERVICE_AUTH": "none",
+ "CODEX_AUDIT_SERVICE_ALLOW_NO_AUTH_FOR_LOCAL_TESTS": "true",
+ "CODEX_AUDIT_SERVICE_JOB_DIR": tmp,
+ }
+ with patch.dict(os.environ, env, clear=False):
+ server = ThreadingHTTPServer(("127.0.0.1", 0), AiGatewayRequestHandler)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ base_url = f"http://127.0.0.1:{server.server_port}"
+ payload = {
+ "source_repository": "local/repo",
+ "task": "monthly_snapshot_audit",
+ "action": "auto_pr",
+ "risk": "low",
+ "changed_paths": ["docs/runbook.md"],
+ "external_url": "https://example.test/pr/12",
+ "issue_number": 7,
+ "pr_number": 12,
+ }
+ request = urllib.request.Request(
+ f"{base_url}/v1/ai/feedback/register",
+ data=json.dumps(payload).encode("utf-8"),
+ method="POST",
+ headers={"Content-Type": "application/json"},
+ )
+ with urllib.request.urlopen(request, timeout=5) as response:
+ self.assertEqual(response.status, 200)
+
+ with urllib.request.urlopen(f"{base_url}/v1/ai/changes?days=7", timeout=5) as response:
+ changes = json.loads(response.read().decode("utf-8"))["changes"]
+ self.assertEqual(changes[0]["repo"], "local/repo")
+ self.assertEqual(changes[0]["external_url"], "https://example.test/pr/12")
+ self.assertEqual(changes[0]["pr_number"], 12)
+ finally:
+ server.shutdown()
+ server.server_close()
diff --git a/tests/test_monthly_orchestrator_workflow.py b/tests/test_monthly_orchestrator_workflow.py
index 03e34243..b8a57c7e 100644
--- a/tests/test_monthly_orchestrator_workflow.py
+++ b/tests/test_monthly_orchestrator_workflow.py
@@ -34,5 +34,5 @@ def test_monthly_orchestrator_does_not_dispatch_without_source_issue() -> None:
assert "peter-evans/repository-dispatch" not in text
assert "event-type: monthly-audit" not in text
- assert "CodexAuditBridge execution requires a source repository issue number." in text
+ assert "AIAuditBridge execution requires a source repository issue number." in text
assert "source_repo and issue_number" in text
diff --git a/tests/test_quota.py b/tests/test_quota.py
index bcb64a22..aa18c65d 100644
--- a/tests/test_quota.py
+++ b/tests/test_quota.py
@@ -149,6 +149,17 @@ def test_get_weekly_budget_respects_repo_overrides(self) -> None:
self.manager._repo_budgets["premium/repo"] = {"weekly": 250.0}
self.assertEqual(self.manager.get_weekly_budget("premium/repo"), 250.0)
+ def test_records_persist_to_store(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ store = str(Path(tmp) / "quota.json")
+ with patch.dict(os.environ, {"CODEX_AUDIT_SERVICE_QUOTA_STORE": store}):
+ first = QuotaManager()
+ first.record_execute("test/repo")
+ second = QuotaManager()
+ status = second.status("test/repo")
+ self.assertEqual(status["codex_calls"], 1)
+ self.assertGreater(status["total_cost_usd"], 0)
+
class TestQuotaConfigLoading(unittest.TestCase):
"""Quota configuration from JSON file."""
diff --git a/tests/test_run_monthly_codex_audit.py b/tests/test_run_monthly_codex_audit.py
index 561cd546..0edd8362 100644
--- a/tests/test_run_monthly_codex_audit.py
+++ b/tests/test_run_monthly_codex_audit.py
@@ -28,6 +28,7 @@
build_service_prompt,
classify_service_failure,
classify_guarded_auto_merge_risk,
+ codex_service_api_url,
codex_service_job_url,
codex_service_jobs_url,
convert_local_markdown_links,
@@ -101,6 +102,12 @@ def test_validate_repo_accepts_owner_repo(self) -> None:
with self.subTest(source_repo=source_repo):
self.assertEqual(validate_repo(source_repo), source_repo)
+ def test_codex_service_api_url_maps_legacy_endpoint_to_ai_route(self) -> None:
+ self.assertEqual(
+ codex_service_api_url("https://codex.quant.example/v1/codex-audit", "/v1/ai/feedback/register"),
+ "https://codex.quant.example/v1/ai/feedback/register",
+ )
+
def test_validate_repo_rejects_invalid_values(self) -> None:
with self.assertRaises(Exception):
validate_repo("QuantStrategyLab/CryptoLivePoolPipelines/extra")
@@ -2255,6 +2262,11 @@ def test_vps_ops_workflow_runs_only_manual_self_hosted_ops(self) -> None:
self.assertIn("- self-hosted", workflow)
self.assertIn("- codex-vps", workflow)
self.assertIn('bash scripts/deploy_codex_audit_service.sh "${{ inputs.mode }}"', workflow)
+ self.assertIn("CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES", workflow)
+ self.assertIn("CODEX_AUDIT_SERVICE_ALLOWED_WORKFLOW_REFS", workflow)
+ self.assertIn("CODEX_AUDIT_SERVICE_ALLOWED_REFS", workflow)
+ self.assertIn("CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES", workflow)
+ self.assertIn("QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge,QuantStrategyLab/CryptoLivePoolPipelines", workflow)
self.assertIn("actions/checkout@v6.0.3", workflow)
def test_vps_deploy_adds_nginx_audit_route_without_router_service(self) -> None:
@@ -2263,10 +2275,20 @@ def test_vps_deploy_adds_nginx_audit_route_without_router_service(self) -> None:
self.assertIn("location = /v1/codex-audit", deploy_script)
self.assertIn("location ^~ /v1/codex-audit/", deploy_script)
self.assertIn("CODEX_AUDIT_SERVICE_JOB_DIR", deploy_script)
+ self.assertIn("codex_pr_review.yml@refs/pull/*/merge", deploy_script)
+ self.assertIn("refs/pull/*/merge", deploy_script)
+ self.assertIn("QuantStrategyLab/AIAuditBridge,QuantStrategyLab/CodexAuditBridge,QuantStrategyLab/CryptoLivePoolPipelines", deploy_script)
self.assertIn("proxy_pass http://127.0.0.1:{port}", deploy_script)
self.assertIn('"# CodexAuditBridge route start" not in block', deploy_script)
self.assertIn("audit service did not become healthy", deploy_script)
self.assertIn("nginx config test failed; restoring previous config", deploy_script)
+ self.assertIn("zzzz-managed-allowlists.conf", deploy_script)
+ self.assertIn('Environment="CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=${ALLOWED_REPOSITORIES}"', deploy_script)
+ self.assertIn("systemctl_environment_brief", deploy_script)
+ self.assertIn('sed -E "s/^[\\"', deploy_script)
+ self.assertIn('s/[\\"\']$//"', deploy_script)
+ self.assertIn("CODEX_AUDIT_SERVICE_(ALLOWED_|AUDIENCE=", deploy_script)
+ self.assertNotIn("^CODEX_AUDIT_SERVICE_TOKEN", deploy_script)
self.assertNotIn("CODEX_SERVICE_ROUTER", deploy_script)
self.assertNotIn("codex_service_router", deploy_script)