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
72 changes: 71 additions & 1 deletion .agent_harness/command_guard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,76 @@ def _command_is_safe_local_cdidx(command: str, cwd: Path, project_root: Path) ->
return dll == expected


def _has_active_shell_syntax(command: str) -> bool:
quote: str | None = None
index = 0
while index < len(command):
char = command[index]
if char == "\\" and quote != "'":
index += 2
continue
if char in {"'", '"'}:
if quote is None:
quote = char
elif quote == char:
quote = None
elif quote != "'" and char in "`$":
return True
elif quote is None and char in "();<>|&\n#":
return True
index += 1
return False


def _command_has_unsupported_cdidx_dll(command: str) -> bool:
tokens = _split_command(command)
if not LOCAL_CDIDX_DLL_RE.search(command) and not any(LOCAL_CDIDX_DLL_RE.search(token) for token in tokens):
return False
# This is a bounded data-argument exception, not a general shell parser.
# Preserve denial for unknown executors, wrappers, comments, redirections
# and compound commands. Quoted/escaped punctuation stays literal data.
if not tokens or _has_active_shell_syntax(command):
return True
if tokens[0] in {"echo", "ls"}:
return False
if tokens[0] == "printf":
args = tokens[1:]
if args and args[0] == "--":
args = args[1:]
# Shell printf can evaluate variable targets (-v / %n) and numeric
# arguments. Only literal text, %% and %s are known display forms.
return not (
args and not args[0].startswith("-")
and re.fullmatch(r"(?:[^%]|%%|%s)*", args[0]) is not None
)
if tokens[0] == "codex" and len(tokens) >= 3 and tokens[1] in {"exec", "review"}:
prompt, rest = _subcommand_args(
tokens[2:],
{"--sandbox", "-s", "--output-last-message", "-o", "--output-schema", "--model", "-m",
"--profile", "-p", "--config", "-c", "--cd", "-C", "--image", "-i", "--color",
"--enable", "--disable", "--base", "--commit", "--title"},
valueless_options={"--ephemeral", "--json", "--skip-git-repo-check", "--uncommitted"},
fail_on_unknown_option=True,
)
return not (
prompt == tokens[-1] and not rest
and not any(LOCAL_CDIDX_DLL_RE.search(token) for token in tokens[:-1])
)
if tokens[0] == "gh":
subcommand, _ = _subcommand_args(
tokens[1:], {"-R", "--repo", "--hostname", "--config"}, fail_on_unknown_option=True,
)
if subcommand in {"pr", "issue"}:
data_options = {"--body", "-b", "--title", "-t", "--search", "-S"}
return any(
LOCAL_CDIDX_DLL_RE.search(token)
and not (index > 0 and tokens[index - 1] in data_options)
and not any(token.startswith(option + "=") for option in data_options if option.startswith("--"))
for index, token in enumerate(tokens)
)
return True


def _token_is_expanded_installed_cdidx(token: str, cwd: Path) -> bool:
if not token.startswith("/"):
return False
Expand Down Expand Up @@ -1267,7 +1337,7 @@ def evaluate_bash_command(command: str, cwd: Path, project_root: Path) -> GuardD
if _command_mentions_local_cdidx(command, project_root):
return _deny("local cdidx commands must not use shell control operators or command substitutions")

if LOCAL_CDIDX_DLL_RE.search(command):
if _command_has_unsupported_cdidx_dll(command):
return _deny("use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll instead")

