fix: make command reconciliation binary safe
Some checks failed
tamq-ci / test (push) Failing after 6s
Some checks failed
tamq-ci / test (push) Failing after 6s
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a03397-4d51-7fd1-8ff2-946eb22ea2bc
This commit is contained in:
parent
92881b6b56
commit
fbb56f5981
7 changed files with 95 additions and 10 deletions
14
Makefile
14
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
|
||||
|
|
|
|||
2
SCOPE.md
2
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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
21
tests/test_makefile.py
Normal file
21
tests/test_makefile.py
Normal file
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue