[framework] Permission confirmation layer bypassable three ways: ask rules miss compound/wrapped shell commands, the persistent-approval file is model-writable, the CLI prompt allows on any unrecognized answer
Maintainer thường phản hồi trong vòng 1 ngày
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 3-5 ngày
- Mức phù hợp với người mới
- 30/100
Hướng nghiên cứu
Start with the permission flow in ms_agent/permission/: read enforcer.py, matcher.py, safety.py, shell_validator.py, memory.py, and handler.py, then compare the reported behavior with the attached suggested_fix.diff. Run the reproduction steps in the issue and the permission tests; done means compound shell commands still trigger ask rules, the consent file cannot be written through file or shell tools, and only explicit yes/no answers are accepted with EOF denied. The issue raises two unresolved design points, so agree on those before implementing.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
Note on the channel: .github/SECURITY.md asks for security reports through GitHub Private Vulnerability Reporting, but that feature is disabled on this repository (the Security tab shows no "Report a vulnerability" button) and SECURITY.md gives no other contact, so this goes in the public tracker. If you enable the private channel I will re-file there and this issue can be closed.
Initial Checks
- I have searched related issues and docs but cannot get the expected help.
- The bug has not been fixed in the latest version.
A clear and concise description of what the bug is.
Summary
The permission layer has two cooperating parts (ms_agent/permission/): an inner SafetyGuard baseline and an outer PermissionEnforcer that consults blacklist, "ask rules", memory, then an interactive handler. Three behaviors let a tool call that the layer is meant to confirm run with no confirmation.
M1 - ask rules only match the whole command string, so compound and wrapped commands skip them. The default ask rules gate network egress and remote access, and the comment at ms_agent/permission/config.py:169 (the _DEFAULT_ASK_RULES tuple, lines 173-181) states they "hold in every mode":
'code_executor---shell_executor:curl *', 'wget *', 'ssh *', 'scp *', 'rsync *', 'nc *', 'netcat *'
A rule's content pattern (curl *) is matched by fnmatch against the single string extracted for the tool: _extract_content returns the entire command argument (ms_agent/permission/matcher.py:17-37), and match_pattern fnmatches the whole thing (ms_agent/utils/pattern_matcher.py:15-19). fnmatch of curl * against true && curl http://x is False, so the rule misses every command where curl/wget/ssh is not the literal start of the string: true && curl ..., cd /tmp; wget ..., /usr/bin/curl ... (absolute path), env curl ... (a stripped wrapper), echo url | xargs curl, a newline-separated ls\ncurl .... In auto mode (the default) the enforcer, finding no matching ask rule, returns allow with no prompt (ms_agent/permission/enforcer.py:185-188, reached because ask_rule is None). Headless (the AutoPermissionHandler fallback) is worse: a bare curl is denied as designed (enforcer.py:174-182), but true && curl ... is allowed outright.
M2 - the persistent-consent file lives in the writable workspace and is not a protected write target. allow_always decisions persist to <workspace>/.ms_agent/permission_memory.json (ms_agent/permission/memory.py:41-43, loaded at :55-56 and wired to the workspace root in ms_agent/agent/llm_agent.py:952). PermissionEnforcer consults memory and returns allow before it reaches the ask rules or the handler (enforcer.py:201-205; memory is also checked for a remembered answer at :148 and :101). That file is inside the project workspace, which the model may write, and it is not in _DEFAULT_SENSITIVE_PATHS (config.py:21-35). So SafetyGuard permits the model's own file_system---write_file or file_system---edit_file to that path, and permits a shell redirect to it (echo ... > .ms_agent/ permission_memory.json), because neither the write-sensitive check in safety.py:94-102 nor the shell redirect check rejects it. The model can therefore write an entry such as [{"pattern": "code_executor---shell_executor"}] and, from the next run on, have every shell command (curl and ssh included) allowed with no prompt in auto and interactive mode alike. docs/zh/design/permission-design.md:490 and :1074 name the analogous .claude/settings.json write as exactly the attack the layer is meant to stop, so the self-grant through its own consent file is a gap in the same defense.
M3 - the CLI prompt treats any unrecognized answer, including a bare Enter, as "allow once"; EOF is uncaught. CLIPermissionHandler.ask (ms_agent/permission/handler.py:102-145) prints a menu whose only "allow" line is [y] Allow this once, then branches on the answer: s, a, e, n are handled and the final else returns ALLOW_ONCE (:144-145). So an empty Enter, a typo such as yse, no, a stray q, or a space all allow the call; only n denies. input() is not wrapped, so EOF (a closed or piped stdin) raises EOFError out of the handler rather than being treated as "no answer". A prompt whose fail-open default is allow is the opposite of a confirmation.
Impact: the three together mean the mechanism meant to force a deliberate click before the agent reaches the network, runs a remote command, or acts when stdin is not a clean TTY can be skipped by phrasing the command with a leading true &&, by writing one JSON file, or by sending any keystroke that is not n.
Versions / Dependencies
main at 94b045a4b559e2a0e9d8814d7c3e41661eaefd5b (version 1.6.0).
Reproduction
poc.py (inlined below) imports ms_agent.permission directly and drives it with stub handlers that record whether a person was asked. No model, no network, no API key.
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -r requirements/framework.txt
.venv/bin/python poc.py . # repo root as argv[1]
Observed on 2026-11-04 at 94b045a (unmodified clone), abbreviated:
== M1 (mode=auto, a person attached)
'curl http://127.0.0.1:1/x' -> allow asked=1 <- the one form the rule catches
'true && curl http://127.0.0.1:1/x' -> allow asked=0
'cd /tmp; wget http://127.0.0.1:1/x' -> allow asked=0
'/usr/bin/curl http://127.0.0.1:1/x' -> allow asked=0
'env curl http://127.0.0.1:1/x' -> allow asked=0
'echo http://127.0.0.1:1/x | xargs curl' -> allow asked=0
'true && ssh user@host' -> allow asked=0
-- headless: 'true && curl http://127.0.0.1:1/x' -> allow
== M2
SafetyGuard file_system---write_file '.ms_agent/permission_memory.json' -> allow
SafetyGuard code_executor---shell_executor 'echo x > .../.ms_agent/permission_memory.json' -> allow
(plant the grant, fresh session) matches curl x: True
mode=auto curl ... -> allow asked=0 (Allowed by remembered permission)
== M3
input '' -> allow_once input 'yse' -> allow_once input 'no' -> allow_once
input ' ' -> allow_once input 'q' -> allow_once EOF -> raised EOFError
AS SHIPPED - gate bypassed in 19 cases
The same script against a copy with the diff below prints asked=1 for every M1 command, deny for every M2 SafetyGuard write and every M3 answer except y/yes, and ends PATCHED - every case above was confirmed or denied.
Suggested fix
suggested_fix.diff (attached, applies to main at 94b045a; tests/permission 374 passed on the patched copy):
- M1: match ask rules per sub-command on the command name. A new
shell_command_viewsinmatcher.pyreuses the existing_split_compound,_strip_redirections,_unwrap_group,_strip_heredoc_bodiesand_extract_command_substitutionsfromshell_validator.pyand thestrip_safe_wrappers/_ENV_ASSIGNhelpers fromwrapper_strip.pyto list each simple command as<basename> <args>, also peelingxargs/exec/commandprefixes; a newmatch_ask_rulefnmatches the rule against each view. Only ask rules use it (the enforcer and the hook rule layer); whitelist/memory matching stays matching the whole command, so a narrow allow can never be widened by appending a second command. - M2: add
.ms_agent/permission_memory.jsonand**/.ms_agent/permission_memory.jsonto_DEFAULT_SENSITIVE_PATHS, and make both the write-sensitive check (safety.py, now comparing the resolved path) and the shell path/redirect validators (shell_validator.py, newsensitive_write_paths) reject writes and redirects that land there. - M3:
CLIPermissionHandler.askallows only ony/yes, denies onn/no, denies with a printed note on any other answer, and catchesEOFErrorfrominput()as a deny.
I can send this as a pull request (DCO signed-off) once the private channel is open and you confirm the direction. Two design points worth your call: the fix keeps ask-rule evasion handling inside matcher.py by importing shell_validator helpers, which introduces a permission-internal import; and M2 only blocks the model from writing the consent file, it does not change the trust model of a file an external process placed in the workspace.
suggested_fix.diff (apply with git apply at the commit named above)
--- a/ms_agent/permission/config.py 2026-11-04 00:38:36
+++ b/ms_agent/permission/config.py 2026-11-04 01:47:08
@@ -32,6 +32,12 @@
'.git/config',
'.git/hooks/*',
'**/.git/**',
+ # The persistent allow_always store. It is read back as policy before any
+ # ask rule, so a write to it (write_file, edit_file, a shell redirect) is a
+ # permission grant the user never clicked. Both spellings, like .git above:
+ # the relative one for a workspace-relative path, the ** one for absolute.
+ '.ms_agent/permission_memory.json',
+ '**/.ms_agent/permission_memory.json',
)
#: Locations whose CONTENTS are a credential. Kept apart from
--- a/ms_agent/permission/safety.py 2026-11-04 00:38:36
+++ b/ms_agent/permission/safety.py 2026-11-04 01:47:08
@@ -9,11 +9,12 @@
import fnmatch
import os
from dataclasses import dataclass
+from pathlib import Path
from typing import Any, Literal, Sequence
from .config import SafetyConfig
from .matcher import PermissionMatcher
-from .path_validator import validate_path
+from .path_validator import matches_sensitive, validate_path
from .shell_validator import (PathSafetyConfig, SafetyDecision,
ShellPathValidator)
@@ -43,6 +44,7 @@
workspace_root=workspace_root,
dangerous_removal_paths=tuple(config.dangerous_removal_paths),
sensitive_read_paths=tuple(config.sensitive_read_paths),
+ sensitive_write_paths=tuple(config.sensitive_paths),
)
self._shell_validator = ShellPathValidator(
allowed_dirs=self._allowed_dirs,
@@ -90,18 +92,26 @@
if not path:
return SafetyDecision(action='deny', reason='Empty file path')
- # Sensitive path check
+ cwd = self._workspace_root or os.getcwd()
+
+ # Sensitive path check: the path as written AND as it resolves, so
+ # ``./.ms_agent/../.ms_agent/permission_memory.json`` is judged by
+ # where it lands.
if op_type == 'write':
expanded = os.path.expanduser(path)
+ try:
+ resolved = str(Path(cwd, expanded).resolve())
+ except (OSError, RuntimeError):
+ resolved = expanded
for sensitive in self._sensitive_paths:
sensitive_expanded = os.path.expanduser(sensitive)
- if fnmatch.fnmatch(expanded, sensitive_expanded):
+ if fnmatch.fnmatch(expanded, sensitive_expanded) or \
+ matches_sensitive(resolved, (sensitive, )):
return SafetyDecision(
action='deny',
reason=f'Write to sensitive path blocked: {path}',
)
- cwd = self._workspace_root or os.getcwd()
result = validate_path(
path,
cwd,
--- a/ms_agent/permission/shell_validator.py 2026-11-04 00:38:36
+++ b/ms_agent/permission/shell_validator.py 2026-11-04 01:47:08
@@ -23,6 +23,7 @@
extract_interpreter_script, find_uses_delete,
interpreter_runs_inline_code)
from .path_validator import (PathValidationResult, is_dangerous_removal_path,
+ matches_sensitive,
validate_path)
from .sed_validator import check_sed_expression_safety, is_sed_read_only
from .wrapper_strip import strip_safe_wrappers
@@ -256,6 +257,10 @@
workspace_root: str | None = None
dangerous_removal_paths: tuple[str, ...] = ()
sensitive_read_paths: tuple[str, ...] = ()
+ #: Locations a shell command may not WRITE or CREATE (``SafetyConfig.
+ #: sensitive_paths``). Until now only write_file/edit_file honoured that
+ #: list; ``echo x > .ms_agent/permission_memory.json`` did not.
+ sensitive_write_paths: tuple[str, ...] = ()
class ShellPathValidator:
@@ -271,6 +276,8 @@
self._read_only_dirs = list(self._config.read_only_directories)
self._workspace_root = self._config.workspace_root or os.getcwd()
self._sensitive_read_paths = list(self._config.sensitive_read_paths)
+ self._sensitive_write_paths = list(
+ self._config.sensitive_write_paths)
self._extractors = build_extractor_registry()
def check(self, command: str, *, _depth: int = 0) -> SafetyDecision:
@@ -583,6 +590,12 @@
action=result.action,
reason=result.reason,
category=result.category)
+ if op_type in ('write', 'create') and matches_sensitive(
+ result.resolved_path, self._sensitive_write_paths):
+ return SafetyDecision(
+ action='deny',
+ reason=f'Write to sensitive path blocked: {path}',
+ )
return SafetyDecision(
action='allow', reason=f'{cmd_name}: all paths validated')
@@ -615,6 +628,12 @@
action=result.action,
reason=result.reason,
category=result.category)
+ if matches_sensitive(result.resolved_path,
+ self._sensitive_write_paths):
+ return SafetyDecision(
+ action='deny',
+ reason=f'Redirect to sensitive path blocked: {target}',
+ )
return SafetyDecision(action='allow', reason='Redirects OK')
--- a/ms_agent/permission/matcher.py 2026-11-04 00:38:36
+++ b/ms_agent/permission/matcher.py 2026-11-04 01:47:31
@@ -6,6 +6,8 @@
from __future__ import annotations
+import os
+import shlex
from typing import Any
from ms_agent.utils.pattern_matcher import match_pattern
@@ -58,6 +60,68 @@
return '|'.join(p for p in out if p)
+#: Commands whose job is to run the command that follows them. ``xargs curl``
+#: and ``exec ssh`` reach the network exactly as ``curl`` and ``ssh`` do.
+_EXEC_PREFIXES: frozenset[str] = frozenset({'xargs', 'exec', 'command'})
+
+_MAX_SUBSTITUTION_DEPTH = 3
+
+
+def shell_command_views(command: str, *, _depth: int = 0) -> list[str]:
+ """Every simple command inside ``command``, as ``<name> <args>``.
+
+ An ask rule such as ``curl *`` is about the program being run, not about
+ the first characters of the tool argument. The whole-string fnmatch missed
+ ``true && curl …``, ``cd /tmp; wget …``, ``/usr/bin/curl …``, ``env curl …``
+ and ``xargs curl``. This splits the compound command the way
+ ShellPathValidator does, strips redirections and the safe wrappers, peels
+ the exec-style prefixes, and reports each sub-command with its program
+ name reduced to a basename. Command substitutions are included because
+ ``echo $(curl …)`` runs curl all the same. A sub-command that cannot be
+ tokenised is reported verbatim so a rule still has a chance at it.
+ """
+ from .shell_validator import (_extract_command_substitutions,
+ _split_compound, _strip_heredoc_bodies,
+ _strip_redirections, _unwrap_group)
+ from .wrapper_strip import _ENV_ASSIGN, strip_safe_wrappers
+
+ views: list[str] = []
+ if not command or _depth > _MAX_SUBSTITUTION_DEPTH:
+ return views
+ command = _strip_heredoc_bodies(command)
+ if _depth < _MAX_SUBSTITUTION_DEPTH:
+ try:
+ bodies = _extract_command_substitutions(command)
+ except ValueError:
+ bodies = []
+ for body in bodies:
+ views.extend(shell_command_views(body, _depth=_depth + 1))
+ for sub in _split_compound(command):
+ sub = _unwrap_group(sub.strip())
+ if not sub or sub.startswith('#'):
+ continue
+ try:
+ tokens = shlex.split(sub)
+ except ValueError:
+ views.append(sub)
+ continue
+ tokens = _strip_redirections(tokens)
+ # Any ``VAR=value`` prefix: it changes the environment the program
+ # sees, never which program runs.
+ while tokens and _ENV_ASSIGN.match(tokens[0]):
+ tokens = tokens[1:]
+ tokens = strip_safe_wrappers(tokens)
+ while tokens and os.path.basename(tokens[0]) in _EXEC_PREFIXES:
+ tokens = tokens[1:]
+ while tokens and tokens[0].startswith('-'):
+ tokens = tokens[1:]
+ if not tokens:
+ continue
+ tokens[0] = os.path.basename(tokens[0])
+ views.append(' '.join(tokens))
+ return views
+
+
class PermissionMatcher:
"""Wildcard matcher for permission rules, shared by both SafetyGuard and PermissionEnforcer."""
@@ -99,3 +163,33 @@
return False
return self.match(_with_bare_command_variants(content_pattern), content)
+
+ def match_ask_rule(
+ self,
+ pattern: str,
+ tool_name: str,
+ tool_args: dict[str, Any],
+ ) -> bool:
+ """``match_with_content`` that, for the shell, also looks at every
+ sub-command (see :func:`shell_command_views`).
+
+ Only ask rules use this. A whitelist entry must keep matching the
+ command as a whole, or ``ls *`` would release ``ls; curl …``; a rule
+ whose purpose is to confirm has the opposite failure mode, and a
+ confirmation that one extra ``true &&`` can skip is no confirmation.
+ """
+ if self.match_with_content(pattern, tool_name, tool_args):
+ return True
+ if CONTENT_SEP not in pattern:
+ return False
+ if not tool_name.endswith(f'{TOOL_SPLITER}shell_executor'):
+ return False
+ tool_pattern, content_pattern = pattern.split(CONTENT_SEP, 1)
+ if not self.match(tool_pattern, tool_name):
+ return False
+ command = tool_args.get('command')
+ if command is None:
+ return False
+ alts = _with_bare_command_variants(content_pattern)
+ return any(
+ self.match(alts, view) for view in shell_command_views(str(command)))
--- a/ms_agent/permission/enforcer.py 2026-11-04 00:38:36
+++ b/ms_agent/permission/enforcer.py 2026-11-04 01:47:08
@@ -168,7 +168,7 @@
# user's own remembered answer below, so consenting once still sticks.
ask_rule = next(
(p for p in self._config.ask_rules
- if self._matcher.match_with_content(p, tool_name, tool_args)),
+ if self._matcher.match_ask_rule(p, tool_name, tool_args)),
None,
)
if ask_rule and not self._can_ask_human():
--- a/ms_agent/hooks/permission_resolve.py 2026-11-04 00:38:36
+++ b/ms_agent/hooks/permission_resolve.py 2026-11-04 01:47:08
@@ -28,7 +28,7 @@
reason=f'Denied by blacklist rule: {pattern}',
)
for pattern in config.ask_rules:
- if m.match_with_content(pattern, tool_name, tool_args):
+ if m.match_ask_rule(pattern, tool_name, tool_args):
return PermissionDecision(
action='ask',
reason=f'Ask rule matched: {pattern}',
--- a/ms_agent/permission/handler.py 2026-11-04 00:38:36
+++ b/ms_agent/permission/handler.py 2026-11-04 01:47:08
@@ -107,29 +107,44 @@
print(f'{"="*60}', file=sys.stderr)
loop = asyncio.get_running_loop()
- choice = await loop.run_in_executor(
- None, lambda: input('Choice [y/s/a/e/n]: ').strip().lower())
- if choice == 's':
+ async def _read(prompt: str) -> str | None:
+ """One line from the terminal, ``None`` when there is none (EOF)."""
+ try:
+ return await loop.run_in_executor(
+ None, lambda: input(prompt).strip())
+ except EOFError:
+ return None
+
+ raw = await _read('Choice [y/s/a/e/n]: ')
+ if raw is None:
+ print('No answer (EOF), denying.', file=sys.stderr)
return PermissionResponse(
+ action=PermissionAction.DENY, feedback='No answer (EOF)')
+ choice = raw.lower()
+
+ if choice in ('y', 'yes'):
+ return PermissionResponse(action=PermissionAction.ALLOW_ONCE)
+ elif choice == 's':
+ return PermissionResponse(
action=PermissionAction.ALLOW_SESSION,
pattern=suggestion,
)
elif choice == 'a':
- edited = await loop.run_in_executor(
- None,
- lambda: input(f'Pattern [{suggestion}]: ').strip(),
- )
+ edited = await _read(f'Pattern [{suggestion}]: ')
+ if edited is None:
+ return PermissionResponse(
+ action=PermissionAction.DENY, feedback='No answer (EOF)')
final_pattern = edited if edited else suggestion
return PermissionResponse(
action=PermissionAction.ALLOW_ALWAYS,
pattern=final_pattern,
)
elif choice == 'e':
- edited_raw = await loop.run_in_executor(
- None,
- lambda: input('New args (JSON): ').strip(),
- )
+ edited_raw = await _read('New args (JSON): ')
+ if edited_raw is None:
+ return PermissionResponse(
+ action=PermissionAction.DENY, feedback='No answer (EOF)')
try:
new_args = json.loads(edited_raw)
except json.JSONDecodeError:
@@ -139,10 +154,15 @@
action=PermissionAction.MODIFY,
updated_args=new_args,
)
- elif choice == 'n':
+ elif choice in ('n', 'no'):
return PermissionResponse(action=PermissionAction.DENY)
else:
- return PermissionResponse(action=PermissionAction.ALLOW_ONCE)
+ # An empty Enter, a typo or a stray key is not consent. The menu
+ # says only ``y`` allows; anything it did not list denies.
+ print(f'Unrecognised choice {raw!r}, denying.', file=sys.stderr)
+ return PermissionResponse(
+ action=PermissionAction.DENY,
+ feedback=f'Unrecognised choice {raw!r}; the call was not run')
class EventEmitter(Protocol):
poc.py (the reproduction script)
"""ms-agent: three ways a shell command reaches the network, or the shell itself, without the confirmation the
permission layer promises.
M1 The default ask rules (``curl *``, ``wget *``, ``ssh *`` ...) are fnmatch'd against the WHOLE command string, so
``true && curl ...``, ``cd /tmp; wget ...``, ``/usr/bin/curl ...``, ``env curl ...``, ``xargs curl`` miss them; in
auto mode (the default) the enforcer then allows with no prompt.
M2 Persistent approvals are read from ``<workspace>/.ms_agent/permission_memory.json``, which SafetyGuard lets the
model write (write_file, edit_file, a shell redirect); the next session honours the planted entry before any
ask rule, in auto AND interactive mode.
M3 The CLI prompt treats any answer other than s/a/e/n, an empty Enter included, as "allow once"; EOF is uncaught.
Runs the repository's own permission code (ms_agent.permission) with stub handlers that record whether a person was
asked. No model, no network, no API key. Usage: python poc.py <repo root> (unmodified clone, or a patched copy)
"""
import asyncio
import contextlib
import io
import json
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(sys.argv[1]).resolve()))
from ms_agent.permission import (AutoPermissionHandler, CLIPermissionHandler, # noqa: E402
PermissionConfig, PermissionEnforcer,
PermissionMemory, SafetyGuard)
from ms_agent.permission.handler import (PermissionAction, # noqa: E402
PermissionResponse)
SHELL = 'code_executor---shell_executor'
findings = []
class RecordingHandler:
"""A person at the keyboard who says "allow once" and remembers being asked."""
def __init__(self):
self.asked = 0
async def ask(self, tool_name, tool_args, context, suggestions=None):
self.asked += 1
return PermissionResponse(action=PermissionAction.ALLOW_ONCE)
def make_enforcer(mode, handler, ws, **extra):
cfg = PermissionConfig.from_dict({'mode': mode, **extra})
return PermissionEnforcer(cfg, handler=handler,
memory=PermissionMemory(project_path=ws))
async def m1():
print('== M1: ask rules vs compound / wrapped commands (mode=auto, a person attached)')
cmds = ['curl http://127.0.0.1:1/x', 'true && curl http://127.0.0.1:1/x',
'cd /tmp; wget http://127.0.0.1:1/x', '/usr/bin/curl http://127.0.0.1:1/x',
'env curl http://127.0.0.1:1/x', 'echo http://127.0.0.1:1/x | xargs curl',
'ls\ncurl http://127.0.0.1:1/x', 'true && ssh user@host']
for cmd in cmds:
h = RecordingHandler()
d = await make_enforcer('auto', h, tempfile.mkdtemp()).check(SHELL, {'command': cmd})
print(f' {cmd!r:<45} -> {d.action:<5} asked={h.asked}')
if cmd != cmds[0] and d.action == 'allow' and h.asked == 0:
findings.append('M1 compound: ' + repr(cmd))
print(' -- headless (AutoPermissionHandler): an ask rule must DENY, nobody can confirm')
for cmd in ('curl http://127.0.0.1:1/x', 'true && curl http://127.0.0.1:1/x'):
d = await make_enforcer('auto', AutoPermissionHandler(), tempfile.mkdtemp()).check(SHELL, {'command': cmd})
print(f' {cmd!r:<45} -> {d.action:<5}')
if d.action == 'allow':
findings.append('M1 headless: ' + repr(cmd))
async def m2():
print('== M2: the persistent-approval file is writable by the model, then honoured as policy')
ws = Path(tempfile.mkdtemp())
cfg = PermissionConfig.from_dict({'mode': 'interactive'})
guard = SafetyGuard(config=cfg.safety,
allowed_dirs=list(cfg.safety.effective_allowed_directories(str(ws))),
workspace_root=str(ws))
target = ws / '.ms_agent' / 'permission_memory.json'
grant_json = json.dumps([{'pattern': SHELL, 'scope': 'project', 'source': 'user'}])
writes = [
('file_system---write_file', {'path': str(target), 'content': grant_json}),
('file_system---write_file', {'path': '.ms_agent/permission_memory.json', 'content': grant_json}),
('file_system---edit_file', {'path': str(target), 'content': grant_json}),
('code_executor---shell_executor',
{'command': 'mkdir -p .ms_agent && echo x > .ms_agent/permission_memory.json'}),
('code_executor---shell_executor', {'command': 'echo x > ' + str(target)}),
]
for tool, args in writes:
d = guard.check(tool, args)
where = args.get('path') or args.get('command')
print(f' SafetyGuard {tool:<26} {where!r:<70} -> {d.action}')
if d.action == 'allow':
findings.append('M2 writable: ' + tool + ' ' + repr(where))
# The grant is honoured once on disk. This is the consequence, not itself a
# finding: the file is planted directly here, so the next two lines only
# confirm that a planted entry outranks the ask rule in every mode. The
# defect is that the model could have written it through the guard above.
print(' -- plant the grant on disk and start a fresh session that reads it back')
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(grant_json)
mem = PermissionMemory(project_path=ws)
print(' loaded entries :', [(e.pattern, e.scope, e.source) for e in mem.list_all()])
print(' matches curl x :', mem.matches(SHELL, {'command': 'curl http://127.0.0.1:1/x'}))
for mode in ('interactive', 'auto'):
h = RecordingHandler()
d = await make_enforcer(mode, h, ws).check(SHELL, {'command': 'curl http://127.0.0.1:1/x'})
print(f' mode={mode:<11} curl ... -> {d.action:<5} asked={h.asked} ({d.reason})')
async def m3():
print('== M3: the CLI prompt allows on anything but s/a/e/n; EOF is uncaught')
handler = CLIPermissionHandler()
import builtins
orig_input = builtins.input
try:
for answer in ['', 'yse', 'no', ' ', 'q']:
builtins.input = (lambda a: lambda prompt='': a)(answer)
with contextlib.redirect_stderr(io.StringIO()):
r = await handler.ask(SHELL, {'command': 'curl http://127.0.0.1:1/x'}, context='')
print(f' input {answer!r:<6} -> {r.action.value}')
if r.action == PermissionAction.ALLOW_ONCE:
findings.append('M3 allow-on: ' + repr(answer))
def raise_eof(prompt=''):
raise EOFError
builtins.input = raise_eof
try:
with contextlib.redirect_stderr(io.StringIO()):
r = await handler.ask(SHELL, {'command': 'curl http://127.0.0.1:1/x'}, context='')
print(' EOF ->', r.action.value)
except BaseException as exc: # noqa: BLE001
print(' EOF -> raised', type(exc).__name__)
findings.append('M3 EOF uncaught')
finally:
builtins.input = orig_input
async def main():
await m1()
await m2()
await m3()
print()
if findings:
print('AS SHIPPED - gate bypassed in', len(findings), 'cases:')
for f in findings:
print(' -', f)
else:
print('PATCHED - every case above was confirmed or denied.')
if __name__ == '__main__':
asyncio.run(main())
- Ngôn ngữ chính
- Python
- Star
- 4.4k
- Fork
- 544
- Merge trung bình
- 2 ngày 12 giờ
- Pull request đã merge (30 ngày)
- 5
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của modelscope/ms-agent
-
Skill search returns stale results after all skills are disabled or removedCó thể đã có người làm @AWUQAQ đã nhận 63 ngày trước. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
modelscope/ms-agent#974 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[llm] Gemini 3 tool loop fails with 400: thought signature is not sent backCó thể đã có người làm @ZhuLinsen đã nhận 12 ngày trước. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 76/100
modelscope/ms-agent#1040 · 1 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
-
fetch_page web tool performs unrestricted server-side HTTP fetch of agent-supplied URLs (SSRF)Có thể đã có người làm @SiyuanChenToDo đã nhận 27 ngày trước. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 45/100
modelscope/ms-agent#1018 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 25/100
modelscope/ms-agent#1011 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[tool] Document authenticated Streamable HTTP MCP setupCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 68/100
modelscope/ms-agent#1003 · 2 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của modelscope/ms-agent
Issue tương tự
-
enhancement
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
Sienna-Platform/SiennaSchemas#130 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
firedrakeproject/Irksome#293 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
ida-arbeitszeit/workers-control#1578 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
aws-samples/sample-per-model-bedrock#898 ·
Maintainer thường phản hồi trong vòng 1 ngày