Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to cueapi-cli will be documented here.
## [Unreleased]

### Added
- **`messages send` + `message-to`: Phase 2 body-verify auto-echo (Mike body-verify directive 2026-05-11).** Default verify-on. CLI sends `X-CueAPI-Verify-Echo: true` request header; substrate-side (Phase 1 / cueapi-core) echoes back received body in the 201 response under `body_received`. CLI diffs sent vs received; on drift exits with code 7 + diagnostic showing byte position of first divergence + first 200 chars of each side + actionable mitigation hint. `--no-verify` flag opts out (rare; perf-sensitive flows only; verify adds zero substrate roundtrips since echo rides in the existing POST response). Backward-compat: pre-Layer-1 substrate omits `body_received` field → CLI no-ops, returns success unchanged.
- **`messages send` + `message-to`: Layer 3 force-file mode (Mike body-verify directive 2026-05-11).** Three body sources accepted (exactly one required): `--message-file <path>` (RECOMMENDED for content with shell metacharacters; zero shell interpolation), `--body-stdin` (read from stdin; for shell-pipe ergonomics), or `--body <inline>` (auto-rejected when content contains `$(...)`, backticks, or `${VAR}`). Inline body with metachars rejected with actionable error suggesting safer paths; override via `--allow-inline-metachars` for legitimate literal-metachar content (e.g., shell-tutorial examples). Closes the caller-side shell-expansion bug class where `BODY="...$(echo X)..."` silently mutates body content at variable-assignment time before reaching the CLI. Design Dock: `cue-message-silent-corruption-substrate-design-2026-05-11`.
- `cueapi message-to <recipient>` top-level wrapper for sending a message by name. Resolves `<recipient>` against your agent roster: `agent_id` (`agt_*`) and slug-form (`slug@user`) pass through unchanged; bare names match case-insensitively against `display_name` and `slug` via `GET /agents`. Same flag set as `messages send` (sans `--to`).
- `agents list --online-only` shortcut for `--status online`. Mutually exclusive with `--status`.
Expand Down
90 changes: 90 additions & 0 deletions cueapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1896,6 +1896,46 @@ def workers_delete(ctx: click.Context, worker_id: str, yes: bool) -> None:
_BODY_METACHAR_RE = re.compile(r"\$\(|`|\$\{")


def _first_divergence_byte(a: str, b: str) -> int:
"""Return byte index of first differing position. -1 if equal OR
one is a proper prefix of the other (caller distinguishes via
len() comparison)."""
common = min(len(a), len(b))
for i in range(common):
if a[i] != b[i]:
return i
return -1


def _emit_body_verify_mismatch_diagnostic(*, sent: str, received: str, msg_id: str) -> None:
"""Print actionable diff + byte-position-of-first-divergence per
Q-C3 contract. CLI exits 7 after this is called (caller responsibility)."""
divergence = _first_divergence_byte(sent, received)
if divergence == -1 and len(sent) != len(received):
# One body is a proper prefix of the other — length mismatch.
divergence = min(len(sent), len(received))
click.echo(
f"[cueapi-cli] body verify-echo MISMATCH at byte {divergence}",
err=True,
)
click.echo(
f"sent ({len(sent)} bytes, first 200 chars): {sent[:200]!r}",
err=True,
)
click.echo(
f"received ({len(received)} bytes, first 200 chars): {received[:200]!r}",
err=True,
)
click.echo("", err=True)
click.echo(
"Caller's shell may have expanded $(...)/backticks/${VAR} in body BEFORE "
"cueapi-cli received the arg. Fix: use --message-file <path> (mint body via "
"heredoc with quoted EOF marker) or --body-stdin to pipe content.",
err=True,
)
click.echo(f"Message ID (mutated content stored server-side): {msg_id}", err=True)


