diff --git a/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/README.md b/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/README.md index c422fca4e..9c6faf693 100644 --- a/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/README.md +++ b/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/README.md @@ -25,6 +25,11 @@ entry, add a probe for it: 1. **A script here** — short, deterministic, side-effect-free, printing the `PROBE:` line above. One failure mode per script: a probe that conflates two restrictions makes the report ambiguous when it fails. + Hold a command's output in a variable, not a file: `/tmp` is read-only + inside the sandbox, and a failed redirection skips the command and + exits 1, which a probe reading exit codes takes for the command's own + answer. Probe 1 once passed every agent that way. Add a test under + [`../tests/`](../tests/) that runs the script against a fake binary. 2. **An interpretation table in `SKILL.md`** — three to five rows mapping result strings to ✓ / ✗ / ⊘ / ⚠. 3. **A remediation link** to the catalogue section that fixes it. Link diff --git a/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/probe-1-ssh-agent.sh b/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/probe-1-ssh-agent.sh index 9d28bccca..5600982ea 100755 --- a/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/probe-1-ssh-agent.sh +++ b/plugins/magpie-setup/skills/isolated-setup-doctor/scripts/probe-1-ssh-agent.sh @@ -22,11 +22,13 @@ elif [ ! -S "$SSH_AUTH_SOCK" ]; then echo "PROBE: ssh-agent → ✗ (socket file at SSH_AUTH_SOCK not stat-able from inside sandbox)" echo " SSH_AUTH_SOCK=$SSH_AUTH_SOCK" else - ssh-add -l > /tmp/ssh-add.out 2>&1; rc=$? + # Held in a variable, not a file: /tmp is read-only inside the sandbox, and a + # failed redirection never runs ssh-add but exits 1, the "no identities" code. + out=$(ssh-add -l 2>&1); rc=$? case "$rc" in - 0) echo "PROBE: ssh-agent → ✓ ($(wc -l < /tmp/ssh-add.out | tr -d ' ') identities listed)" ;; + 0) echo "PROBE: ssh-agent → ✓ ($(printf '%s\n' "$out" | wc -l | tr -d ' ') identities listed)" ;; 1) echo "PROBE: ssh-agent → ✓ (agent reachable, no identities configured)" ;; - 2) echo "PROBE: ssh-agent → ✗ (agent unreachable: $(head -1 /tmp/ssh-add.out))" ;; - *) echo "PROBE: ssh-agent → ⚠ (unexpected rc=$rc: $(head -1 /tmp/ssh-add.out))" ;; + 2) echo "PROBE: ssh-agent → ✗ (agent unreachable: $(printf '%s\n' "$out" | head -1))" ;; + *) echo "PROBE: ssh-agent → ⚠ (unexpected rc=$rc: $(printf '%s\n' "$out" | head -1))" ;; esac fi diff --git a/plugins/magpie-setup/skills/isolated-setup-doctor/tests/test_probe_1_ssh_agent.py b/plugins/magpie-setup/skills/isolated-setup-doctor/tests/test_probe_1_ssh_agent.py new file mode 100644 index 000000000..6acdc70ea --- /dev/null +++ b/plugins/magpie-setup/skills/isolated-setup-doctor/tests/test_probe_1_ssh_agent.py @@ -0,0 +1,98 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Probe 1 reports what `ssh-add -l` returned, and writes no file. + +The probe used to capture `ssh-add -l` into `/tmp/ssh-add.out`. Inside +the sandbox `/tmp` is read-only, so the redirection failed, `ssh-add` +never ran, and the redirection's own exit code of 1 was read as "agent +reachable, no identities": a pass for an agent the probe never asked. +""" + +from __future__ import annotations + +import os +import socket +import stat +import subprocess +import tempfile +import time +import unittest +from pathlib import Path + +PROBE = Path(__file__).resolve().parents[1] / "scripts" / "probe-1-ssh-agent.sh" +OLD_OUTPUT = Path("/tmp/ssh-add.out") + + +class ProbeOneSshAgent(unittest.TestCase): + def setUp(self) -> None: + self._tmp = tempfile.TemporaryDirectory() + self.tmp = Path(self._tmp.name) + self.bin = self.tmp / "bin" + self.bin.mkdir() + self.addCleanup(self._tmp.cleanup) + # The probe only checks that SSH_AUTH_SOCK is a socket; the fake + # ssh-add never connects to it. A sandbox may refuse bind(2), so fall + # back to the session's own agent socket. + self.sock_path = self.tmp / "agent.sock" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + try: + sock.bind(str(self.sock_path)) + except OSError: + existing = os.environ.get("SSH_AUTH_SOCK", "") + if not (existing and Path(existing).is_socket()): + self.skipTest("cannot bind a unix socket and no SSH_AUTH_SOCK to reuse") + self.sock_path = Path(existing) + + def fake_ssh_add(self, output: str, rc: int) -> None: + script = self.bin / "ssh-add" + script.write_text(f"#!/bin/sh\nprintf '{output}'\nexit {rc}\n") + script.chmod(script.stat().st_mode | stat.S_IXUSR) + + def run_probe(self) -> str: + env = { + "PATH": f"{self.bin}{os.pathsep}{os.environ.get('PATH', '')}", + "SSH_AUTH_SOCK": str(self.sock_path), + "TMPDIR": str(self.tmp), + } + result = subprocess.run( + ["bash", str(PROBE)], env=env, capture_output=True, text=True, check=True + ) + return result.stdout.strip() + + def test_counts_listed_identities(self) -> None: + self.fake_ssh_add("4096 SHA256:aaa key-a (RSA)\n4096 SHA256:bbb key-b (RSA)\n", 0) + self.assertEqual(self.run_probe(), "PROBE: ssh-agent → ✓ (2 identities listed)") + + def test_reports_unreachable_agent_with_its_message(self) -> None: + self.fake_ssh_add("Could not open a connection to your authentication agent.\n", 2) + self.assertEqual( + self.run_probe(), + "PROBE: ssh-agent → ✗ (agent unreachable: " + "Could not open a connection to your authentication agent.)", + ) + + def test_writes_nothing_under_tmp(self) -> None: + self.fake_ssh_add("4096 SHA256:aaa key-a (RSA)\n", 0) + started = time.time() + self.run_probe() + if OLD_OUTPUT.exists(): + self.assertLess(OLD_OUTPUT.stat().st_mtime, started) + + +if __name__ == "__main__": + unittest.main()