From cf96b16301e0ed2e111fe228ac6b50a4d099980d Mon Sep 17 00:00:00 2001 From: kikashy Date: Thu, 22 Oct 2026 14:57:29 -0400 Subject: [PATCH 1/3] Handle backticked brief words and directory additions Ignore filter-list words inside backticks and remove the product's own adversarial-review term from the base list. Allow --add directories to stage their eligible files while excluding handover files and reporting an empty directory by path. Co-Authored-By: Codex Signed-off-by: kikashy --- README.md | 8 +++++--- bin/jpdev | 29 ++++++++++++++++++++++++++--- config/codex-words.txt | 1 - lib/codex_words.py | 16 ++++++++++++++-- tests/run.sh | 34 +++++++++++++++++++++++++++++++++- 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5bb825a..eb9f217 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/bin/jpdev b/bin/jpdev index 7c3a0f4..dcd3aaf 100755 --- a/bin/jpdev +++ b/bin/jpdev @@ -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 " tmpd + local trailer="Co-Authored-By: Codex " 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 @@ -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=() @@ -760,11 +767,27 @@ 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=() + 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 [ "$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 diff --git a/config/codex-words.txt b/config/codex-words.txt index b5b57ba..3ee5649 100644 --- a/config/codex-words.txt +++ b/config/codex-words.txt @@ -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 diff --git a/lib/codex_words.py b/lib/codex_words.py index ce11742..ca5c031 100644 --- a/lib/codex_words.py +++ b/lib/codex_words.py @@ -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. """ @@ -31,6 +33,16 @@ def load(path): return entries +def mask_backticks(text): + """Blank paired backtick spans while preserving offsets and line numbers.""" + return re.sub( + r"(`+).*?\1", + lambda found: re.sub(r"[^\n]", " ", found.group(0)), + text, + flags=re.DOTALL, + ) + + def main(argv): if len(argv) < 3: print("usage: codex_words.py BRIEF WORDFILE...", file=sys.stderr) @@ -45,7 +57,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()] diff --git a/tests/run.sh b/tests/run.sh index 9b41655..0c9a320 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -750,6 +750,15 @@ 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" +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" +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 @@ -823,7 +832,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" @@ -889,6 +898,21 @@ 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" +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 +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" @@ -938,6 +962,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=$? From e1ead3991be8e0a8781d4b066eda00f6d93eed7a Mon Sep 17 00:00:00 2001 From: kikashy Date: Thu, 22 Oct 2026 14:58:51 -0400 Subject: [PATCH 2/3] Silence SC2016 on the literal expression the test writes into a fixture Signed-off-by: kikashy --- tests/run.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run.sh b/tests/run.sh index 0c9a320..640ba8b 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -750,6 +750,7 @@ 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" From fdefeeeb970549152498f50891e9f133fdc6c5b6 Mon Sep 17 00:00:00 2001 From: kikashy Date: Thu, 22 Oct 2026 15:13:33 -0400 Subject: [PATCH 3/3] Exclude nested handovers and fail closed on backticks Remove handover basenames at every depth of an added directory before checking whether it is empty. Parse inline spans and fenced blocks so unbalanced backtick delimiters exempt no text in their scope. Co-Authored-By: Codex Signed-off-by: kikashy --- bin/jpdev | 21 ++++++++++++++++- lib/codex_words.py | 56 ++++++++++++++++++++++++++++++++++++++++------ tests/run.sh | 23 +++++++++++++++++++ 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/bin/jpdev b/bin/jpdev index dcd3aaf..98cb281 100755 --- a/bin/jpdev +++ b/bin/jpdev @@ -767,7 +767,23 @@ 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 - local tracked_handover=0 handover_paths=() + 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 @@ -777,6 +793,9 @@ codex_publish() { 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 diff --git a/lib/codex_words.py b/lib/codex_words.py index ca5c031..91c1d54 100644 --- a/lib/codex_words.py +++ b/lib/codex_words.py @@ -33,14 +33,56 @@ 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 paired backtick spans while preserving offsets and line numbers.""" - return re.sub( - r"(`+).*?\1", - lambda found: re.sub(r"[^\n]", " ", found.group(0)), - text, - flags=re.DOTALL, - ) + """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): diff --git a/tests/run.sh b/tests/run.sh index 640ba8b..08ac44a 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -754,6 +754,18 @@ absent "check-brief does not flag a word that merely contains a listed one" 'sha 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" @@ -904,11 +916,22 @@ 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"