Skip to content

Commit 652abe7

Browse files
committed
feat: add async codex audit service
1 parent 2329239 commit 652abe7

15 files changed

Lines changed: 756 additions & 106 deletions

.github/workflows/selfhosted_monthly_review.yml renamed to .github/workflows/codex_audit.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
name: Codex Monthly Review
1+
name: Codex Audit
22

33
on:
44
workflow_dispatch:
55
inputs:
66
source_repo:
77
description: "Repository that owns the monthly review issue"
88
required: true
9-
default: "QuantStrategyLab/CryptoSnapshotPipelines"
9+
default: "QuantStrategyLab/CryptoLivePoolPipelines"
1010
issue_number:
1111
description: "Monthly review issue number in source_repo"
1212
required: true
@@ -55,16 +55,16 @@ permissions:
5555
id-token: write
5656

5757
concurrency:
58-
group: selfhosted-codex-monthly-${{ github.event.client_payload.source_repo || inputs.source_repo }}-${{ github.event.client_payload.issue_number || inputs.issue_number }}
58+
group: codex-audit-${{ github.event.client_payload.source_repo || inputs.source_repo }}-${{ github.event.client_payload.issue_number || inputs.issue_number }}
5959
cancel-in-progress: false
6060

6161
jobs:
62-
codex-monthly-review:
62+
codex-audit:
6363
runs-on: ubuntu-latest
6464
timeout-minutes: 60
6565
env:
6666
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
67-
SOURCE_REPO: ${{ github.event.client_payload.source_repo || inputs.source_repo || 'QuantStrategyLab/CryptoSnapshotPipelines' }}
67+
SOURCE_REPO: ${{ github.event.client_payload.source_repo || inputs.source_repo || 'QuantStrategyLab/CryptoLivePoolPipelines' }}
6868
ISSUE_NUMBER: ${{ github.event.client_payload.issue_number || inputs.issue_number }}
6969
SOURCE_REF: ${{ github.event.client_payload.source_ref || inputs.source_ref || 'main' }}
7070
CODEX_AUDIT_MODE: ${{ github.event.client_payload.mode || inputs.mode || 'review_and_fix' }}
@@ -114,7 +114,7 @@ jobs:
114114
permission-issues: write
115115
permission-pull-requests: write
116116

117-
- name: Run Monthly Codex Audit
117+
- name: Run Codex Audit
118118
env:
119119
CODEX_AUDIT_GH_TOKEN: ${{ steps.source_app_token.outputs.token || secrets.CODEX_AUDIT_GH_TOKEN }}
120120
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Thanks for contributing to `CodexAuditBridge`.
1414

1515
- Prefer small pull requests with one clear purpose.
1616
- Keep refactors separate from behavior, contract, workflow, or documentation changes.
17-
- Preserve this repository's boundary as a self-hosted audit automation bridge; do not move broker execution, live-allocation decisions, private credentials, or unrelated platform logic into it.
17+
- Preserve this repository's boundary as a service-backed audit automation bridge; do not move broker execution, live-allocation decisions, private credentials, or unrelated platform logic into it.
1818
- Add or update tests, examples, docs, or reproducible evidence when changing behavior or public contracts.
1919

2020
## Documentation Standards

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
## What this repository is
88

9-
CodexAuditBridge is a QuantStrategyLab audit automation bridge. It runs self-hosted Codex audit workflows for snapshot reviews and low-risk fix pull requests.
9+
CodexAuditBridge is a QuantStrategyLab audit automation bridge. It runs service-backed Codex audit workflows for snapshot reviews and low-risk fix pull requests.
1010

1111
It produces research, audit, or orchestration artifacts. It should not submit broker orders or mutate live allocations by itself.
1212

@@ -17,7 +17,7 @@ CodexAuditBridge is the organization-local Codex boundary for QuantStrategyLab.
1717
Current execution model:
1818

1919
1. A source repository creates or identifies an audit issue.
20-
2. The source repository dispatches `.github/workflows/selfhosted_monthly_review.yml` in this repository.
20+
2. The source repository dispatches this repository's monthly review workflow. The workflow filename is still `codex_audit.yml` for dispatch compatibility, but Codex execution is service-backed.
2121
3. CodexAuditBridge validates the source repository and task mapping, clones the source repository with a scoped GitHub token, and runs the selected provider/backend.
2222
4. Only CodexAuditBridge performs GitHub writes such as comments, branches, commits, pushes, and pull requests.
2323

