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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ the same name overrides it. `config/env.example` documents each setting with its
asked for was done. `--sandbox danger-full-access` is an explicit option only. Worktree runs
share caches below `BASE/.jpdev-shared`; `--small` enforces the manifest handover paths and line
bound after the run; `--resume` continues the named thread. Every detected thread id is written
to `.jpdev/output/thread`.
to `.jpdev/output/thread`. A directory passed to `--add` adds every non-ignored file below it,
except the handover files and caches, and is refused with its path when nothing remains to add.
- `jpdev codex review --brief FILE --model M --effort E --dir CLONE [--pr N | --issue N]
[--repo OWNER/NAME] [--out FILE] [--sandbox read-only|workspace-write] [--network]`: checks the brief and
runs `codex exec` with the model, effort and sandbox explicit and stdin closed. It writes the
Expand All @@ -150,8 +151,9 @@ the same name overrides it. `config/env.example` documents each setting with its
`--review` prints the reviewer skeleton with its read-only rules and findings-first report.
- `jpdev codex check-brief FILE`: exits 1 and names the neutral phrasing for each word of
[`config/codex-words.txt`](config/codex-words.txt) (and of the file `JPDEV_CODEX_WORDS` names)
that the brief holds. `codex run` runs the same check first and does not start the run on a
refusal.
that the brief holds. A word inside backticks is treated as part of an identifier, path or
command and is ignored; the product term “adversarial” is not in the base list. `codex run` runs
the same check first and does not start the run on a refusal.
- `jpdev env`: the effective settings and where each came from.