def _acquire_message_body(
body_text: Optional[str],
message_file: Optional[str],
Expand Down Expand Up @@ -2016,6 +2056,20 @@ def messages() -> None:
"literal $(...) text). Otherwise prefer --message-file."
),
)
@click.option(
"--no-verify",
"no_verify",
is_flag=True,
default=False,
help=(
"Opt out of Phase 2 body-verify auto-echo (Mike body-verify "
"directive 2026-05-11). Default is verify-on: SDK sends "
"X-CueAPI-Verify-Echo header + checks substrate-echoed body matches "
"sent body, exiting 7 with diff on drift. Use --no-verify for "
"perf-sensitive flows (rare; verify adds zero substrate roundtrips "
"since the echo rides in the same POST response)."
),
)
@click.option("--subject", default=None, help="Optional subject line (max 255 chars)")
@click.option(
"--reply-to",
Expand Down Expand Up @@ -2104,6 +2158,7 @@ def messages_send(
message_file: Optional[str],
body_stdin: bool,
allow_inline_metachars: bool,
no_verify: bool,
subject: Optional[str],
reply_to: Optional[str],
priority: Optional[int],
Expand Down Expand Up @@ -2161,12 +2216,28 @@ def messages_send(
if len(idempotency_key) > 255:
raise click.UsageError("--idempotency-key must be ≤255 characters")
headers["Idempotency-Key"] = idempotency_key
if not no_verify:
# Phase 2 of body-verify defense in depth (Mike directive 2026-05-11).
# Substrate echoes body_received in the 201 response when this header
# is set. CLI diffs sent vs received + exits 7 on mismatch with diff
# output. --no-verify opts out (rare; perf-sensitive flows only).
headers["X-CueAPI-Verify-Echo"] = "true"

try:
with CueAPIClient(api_key=ctx.obj.get("api_key"), profile=ctx.obj.get("profile")) as client:
resp = client.post("/messages", json=body, headers=headers)
if resp.status_code in (200, 201):
m = resp.json()
# Phase 2 body-verify check. Substrate echoes body_received
# when header set; missing field = pre-Layer-1 substrate, no-op.
if not no_verify and isinstance(m, dict):
received = m.get("body_received")
if received is not None and received != resolved_body:
_emit_body_verify_mismatch_diagnostic(
sent=resolved_body, received=received,
msg_id=m.get("id", "<unknown>"),
)
ctx.exit(7)
click.echo()
if resp.status_code == 200:
# Dedup hit on Idempotency-Key — same key + same body returned
Expand Down Expand Up @@ -2406,6 +2477,13 @@ def _resolve_recipient(client, recipient: str) -> str:
default=False,
help="Override the Layer 3 force-file guard for legitimate literal-metachar inline content.",
)
@click.option(
"--no-verify",
"no_verify",
is_flag=True,
default=False,
help="Opt out of Phase 2 body-verify auto-echo. Default verify-on.",
)
@click.option("--subject", default=None, help="Optional subject line (max 255 chars)")
@click.option(
"--reply-to",
Expand Down Expand Up @@ -2491,6 +2569,7 @@ def message_to(
message_file: Optional[str],
body_stdin: bool,
allow_inline_metachars: bool,
no_verify: bool,
subject: Optional[str],
reply_to: Optional[str],
priority: Optional[int],
Expand Down Expand Up @@ -2551,6 +2630,9 @@ def message_to(
if len(idempotency_key) > 255:
raise click.UsageError("--idempotency-key must be ≤255 characters")
headers["Idempotency-Key"] = idempotency_key
if not no_verify:
# Phase 2 body-verify echo (mirror of messages_send).
headers["X-CueAPI-Verify-Echo"] = "true"

try:
with CueAPIClient(api_key=ctx.obj.get("api_key"), profile=ctx.obj.get("profile")) as client:
Expand All @@ -2564,6 +2646,14 @@ def message_to(
resp = client.post("/messages", json=body, headers=headers)
if resp.status_code in (200, 201):
m = resp.json()
if not no_verify and isinstance(m, dict):
received = m.get("body_received")
if received is not None and received != resolved_body:
_emit_body_verify_mismatch_diagnostic(
sent=resolved_body, received=received,
msg_id=m.get("id", "<unknown>"),
)
ctx.exit(7)
click.echo()
if resp.status_code == 200:
echo_info("Idempotency-Key dedup hit:", "existing message returned")
Expand Down
109 changes: 109 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,115 @@ def test_message_to_rejects_inline_body_with_metachars():
assert "shell metacharacters" in r.output


# --- Phase 2: auto-verify body echo ---


def test_messages_send_default_adds_verify_echo_header(monkeypatch):
"""Default auto-verify-on adds X-CueAPI-Verify-Echo: true header."""
holder: dict = {}
_patch_messages_client(
monkeypatch,
holder,
responses={
("POST", "/messages"): lambda: _FakeResp(
201, {"id": "msg_x", "delivery_state": "queued"},
)
},
)
r = runner.invoke(
main,
["messages", "send", "--from", "a@x", "--to", "b@y", "--body", "plain"],
)
assert r.exit_code == 0, r.output
_, _, _, headers = holder["client"].calls[-1]
assert headers.get("X-CueAPI-Verify-Echo") == "true"


def test_messages_send_no_verify_omits_header(monkeypatch):
"""--no-verify opt-out omits the X-CueAPI-Verify-Echo header."""
holder: dict = {}
_patch_messages_client(
monkeypatch,
holder,
responses={
("POST", "/messages"): lambda: _FakeResp(
201, {"id": "msg_x", "delivery_state": "queued"},
)
},
)
r = runner.invoke(
main,
["messages", "send", "--from", "a@x", "--to", "b@y",
"--body", "plain", "--no-verify"],
)
assert r.exit_code == 0, r.output
_, _, _, headers = holder["client"].calls[-1]
assert "X-CueAPI-Verify-Echo" not in headers


def test_messages_send_verify_passes_byte_identical(monkeypatch):
"""Substrate echoes back same body → success."""
holder: dict = {}
_patch_messages_client(
monkeypatch,
holder,
responses={
("POST", "/messages"): lambda: _FakeResp(
201, {"id": "msg_x", "delivery_state": "queued",
"body_received": "plain"},
)
},
)
r = runner.invoke(
main,
["messages", "send", "--from", "a@x", "--to", "b@y", "--body", "plain"],
)
assert r.exit_code == 0, r.output


def test_messages_send_verify_fails_loud_on_mismatch(monkeypatch):
"""Substrate echoes back DIFFERENT body → exit 7 + diff diagnostic."""
holder: dict = {}
_patch_messages_client(
monkeypatch,
holder,
responses={
("POST", "/messages"): lambda: _FakeResp(
201, {"id": "msg_mutated", "delivery_state": "queued",
"body_received": "body received by substrate (mutated)"},
)
},
)
r = runner.invoke(
main,
["messages", "send", "--from", "a@x", "--to", "b@y",
"--body", "body sent by caller (intended)"],
)
# Exit code 7 = body verify mismatch (distinct from generic failure)
assert r.exit_code == 7
assert "MISMATCH" in r.output


def test_messages_send_verify_noop_when_substrate_omits_echo(monkeypatch):
"""Backward-compat: pre-Layer-1 substrate omits body_received → no raise."""
holder: dict = {}
_patch_messages_client(
monkeypatch,
holder,
responses={
("POST", "/messages"): lambda: _FakeResp(
201, {"id": "msg_x", "delivery_state": "queued"},
# No body_received field
)
},
)
r = runner.invoke(
main,
["messages", "send", "--from", "a@x", "--to", "b@y", "--body", "plain"],
)
assert r.exit_code == 0, r.output


# --- send body + headers ---


Expand Down