"""Prompt text for the shell ``bash`` tool. Implementation notes: ``tools/BashTool/prompt.ts``. Implemented here: - ``getDefaultTimeoutMs`` / ``getMaxTimeoutMs`` equivalents - ``getSimplePrompt`` core guidance - background, multiple-command, sleep, and git safety branches Not implemented in this step: - Ant/internal undercover and skill-shortcut branches; OS has no equivalent build flags or attribution system. - Monitor-tool branch; OS has background bash lifecycle work in phase 24. """ from __future__ import annotations import json import os from typing import Iterable from openspace.prompts.grounding_agent_prompts import prepend_bullets BASH_TOOL_NAME = "bash" FILE_READ_TOOL_NAME = "read" FILE_EDIT_TOOL_NAME = "edit" FILE_WRITE_TOOL_NAME = "write" GLOB_TOOL_NAME = "glob" GREP_TOOL_NAME = "grep" TODO_WRITE_TOOL_NAME = "todo_write" AGENT_TOOL_NAME = "agent" _DEFAULT_BASH_TIMEOUT_MS = 2 * 60 * 1000 _MAX_BASH_TIMEOUT_MS = 10 * 60 * 1000 def get_default_timeout_ms() -> int: """OpenSpace ``getDefaultTimeoutMs`` equivalent.""" return _DEFAULT_BASH_TIMEOUT_MS def get_max_timeout_ms() -> int: """OpenSpace ``getMaxTimeoutMs`` equivalent.""" return _MAX_BASH_TIMEOUT_MS def _background_usage_note() -> str | None: if _is_env_truthy("OPENSPACE_DISABLE_BACKGROUND_TASKS"): return None return ( "You can use the `run_in_background` parameter to run the command in " "the background. Only use this if you do not need the result " "immediately and are OK being notified when the command completes " "later. You do not need to use '&' at the end of the command when " "using this parameter. If you need the output before continuing, use " "a short follow-up bash command such as `cat ` or " "`tail ` on the output file path returned by the tool." ) def _commit_and_pr_instructions() -> str: return f"""# Committing changes with git Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully: Git Safety Protocol: - NEVER update the git config - NEVER run destructive git commands (push --force, reset --hard, checkout ., restore ., clean -f, branch -D) unless the user explicitly requests these actions - NEVER skip hooks (--no-verify, --no-gpg-sign, etc) unless the user explicitly requests it - NEVER run force push to main/master, warn the user if they request it - When a pre-commit hook fails, fix the issue and create a NEW commit rather than amending the previous commit - When staging files, prefer adding specific files by name rather than using `git add -A` or `git add .` 1. Run the following {BASH_TOOL_NAME} commands in parallel when possible: - `git status --short` to see changes and untracked files - `git diff` to inspect unstaged changes - `git diff --staged` to inspect staged changes - `git log --oneline -5` to follow the repository's commit style 2. Analyze all staged changes and draft a concise commit message that focuses on why the change was made. 3. Add only relevant files, commit with a HEREDOC message, then run `git status` after the commit completes. 4. If the commit fails due to a hook, fix the issue, re-stage, and create a new commit. Important notes: - NEVER use interactive git commands such as `git rebase -i` or `git add -i`. - DO NOT push to the remote repository unless the user explicitly asks you to do so. - If there are no changes to commit, do not create an empty commit. # Creating pull requests Use the `gh` command via the {BASH_TOOL_NAME} tool for GitHub tasks. If given a GitHub URL, use `gh` to get the information needed. Before creating a pull request, inspect branch state, push only if needed, and create the PR with a short title and a body that includes summary and test plan. # Other common operations - View comments on a Github PR: `gh api repos/foo/bar/pulls/123/comments`""" def _simple_sandbox_section() -> str: try: from openspace.services.sandbox import get_process_sandbox_manager manager = get_process_sandbox_manager() if not manager.is_sandboxing_enabled(): return "" filesystem_config = { "read": manager.get_fs_read_config(), "write": manager.get_fs_write_config(), } network_config = manager.get_network_restriction_config() ignore_violations = manager.get_ignore_violations() allow_unsandboxed = manager.are_unsandboxed_commands_allowed() except Exception: return "" restrictions_lines = [ "Filesystem: " + json.dumps(_dedupe_nested(filesystem_config), sort_keys=True), "Network: " + json.dumps(_dedupe_nested(network_config), sort_keys=True), ] if ignore_violations: restrictions_lines.append( "Ignored violations: " + json.dumps(_dedupe_nested(ignore_violations), sort_keys=True) ) if allow_unsandboxed: sandbox_override_items: list[str | Iterable[str]] = [ "You should always default to running commands within the sandbox. Do NOT attempt to set `dangerously_disable_sandbox: true` unless:", [ "The user explicitly asks you to bypass sandbox", "A specific command just failed and you see evidence of sandbox restrictions causing the failure.", ], "Evidence of sandbox-caused failures includes:", [ '"Operation not permitted" errors for file/network operations', "Access denied to specific paths outside allowed directories", "Network connection failures to non-whitelisted hosts", "Unix socket connection errors", ], "When you see evidence of sandbox-caused failure:", [ "Immediately retry with `dangerously_disable_sandbox: true`", "Briefly explain the likely sandbox restriction and mention that `/sandbox` manages restrictions.", "This will prompt the user for permission", ], "Treat each command you execute with `dangerously_disable_sandbox: true` individually. Default future commands back to sandbox mode.", "Do not suggest adding sensitive paths like ~/.bashrc, ~/.zshrc, ~/.ssh/*, or credential files to the sandbox allowlist.", ] else: sandbox_override_items = [ "All commands MUST run in sandbox mode - the `dangerously_disable_sandbox` parameter is disabled by policy.", "Commands cannot run outside the sandbox under any circumstances.", "If a command fails due to sandbox restrictions, work with the user to adjust sandbox settings instead.", ] items: list[str | Iterable[str]] = [ *sandbox_override_items, "For temporary files, use the `$TMPDIR` environment variable instead of `/tmp` directly.", ] return "\n".join( [ "", "## Command sandbox", "By default, your command will be run in a sandbox. This sandbox controls which directories and network hosts commands may access or modify without an explicit override.", "", "The sandbox has the following restrictions:", "\n".join(restrictions_lines), "", *prepend_bullets(items), ] ) def get_simple_prompt() -> str: """OpenSpace ``getSimplePrompt`` translated for OS tool names.""" tool_preference_items: list[str | Iterable[str]] = [ f"File search: Use {GLOB_TOOL_NAME} (NOT find or ls)", f"Content search: Use {GREP_TOOL_NAME} (NOT grep or rg)", f"Read files: Use {FILE_READ_TOOL_NAME} (NOT cat/head/tail)", f"Edit files: Use {FILE_EDIT_TOOL_NAME} (NOT sed/awk)", f"Write files: Use {FILE_WRITE_TOOL_NAME} (NOT echo >/cat < bool: value = os.environ.get(name, "") return value.lower() in {"1", "true", "yes", "on"} def _dedupe_nested(value): if isinstance(value, dict): return {key: _dedupe_nested(item) for key, item in value.items()} if isinstance(value, list): seen = set() out = [] for item in value: key = json.dumps(item, sort_keys=True) if isinstance(item, (dict, list)) else item if key in seen: continue seen.add(key) out.append(item) return out return value