@@ -31,9 +31,7 @@ This avoids hard-coding Codex CLI setup in every source repository and avoids de
3131

3232
| Source repository | Allowed task |
3333
| --- | --- |
34-
| `QuantStrategyLab/AiLongHorizonSignalPipelines` | `long_horizon_signal_shadow` |
3534
| `QuantStrategyLab/CryptoLivePoolPipelines` | `monthly_snapshot_audit` |
36-
| `QuantStrategyLab/CryptoSnapshotPipelines` | `monthly_snapshot_audit` |
3735
| `QuantStrategyLab/HkEquitySnapshotPipelines` | `monthly_snapshot_audit` |
3836
| `QuantStrategyLab/ResearchSignalContextPipelines` | `long_horizon_signal_shadow` |
3937
| `QuantStrategyLab/UsEquitySnapshotPipelines` | `monthly_snapshot_audit` |
@@ -55,15 +53,17 @@ Run the service host with:
5553

5654
```bash
5755
CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/CodexAuditBridge \
58-
CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/CryptoSnapshotPipelines,QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/AiLongHorizonSignalPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
56+
CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
5957
CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
6058
OPENAI_API_KEY=... \
6159
python3 scripts/codex_audit_service.py
6260
```
6361

6462
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.
6563

66-
The manual `VPS Codex Service Ops` workflow can be used by maintainers to inspect or deploy the VPS-side service through the existing `self-hosted,codex-vps` runner. The deployment keeps the Pigbibi `/v1/codex` gateway unchanged and adds an exact nginx route from `/v1/codex-audit` to this repository's audit service.
64+
If no custom domain is available, `cloudflare/codex-audit-proxy/` contains a minimal Cloudflare Worker that can publish a free `workers.dev` HTTPS entry point while keeping the VPS origin URL in a Cloudflare secret. The production service path is async: submit `POST /v1/codex-audit/jobs`, then poll `GET /v1/codex-audit/jobs/{job_id}`. See `docs/async_service_deployment.md` for the deployment and open-source repository checklist.
65+
66+
The manual `VPS Codex Service Ops` workflow can be used by maintainers to inspect or deploy the VPS-side service through the existing `self-hosted,codex-vps` runner. The deployment keeps the Pigbibi `/v1/codex` gateway unchanged and adds an nginx route for `/v1/codex-audit` to this repository's audit service.
6767

6868
### Service patch contract
6969

README.zh-CN.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
## 这个仓库是什么
88

9-
CodexAuditBridge 是 QuantStrategyLab 的审计自动化桥接工具。运行 self-hosted Codex 审计 workflow,用于 snapshot review 和低风险修复 PR。
9+
CodexAuditBridge 是 QuantStrategyLab 的审计自动化桥接工具。运行 service-backed Codex 审计 workflow,用于 snapshot review 和低风险修复 PR。
1010

1111
它产出研究、审计或编排类 artifact,不应自行提交券商订单,也不应直接修改 live allocation。
1212

@@ -17,7 +17,7 @@ CodexAuditBridge 是 QuantStrategyLab 组织内的 Codex 调用边界。各 sour
1717
当前执行模型:
1818

1919
1. source repository 创建或定位审计 issue。
20-
2. source repository 派发本仓库的 `.github/workflows/selfhosted_monthly_review.yml`
20+
2. source repository 派发本仓库的 monthly review workflow。workflow 文件名仍为 `codex_audit.yml` 以保持 dispatch 入口稳定,但 Codex 执行已经是 service-backed
2121
3. CodexAuditBridge 校验 source repository 和 task mapping,使用受限 GitHub token clone source repository,并运行指定 provider/backend。
2222
4. 评论、分支、commit、push、PR 等 GitHub 写操作只由 CodexAuditBridge 负责。
2323

@@ -31,9 +31,7 @@ Codex 执行现在只走 service backend:workflow 从 GitHub-hosted runner 调
3131

