diff --git a/Makefile b/Makefile index 21f35b2..65465d5 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,22 @@ -.PHONY: install uninstall test check +.DEFAULT_GOAL := help +.PHONY: help install uninstall test check -install: +help: ## List available targets + @awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z0-9_-]+:.*## / {printf " %-12s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +install: ## Install or refresh tamq as a user-level uv tool @command -v uv >/dev/null || { echo "tamq: uv is required; install it from https://docs.astral.sh/uv/" >&2; exit 1; } uv tool install --force --reinstall --refresh . @command -v tamq >/dev/null || { echo "tamq: installed, but the uv tool bin directory is not on PATH; run 'uv tool update-shell'" >&2; exit 1; } tamq --version -uninstall: +uninstall: ## Uninstall the user-level tamq tool uv tool uninstall tmux-amq -test: +test: ## Run the test suite uv run pytest -check: +check: ## Run tests and repository consistency checks uv run pytest git diff --check python3 -m compileall -q src diff --git a/SCOPE.md b/SCOPE.md index 2b300eb..ab61c1b 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -103,7 +103,7 @@ Not yet suitable: and stronger process-supervision evidence. - Cross-host messaging or use as a general-purpose broker. -The suite currently has 133 passing tests. It includes atomic counter races, +The suite currently has 136 passing tests. It includes atomic counter races, pseudo-terminal normalization, real tmux pushy/trigger behavior, operator-echo suppression, and an isolated installed-package workflow with deterministic gita fixtures. Forgejo CI installs tmux and uv and retains CLI help/version smoke diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 06b2952..5f9c850 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -26,6 +26,7 @@ | workplan | TAMQ-WP-0014 | finished | — | workplans/TAMQ-WP-0014-readable-duplex-messaging-and-limits.md | | task | TAMQ-WP-ADHOC-2026-08-24-T01 | done | — | workplans/ADHOC-2026-08-24.md | | task | TAMQ-WP-ADHOC-2026-08-25-T01 | done | — | workplans/ADHOC-2026-08-25.md | +| task | TAMQ-WP-ADHOC-2026-08-25-T02 | done | — | workplans/ADHOC-2026-08-25.md | | task | TAMQ-WP-0001-T01 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md | | task | TAMQ-WP-0001-T02 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md | | task | TAMQ-WP-0001-T03 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md | diff --git a/src/tamq/tmux.py b/src/tamq/tmux.py index 37c4967..2bd5cf5 100644 --- a/src/tamq/tmux.py +++ b/src/tamq/tmux.py @@ -80,11 +80,15 @@ class TmuxManager: if not os.access(self.command_dir, os.W_OK | os.X_OK): raise TmuxError(f"address command directory is not writable: {self.command_dir}") for existing in self.command_dir.iterdir(): - if not existing.is_file() or existing.is_symlink(): + if ( + not existing.name.startswith("@") + or not existing.is_file() + or existing.is_symlink() + ): continue try: content = existing.read_text(encoding="utf-8") - except OSError: + except (OSError, UnicodeError): continue if "# tamq-address-command v1" in content: existing.unlink() @@ -106,7 +110,7 @@ class TmuxManager: raise TmuxError(f"refusing to replace existing command: {destination}") try: existing = destination.read_text(encoding="utf-8") - except OSError as exc: + except (OSError, UnicodeError) as exc: raise TmuxError(f"cannot inspect existing command: {destination}") from exc if "# tamq-protocol-command v2" not in existing: raise TmuxError(f"refusing to replace existing command: {destination}") diff --git a/tests/test_makefile.py b/tests/test_makefile.py new file mode 100644 index 0000000..51785c1 --- /dev/null +++ b/tests/test_makefile.py @@ -0,0 +1,21 @@ +import subprocess +from pathlib import Path + + +def test_plain_make_lists_available_targets(): + project = Path(__file__).resolve().parents[1] + result = subprocess.run( + ["make", "--no-print-directory"], + cwd=project, + text=True, + capture_output=True, + check=True, + ) + + assert result.stdout.splitlines() == [ + " help List available targets", + " install Install or refresh tamq as a user-level uv tool", + " uninstall Uninstall the user-level tamq tool", + " test Run the test suite", + " check Run tests and repository consistency checks", + ] diff --git a/tests/test_tmux_manager.py b/tests/test_tmux_manager.py index 5b9c9ac..8ae5f10 100644 --- a/tests/test_tmux_manager.py +++ b/tests/test_tmux_manager.py @@ -184,6 +184,37 @@ def test_protocol_commands_absorb_forwarded_shell_lines(tmp_path): assert result.stdout == "" +def test_protocol_reconciliation_skips_unrelated_binary_executables(tmp_path): + command_dir = tmp_path / "commands" + command_dir.mkdir() + binary = command_dir / "other-tool" + binary.write_bytes(b"\x7fELF\x8e\xff\x00") + legacy = command_dir / "@audit-core" + legacy.write_text( + "#!/bin/sh\n# tamq-address-command v1\nexit 0\n", + encoding="utf-8", + ) + manager = tmux.TmuxManager("tamq-test", command_dir=command_dir) + + manager._install_address_commands(["audit-core"]) + + assert binary.read_bytes() == b"\x7fELF\x8e\xff\x00" + assert not legacy.exists() + assert (command_dir / "To:audit-core:").is_file() + + +def test_protocol_reconciliation_refuses_undecodable_command_collision(tmp_path): + command_dir = tmp_path / "commands" + command_dir.mkdir() + collision = command_dir / "To:audit-core:" + collision.write_bytes(b"\x8e\xff") + manager = tmux.TmuxManager("tamq-test", command_dir=command_dir) + + with pytest.raises(tmux.TmuxError, match="cannot inspect existing command"): + manager._install_address_commands(["audit-core"]) + assert collision.read_bytes() == b"\x8e\xff" + + def test_address_commands_reject_unsafe_repository_names(tmp_path): manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands") with pytest.raises(tmux.TmuxError, match="cannot be exposed"): diff --git a/workplans/ADHOC-2026-08-25.md b/workplans/ADHOC-2026-08-25.md index 79be09f..0afd179 100644 --- a/workplans/ADHOC-2026-08-25.md +++ b/workplans/ADHOC-2026-08-25.md @@ -1,7 +1,7 @@ --- id: TAMQ-WP-ADHOC-2026-08-25 type: workplan -title: "Recognize enhanced terminal submission keys" +title: "Terminal and local operator fixes" domain: communication repo: tmux-amq status: finished @@ -42,3 +42,27 @@ fix, and update the operator documentation. the target's queued follow-up inputs. - Full checks and installed-package verification passed. No user session or durable user message was changed during diagnosis. + +## Make target help and binary-safe shim reconciliation + +```task +id: TAMQ-WP-ADHOC-2026-08-25-T02 +status: done +priority: high +``` + +Make plain `make` list the documented targets. Restrict legacy command-shim +reconciliation to `@` candidates, skip unrelated binary executables, and turn +an undecodable collision with a current protocol command into a safe refusal +rather than a traceback. + +### Evidence + +- Reproduced the installed startup traceback as an unconditional UTF-8 read of + unrelated executables in the command directory. +- Regression coverage preserves an unrelated ELF-like binary, removes only a + marker-owned v1 shim, and refuses an undecodable `To:repo:` collision without + replacing it. +- Plain `make` lists `help`, `install`, `uninstall`, `test`, and `check` with + descriptions. `make check` passes 136 tests and `make install` refreshes the + installed `tmux-amq==0.1.0` tool.