|
oc_chat
|
Classes | |
| class | HumanBudget |
Functions | |
| str | _classify (str command) |
| bool | _is_allowed (str command) |
| Iterator[None] | human_confirmed_command (str command) |
| Iterator[None] | human_budget ("int | None" seconds, *, "Callable[[], int | None] | None" on_expired=None) |
| dict | deny_unconfirmed_tier2 (dict result, *, str note="") |
| "tuple[str, str]" | _first_value_option (str token) |
| int | _python_m_pip_at (list[str] argv) |
| int | _pip_subcommand_at (list[str] argv, str program) |
| "tuple[list[str] | None, str]" | _pip_index_pin (list[str] argv, str program) |
| CommandResult|ConfirmationRequired|ToolError | run_command (str command, int timeout=30, bool confirmed=False) |
| dict | text_search (str pattern, str search_dir=".", *, str|None file_path=None, bool case_sensitive=False, int max_results=100) |
Variables | |
| contextvars | _human_approved_command |
| frozenset | _PYTHON_PROGRAMS = frozenset({"python", "python3", "py"}) |
| frozenset | _PY_SHORT_VALUE_OPTIONS = frozenset({"c", "m", "W", "X"}) |
| frozenset | _PY_SHORT_FLAG_OPTIONS = frozenset("bdhiqstuvxBEIOPRSV?") |
| str | _PY_UNKNOWN_OPTION = "\0" |
| dict | TOOLS |
| list | SCHEMAS |
Sandboxed shell execution with a whole-argv command allowlist (M8.0 WU-3).
Purpose
-------
Provides :func:`run_command`, which parses a command string to argv,
rejects shell metacharacters outside quotes, and validates the WHOLE argv against the
locked Appendix A allowlist (:mod:`oc_chat.tools._argv_policy`) before
executing it ``shell=False`` with a scrubbed environment. There is no
``shell=True`` lane and no Unix→PowerShell conversion (F55).
**Tier 1 — allowed (read-only / no side-effects):** ``pip list``/``show``/
``freeze``, ``where``/``which``, ``dir``/``ls`` (a small option set + ≤1
in-sandbox path arg), ``echo`` (argv-only). Reads go through the filesystem
tools and git through ``oct_git_*`` — so ``git`` (freeform), ``cat``/``type``/
``grep``/``findstr``, ``curl``/``wget``, the shells, and the cmd.exe mutators
are **denied**.
**Tier 2 — human-confirm / grant-gated (argv-only):** ``python``/``pytest``/
``pip install``|``uninstall``/``oct``. M8.0-A (LD-4): a Tier-2 command is **not
executable by the agent** — a *model-supplied* flag never authorizes it. It
returns a ``requires_confirmation`` dict that no model-reachable path can satisfy
(fail closed). M8.0-C: the react-loop relays that signal to an interactive human
and, on 'y', re-authorizes the exact command via :func:`human_confirmed_command`
(a ContextVar the model cannot set); headless / sub-loop / batch fail closed.
One-use signed grants for headless Tier-2 are M8.1.
**The two timeouts (plan 283).** ``clamp_timeout`` bounds the ``timeout`` the
MODEL supplies — the M8 Appendix B ceiling, 300 s, with no out-of-band raise
path — and applies to every command. :func:`human_budget` is a second, separate
value carried the same way the approval is, read ONLY after the exact-string
approval matched, and it is the approving human's own patience: a number of
seconds, or ``None`` for "until it finishes". The ceiling is what the agent may
decide; the budget is what the human decided.
Responsibilities
----------------
- :func:`_classify` / :func:`_is_allowed` — back-compat wrappers over the
whole-argv policy decision (``_argv_policy.classify_command``).
- :func:`run_command` — orchestrate validation, optional confirmation
prompt, and ``subprocess.run`` execution with timeout and captured
output.
- :func:`human_confirmed_command` / :func:`human_budget` — the two ContextVar
seams the interactive react-loop sets after a human answers the Tier-2 ask:
what was approved, and how long they will wait for it.
- :func:`_pip_index_pin` — the F78 index pin: an index-reaching ``pip``
subcommand executes with ``--isolated --index-url <operator-trusted>``
appended after the subcommand, and the ``python -m pip`` spelling of a
command that names its own index is refused on the argv policy's terms.
- :func:`_python_m_pip_at` / :func:`_first_value_option` — CPython's own
``-m`` spellings, so the attached and clustered forms (``-mpip``, ``-qmpip``)
reach the pin and ``-Xmpip`` (X's value) correctly does not.
- ``TOOLS`` and ``SCHEMAS`` for auto-registration.
Diagnostics
-----------
Domain: OC_CHAT.TOOLS
Levels:
L2 — lifecycle events
L3 — semantic details
L4 — deep tracing
Contracts
---------
- Commands not matching any whitelist are *blocked* — they return an
error dict, never silently execute.
- Mutating ``git`` (add/commit/push) is intentionally *not* in either
whitelist; agents must use ``oct_git_*`` instead.
- Subprocess calls always run with a finite timeout and captured
stdout/stderr — no streaming output, no PTY.
- Tier-2 (code-exec / mutation) commands are not agent-runnable (M8.0-A,
LD-4): a model-supplied flag never authorizes them; they return
``requires_confirmation`` and fail closed. Only the out-of-band human-confirm
channel (M8.0-C, via :func:`human_confirmed_command`) or a one-use grant
(M8.1) authorizes them.
- A granted ``pip install`` / ``pip download`` runs against the operator-trusted
index and nothing else (M8 F78, SUPPLY-02 / plan 253). The pin is computed
BEFORE the confirmation is offered and carried in the reason the human reads,
so what is approved is what runs; it changes what EXECUTES and never what
AUTHORIZES — the exact approved command string remains the only key.
Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- oc_chat.core.tool_results (CommandResult, ConfirmationRequired, ToolError - the run_command return types)
- oc_chat.tools._sandbox (scrubbed child environment, output secret redaction and the protected-read check)
- oc_chat.tools._argv_policy (classify_command, the pip index helpers and WINDOWS_BUILTINS - the whole-argv allowlist, the F78 index pin and refusal, and the built-ins run through cmd)
- oc_chat.tools.filesystem (lazy; _sandbox_root, _safe_path, _outside_sandbox_error - the sandbox root and search path checks)
- oc_chat.core.defaults (lazy; authority - the operator-trusted pip index URL for the F78 pin)
- oc_chat.core.resource_caps (lazy; clamp_timeout, clamp_results - the M8 ceilings on model-supplied arguments)
- oc_chat.tools._proc (lazy; run_cancellable - the cancellable subprocess runner)
- oc_chat.core.tool_activity (lazy; TOOL_ACTIVITY - the terminal activity record fed redacted output lines)
- oc_chat.core.cancellation (lazy; tool_cancel_requested - cooperative Ctrl+C polling in text_search)
- cmd (not imported; the Windows command interpreter, run as cmd /v:off /c for the built-ins dir and echo)
See Also
--------
oc_chat/docs/conventions/CONTRACTS.md §3 Tool registry contract; §4 Tool return-
value conventions (run_command → §4.1; confirmation gating → §4.5).
|
protected |
|
protected |
The first value-taking option letter in a single-dash CPython *token*,
and the value attached to it inside that same token.
Returns ``("", "")`` for a cluster of pure flags and
``(_PY_UNKNOWN_OPTION, "")`` for a letter the interpreter refuses. A flag
continues the cluster; the first value-taking letter ends it and takes the
remainder of the token as its value — empty when the token ends there, in
which case the value is the next argv element.
Definition at line 301 of file shell.py.
|
protected |
|
protected |
Pin an index-reaching pip *argv* to the operator-trusted index (F78). Returns ``(executed_argv, denial)``. *executed_argv* is ``None`` when *argv* is not an index-reaching pip command, and the caller executes it unchanged. *denial* is non-empty when the command names an index or a trusted host itself: the argv policy already refuses that for the bare ``pip`` spelling, and this closes the ``python -m pip`` spelling on the same terms. Refusing rather than overriding is deliberate — appending a pin to a command that already carries ``--index-url`` would leave which index wins to pip's own last-one-wins parsing, and would leave the human approving a string whose index quietly did not apply.
Definition at line 379 of file shell.py.
|
protected |
Position of an index-reaching pip subcommand in *argv*, or ``-1``. Both Tier-2 spellings are matched: a direct ``pip install ...`` and ``python -m pip install ...`` — the pin applies after ``pip`` wherever it sits in the argv, in whichever spelling the interpreter accepts (:func:`_python_m_pip_at`).
Definition at line 360 of file shell.py.
|
protected |
Index of the token after ``-m pip`` in a *python* argv, or ``-1``. ``-m`` takes a value, and CPython's own parser accepts that value ATTACHED to the option token as readily as separated: ``-mpip``, and inside a flag cluster ``-qmpip`` / ``-Impip`` / ``-SEmpip``, are all ``-m pip``. Matching only a bare ``-m`` token let the attached spelling walk past both the refusal and the pin (gate finding, 2026-09-09) — the same class of gap the optparse abbreviations were for pip's own options. The scan is deliberately no WIDER than the interpreter. ``-W`` and ``-X`` take their values the same way, so ``-Xmpip`` is X's value and not a module; ``-c`` terminates the option list, so ``-cmpip`` is code; and a token that is not an option at all is the script path, after which no module can follow. Each of those was verified by running CPython 3.14, because a scan that matched more than the interpreter does would deny commands that never reach pip at all.
Definition at line 319 of file shell.py.
| dict oc_chat.tools.shell.deny_unconfirmed_tier2 | ( | dict | result, |
| * | , | ||
| str | note = "" ) |
Fail-closed converter for contexts with no interactive human. A Tier-2 ``requires_confirmation`` cannot be satisfied without a human, so in a sub-loop / batch / headless run it is converted to a terminal ``error`` the model can act on (stop asking) instead of being handed back the un-satisfiable signal. Any other result passes through unchanged (identity).
| Iterator[None] oc_chat.tools.shell.human_budget | ( | "int | None" | seconds, |
| * | , | ||
| "Callable[[], int | None] | None" | on_expired = None ) |
Set the approving human's wait for one confirmed :func:`run_command`. For use ONLY by the interactive react-loop, alongside :func:`human_confirmed_command`, after a human answered the Tier-2 ask (plan 283). *seconds* is the budget they chose and ``None`` means they chose "until it finishes"; *on_expired* is the callable that will ask them for one extension when the budget runs out. Like the approval token, this is reset on exit, so a budget never leaks to another command or outlives the one call. It authorises NOTHING on its own: a budget without a matching :func:`human_confirmed_command` is never read, because the branch that reads it is only reached once the exact-string approval has already matched.
| Iterator[None] oc_chat.tools.shell.human_confirmed_command | ( | str | command | ) |
Authorize exactly *command* for one Tier-2 :func:`run_command` execution. For use ONLY by the interactive react-loop after an out-of-band human 'y' (M8.0-C). Not authority the model can forge: it sets no env var and is not a tool argument. The token is reset on exit so the grant is scoped to the single re-dispatch this brackets.
| CommandResult | ConfirmationRequired | ToolError oc_chat.tools.shell.run_command | ( | str | command, |
| int | timeout = 30, | ||
| bool | confirmed = False ) |
Execute a whitelisted shell command and return its output.
Parameters
----------
command : str
Shell command string.
timeout : int
Maximum seconds to wait (default 30), clamped to the M8 ceiling of
300 s. On the human-confirmed Tier-2 path an out-of-band
:func:`human_budget` replaces it with the approving human's own wait —
see the module docstring's "The two timeouts".
confirmed : bool
Retained for back-compat and **ignored** (M8.0-A): a model-supplied flag
never authorizes a Tier-2 command. The only authorization is the out-of-band
:func:`human_confirmed_command` ContextVar (interactive human 'y', M8.0-C) —
unreachable by the model — or a one-use grant (M8.1).
Returns
-------
dict
``{"stdout": str, "stderr": str, "returncode": int}`` on success, or
``{"requires_confirmation": True, "command": str, "reason": str, "message":
str}`` for a Tier-2 command the agent cannot self-authorize (the react-loop
relays it to a human via :func:`_resolve_tier2_confirmation`).
Definition at line 407 of file shell.py.
| dict oc_chat.tools.shell.text_search | ( | str | pattern, |
| str | search_dir = ".", | ||
| * | , | ||
| str | None | file_path = None, | ||
| bool | case_sensitive = False, | ||
| int | max_results = 100 ) |
Search for a text pattern in files under *search_dir*.
Phase P.10a: the historical parameter name was ``file_path``. That
name was misleading (the argument is a *directory*, not a file path).
The canonical name is now ``search_dir``. Callers passing
``file_path=`` still work for now but get a one-time
:class:`DeprecationWarning`; on conflict (both keywords supplied),
``file_path`` wins for backwards compatibility.
A *single file* is also a valid target (F26): ``search_dir`` may name a
file, in which case only that file is searched; a directory walks the tree.
Args:
pattern: Regular-expression pattern to search for.
search_dir: File or directory to search (default: project root).
file_path: **Deprecated** alias for ``search_dir``. Keyword-only.
Will be removed in a future release.
case_sensitive: Match case when True (default False).
max_results: Maximum number of matching files to return (default 100).
Returns:
Dict with ``matches`` list of ``{file, line_numbers, match_count}``,
plus ``total_matches``, ``searched_files``, ``pattern``,
and ``case_sensitive``.
|
protected |
|
protected |
|
protected |
|
protected |
|
protected |