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; }
|
@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 .
|
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; }
|
@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
|
tamq --version
|
||||||
|
|
||||||
uninstall:
|
uninstall: ## Uninstall the user-level tamq tool
|
||||||
uv tool uninstall tmux-amq
|
uv tool uninstall tmux-amq
|
||||||
|
|
||||||
test:
|
test: ## Run the test suite
|
||||||
uv run pytest
|
uv run pytest
|
||||||
|
|
||||||
check:
|
check: ## Run tests and repository consistency checks
|
||||||
uv run pytest
|
uv run pytest
|
||||||
git diff --check
|
git diff --check
|
||||||
python3 -m compileall -q src
|
python3 -m compileall -q src
|
||||||
|
|
|
||||||
2
SCOPE.md
2
SCOPE.md
|
|
@ -103,7 +103,7 @@ Not yet suitable:
|
||||||
and stronger process-supervision evidence.
|
and stronger process-supervision evidence.
|
||||||
- Cross-host messaging or use as a general-purpose broker.
|
- 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
|
pseudo-terminal normalization, real tmux pushy/trigger behavior, operator-echo
|
||||||
suppression, and an isolated installed-package workflow with deterministic gita
|
suppression, and an isolated installed-package workflow with deterministic gita
|
||||||
fixtures. Forgejo CI installs tmux and uv and retains CLI help/version smoke
|
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 |
|
| 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-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-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-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-T02 | done | — | workplans/TAMQ-WP-0001-statehub-bootstrap.md |
|
||||||
| task | TAMQ-WP-0001-T03 | 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):
|
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}")
|
raise TmuxError(f"address command directory is not writable: {self.command_dir}")
|
||||||
for existing in self.command_dir.iterdir():
|
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
|
continue
|
||||||
try:
|
try:
|
||||||
content = existing.read_text(encoding="utf-8")
|
content = existing.read_text(encoding="utf-8")
|
||||||
except OSError:
|
except (OSError, UnicodeError):
|
||||||
continue
|
continue
|
||||||
if "# tamq-address-command v1" in content:
|
if "# tamq-address-command v1" in content:
|
||||||
existing.unlink()
|
existing.unlink()
|
||||||
|
|
@ -106,7 +110,7 @@ class TmuxManager:
|
||||||
raise TmuxError(f"refusing to replace existing command: {destination}")
|
raise TmuxError(f"refusing to replace existing command: {destination}")
|
||||||
try:
|
try:
|
||||||
existing = destination.read_text(encoding="utf-8")
|
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
|
raise TmuxError(f"cannot inspect existing command: {destination}") from exc
|
||||||
if "# tamq-protocol-command v2" not in existing:
|
if "# tamq-protocol-command v2" not in existing:
|
||||||
raise TmuxError(f"refusing to replace existing command: {destination}")
|
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 == ""
|
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):
|
def test_address_commands_reject_unsafe_repository_names(tmp_path):
|
||||||
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
|
manager = tmux.TmuxManager("tamq-test", command_dir=tmp_path / "commands")
|
||||||
with pytest.raises(tmux.TmuxError, match="cannot be exposed"):
|
with pytest.raises(tmux.TmuxError, match="cannot be exposed"):
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
id: TAMQ-WP-ADHOC-2026-08-25
|
id: TAMQ-WP-ADHOC-2026-08-25
|
||||||
type: workplan
|
type: workplan
|
||||||
title: "Recognize enhanced terminal submission keys"
|
title: "Terminal and local operator fixes"
|
||||||
domain: communication
|
domain: communication
|
||||||
repo: tmux-amq
|
repo: tmux-amq
|
||||||
status: finished
|
status: finished
|
||||||
|
|
@ -42,3 +42,27 @@ fix, and update the operator documentation.
|
||||||
the target's queued follow-up inputs.
|
the target's queued follow-up inputs.
|
||||||
- Full checks and installed-package verification passed. No user session or
|
- Full checks and installed-package verification passed. No user session or
|
||||||
durable user message was changed during diagnosis.
|
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