if _command_is_safe_cdidx_resolver(command):
Expand Down
265 changes: 265 additions & 0 deletions .agent_harness/guard_policy_contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,271 @@
"command": "dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll search SymbolExtractor",
"expected": "allow"
},
{
"name": "review_prompt_local_dll_single_quoted",
"command": "codex exec --sandbox read-only --ephemeral --output-last-message /tmp/issue5417-review.txt 'For source discovery/search use only the repository-built binary dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll with --db .cdidx/codeindex.db'",
"expected": "allow"
},
{
"name": "review_prompt_local_dll_double_quoted",
"command": "codex exec --sandbox read-only \"Use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll with --db .cdidx/codeindex.db\"",
"expected": "allow"
},
{
"name": "review_prompt_local_dll_multiline_markdown",
"command": "codex exec --sandbox read-only 'Review the change.\nUse `dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll` for discovery.'",
"expected": "allow"
},
{
"name": "document_body_local_dll_literal_argument",
"command": "gh pr create --title Update --body 'dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll'",
"expected": "allow"
},
{
"name": "local_dll_read_operand",
"command": "ls -l './src/CodeIndex/bin/Debug/net8.0/cdidx.dll'",
"expected": "allow"
},
{
"name": "local_cdidx_quoted_operand",
"command": "dotnet './src/CodeIndex/bin/Debug/net8.0/cdidx.dll' status",
"expected": "allow"
},
{
"name": "wrapped_review_prompt_local_dll",
"command": "env REVIEW_MODE=readonly timeout 30 codex exec 'Use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll'",
"expected": "deny"
},
{
"name": "unsupported_cdidx_dll",
"command": "dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "unsupported_cdidx_dll_fragmented_quotes",
"command": "dotnet ./src/CodeIndex/bin/Release/net8.0/cdi''dx.dll status",
"expected": "deny"
},
{
"name": "absolute_dotnet_host_cdidx_dll",
"command": "/usr/local/bin/dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "local_cdidx_trailing_command",
"command": "dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status; true",
"expected": "deny"
},
{
"name": "direct_cdidx_dll",
"command": "'./src/CodeIndex/bin/Debug/net8.0/cdidx.dll' status",
"expected": "deny"
},
{
"name": "dotnet_exec_cdidx_dll",
"command": "dotnet exec './src/CodeIndex/bin/Debug/net8.0/cdidx.dll' status",
"expected": "deny"
},
{
"name": "wrapped_local_cdidx_dll",
"command": "env REVIEW_MODE=readonly timeout 30 dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "split_string_local_cdidx_dll",
"command": "env -S 'dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status'",
"expected": "deny"
},
{
"name": "chained_local_cdidx_dll",
"command": "true && dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "newline_local_cdidx_dll",
"command": "true\ndotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "subshell_local_cdidx_dll",
"command": "true;(dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status)",
"expected": "deny"
},
{
"name": "process_substitution_local_cdidx_dll",
"command": "cat <(dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status)",
"expected": "deny"
},
{
"name": "conditional_local_cdidx_dll",
"command": "if dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status; then true; fi",
"expected": "deny"
},
{
"name": "inline_shell_local_cdidx_dll",
"command": "bash -lc 'dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status'",
"expected": "deny"
},
{
"name": "eval_local_cdidx_dll",
"command": "eval 'dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status'",
"expected": "deny"
},
{
"name": "review_prompt_dll_command_substitution",
"command": "codex exec \"Review $(dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status)\"",
"expected": "deny"
},
{
"name": "review_prompt_dll_backtick_substitution",
"command": "codex exec \"Review `dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status`\"",
"expected": "deny"
},
{
"name": "review_prompt_dll_literal_substitution",
"command": "codex exec 'Review $(dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status)'",
"expected": "allow"
},
{
"name": "review_prompt_dll_escaped_substitution",
"command": "codex exec \"Review \\$(dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status)\"",
"expected": "allow"
},
{
"name": "review_prompt_dll_unterminated_quote",
"command": "codex exec 'Use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll",
"expected": "deny"
},
{
"name": "global_cdidx_execution",
"command": "cdidx status",
"expected": "deny"
},
{
"name": "comment_quotes_cannot_hide_dll_substitution",
"command": "true # '\nprintf '%s' \"$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)\"\n#'",
"expected": "deny"
},
{
"name": "comment_quotes_cannot_hide_dll_execution",
"command": "echo ignore # 'fake\ndotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status\n#'",
"expected": "deny"
},
{
"name": "interspersed_redirection_dll_execution",
"command": "dotnet 2>&1 ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "leading_redirection_dll_execution",
"command": "2>&1 dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "negated_dll_execution",
"command": "! dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "brace_group_dll_execution",
"command": "{ dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status; }",
"expected": "deny"
},
{
"name": "xargs_dll_execution",
"command": "printf 'status\\n' | xargs dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll",
"expected": "deny"
},
{
"name": "legacy_nice_dll_execution",
"command": "nice -5 dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "builtin_command_dll_execution",
"command": "builtin command dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "variable_host_dll_execution",
"command": "RUNNER=dotnet; \"$RUNNER\" ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "line_continuation_dll_host",
"command": "dot\\\nnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status",
"expected": "deny"
},
{
"name": "printed_dll_document_with_literal_separator",
"command": "printf '%s\\n' ';' 'dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status'",
"expected": "allow"
},
{
"name": "printed_dll_document_with_escaped_separator",
"command": "printf '%s\\n' \\; 'dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll status'",
"expected": "allow"
},
{
"name": "review_dll_config_is_not_prompt_data",
"command": "codex exec --config 'mcp_servers.example.args=[\"/tmp/cdidx.dll\"]' Review",
"expected": "deny"
},
{
"name": "review_dll_config_without_prompt",
"command": "codex exec --config 'mcp_servers.example.args=[\"/tmp/cdidx.dll\"]'",
"expected": "deny"
},
{
"name": "review_prompt_supported_review_subcommand",
"command": "codex review --base origin/main 'Use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll'",
"expected": "allow"
},
{
"name": "document_body_dll_equals_option",
"command": "gh --repo Widthdom/CodeIndex issue create --title Update --body='Use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll'",
"expected": "allow"
},
{
"name": "review_nested_parameter_dll_substitution",
"command": "codex exec \"${review_text:-\"'$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)'\"}\"",
"expected": "deny"
},
{
"name": "echo_nested_parameter_dll_substitution",
"command": "echo \"${review_text:-\"'$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)'\"}\"",
"expected": "deny"
},
{
"name": "review_literal_parameter_dll_text",
"command": "codex exec '${review_text:-$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)}'",
"expected": "allow"
},
{
"name": "printf_variable_target_dll_substitution",
"command": "printf -v 'a[$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)]' '%s' x",
"expected": "deny"
},
{
"name": "printf_count_target_dll_substitution",
"command": "printf '%n' 'a[$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)]'",
"expected": "deny"
},
{
"name": "printf_numeric_dll_argument",
"command": "printf '%d' 'a[$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)]'",
"expected": "deny"
},
{
"name": "printf_string_dll_argument",
"command": "printf -- '%s%%\\n' 'a[$(dotnet ./src/CodeIndex/bin/Release/net8.0/cdidx.dll status)]'",
"expected": "allow"
},
{
"name": "printf_literal_dll_format",
"command": "printf 'Use dotnet ./src/CodeIndex/bin/Debug/net8.0/cdidx.dll'",
"expected": "allow"
},
{
"name": "repo_local_installer_doctor",
"command": "bash ./install.sh --doctor v1.2.3",
Expand Down
1 change: 1 addition & 0 deletions AGENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Command-search enforcement is tool-specific and adapter-driven:
- Codex uses `.codex/hooks.json`, which invokes `.codex/hooks/bash_guard.py` and `.codex/hooks/permission_request_guard.py`.
- Claude Code uses `.claude/settings.json`, which invokes `.claude/hooks/bash-guard.py`.
- Both Bash guard adapters delegate shared command policy to `.agent_harness/command_guard_core.py`; update the shared core for common command policy and review both adapters only when tool-specific behavior changes.
- The DLL guard permits `cdidx.dll` text in a final prompt to direct `codex exec` / `codex review` commands with recognized options, in `gh pr` / `gh issue` body/title/search arguments, and in direct `echo` / `ls` arguments. Direct `printf` is limited to literal text, `%%` and `%s` formats, with optional `--`; variable assignment and other conversions remain blocked. Unknown command forms, wrappers, comments, redirections, compound commands and active substitutions/parameter expansions remain conservatively blocked when they mention the DLL. Quoted or escaped punctuation stays data; other guard checks still apply to the entire command. Use a direct review command and a body file for more complex document content.
- Codex uses the `codeindex_workspace` permission profile for workspace writes plus limited GitHub CLI network access to `github.com` and `api.github.com`.
- Codex may use normal development GitHub CLI commands including `gh issue list/view/create/edit/comment`, `gh pr list/view/create/edit/comment/ready/close`, `gh repo view`, and `gh status`.
- Keep `gh auth`, `gh api`, `gh secret`, `gh release`, `gh repo create`, `gh repo fork`, `gh repo delete`, and `gh pr merge` blocked. `gh api` is blocked because arbitrary REST/GraphQL calls can bypass subcommand-level policy intent; `gh pr merge` is blocked because it mutates remote PR state in a high-risk way.
Expand Down
Loading
Loading