mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47: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)
|
raise FetchPolicyError(msg)
|
||||||
|
|
||||||
# PRIVATE
|
# PRIVATE
|
||||||
|
from plyngent.agent import Approval
|
||||||
|
from plyngent.tools.context import record_approval
|
||||||
|
|
||||||
if has_private_grant(parsed.host, parsed.port, instance=inst):
|
if has_private_grant(parsed.host, parsed.port, instance=inst):
|
||||||
|
record_approval(Approval("grant", "process"))
|
||||||
return
|
return
|
||||||
|
|
||||||
from plyngent.tools.workspace import get_policy_confirm_timeout
|
from plyngent.tools.workspace import get_policy_confirm_timeout
|
||||||
@@ -116,6 +120,7 @@ async def ensure_host_allowed(
|
|||||||
)
|
)
|
||||||
raise FetchPolicyError(msg)
|
raise FetchPolicyError(msg)
|
||||||
_ = grant_private_host(parsed.host, parsed.port, instance=inst)
|
_ = 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]:
|
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 pathlib import Path
|
||||||
from typing import TYPE_CHECKING, cast
|
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.agent.budget import DEFAULT_TOOL_RESULT_MAX_CHARS
|
||||||
from plyngent.skills import (
|
from plyngent.skills import (
|
||||||
SKILL_FILE,
|
SKILL_FILE,
|
||||||
@@ -30,6 +30,7 @@ from plyngent.skills import (
|
|||||||
resolve_skill_file,
|
resolve_skill_file,
|
||||||
validate_skill_name,
|
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.read import read_file as _read_file
|
||||||
from plyngent.tools.file.regex_files import DEFAULT_MAX_MATCHES
|
from plyngent.tools.file.regex_files import DEFAULT_MAX_MATCHES
|
||||||
from plyngent.tools.file.regex_files import regex_files as _regex_files
|
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)
|
allowed = decision is True or (isinstance(decision, SkillWriteDecision) and decision.allow)
|
||||||
if not allowed:
|
if not allowed:
|
||||||
return f"error: skill write to {skill_dir} denied (declined or timed out after {policy.timeout:g}s)"
|
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)
|
session.skill_write_roots.add(skill_dir)
|
||||||
|
record_approval(Approval("user", "session" if keys_session else "once"))
|
||||||
return None
|
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
|
checked first and always wins; then a session grant, then
|
||||||
``[skills].allow_write``, then the host's confirm hook. Without a hook the
|
``[skills].allow_write``, then the host's confirm hook. Without a hook the
|
||||||
write is denied rather than silently allowed.
|
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)
|
denied = _denied_by_policy(target, skill_dir)
|
||||||
if denied is not None:
|
if denied is not None:
|
||||||
@@ -350,6 +356,7 @@ def _authorize_write(skill_dir: Path, target: Path, *, action: str, detail: str)
|
|||||||
|
|
||||||
session = get_session()
|
session = get_session()
|
||||||
if session is not None and skill_dir in session.skill_write_roots:
|
if session is not None and skill_dir in session.skill_write_roots:
|
||||||
|
record_approval(Approval("grant", "session"))
|
||||||
return None
|
return None
|
||||||
policy = get_skill_write_policy()
|
policy = get_skill_write_policy()
|
||||||
if policy is None or (policy.confirm is None and not policy.allow_all):
|
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"
|
"set [skills].allow_write = true to allow skill writes"
|
||||||
)
|
)
|
||||||
if policy.allow_all:
|
if policy.allow_all:
|
||||||
|
record_approval(Approval("config", "policy"))
|
||||||
return None
|
return None
|
||||||
return _prompt_write(skill_dir, action=action, detail=detail, policy=policy, session=session)
|
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
|
Denylisted programs are not hard-rejected when a policy confirm hook is
|
||||||
installed: the human is asked (with a timeout; default deny). Session grants
|
installed: the human is asked (with a timeout; default deny). Session grants
|
||||||
skip re-prompting for the same program. Independent of YOLO soft-confirm.
|
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)
|
shape_error = argv_shape_error(argv)
|
||||||
if shape_error is not None:
|
if shape_error is not None:
|
||||||
msg = f"command {shape_error}"
|
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):
|
for hit in match_command_denylist(argv, policy.command_denylist):
|
||||||
binary = hit.program
|
binary = hit.program
|
||||||
if binary in policy.policy_allowed_commands:
|
if binary in policy.policy_allowed_commands:
|
||||||
|
record_approval(Approval("grant", "process"))
|
||||||
continue
|
continue
|
||||||
why = f": {hit.keyword!r}" if hit.keyword else ""
|
why = f": {hit.keyword!r}" if hit.keyword else ""
|
||||||
hook = policy.policy_confirm_hook
|
hook = policy.policy_confirm_hook
|
||||||
@@ -596,3 +604,4 @@ def check_command_allowed(argv: list[str]) -> None:
|
|||||||
)
|
)
|
||||||
raise WorkspaceError(msg)
|
raise WorkspaceError(msg)
|
||||||
policy.policy_allowed_commands.add(binary)
|
policy.policy_allowed_commands.add(binary)
|
||||||
|
record_approval(Approval("user", "process"))
|
||||||
|
|||||||
Reference in New Issue
Block a user