core/cli: wire fetch private-host policy confirm

This commit is contained in:
2026-07-24 19:07:39 +08:00
parent d74b03923f
commit 05fe1b4070
3 changed files with 63 additions and 1 deletions
+10 -1
View File
@@ -300,9 +300,14 @@ async def _run_chat( # noqa: C901, PLR0912, PLR0915 — chat orchestration
# Path denylist and policy confirm live on instance.workspace (no process bag).
state.instance_state.workspace.path_denylist = tuple(store.agent_config.path_denylist or ())
if interactive:
from plyngent.cli.limits import prompt_policy_command_confirm
from plyngent.cli.limits import prompt_policy_command_confirm, prompt_policy_fetch_confirm
from plyngent.tools.net import set_fetch_policy_confirm_hook
state.instance_state.workspace.policy_confirm_hook = prompt_policy_command_confirm
set_fetch_policy_confirm_hook(
prompt_policy_fetch_confirm,
instance=state.instance_state,
)
# Seed cache if we already fetched; else warm in background for Tab.
if remote_ids is not None:
state.seed_remote_models(remote_ids)
@@ -334,6 +339,10 @@ async def _run_chat( # noqa: C901, PLR0912, PLR0915 — chat orchestration
if isinstance(state_obj, ReplState):
state_obj.instance_state.workspace.policy_confirm_hook = None
state_obj.instance_state.workspace.policy_allowed_commands.clear()
from plyngent.tools.net import clear_private_grants, set_fetch_policy_confirm_hook
set_fetch_policy_confirm_hook(None, instance=state_obj.instance_state)
clear_private_grants(instance=state_obj.instance_state)
await state_obj.instance_state.shutdown()
else:
# No ReplState: only PTY class cleanup (temps require instance allowlist).
+45
View File
@@ -232,6 +232,51 @@ def prompt_policy_command_confirm(
return False
def prompt_policy_fetch_confirm(
host: str,
port: int,
url: str,
timeout_seconds: float,
) -> bool:
"""Ask whether to allow fetch to a private/loopback host (timed).
Independent of YOLO: always prompts when interactive. Default is **deny**.
Timeout, cancel, or non-interactive → False. On approve, the host:port is
granted for the rest of this process (see net grants).
"""
reason = (
f"fetch targets private/loopback host {host!r} port {port}\n"
f"url: {url}\n"
f"Allow this host:port for the rest of this process?\n"
f"(timeout {timeout_seconds:g}s defaults to DENY; not skipped by YOLO)"
)
try:
with pause_task_cancel_for_prompt():
backend = get_prompt_backend()
if not backend.is_interactive():
return False
backend.echo()
backend.secho(format_tool_confirm_box("policy", reason), fg="yellow")
backend.echo()
grant = f"{host}:{port}"
prompt = f"[policy] allow fetch {grant!r}? [y/N] (timeout {timeout_seconds:g}s): "
with contextlib.suppress(OSError):
_ = sys.stderr.write(prompt)
_ = sys.stderr.flush()
raw = _read_yes_no_line_with_timeout(timeout_seconds)
if raw is None:
with contextlib.suppress(OSError, NonInteractiveError):
backend.secho(
f"[policy] timed out after {timeout_seconds:g}s — denied",
fg="red",
err=True,
)
return False
return raw.strip().lower() in {"y", "yes"}
except NonInteractiveError, KeyboardInterrupt, EOFError:
return False
async def prompt_confirm_tool_async(name: str, args: Mapping[str, object], reason: str) -> bool | str:
"""Async confirm: True allow, False deny, str = deny with user comment."""
del args
+8
View File
@@ -29,6 +29,14 @@ raise `max_replaces` or narrow `old_string`.
for ordered pipelines (`pipe_out` / `mix_stderr` as needed).
- Prefer `vcs_*` for status/diff/log/branch when enough.
### Network
- Prefer `fetch` (GET/POST/PUT/DELETE) for HTTP(S) docs/APIs over curl/wget via shell.
- Set `user_agent` (or a User-Agent header) when the remote expects a specific client; \
otherwise a small default is used. Never rely on shell to spoof identity.
- Private/loopback/LAN hosts need an explicit human policy allow (not skipped by YOLO).
- Prefer provider hosted search (e.g. web_search) for open-ended research; use `fetch` for known URLs.
- Respect denials, size limits, and HTTPS→HTTP redirect blocks; do not re-fetch the same URL repeatedly.
### Humans & safety
- Prefer `ask_user_line` / `ask_user_choice` / `ask_user_form` over waiting for the next free-form turn.
- Overwrites, deletes, shells, and risky ops may require human confirm; hard denylists are not skipped by YOLO.