mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
tools: record the policy answers that let a call through
The command denylist, the private-host fetch policy and the skill write grant asked the human and then said nothing about it in the result. Each now records the answer for the running call: `user (process)` for this call's override and `grant (process)` when an earlier one already covers the program or host, `user (once|session)` / `grant (session)` / `config (policy)` for a skill write. `request_directory_access` is unchanged: its own text already names the level and scope it granted.
This commit is contained in:
@@ -89,7 +89,11 @@ async def ensure_host_allowed(
|
||||
raise FetchPolicyError(msg)
|
||||
|
||||
# PRIVATE
|
||||
from plyngent.agent import Approval
|
||||
from plyngent.tools.context import record_approval
|
||||
|
||||
if has_private_grant(parsed.host, parsed.port, instance=inst):
|
||||
record_approval(Approval("grant", "process"))
|
||||
return
|
||||
|
||||
from plyngent.tools.workspace import get_policy_confirm_timeout
|
||||
@@ -116,6 +120,7 @@ async def ensure_host_allowed(
|
||||
)
|
||||
raise FetchPolicyError(msg)
|
||||
_ = grant_private_host(parsed.host, parsed.port, instance=inst)
|
||||
record_approval(Approval("user", "process"))
|
||||
|
||||
|
||||
def format_grant_preview(host: str, port: int, url: str) -> Sequence[str]:
|
||||
|
||||
@@ -18,7 +18,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from plyngent.agent import ToolTag, tool
|
||||
from plyngent.agent import Approval, ToolTag, tool
|
||||
from plyngent.agent.budget import DEFAULT_TOOL_RESULT_MAX_CHARS
|
||||
from plyngent.skills import (
|
||||
SKILL_FILE,
|
||||
@@ -30,6 +30,7 @@ from plyngent.skills import (
|
||||
resolve_skill_file,
|
||||
validate_skill_name,
|
||||
)
|
||||
from plyngent.tools.context import record_approval
|
||||
from plyngent.tools.file.read import read_file as _read_file
|
||||
from plyngent.tools.file.regex_files import DEFAULT_MAX_MATCHES
|
||||
from plyngent.tools.file.regex_files import regex_files as _regex_files
|
||||
@@ -329,8 +330,10 @@ def _prompt_write(
|
||||
allowed = decision is True or (isinstance(decision, SkillWriteDecision) and decision.allow)
|
||||
if not allowed:
|
||||
return f"error: skill write to {skill_dir} denied (declined or timed out after {policy.timeout:g}s)"
|
||||
if isinstance(decision, SkillWriteDecision) and decision.session and session is not None:
|
||||
keys_session = isinstance(decision, SkillWriteDecision) and decision.session
|
||||
if keys_session and session is not None:
|
||||
session.skill_write_roots.add(skill_dir)
|
||||
record_approval(Approval("user", "session" if keys_session else "once"))
|
||||
return None
|
||||
|
||||
|
||||
@@ -342,6 +345,9 @@ def _authorize_write(skill_dir: Path, target: Path, *, action: str, detail: str)
|
||||
checked first and always wins; then a session grant, then
|
||||
``[skills].allow_write``, then the host's confirm hook. Without a hook the
|
||||
write is denied rather than silently allowed.
|
||||
|
||||
Each allow that skipped the prompt is recorded too, so the result says why
|
||||
nobody was asked: ``grant (session)`` or ``config (policy)``.
|
||||
"""
|
||||
denied = _denied_by_policy(target, skill_dir)
|
||||
if denied is not None:
|
||||
@@ -350,6 +356,7 @@ def _authorize_write(skill_dir: Path, target: Path, *, action: str, detail: str)
|
||||
|
||||
session = get_session()
|
||||
if session is not None and skill_dir in session.skill_write_roots:
|
||||
record_approval(Approval("grant", "session"))
|
||||
return None
|
||||
policy = get_skill_write_policy()
|
||||
if policy is None or (policy.confirm is None and not policy.allow_all):
|
||||
@@ -358,6 +365,7 @@ def _authorize_write(skill_dir: Path, target: Path, *, action: str, detail: str)
|
||||
"set [skills].allow_write = true to allow skill writes"
|
||||
)
|
||||
if policy.allow_all:
|
||||
record_approval(Approval("config", "policy"))
|
||||
return None
|
||||
return _prompt_write(skill_dir, action=action, detail=detail, policy=policy, session=session)
|
||||
|
||||
|
||||
@@ -565,7 +565,14 @@ def check_command_allowed(argv: list[str]) -> None:
|
||||
Denylisted programs are not hard-rejected when a policy confirm hook is
|
||||
installed: the human is asked (with a timeout; default deny). Session grants
|
||||
skip re-prompting for the same program. Independent of YOLO soft-confirm.
|
||||
|
||||
Either way the call records how it got past the denylist, so the tool result
|
||||
says it: ``user (process)`` for this call's answer, ``grant (process)`` when
|
||||
an earlier one already covers this program.
|
||||
"""
|
||||
from plyngent.agent import Approval
|
||||
from plyngent.tools.context import record_approval
|
||||
|
||||
shape_error = argv_shape_error(argv)
|
||||
if shape_error is not None:
|
||||
msg = f"command {shape_error}"
|
||||
@@ -577,6 +584,7 @@ def check_command_allowed(argv: list[str]) -> None:
|
||||
for hit in match_command_denylist(argv, policy.command_denylist):
|
||||
binary = hit.program
|
||||
if binary in policy.policy_allowed_commands:
|
||||
record_approval(Approval("grant", "process"))
|
||||
continue
|
||||
why = f": {hit.keyword!r}" if hit.keyword else ""
|
||||
hook = policy.policy_confirm_hook
|
||||
@@ -596,3 +604,4 @@ def check_command_allowed(argv: list[str]) -> None:
|
||||
)
|
||||
raise WorkspaceError(msg)
|
||||
policy.policy_allowed_commands.add(binary)
|
||||
record_approval(Approval("user", "process"))
|
||||
|
||||
Reference in New Issue
Block a user