## The caps
Expand Down
48 changes: 45 additions & 3 deletions bin/jpdev
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ PY
codex_publish() {
local dir=$1 branch=$2 push=$3 pr=$4 repo=$5 base=$6
shift 6
local trailer="Co-Authored-By: Codex <noreply@openai.com>" tmpd
local trailer="Co-Authored-By: Codex <noreply@openai.com>" tmpd msg_tracked=0 pr_tracked=0
cd "$dir" || exit 2
if [ ! -s msg.txt ]; then
echo "jpdev codex run: refused: codex wrote no msg.txt (the commit message); nothing was committed" >&2
Expand All @@ -743,9 +743,16 @@ codex_publish() {
echo "jpdev codex run: refused: codex wrote no pr.md (the pull request body); nothing was committed" >&2
exit 1
fi
git ls-files --error-unmatch -- msg.txt > /dev/null 2>&1 && msg_tracked=1
git ls-files --error-unmatch -- pr.md > /dev/null 2>&1 && pr_tracked=1
git add -u || exit 1
local path
local path pending add_dirs=()
for path in "$@"; do
if [ -d "$path" ]; then
add_dirs+=("$path")
pending=$(git status --porcelain --untracked-files=all -- "$path") || exit 1
if [ -z "$pending" ]; then continue; fi
fi
git add -- "$path" || { echo "jpdev codex run: --add $path could not be staged" >&2; exit 1; }
done
local cache_paths=()
Expand All @@ -760,11 +767,46 @@ codex_publish() {
if git diff --cached --name-only | grep -qE '(^|/)\.(jpdev|jpdev-shared|gocache|npm-cache|xdg-cache)(/|$)'; then
jpdev_die "codex run: a sandbox cache directory remained staged"
fi
if git diff --cached --name-only | grep -qxE 'msg\.txt|pr\.md'; then
local tracked_handover=0 handover_paths=() nested_tracked_handover=() add_path
local -A seen_nested_handover=()
for add_path in "${add_dirs[@]}"; do
while IFS= read -r -d '' path; do
case $path in
*/msg.txt | */pr.md)
if [ -n "${seen_nested_handover[$path]:-}" ]; then continue; fi
seen_nested_handover[$path]=1
if git cat-file -e "HEAD:$path" 2>/dev/null; then
nested_tracked_handover+=("$path")
else
handover_paths+=("$path")
fi
;;
esac
done < <(git diff --cached --name-only -z -- "$add_path")
done
if ! git diff --cached --quiet -- msg.txt; then
if [ "$msg_tracked" -eq 1 ]; then tracked_handover=1; else handover_paths+=(msg.txt); fi
fi
if ! git diff --cached --quiet -- pr.md; then
if [ "$pr_tracked" -eq 1 ]; then tracked_handover=1; else handover_paths+=(pr.md); fi
fi
if [ "${#handover_paths[@]}" -gt 0 ]; then
git rm --cached --quiet --ignore-unmatch -- "${handover_paths[@]}" || exit 1
fi
if [ "${#nested_tracked_handover[@]}" -gt 0 ]; then
git reset -q -- "${nested_tracked_handover[@]}" || exit 1
fi
if [ "$tracked_handover" -eq 1 ]; then
git reset -q -- msg.txt pr.md
echo "jpdev codex run: refused: msg.txt or pr.md is tracked and changed; they are read, never committed" >&2
exit 1
fi
for path in "${add_dirs[@]}"; do
if git diff --cached --quiet -- "$path"; then
echo "jpdev codex run: --add $path: directory holds nothing to add" >&2
exit 1
fi
done
if git diff --cached --quiet; then
echo "jpdev codex run: nothing to commit: codex made no change to a tracked file" >&2
exit 1
Expand Down
1 change: 0 additions & 1 deletion config/codex-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ hack => describe the unsupported use and the refusal that documents it
breach => describe the boundary and the check that must hold at it
malicious => describe the input as unexpected, unsupported or not allowed
hostile => describe the input as unexpected, unsupported or not allowed
adversar => describe the input as unexpected or not allowed, and the refusal
jailbreak => describe the instruction the model must not follow and the refusal
prompt injection => describe the untrusted text in the input and the refusal that keeps it from acting as an instruction
code injection => describe the untrusted text in the input and the refusal
Expand Down
58 changes: 56 additions & 2 deletions lib/codex_words.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
start of a word, with any letters after it, and a TERM of several words matches across any run
of whitespace and hyphens (so a term wrapped over two lines, or written with hyphens, is found;
the line reported is the line the match starts on). Evasion inside a single word (a letter
replaced or inserted) is out of scope: this is a vocabulary check, not a filter. A word file that does not exist is skipped (the config may extend the list).
replaced or inserted) is out of scope: this is a vocabulary check, not a filter. Text inside a
paired run of backticks is an identifier, path or command and is not checked. A word file that
does not exist is skipped (the config may extend the list).
Prints one line per hit and exits 1 when there is any, 0 when the brief is clean, 2 on a
usage or read error.
"""
Expand All @@ -31,6 +33,58 @@ def load(path):
return entries


def blank(text):
"""Blank text while preserving line boundaries and offsets."""
return re.sub(r"[^\n]", " ", text)


def mask_inline_backticks(line):
"""Blank paired inline spans, or return the whole line unchanged if unbalanced."""
runs = list(re.finditer(r"`+", line))
spans = []
index = 0
while index < len(runs):
opener = runs[index]
for closing in range(index + 1, len(runs)):
if len(runs[closing].group(0)) == len(opener.group(0)):
spans.append((opener.start(), runs[closing].end()))
index = closing + 1
break
else:
return line
chars = list(line)
for start, end in spans:
chars[start:end] = blank(line[start:end])
return "".join(chars)


def mask_backticks(text):
"""Blank balanced inline spans and fenced blocks, preserving unbalanced text."""
lines = text.splitlines(keepends=True)
masked = []
index = 0
while index < len(lines):
body = lines[index].rstrip("\r\n")
opened = re.fullmatch(r"[ \t]{0,3}(`{3,})[^`]*", body)
if not opened:
masked.append(mask_inline_backticks(lines[index]))
index += 1
continue
width = len(opened.group(1))
closing = index + 1
while closing < len(lines):
candidate = lines[closing].rstrip("\r\n")
if re.fullmatch(r"[ \t]{0,3}`{%d,}[ \t]*" % width, candidate):
break
closing += 1
if closing == len(lines):
masked.extend(lines[index:])
break
masked.extend(blank(line) for line in lines[index:closing + 1])
index = closing + 1
return "".join(masked)


def main(argv):
if len(argv) < 3:
print("usage: codex_words.py BRIEF WORDFILE...", file=sys.stderr)
Expand All @@ -45,7 +99,7 @@ def main(argv):
except OSError as err:
print("codex_words.py: %s" % err, file=sys.stderr)
return 2
text = "\n".join(lines)
text = mask_backticks("\n".join(lines))
hits = 0
for term, suggestion in entries:
words = [re.escape(part) for part in term.split()]
Expand Down
58 changes: 57 additions & 1 deletion tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,28 @@ expect "check-brief matches regardless of case and ending" 1 'line 2: "EXPLOITAT
expect "check-brief suggests the neutral phrasing" 1 'describe the input the code receives and the refusal it must produce' "$out" "$s"
expect "check-brief points at the documented refusal" 1 'describe the documented refusal instead' "$out" "$s"
absent "check-brief does not flag a word that merely contains a listed one" 'shackle' "$out"
# shellcheck disable=SC2016 # a literal expression the test writes into a fixture
printf 'Name the `exploitParser` identifier.\n\n## Before you write msg.txt\n' > "$work/backticked-brief.md"
out=$("$jpdev" codex check-brief "$work/backticked-brief.md" 2>&1) && s=0 || s=$?
expect "check-brief ignores a listed word inside backticks" 0 'no word of the filter list' "$out" "$s"
# shellcheck disable=SC2016 # literal unmatched and paired delimiters in the fixture
printf 'An unmatched ` marker leaves exploitParser bare before `safe`.\n\n## Before you write msg.txt\n' > "$work/unbalanced-backtick-brief.md"
out=$("$jpdev" codex check-brief "$work/unbalanced-backtick-brief.md" 2>&1) && s=0 || s=$?
expect "check-brief fails closed on an odd run of backticks" 1 'line 1: "exploitParser"' "$out" "$s"
# shellcheck disable=SC2016 # literal fenced blocks in the fixtures
printf '```text\nexploitParser\n```\n\n## Before you write msg.txt\n' > "$work/balanced-fence-brief.md"
out=$("$jpdev" codex check-brief "$work/balanced-fence-brief.md" 2>&1) && s=0 || s=$?
expect "check-brief exempts a balanced fenced block" 0 'no word of the filter list' "$out" "$s"
# shellcheck disable=SC2016 # literal unclosed fenced block in the fixture
printf '```text\nexploitParser\n\n## Before you write msg.txt\n' > "$work/unbalanced-fence-brief.md"
out=$("$jpdev" codex check-brief "$work/unbalanced-fence-brief.md" 2>&1) && s=0 || s=$?
expect "check-brief fails closed on an unbalanced fenced block" 1 'line 2: "exploitParser"' "$out" "$s"
printf 'Name the exploitParser identifier.\n\n## Before you write msg.txt\n' > "$work/bare-brief.md"
out=$("$jpdev" codex check-brief "$work/bare-brief.md" 2>&1) && s=0 || s=$?
expect "check-brief still refuses the same word bare" 1 'line 1: "exploitParser": describe the malformed or unexpected input and the documented refusal' "$out" "$s"
printf 'Describe the adversarial review.\n\n## Before you write msg.txt\n' > "$work/product-term-brief.md"
out=$("$jpdev" codex check-brief "$work/product-term-brief.md" 2>&1) && s=0 || s=$?
expect "check-brief accepts the product term in prose" 0 'no word of the filter list' "$out" "$s"
n=0
while IFS= read -r line; do
case $line in '' | '#'*) continue ;; esac
Expand Down Expand Up @@ -823,7 +845,7 @@ git init -q -b main "$cxrepo"
printf '# Rules\n' > "$cxrepo/AGENTS.md"
printf 'one\n' > "$cxrepo/file.txt"
printf '{"version":1,"checks":[{"name":"one","run":"true"}],"handover":{"shared_paths":["file.txt"],"boundary_paths":[]}}\n' > "$cxrepo/dev.json"
printf 'msg.txt\npr.md\n' > "$cxrepo/.gitignore"
printf 'ignored.txt\n' > "$cxrepo/.gitignore"
git -C "$cxrepo" add .gitignore AGENTS.md dev.json file.txt
git -C "$cxrepo" commit -q -m seed
git -C "$cxrepo" remote add origin "$cxorigin"
Expand Down Expand Up @@ -889,6 +911,32 @@ out=$(STUB_CODEX_EDIT=1 STUB_CODEX_MSG=$work/msg-plain.txt cx_run --add created.
expect "codex run --add commits a new file named" 0 'committed' "$out" "$s"
if [ "$(git -C "$cxrepo" show --name-only --format= HEAD | sort | paste -sd' ')" = "created.txt file.txt" ]; then ok "--add puts the named file in the commit"; else bad "--add puts the named file in the commit"; fi

cx_reset
mkdir -p "$cxrepo/bundle"
printf 'one\n' > "$cxrepo/bundle/one.txt"
printf 'two\n' > "$cxrepo/bundle/two.txt"
printf 'ignored\n' > "$cxrepo/bundle/ignored.txt"
mkdir -p "$cxrepo/bundle/handover"
printf 'nested message\n' > "$cxrepo/bundle/handover/msg.txt"
printf 'nested body\n' > "$cxrepo/bundle/handover/pr.md"
out=$(STUB_CODEX_EDIT=1 STUB_CODEX_MSG=$work/msg-plain.txt cx_run --add bundle) && s=0 || s=$?
expect "codex run --add accepts a directory" 0 'committed' "$out" "$s"
names=$(git -C "$cxrepo" show --name-only --format= HEAD | sort | paste -sd' ')
if [ "$names" = "bundle/one.txt bundle/two.txt file.txt" ]; then ok "--add commits every non-ignored new file in a directory"; else bad "--add commits every non-ignored new file in a directory" "$names"; fi

cx_reset
mkdir -p "$cxrepo/nested/handover"
printf 'nested message\n' > "$cxrepo/nested/handover/msg.txt"
printf 'nested body\n' > "$cxrepo/nested/handover/pr.md"
out=$(STUB_CODEX_MSG=$work/msg-plain.txt cx_run --add nested) && s=0 || s=$?
expect "codex run --add refuses a directory holding only nested handover files" 1 'codex run: --add nested: directory holds nothing to add' "$out" "$s"
if [ "$(git -C "$cxrepo" rev-list --count "$base..HEAD")" = 0 ]; then ok "a nested handover-only directory makes no commit"; else bad "a nested handover-only directory makes no commit"; fi

cx_reset
out=$(STUB_CODEX_MSG=$work/msg-plain.txt cx_run --add .) && s=0 || s=$?
expect "codex run --add refuses a directory holding only the handover files" 1 'codex run: --add \.: directory holds nothing to add' "$out" "$s"
if [ "$(git -C "$cxrepo" rev-list --count "$base..HEAD")" = 0 ]; then ok "an empty --add directory makes no commit"; else bad "an empty --add directory makes no commit"; fi

cx_reset
for cache in .jpdev .gocache .npm-cache .xdg-cache; do
mkdir -p "$cxrepo/$cache"
Expand Down Expand Up @@ -938,6 +986,14 @@ out=$(STUB_CODEX_EDIT=1 STUB_CODEX_EXIT=4 STUB_CODEX_MSG=$work/msg-plain.txt cx_
expect "codex run does not commit after a failed codex run" 4 'codex exit 4' "$out" "$s"
if [ "$(git -C "$cxrepo" rev-list --count "$base..HEAD")" = 0 ]; then ok "a failed codex run: no commit"; else bad "a failed codex run: no commit"; fi
cx_reset
printf 'tracked message\n' > "$cxrepo/msg.txt"
git -C "$cxrepo" add -f msg.txt
git -C "$cxrepo" commit -q -m 'track handover message for refusal probe'
tracked_base=$(git -C "$cxrepo" rev-parse HEAD)
out=$(STUB_CODEX_EDIT=1 STUB_CODEX_MSG=$work/msg-plain.txt cx_run) && s=0 || s=$?
expect "codex run still refuses a tracked and changed handover file" 1 'msg.txt or pr.md is tracked and changed' "$out" "$s"
if [ "$(git -C "$cxrepo" rev-parse HEAD)" = "$tracked_base" ]; then ok "a tracked handover refusal makes no commit"; else bad "a tracked handover refusal makes no commit"; fi
cx_reset
out=$(STUB_CODEX_EDIT=1 STUB_CODEX_MSG=$work/msg-plain.txt cx_run --add msg.txt) && s=0 || s=$?
expect "codex run refuses --add msg.txt" 2 'read, never committed' "$out" "$s"
out=$(STUB_CODEX_EDIT=1 STUB_CODEX_MSG=$work/msg-plain.txt cx_run --add .jpdev) && s=0 || s=$?
Expand Down
Loading