diff --git a/src/plyngent/cli/app.py b/src/plyngent/cli/app.py index a722059..0860d3c 100644 --- a/src/plyngent/cli/app.py +++ b/src/plyngent/cli/app.py @@ -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). diff --git a/src/plyngent/cli/limits.py b/src/plyngent/cli/limits.py index 553d01e..b987940 100644 --- a/src/plyngent/cli/limits.py +++ b/src/plyngent/cli/limits.py @@ -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 diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index e9fccb9..8315d73 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -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.