3232
| Source repository | 允许的 task |
3333
| --- | --- |
34-
| `QuantStrategyLab/AiLongHorizonSignalPipelines` | `long_horizon_signal_shadow` |
3534
| `QuantStrategyLab/CryptoLivePoolPipelines` | `monthly_snapshot_audit` |
36-
| `QuantStrategyLab/CryptoSnapshotPipelines` | `monthly_snapshot_audit` |
3735
| `QuantStrategyLab/HkEquitySnapshotPipelines` | `monthly_snapshot_audit` |
3836
| `QuantStrategyLab/ResearchSignalContextPipelines` | `long_horizon_signal_shadow` |
3937
| `QuantStrategyLab/UsEquitySnapshotPipelines` | `monthly_snapshot_audit` |
@@ -55,15 +53,17 @@ service host 启动示例:
5553

5654
```bash
5755
CODEX_AUDIT_SERVICE_ALLOWED_REPOSITORIES=QuantStrategyLab/CodexAuditBridge \
58-
CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/CryptoSnapshotPipelines,QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/AiLongHorizonSignalPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
56+
CODEX_AUDIT_SERVICE_ALLOWED_SOURCE_REPOSITORIES='QuantStrategyLab/CryptoLivePoolPipelines,QuantStrategyLab/HkEquitySnapshotPipelines,QuantStrategyLab/UsEquitySnapshotPipelines,QuantStrategyLab/ResearchSignalContextPipelines' \
5957
CODEX_AUDIT_SERVICE_AUDIENCE=quant-codex-audit \
6058
OPENAI_API_KEY=... \
6159
python3 scripts/codex_audit_service.py
6260
```
6361

6462
443/TLS 建议由平台负载均衡或反向代理负责,并把 `/v1/codex-audit` 转发到 service 端口。不要把 GitHub 写 token 传给这个 service。
6563

66-
维护者可以通过手动触发 `VPS Codex Service Ops` workflow,借助现有 `self-hosted,codex-vps` runner 巡检或部署 VPS 侧服务。部署时保持 Pigbibi `/v1/codex` gateway 不变,只在 nginx 上增加 `/v1/codex-audit` 精确路由到本仓库的 audit service。
64+
如果暂时没有自定义域名,`cloudflare/codex-audit-proxy/` 提供了一个最小 Cloudflare Worker,可用免费的 `workers.dev` HTTPS 入口,并把 VPS origin URL 保存在 Cloudflare secret 中。生产服务路径使用异步模式:先 `POST /v1/codex-audit/jobs`,再轮询 `GET /v1/codex-audit/jobs/{job_id}`。部署步骤和开源仓库注意事项见 `docs/async_service_deployment.md`
65+
66+
维护者可以通过手动触发 `VPS Codex Service Ops` workflow,借助现有 `self-hosted,codex-vps` runner 巡检或部署 VPS 侧服务。部署时保持 Pigbibi `/v1/codex` gateway 不变,只在 nginx 上增加 `/v1/codex-audit` 路由到本仓库的 audit service。
6767

