Skip to content
Open
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
42 changes: 42 additions & 0 deletions docs/local_agent_loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,48 @@ Currently supported local agent CLIs:
- OpenAI Codex CLI via `codex`
- Gemini CLI via `gemini` (best-effort support for users whose organization or API-key setup still has access)

### Antigravity native-tool capability probe for issue #568

Production Antigravity settings injection is intentionally not changed until a
real `agy --print` probe verifies a checkout-scoped recursive native read while
denying all native writes and outside access. This section records the probe
run on 2026-07-15 with `agy` version `1.1.3` and model `Gemini 3.5 Flash (High)`.

The isolated profile was selected with `HOME` set to a temporary directory;
`strace` confirmed that `agy` read that profile's
`.gemini/antigravity-cli/settings.json`. The nested fixture was
`/tmp/coding-review-agent-loop/scratch/issue-568-probe/fixture/nested/file.txt`
and contained `nested-probe-secret`.

The following candidate settings were each tested with a native nested read:

```json
{"toolPermission":"strict","permissions":{"allow":["read_file(/tmp/coding-review-agent-loop/scratch/issue-568-probe/fixture)"]}}
{"toolPermission":"strict","permissions":{"allow":["read_file(/tmp/coding-review-agent-loop/scratch/issue-568-probe/fixture/**)"]}}
{"toolPermission":"strict","permissions":{"allow":["read_file(/tmp/coding-review-agent-loop/scratch/issue-568-probe/fixture/*)"]}}
{"trustedWorkspaces":["/tmp/coding-review-agent-loop/scratch/issue-568-probe/fixture"],"allowNonWorkspaceAccess":false}
```

Every candidate failed before producing a response with this diagnostic:

```text
jetski: no output produced — a tool required the "read_file" permission that headless mode cannot prompt for, so it was auto-denied. Add an allow-rule under permissions.allow in settings.json (e.g. read_file(<target>)). Alternatively, re-run with --dangerously-skip-permissions to auto-approve all tools.
```

The exact-file and wildcard probes were also denied; adding
`read_file(*)` did not change the result. The trusted-workspace probe was
repeated after initializing the fixture as a Git repository and still failed.

For each candidate, native `write_file` probes targeting a fresh path inside
the fixture and a fresh path outside it were denied, and neither target was
created. The trusted-workspace profile also denied a native `command` probe
(`pwd`). Those denials are safety-positive, but they do not compensate for
the missing nested read grant: no candidate passed the required combination
of nested read plus inside- and outside-checkout write denial. No production
permission rule is therefore inferred or shipped. An Antigravity-supported
recursive-read mechanism (or explicit human approval of an alternative) is
required before implementation can continue.

## Prerequisites

- `gh` is installed and authenticated for the target GitHub repository.
Expand Down
31 changes: 31 additions & 0 deletions src/coding_review_agent_loop/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,13 +734,38 @@ def _is_retryable_marker_near_miss(text: str) -> bool:
)


_NATIVE_PERMISSION_DENIAL_RE = re.compile(
r"(?:required the\s+\"(?P<quoted>read_file|write_file|command)\"\s+permission|"
r"permission(?: request)?(?: for| to)?\s+[`\"]?(?P<named>read_file|write_file|command)[`\"]?)",
re.I,
)


def _native_permission_denial_tool(text: str) -> str | None:
"""Return the native tool denied by headless Antigravity, if any.

Antigravity reports these failures on stdout and exits without a public
response. Keeping this classification separate from ``empty-response``
makes the actionable CLI diagnostic visible to callers and logs.
"""
if not re.search(r"(?:headless|no output produced|auto-denied|auto denied)", text, re.I):
return None
match = _NATIVE_PERMISSION_DENIAL_RE.search(text)
if not match:
return None
return (match.group("quoted") or match.group("named")).lower()


def _failure_category(
text: str,
*,
public_response: bool = False,
repair_expected_kind: str | None = None,
) -> str:
"""Classify a failure for logging: helps users decide whether to rerun or fix config/code."""
native_tool = _native_permission_denial_tool(text)
if native_tool:
return f"native-tool-denied-{native_tool}"
if not text.strip():
return "empty-response"
if _unsupported_model_classification_text(
Expand Down Expand Up @@ -1231,6 +1256,12 @@ def _failure_suggestion(
if re.search(r"\bdirty\b", combined, re.I):
return "Suggestion: clean up the dirty working tree or workdir, then re-run."
return f"Suggestion: check that {agent_name} is installed and authenticated, then re-run."
if category and category.startswith("native-tool-denied-"):
tool = category.removeprefix("native-tool-denied-")
return (
f"Suggestion: inspect the Antigravity {tool} permission policy for the assigned "
"checkout, then re-run; do not enable unrestricted permission bypasses."
)
if category == "deterministic":
if "repair invocation failure" in reason and "invalid_output" in reason:
return (
Expand Down
34 changes: 34 additions & 0 deletions tests/test_agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
_format_reset_duration,
_is_transient_agent_output,
_is_transient_public_response,
_native_permission_denial_tool,
_parse_rate_limit_reset_seconds,
_plan_subject,
_resume_plan_round,
Expand Down Expand Up @@ -4274,6 +4275,39 @@ def test_failure_suggestion_empty_response():
assert msg == ""


@pytest.mark.parametrize(
("tool", "diagnostic"),
[
(
"read_file",
'jetski: no output produced — a tool required the "read_file" permission '
"that headless mode cannot prompt for, so it was auto-denied.",
),
(
"write_file",
'headless mode auto-denied a tool required the "write_file" permission.',
),
(
"command",
'headless mode auto-denied a tool required the "command" permission.',
),
],
)
def test_headless_native_permission_denials_are_categorized_by_tool(tool, diagnostic):
assert _native_permission_denial_tool(diagnostic) == tool
assert _failure_category(diagnostic) == f"native-tool-denied-{tool}"
suggestion = _failure_suggestion(
f"native-tool-denied-{tool}", diagnostic, "Antigravity"
)
assert tool in suggestion
assert "unrestricted" in suggestion


def test_unrelated_blank_output_remains_empty_response():
assert _native_permission_denial_tool("") is None
assert _failure_category("") == "empty-response"


def test_failure_suggestion_none_category():
msg = _failure_suggestion(None, "unknown error", "TestAgent")
assert msg == ""
Expand Down
Loading