feat: ensure sqlite3 installation

This commit is contained in:
Timothy
2026-04-15 18:34:33 -07:00
parent 2231dc5742
commit 45df68c146
5 changed files with 215 additions and 13 deletions
+11 -1
View File
@@ -502,12 +502,22 @@ class ToolRegistry:
config["cwd"] = str(resolved_cwd)
return config
# For coder_tools_server, inject --project-root so writes go to the expected workspace
# For coder_tools_server, inject --project-root so reads land
# in the expected workspace (hive repo, for framework skills
# and docs), and inject --write-root so writes land under
# ~/.hive/workspace/ instead of polluting the git checkout
# with queen-authored skills, ledgers, and scripts. Without
# the split, every ``write_file`` call from the queen landed
# in the hive repo root.
if script_name and "coder_tools" in script_name:
project_root = str(resolved_cwd.parent.resolve())
args = list(args)
if "--project-root" not in args:
args.extend(["--project-root", project_root])
if "--write-root" not in args:
_write_root = Path.home() / ".hive" / "workspace"
_write_root.mkdir(parents=True, exist_ok=True)
args.extend(["--write-root", str(_write_root)])
config["args"] = args
if os.name == "nt":
@@ -1495,6 +1495,35 @@ def register_queen_lifecycle_tools(
except OSError as e:
return None, f"failed to install skill into {target}: {e}"
# Cleanup the source directory after a successful install so
# the authored skill doesn't linger as debris in the agent
# workspace (or — pre-sandbox-split — in the hive git
# checkout). Only removes paths that are OUTSIDE
# ``~/.hive/skills/`` so we never nuke the canonical install
# target or user-owned skill dirs.
try:
src_resolved = src.resolve()
skills_root_resolved = target_root.resolve()
try:
src_resolved.relative_to(skills_root_resolved)
_under_skills_root = True
except ValueError:
_under_skills_root = False
if not _under_skills_root:
_shutil.rmtree(src_resolved)
logger.info(
"create_colony: cleaned up authored skill source at %s "
"(installed to %s)",
src_resolved,
target,
)
except OSError as e:
logger.warning(
"create_colony: failed to clean up skill source at %s (non-fatal): %s",
src,
e,
)
return target, None
async def create_colony(
+42
View File
@@ -271,6 +271,48 @@ else
exit 1
fi
# Check for sqlite3 CLI (required for colony progress tracking)
echo -n " Checking for sqlite3... "
if command -v sqlite3 &> /dev/null; then
echo -e "${GREEN}ok${NC}"
else
echo -e "${YELLOW}not found${NC}"
# Attempt auto-install on common package managers
SQLITE_INSTALLED=false
if command -v apt-get &> /dev/null; then
echo -n " Installing sqlite3 via apt... "
if sudo apt-get install -y sqlite3 > /dev/null 2>&1; then
SQLITE_INSTALLED=true
fi
elif command -v brew &> /dev/null; then
echo -n " Installing sqlite3 via brew... "
if brew install sqlite > /dev/null 2>&1; then
SQLITE_INSTALLED=true
fi
elif command -v apk &> /dev/null; then
echo -n " Installing sqlite3 via apk... "
if apk add sqlite > /dev/null 2>&1; then
SQLITE_INSTALLED=true
fi
elif command -v dnf &> /dev/null; then
echo -n " Installing sqlite3 via dnf... "
if sudo dnf install -y sqlite > /dev/null 2>&1; then
SQLITE_INSTALLED=true
fi
elif command -v pacman &> /dev/null; then
echo -n " Installing sqlite3 via pacman... "
if sudo pacman -S --noconfirm sqlite > /dev/null 2>&1; then
SQLITE_INSTALLED=true
fi
fi
if [ "$SQLITE_INSTALLED" = true ]; then
echo -e "${GREEN}ok${NC}"
else
echo -e "${YELLOW} ⚠ Could not install sqlite3 automatically${NC}"
echo -e "${DIM} Install manually: apt install sqlite3 / brew install sqlite / apk add sqlite${NC}"
fi
fi
# Check for Chrome/Edge (required for GCU browser tools)
echo -n " Checking for Chrome/Edge browser... "
# Check common browser locations
+121 -7
View File
@@ -82,10 +82,29 @@ def _find_project_root() -> str:
return os.path.dirname(os.path.abspath(__file__))
def _resolve_path(path: str) -> str:
"""Resolve path relative to PROJECT_ROOT. Raises ValueError if outside.
# When ``--write-root`` is passed on the CLI, ``WRITE_ROOT`` diverges
# from ``PROJECT_ROOT``: reads stay permissive (so the queen can
# reference framework skills, docs, and the hive repo), but writes
# are confined to the write root plus the ``~/.hive/`` escape hatch.
# Without this split, the coder-tools sandbox IS the hive git
# checkout — every queen-authored skill/ledger/script lands there as
# untracked debris, which was the 2026-04-15 incident
# (``~/aden/hive/x-rapid-reply/`` and siblings).
WRITE_ROOT: str = ""
Also allows access to ~/.hive/ directory for agent session data files.
def _resolve_read_path(path: str) -> str:
"""Resolve path for READ operations.
Allowlist (in order):
1. Paths under ``~/.hive/`` agent session data, colonies, skills.
2. Paths under ``PROJECT_ROOT`` hive repo, for reading framework
defaults, docs, examples, etc.
3. Relative paths joined against ``PROJECT_ROOT`` (read-side
default; writes use ``WRITE_ROOT`` instead).
Raises ``ValueError`` when the resolved path falls outside all
allowed roots.
"""
# Normalize slashes for cross-platform (e.g. exports/hi_agent from LLM)
path = path.replace("/", os.sep)
@@ -155,6 +174,88 @@ def _resolve_path(path: str) -> str:
return resolved
def _resolve_write_path(path: str) -> str:
"""Resolve path for WRITE operations.
Stricter than the read resolver: only allows writes under:
1. ``WRITE_ROOT`` the agent workspace (default: ``~/.hive/workspace/``
when ``--write-root`` is passed).
2. ``~/.hive/`` agent session data.
Writes to the hive repo (``PROJECT_ROOT``) are REJECTED to keep
the git checkout clean of queen-authored debris. Relative paths
resolve against ``WRITE_ROOT``, not ``PROJECT_ROOT``.
When ``WRITE_ROOT`` equals ``PROJECT_ROOT`` (no split configured),
this function is semantically identical to ``_resolve_read_path``.
"""
# Normalize slashes + expand ~
path = path.replace("/", os.sep)
if path.startswith("~"):
path = os.path.expanduser(path)
hive_dir = os.path.expanduser("~/.hive")
if os.path.isabs(path):
resolved = os.path.abspath(path)
# Always allow writes under ~/.hive/
try:
if os.path.commonpath([resolved, hive_dir]) == hive_dir:
return resolved
except ValueError:
pass
# Writes are ALSO allowed under WRITE_ROOT (the agent workspace).
try:
if os.path.commonpath([resolved, WRITE_ROOT]) == WRITE_ROOT:
return resolved
except ValueError:
pass
# If WRITE_ROOT == PROJECT_ROOT (legacy behavior: no split),
# fall through to the read-side resolver so existing callers
# keep working unchanged.
if WRITE_ROOT == PROJECT_ROOT:
return _resolve_read_path(path)
# Split configured AND the path isn't under WRITE_ROOT or
# ~/.hive/. Reject — this is the whole point of the split.
raise ValueError(
f"Access denied: writes must be under '{WRITE_ROOT}' or "
f"'{hive_dir}'. Path '{path}' is outside both "
"(use an absolute path under one of those roots, or a "
"relative path which will resolve under the write root)."
)
else:
# Relative path: resolve against WRITE_ROOT, not PROJECT_ROOT.
resolved = os.path.abspath(os.path.join(WRITE_ROOT, path))
# Double-check the resolved absolute path is inside WRITE_ROOT or
# ~/.hive/ (covers edge cases like "../../etc/passwd" that escape).
try:
wr_common = os.path.commonpath([resolved, WRITE_ROOT])
except ValueError:
wr_common = ""
try:
hv_common = os.path.commonpath([resolved, hive_dir])
except ValueError:
hv_common = ""
if wr_common != WRITE_ROOT and hv_common != hive_dir:
raise ValueError(
f"Access denied: resolved write path '{resolved}' escaped the "
f"allowed roots ('{WRITE_ROOT}', '{hive_dir}')."
)
return resolved
# Back-compat alias: existing call sites in this module call
# ``_resolve_path`` directly (e.g. for snapshot dirs, agent tool
# introspection). Those are all non-user-driven paths; route them
# through the read resolver.
_resolve_path = _resolve_read_path
# ── Git snapshot system (ported from opencode's shadow git) ───────────────
@@ -1689,32 +1790,45 @@ def validate_agent_package(agent_name: str) -> str:
def main() -> None:
global PROJECT_ROOT, SNAPSHOT_DIR
global PROJECT_ROOT, SNAPSHOT_DIR, WRITE_ROOT
from aden_tools.file_ops import register_file_tools
parser = argparse.ArgumentParser(description="Coder Tools MCP Server")
parser.add_argument("--project-root", default="")
# ``--write-root`` isolates file writes from the project root so
# queen-authored skills, ledgers, and scripts don't land in the
# hive git checkout. Reads remain permissive under PROJECT_ROOT
# so framework skills, docs, and examples stay accessible.
# Defaults to PROJECT_ROOT when empty (legacy behavior).
parser.add_argument("--write-root", default="")
parser.add_argument("--port", type=int, default=int(os.getenv("CODER_TOOLS_PORT", "4002")))
parser.add_argument("--host", default="0.0.0.0")
parser.add_argument("--stdio", action="store_true")
args = parser.parse_args()
PROJECT_ROOT = os.path.abspath(args.project_root) if args.project_root else _find_project_root()
if args.write_root:
WRITE_ROOT = os.path.abspath(os.path.expanduser(args.write_root))
os.makedirs(WRITE_ROOT, exist_ok=True)
else:
WRITE_ROOT = PROJECT_ROOT # legacy: no split
SNAPSHOT_DIR = os.path.join(
os.path.expanduser("~"),
".hive",
"snapshots",
os.path.basename(PROJECT_ROOT),
)
logger.info(f"Project root: {PROJECT_ROOT}")
logger.info(f"Project root (reads): {PROJECT_ROOT}")
logger.info(f"Write root (writes): {WRITE_ROOT}")
logger.info(f"Snapshot dir: {SNAPSHOT_DIR}")
register_file_tools(
mcp,
resolve_path=_resolve_path,
resolve_path=_resolve_read_path,
resolve_path_write=_resolve_write_path,
before_write=None, # Git snapshot causes stdio deadlock on Windows; undo_changes limited
project_root=PROJECT_ROOT,
project_root=WRITE_ROOT,
)
if args.stdio:
+12 -5
View File
@@ -328,6 +328,7 @@ def register_file_tools(
mcp: FastMCP,
*,
resolve_path: Callable[[str], str] | None = None,
resolve_path_write: Callable[[str], str] | None = None,
before_write: Callable[[], None] | None = None,
project_root: str | None = None,
) -> None:
@@ -335,12 +336,18 @@ def register_file_tools(
Args:
mcp: FastMCP instance to register tools on.
resolve_path: Path resolver. Default: resolve to absolute path.
Raise ValueError to reject paths (e.g. outside sandbox).
resolve_path: Path resolver for READ operations. Default:
resolve to absolute path. Raise ValueError to reject paths
(e.g. outside sandbox).
resolve_path_write: Path resolver for WRITE/EDIT operations.
Defaults to ``resolve_path`` when not provided. Split
resolvers let callers keep reads permissive (framework
skills, docs) while confining writes to an agent workspace.
before_write: Hook called before write/edit operations (e.g. git snapshot).
project_root: If set, search_files relativizes output paths to this root.
"""
_resolve = resolve_path or _default_resolve_path
_resolve_write = resolve_path_write or _resolve
@mcp.tool()
def read_file(path: str, offset: int = 1, limit: int = 0, hashline: bool = False) -> str:
@@ -440,7 +447,7 @@ def register_file_tools(
path: Absolute file path to write.
content: Complete file content to write.
"""
resolved = _resolve(path)
resolved = _resolve_write(path)
resolved_path = Path(resolved)
# Stale-edit guard: an existing file must have been read recently
@@ -512,7 +519,7 @@ def register_file_tools(
new_text: Replacement text.
replace_all: Replace all occurrences (default: first only).
"""
resolved = _resolve(path)
resolved = _resolve_write(path)
if not os.path.isfile(resolved):
return f"Error: File not found: {path}"
@@ -829,7 +836,7 @@ def register_file_tools(
return "Error: Too many edits in one call (max 100). Split into multiple calls."
# 2. Read file
resolved = _resolve(path)
resolved = _resolve_write(path)
if not os.path.isfile(resolved):
return f"Error: File not found: {path}"