6868
### Service patch contract
6969

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Cloudflare Worker Proxy for CodexAuditBridge
2+
3+
This Worker provides a free `workers.dev` HTTPS entry point for CodexAuditBridge when no custom domain is available.
4+
5+
This Worker should stay separate from the Pigbibi CodexGateway Worker so origin URLs, repository allowlists, and logs remain isolated.
6+
7+
The Worker is intentionally thin:
8+
9+
- serves `GET /healthz` locally, proxies legacy `POST /v1/codex-audit`, and proxies async `POST /v1/codex-audit/jobs` plus `GET /v1/codex-audit/jobs/{job_id}`;
10+
- forwards the GitHub Actions OIDC `Authorization` header to the existing Codex audit service;
11+
- keeps the VPS origin URL in a Cloudflare Worker secret, not in git;
12+
- does not store provider keys, GitHub tokens, or Codex credentials.
13+
14+
## Deploy
15+
16+
From this directory:
17+
18+
```bash
19+
npx -y wrangler@latest secret put CODEX_AUDIT_ORIGIN_URL
20+
npx -y wrangler@latest deploy
21+
```
22+
23+
`CODEX_AUDIT_ORIGIN_URL` should be the current HTTPS origin for the Codex audit service. It may be either the service base URL or the full `/v1/codex-audit` URL.
24+
25+
After deploy, set the `CodexAuditBridge` GitHub secret `CODEX_AUDIT_SERVICE_URL` to the Worker URL, for example:
26+
27+
```text
28+
https://quantstrategylab-codex-audit-proxy.<cloudflare-account-subdomain>.workers.dev
29+
```
30+
31+
The account subdomain is controlled by the Cloudflare account. If it is still `pigbibi`, the organization name should be represented in the Worker name, not by changing the whole account subdomain.
32+
33+
Keep `CODEX_AUDIT_SERVICE_AUDIENCE` unchanged unless the origin service audience changes.
34+
35+
## Smoke test
36+
37+
```bash
38+
curl -fsS https://quantstrategylab-codex-audit-proxy.<cloudflare-account-subdomain>.workers.dev/healthz
39+
```
40+
41+
A full async audit request still requires a valid GitHub Actions OIDC bearer token and should be tested from the bridge workflow. An unauthenticated `POST /v1/codex-audit/jobs` should return `401`, proving that the Worker reached the authenticated origin.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
const AUDIT_ROUTE = "/v1/codex-audit";
2+
const HEALTH_ROUTE = "/healthz";
3+
const JOB_ID_PATTERN = /^[A-Za-z0-9_-]{24,96}$/;
4+
5+
function jsonResponse(status, payload) {
6+
return new Response(JSON.stringify(payload), {
7+
status,
8+
headers: {
9+
"Content-Type": "application/json; charset=utf-8",
10+
"Cache-Control": "no-store",
11+
},
12+
});
13+
}
14+
15+
function isLocalhost(hostname) {
16+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
17+
}
18+
19+
function withoutTrailingSlash(pathname) {
20+
return pathname.replace(/\/+$/, "");
21+
}
22+
23+
function isAuditPath(pathname) {
24+
if (pathname === AUDIT_ROUTE || pathname === `${AUDIT_ROUTE}/jobs`) {
25+
return true;
26+
}
27+
const prefix = `${AUDIT_ROUTE}/jobs/`;
28+
return pathname.startsWith(prefix) && JOB_ID_PATTERN.test(pathname.slice(prefix.length));
29+
}
30+
31+
function methodAllowed(method, pathname) {
32+
if (pathname === HEALTH_ROUTE) {
33+
return method === "GET";
34+
}
35+
if (pathname === AUDIT_ROUTE || pathname === `${AUDIT_ROUTE}/jobs`) {
36+
return method === "POST";
37+
}
38+
if (pathname.startsWith(`${AUDIT_ROUTE}/jobs/`)) {
39+
return method === "GET";
40+
}
41+
return false;
42+
}
43+
44+
export function buildOriginUrl(rawOriginUrl, routePath, search = "") {
45+
if (!rawOriginUrl || !rawOriginUrl.trim()) {
46+
throw new Error("CODEX_AUDIT_ORIGIN_URL is required");
47+
}
48+
if (!isAuditPath(routePath)) {
49+
throw new Error("route is not allowed");
50+
}
51+
52+
const origin = new URL(rawOriginUrl.trim());
53+
if (origin.protocol !== "https:" && !(origin.protocol === "http:" && isLocalhost(origin.hostname))) {
54+
throw new Error("CODEX_AUDIT_ORIGIN_URL must use HTTPS");
55+
}
56+
57+
let basePath = withoutTrailingSlash(origin.pathname);
58+
if (!basePath.endsWith(AUDIT_ROUTE)) {
59+
basePath = `${basePath}${AUDIT_ROUTE}`;
60+
}
61+
const suffix = routePath.slice(AUDIT_ROUTE.length);
62+
63+
origin.pathname = `${basePath}${suffix}`;
64+
origin.search = search;
65+
origin.hash = "";
66+
return origin.toString();
67+
}
68+
69+
function forwardedHeaders(request, url) {
70+
const headers = new Headers(request.headers);
71+
headers.delete("host");
72+
headers.delete("content-length");
73+
headers.set("X-Forwarded-Host", url.host);
74+
headers.set("X-Forwarded-Proto", "https");
75+
headers.set("X-Codex-Audit-Proxy", "cloudflare-worker");
76+
return headers;
77+
}
78+
79+
async function proxyRequest(request, env) {
80+
const url = new URL(request.url);
81+
const originUrl = buildOriginUrl(env.CODEX_AUDIT_ORIGIN_URL, url.pathname, url.search);
82+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
83+
84+
return fetch(originUrl, {
85+
method: request.method,
86+
headers: forwardedHeaders(request, url),
87+
body: hasBody ? request.body : undefined,
88+
redirect: "manual",
89+
});
90+
}
91+
92+
export default {
93+
async fetch(request, env) {
94+
const url = new URL(request.url);
95+
if (url.pathname !== HEALTH_ROUTE && !isAuditPath(url.pathname)) {
96+
return jsonResponse(404, { status: "error", error: "not found" });
97+
}
98+
if (!methodAllowed(request.method, url.pathname)) {
99+
return jsonResponse(405, { status: "error", error: "method not allowed" });
100+
}
101+
if (url.pathname === HEALTH_ROUTE) {
102+
return jsonResponse(200, { status: "ok" });
103+
}
104+
105+
try {
106+
return await proxyRequest(request, env);
107+
} catch (error) {
108+
const message = error instanceof Error ? error.message : "origin request failed";
109+
if (message.includes("CODEX_AUDIT_ORIGIN_URL") || message.includes("HTTPS")) {
110+
return jsonResponse(500, { status: "error", error: message });
111+
}
112+
return jsonResponse(502, { status: "error", error: "origin request failed" });
113+
}
114+
},
115+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import worker, { buildOriginUrl } from "../src/index.mjs";
5+
6+
const JOB_ID = "abcdefghijklmnopqrstuvwxyzABCD12";
7+
8+
test("buildOriginUrl appends audit route to base origin", () => {
9+
assert.equal(
10+
buildOriginUrl("https://origin.example", "/v1/codex-audit", "?run=1"),
11+
"https://origin.example/v1/codex-audit?run=1",
12+
);
13+
});
14+
15+
test("buildOriginUrl accepts full audit endpoint origin", () => {
16+
assert.equal(
17+
buildOriginUrl("https://origin.example/v1/codex-audit", "/v1/codex-audit"),
18+
"https://origin.example/v1/codex-audit",
19+
);
20+
});
21+
22+
test("buildOriginUrl maps async submit route next to audit endpoint", () => {
23+
assert.equal(
24+
buildOriginUrl("https://origin.example/v1/codex-audit", "/v1/codex-audit/jobs"),
25+
"https://origin.example/v1/codex-audit/jobs",
26+
);
27+
});
28+
29+
test("buildOriginUrl maps async status route next to audit endpoint", () => {
30+
assert.equal(
31+
buildOriginUrl("https://origin.example/v1/codex-audit", `/v1/codex-audit/jobs/${JOB_ID}`),
32+
`https://origin.example/v1/codex-audit/jobs/${JOB_ID}`,
33+
);
34+
});
35+
36+
test("buildOriginUrl preserves nested base path", () => {
37+
assert.equal(
38+
buildOriginUrl("https://origin.example/codex", "/v1/codex-audit"),
39+
"https://origin.example/codex/v1/codex-audit",
40+
);
41+
});
42+
43+
test("buildOriginUrl rejects insecure external origins", () => {
44+
assert.throws(
45+
() => buildOriginUrl("http://origin.example", "/v1/codex-audit"),
46+
/must use HTTPS/,
47+
);
48+
});
49+
50+
test("worker health check is local and does not require origin", async () => {
51+
const response = await worker.fetch(new Request("https://proxy.example/healthz"), {});
52+
assert.equal(response.status, 200);
53+
assert.deepEqual(await response.json(), { status: "ok" });
54+
});
55+
56+
test("worker rejects unsupported route before origin lookup", async () => {
57+
const response = await worker.fetch(new Request("https://proxy.example/other"), {});
58+
assert.equal(response.status, 404);
59+
assert.deepEqual(await response.json(), { status: "error", error: "not found" });
60+
});
61+
62+
test("worker rejects unsupported method before origin lookup", async () => {
63+
const response = await worker.fetch(new Request("https://proxy.example/v1/codex-audit", { method: "GET" }), {});
64+
assert.equal(response.status, 405);
65+
assert.deepEqual(await response.json(), { status: "error", error: "method not allowed" });
66+
});
67+
68+
test("worker rejects malformed job ids before origin lookup", async () => {
69+
const response = await worker.fetch(new Request("https://proxy.example/v1/codex-audit/jobs/nope"), {});
70+
assert.equal(response.status, 404);
71+
assert.deepEqual(await response.json(), { status: "error", error: "not found" });
72+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "node_modules/wrangler/config-schema.json",
3+
"name": "quantstrategylab-codex-audit-proxy",
4+
"main": "src/index.mjs",
5+
"compatibility_date": "2025-12-01",
6+
"workers_dev": true,
7+
"observability": {
8+
"enabled": true
9+
}
10+
}

0 commit comments

Comments
 (0)