Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions apps/decodex/src/radar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2848,6 +2848,7 @@ fn run_codex_analysis(
) -> crate::prelude::Result<()> {
let mut command = helper_command(root, request, RUN_CODEX_ANALYSIS_SCRIPT);

command.arg("--allow-ai-analysis-boundary");
command.args([
"--bundle",
&path_arg(root, bundle),
Expand Down Expand Up @@ -5516,6 +5517,7 @@ mod tests {
ffi::OsString,
fs,
path::{Path, PathBuf},
process::Command,
};

use serde_json::{self, Value};
Expand Down Expand Up @@ -5847,6 +5849,37 @@ mod tests {
assert!(rendered.get("how_to_try").is_none());
}

#[test]
fn analysis_helper_fails_closed_without_explicit_boundary_opt_in() {
let _env = TestEnvVars::set(&[("DECODEX_ALLOW_CODEX_ANALYSIS", None)]);
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("apps/decodex should live two levels under the repo root");
let temp_dir = tempfile::tempdir().expect("temporary directory should be created");
let bundle_path = temp_dir.path().join("missing-bundle.json");
let output_path = temp_dir.path().join("analysis.json");
let output = Command::new("python3")
.current_dir(repo_root)
.arg(repo_root.join(super::RUN_CODEX_ANALYSIS_SCRIPT))
.arg("--bundle")
.arg(&bundle_path)
.arg("--out")
.arg(&output_path)
.arg("--repo-root")
.arg(repo_root)
.output()
.expect("Python analysis helper smoke command should execute");
let stderr = String::from_utf8_lossy(&output.stderr);

assert!(!output.status.success());
assert!(
stderr.contains("requires --allow-ai-analysis-boundary"),
"unexpected stderr: {stderr}"
);
assert!(!output_path.exists());
}

#[test]
fn dry_run_backfill_selects_unpublished_release_window_prs() {
let temp_dir = tempfile::tempdir().expect("temporary directory should be created");
Expand Down
10 changes: 9 additions & 1 deletion docs/runbook/local-github-signal-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ selects the release-window gaps and sequences deterministic Radar commands, whil
AI review step follows the repo-local skills and schemas instead of running inside
GitHub Actions.

`scripts/github/run_codex_analysis.py` remains only the bounded deterministic process
wrapper for that AI review step. Prefer `decodex radar backfill-release-range` or a
normal Codex automation session. Direct helper recovery runs must pass
`--allow-ai-analysis-boundary` or set `DECODEX_ALLOW_CODEX_ANALYSIS=1`, and the helper
still validates both the input bundle and the returned `analysis_draft` before writing
output.

The repository already includes a real sample for this flow:

- bundle: `artifacts/github/bundles/openai-codex-pr-22414.json`
Expand Down Expand Up @@ -189,7 +196,8 @@ The current Decodex boundary is:
- GitHub Actions: deterministic upstream commit discovery, PR mapping, review-queue
refresh, release-delta refresh, validation, and commit/push of changed metadata.
Actions must not run Codex AI analysis, create `analysis_draft`, or execute release
backfills that cross that AI boundary.
backfills that cross that AI boundary. Actions also must not pass
`--allow-ai-analysis-boundary` or set `DECODEX_ALLOW_CODEX_ANALYSIS`.
- Codex automation: AI source review, compatibility judgment, Publisher judgment,
social publication, `analysis_draft` creation, `decodex radar render-signal`, and
any promotion into signal or follow-up artifacts.
Expand Down
8 changes: 8 additions & 0 deletions docs/spec/upstream-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ or public value.
AI review must read enough source evidence to explain behavior. A PR title, release
title, or deterministic queue hint is not enough for a confirmed claim.

The remaining Python analysis helper, `scripts/github/run_codex_analysis.py`, is only
the bounded deterministic process wrapper for this AI review boundary. It must validate
the input `github_change_bundle/v1`, run Codex with the checked `analysis_draft` output
schema, validate the returned draft again before writing it, and require an explicit
`--allow-ai-analysis-boundary` flag or `DECODEX_ALLOW_CODEX_ANALYSIS=1` environment
acknowledgement. The normal operator command surface remains Rust-owned
`decodex radar ...`; GitHub Actions must not set that acknowledgement.

## Promotion boundary

Promote an upstream review into:
Expand Down
13 changes: 12 additions & 1 deletion scripts/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ Current helper:

- `run_codex_analysis.py` invokes Codex in a read-only session and writes a validated
`analysis_draft` artifact. It is the explicit AI boundary and is not a GitHub
Actions entrypoint.
Actions entrypoint. The wrapper behavior is deterministic: validate the bundle, run
Codex with the checked prompt and output schema, validate the returned draft, then
write the artifact. Direct invocation is a recovery or automation-only path and must
pass `--allow-ai-analysis-boundary` or set `DECODEX_ALLOW_CODEX_ANALYSIS=1`.
Normal operator workflows should reach it through Rust-owned `decodex radar`
commands such as `backfill-release-range`.

Shared support:

Expand Down Expand Up @@ -117,6 +122,12 @@ GitHub Actions may refresh upstream queues, release deltas, and validation throu
promote source-backed conclusions into `upstream_impact/v1`, `analysis_draft`,
`decodex radar render-signal` output, or `social_post/v1`.

Do not wire `run_codex_analysis.py` into GitHub Actions. Actions must not pass
`--allow-ai-analysis-boundary` or set `DECODEX_ALLOW_CODEX_ANALYSIS`; that
acknowledgement is reserved for Rust-owned local automation and explicit operator
recovery runs that still keep bundle validation and `analysis_draft` schema validation
inside the helper.

Repo-local skills under `dev/skills/` are reasoning instructions for the Codex
analysis step and for manual Radar/Publisher work. They do not introduce extra
intermediate artifact schemas unless the conclusion is promoted into one of the
Expand Down
22 changes: 22 additions & 0 deletions scripts/github/run_codex_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import argparse
import json
import os
import subprocess
import sys
import tempfile
Expand All @@ -17,9 +18,19 @@

from contracts import dump_json, load_json, validate_analysis_draft, validate_bundle # noqa: E402

ALLOW_ANALYSIS_ENV = "DECODEX_ALLOW_CODEX_ANALYSIS"


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--allow-ai-analysis-boundary",
action="store_true",
help=(
"Required acknowledgement that this helper is crossing the Codex AI analysis "
"boundary. GitHub Actions must not set this."
),
)
parser.add_argument("--bundle", required=True, help="Path to github_change_bundle/v1 JSON.")
parser.add_argument("--out", required=True, help="Path to write the validated analysis JSON.")
parser.add_argument("--repo-root", help="Repository root for codex exec. Defaults to the current repo root.")
Expand All @@ -28,6 +39,10 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()


def analysis_boundary_allowed(args: argparse.Namespace) -> bool:
return args.allow_ai_analysis_boundary or os.environ.get(ALLOW_ANALYSIS_ENV) == "1"


def repo_root_from(bundle_path: Path) -> Path:
resolved = bundle_path.resolve()
for root in resolved.parents:
Expand Down Expand Up @@ -83,6 +98,13 @@ def extract_json_payload(raw: str) -> dict[str, Any]:

def main() -> None:
args = parse_args()
if not analysis_boundary_allowed(args):
raise SystemExit(
"Codex analysis helper requires --allow-ai-analysis-boundary or "
f"{ALLOW_ANALYSIS_ENV}=1. Use Rust-owned decodex radar commands for "
"deterministic Radar workflows; GitHub Actions must not run this helper."
)

bundle_path = Path(args.bundle)
bundle = load_json(bundle_path)
bundle_validation = validate_bundle(bundle)
Expand Down