From e54ba1a0494626f6a60bd9aecd2d9013c77cd0e2 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Mon, 23 Feb 2026 23:00:27 -0500 Subject: [PATCH 1/2] bridge: migrate protected slack-bridge path to /opt release --- bin/deploy.sh | 8 ++++++++ pi/extensions/tool-guard.test.mjs | 20 ++++++++++---------- pi/extensions/tool-guard.ts | 6 +++--- pi/skills/control-agent/startup-cleanup.sh | 17 ++++++++++++++--- 4 files changed, 35 insertions(+), 16 deletions(-) diff --git a/bin/deploy.sh b/bin/deploy.sh index 954ad81..7f1d6db 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -293,6 +293,14 @@ deploy_runtime_asset_entry() { bb_manifest_for_each RUNTIME_ASSET_MANIFEST deploy_runtime_asset_entry +# Clean up legacy bridge runtime path; bridge now runs from /opt release only. +if [ "$DRY_RUN" -eq 0 ]; then + as_agent bash -c "rm -rf '$BAUDBOT_HOME/runtime/slack-bridge'" + log "✓ removed legacy runtime/slack-bridge" +else + log "would remove: runtime/slack-bridge (legacy path)" +fi + # ── Memory Seeds ───────────────────────────────────────────────────────────── MEMORY_SEED_DIR="$STAGE_DIR/skills/control-agent/memory" diff --git a/pi/extensions/tool-guard.test.mjs b/pi/extensions/tool-guard.test.mjs index b66f69c..fbe6121 100644 --- a/pi/extensions/tool-guard.test.mjs +++ b/pi/extensions/tool-guard.test.mjs @@ -46,7 +46,7 @@ const BASH_DENY_RULES = [ { id: "chmod-baudbot-source", pattern: new RegExp(`chmod\\b.*${escapeRegex(BAUDBOT_SOURCE_DIR)}`), label: "chmod on baudbot source repo", severity: "block" }, { id: "chown-baudbot-source", pattern: new RegExp(`chown\\b.*${escapeRegex(BAUDBOT_SOURCE_DIR)}`), label: "chown on baudbot source repo", severity: "block" }, { id: "tee-baudbot-source", pattern: new RegExp(`tee\\s+.*${escapeRegex(BAUDBOT_SOURCE_DIR)}/`), label: "tee write to baudbot source repo", severity: "block" }, - { id: "chmod-runtime-security", pattern: /chmod\b.*\/(\.pi\/agent\/extensions\/tool-guard|runtime\/slack-bridge\/security)\./, label: "chmod on protected runtime security file", severity: "block" }, + { id: "chmod-runtime-security", pattern: /chmod\b.*\/(\.pi\/agent\/extensions\/tool-guard|runtime\/slack-bridge\/security|opt\/baudbot\/current\/slack-bridge\/security)\./, label: "chmod on protected runtime security file", severity: "block" }, // Credential exfiltration { id: "env-exfil-curl", pattern: /\benv\b.*\|\s*(curl|wget|nc)\b/, label: "Piping environment to network tool", severity: "block" }, { id: "cat-env-curl", pattern: /cat\s+.*\.env.*\|\s*(curl|wget|nc)\b/, label: "Exfiltrating .env via network", severity: "block" }, @@ -77,8 +77,8 @@ function isAllowedWritePath(filePath) { const PROTECTED_RUNTIME_FILES = [ `${AGENT_HOME}/.pi/agent/extensions/tool-guard.ts`, `${AGENT_HOME}/.pi/agent/extensions/tool-guard.test.mjs`, - `${AGENT_HOME}/runtime/slack-bridge/security.mjs`, - `${AGENT_HOME}/runtime/slack-bridge/security.test.mjs`, + `/opt/baudbot/current/slack-bridge/security.mjs`, + `/opt/baudbot/current/slack-bridge/security.test.mjs`, ]; function isProtectedPath(filePath) { @@ -307,7 +307,7 @@ describe("tool-guard: source repo protection (bash)", () => { assert.equal(checkBashCommand(`chmod a+w ${AGENT_HOME}/.pi/agent/extensions/tool-guard.ts`).blocked, true); }); it("blocks chmod on runtime security.mjs", () => { - assert.equal(checkBashCommand(`chmod 777 ${AGENT_HOME}/runtime/slack-bridge/security.mjs`).blocked, true); + assert.equal(checkBashCommand("chmod 777 /opt/baudbot/current/slack-bridge/security.mjs").blocked, true); }); }); @@ -358,8 +358,8 @@ describe("tool-guard: workspace confinement (allow-list)", () => { it(`allows write to ${AGENT_HOME}/.pi/agent/skills/new-skill/SKILL.md`, () => { assert.equal(checkWritePath(`${AGENT_HOME}/.pi/agent/skills/new-skill/SKILL.md`), false); }); - it(`allows write to ${AGENT_HOME}/runtime/slack-bridge/bridge.mjs`, () => { - assert.equal(checkWritePath(`${AGENT_HOME}/runtime/slack-bridge/bridge.mjs`), false); + it("blocks write to /opt/baudbot/current/slack-bridge/bridge.mjs", () => { + assert.equal(checkWritePath("/opt/baudbot/current/slack-bridge/bridge.mjs"), true); }); // BLOCKED: outside agent home @@ -430,13 +430,13 @@ describe("tool-guard: protected runtime security files", () => { assert.equal(checkWritePath(`${AGENT_HOME}/.pi/agent/extensions/tool-guard.test.mjs`), true); }); it("blocks write to runtime security.mjs", () => { - assert.equal(checkWritePath(`${AGENT_HOME}/runtime/slack-bridge/security.mjs`), true); + assert.equal(checkWritePath("/opt/baudbot/current/slack-bridge/security.mjs"), true); }); it("blocks write to runtime security.test.mjs", () => { - assert.equal(checkWritePath(`${AGENT_HOME}/runtime/slack-bridge/security.test.mjs`), true); + assert.equal(checkWritePath("/opt/baudbot/current/slack-bridge/security.test.mjs"), true); }); - it("allows write to runtime bridge.mjs (agent-modifiable)", () => { - assert.equal(checkWritePath(`${AGENT_HOME}/runtime/slack-bridge/bridge.mjs`), false); + it("blocks write to runtime bridge.mjs (immutable release path)", () => { + assert.equal(checkWritePath("/opt/baudbot/current/slack-bridge/bridge.mjs"), true); }); it("allows write to runtime non-security extensions", () => { assert.equal(checkWritePath(`${AGENT_HOME}/.pi/agent/extensions/auto-name.ts`), false); diff --git a/pi/extensions/tool-guard.ts b/pi/extensions/tool-guard.ts index d887ea3..cf8af78 100644 --- a/pi/extensions/tool-guard.ts +++ b/pi/extensions/tool-guard.ts @@ -253,7 +253,7 @@ const BASH_DENY_RULES: DenyRule[] = [ ] : []), { id: "chmod-runtime-security", - pattern: /chmod\b.*\/(\.pi\/agent\/extensions\/tool-guard|runtime\/slack-bridge\/security)\./, + pattern: /chmod\b.*\/(\.pi\/agent\/extensions\/tool-guard|runtime\/slack-bridge\/security|opt\/baudbot\/current\/slack-bridge\/security)\./, label: "chmod on protected runtime security file", severity: "block" as const, tier: "high" as const, @@ -311,8 +311,8 @@ function isAllowedWritePath(filePath: string): boolean { const PROTECTED_RUNTIME_FILES = [ `${AGENT_HOME}/.pi/agent/extensions/tool-guard.ts`, `${AGENT_HOME}/.pi/agent/extensions/tool-guard.test.mjs`, - `${AGENT_HOME}/runtime/slack-bridge/security.mjs`, - `${AGENT_HOME}/runtime/slack-bridge/security.test.mjs`, + `/opt/baudbot/current/slack-bridge/security.mjs`, + `/opt/baudbot/current/slack-bridge/security.test.mjs`, ]; function isProtectedPath(filePath: string): boolean { diff --git a/pi/skills/control-agent/startup-cleanup.sh b/pi/skills/control-agent/startup-cleanup.sh index 5961136..98bd5a2 100755 --- a/pi/skills/control-agent/startup-cleanup.sh +++ b/pi/skills/control-agent/startup-cleanup.sh @@ -16,6 +16,12 @@ set -euo pipefail # processes and causes `varlock run` to treat subcommands as Node module paths). unset PKG_EXECPATH 2>/dev/null || true +RUNTIME_NODE_HELPER="$HOME/runtime/bin/lib/runtime-node.sh" +if [ -r "$RUNTIME_NODE_HELPER" ]; then + # shellcheck source=bin/lib/runtime-node.sh + source "$RUNTIME_NODE_HELPER" +fi + SOCKET_DIR="$HOME/.pi/session-control" if [ $# -eq 0 ]; then @@ -123,7 +129,10 @@ fi # The tmux session stays alive independently of this script (same pattern as # sentry-agent). If the bridge crashes, the loop restarts it after 5 seconds. echo "Starting slack-bridge ($BRIDGE_SCRIPT) via tmux..." -NODE_BIN_DIR="$HOME/opt/node/bin" +NODE_BIN_DIR="${NODE_BIN_DIR:-$HOME/opt/node/bin}" +if command -v bb_resolve_runtime_node_bin_dir >/dev/null 2>&1; then + NODE_BIN_DIR="$(bb_resolve_runtime_node_bin_dir "$HOME")" +fi if [ ! -d "$NODE_BIN_DIR" ]; then # Fallback: resolve versioned node dir NODE_BIN_DIR="$(echo "$HOME"/opt/node-v*-linux-x64/bin | awk '{print $1}')" @@ -131,12 +140,14 @@ fi tmux new-session -d -s "$BRIDGE_TMUX_SESSION" "\ unset PKG_EXECPATH; \ - export PATH=\$HOME/.varlock/bin:$NODE_BIN_DIR:\$PATH; \ + export PATH=$NODE_BIN_DIR:\$PATH; \ export PI_SESSION_ID=$MY_UUID; \ cd $BRIDGE_DIR; \ while true; do \ echo \"[\$(date -Is)] bridge: starting $BRIDGE_SCRIPT\" >> $BRIDGE_LOG_FILE; \ - varlock run --path \$HOME/.config/ -- node $BRIDGE_SCRIPT >> $BRIDGE_LOG_FILE 2>&1; \ + for v in \$(env | grep ^SLACK_BROKER_ | cut -d= -f1 || true); do unset \$v; done; \ + set -a; source \$HOME/.config/.env; set +a; \ + node $BRIDGE_SCRIPT >> $BRIDGE_LOG_FILE 2>&1; \ exit_code=\$?; \ echo \"[\$(date -Is)] bridge: exited with code \$exit_code, restarting in 5s\" >> $BRIDGE_LOG_FILE; \ sleep 5; \ From 0dea634ff43e84c484a84a1830a5015c9577e6f7 Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Mon, 23 Feb 2026 23:07:51 -0500 Subject: [PATCH 2/2] ci: retry runtime RPC probe to avoid startup race --- bin/ci/smoke-agent-runtime.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bin/ci/smoke-agent-runtime.sh b/bin/ci/smoke-agent-runtime.sh index 1cd710a..cc04a17 100755 --- a/bin/ci/smoke-agent-runtime.sh +++ b/bin/ci/smoke-agent-runtime.sh @@ -68,7 +68,11 @@ wait_for_control_socket() { probe_rpc_get_message() { local socket_path="$1" - sudo -u "$AGENT_USER" python3 - "$socket_path" <<'PY' + local attempts=8 + local delay_seconds=1 + + for ((i = 1; i <= attempts; i++)); do + if sudo -u "$AGENT_USER" python3 - "$socket_path" <<'PY' import json import socket import sys @@ -103,6 +107,15 @@ try: finally: client.close() PY + then + return 0 + fi + + log "rpc probe attempt ${i}/${attempts} failed; retrying in ${delay_seconds}s" + sleep "$delay_seconds" + done + + return 1 } main() {