[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
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 30/100
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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())
- Lenguaje dominante
- Python
- Estrellas
- 4.4k
- Forks
- 544
- Merge medio
- 2 d 12 h
- PR fusionados (30 d)
- 5
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de modelscope/ms-agent
-
Skill search returns stale results after all skills are disabled or removedPosiblemente ocupada @AWUQAQ la tomó hace 63 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
modelscope/ms-agent#974 ·
Los mantenedores suelen responder en 1 día
-
[llm] Gemini 3 tool loop fails with 400: thought signature is not sent backPosiblemente ocupada @ZhuLinsen la tomó hace 12 días. Abierto
Dificultad 3/5 1-2 días Aptitud para principiantes 76/100
modelscope/ms-agent#1040 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
fetch_page web tool performs unrestricted server-side HTTP fetch of agent-supplied URLs (SSRF)Posiblemente ocupada @SiyuanChenToDo la tomó hace 27 días. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
modelscope/ms-agent#1018 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
modelscope/ms-agent#1011 ·
Los mantenedores suelen responder en 1 día
-
[tool] Document authenticated Streamable HTTP MCP setupPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 3/5 1-2 días Aptitud para principiantes 68/100
modelscope/ms-agent#1003 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
Todos los issues de modelscope/ms-agent
Issues similares
-
enhancement
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
Sienna-Platform/SiennaSchemas#130 ·
Los mantenedores suelen responder en 3 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 85/100
firedrakeproject/Irksome#293 ·
Los mantenedores suelen responder en 2 días
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
ida-arbeitszeit/workers-control#1578 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
aws-samples/sample-per-model-bedrock#898 ·
Los mantenedores suelen responder en 1 día