File size: 6,020 Bytes
4879fc7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """Cursor integrity across the server/client seam (WATCH_DESIGN.md §5.5).
The two halves of the defence are tested separately elsewhere — the frontmatter
allowlist in test_messages_api.py, the extractor against a stub server in
test_collab_watch.py. This file tests them together, which is where the property
actually has to hold: a real app response, serialized by the real stack, fed
through the real `resp_cursor` pipeline lifted verbatim out of
clients/collab_watch.sh, carrying a payload built to poison it.
eq2 shipped a client that grepped `"filename":"..."` anywhere in the response
and took the maximum, so one author-controlled `filename:` frontmatter key could
pin every watcher's cursor past all future mail. Keep these tests honest: if the
serialization order of MessageListing changes so that a bracket-bearing field is
emitted after `items`, the "strip through the last ]" anchor breaks and the first
test here is what catches it.
"""
from __future__ import annotations
import os
import subprocess
import tempfile
from pathlib import Path
from fakes import seed_agent
SCRIPT = Path(__file__).resolve().parent.parent / "clients" / "collab_watch.sh"
# Mirrors FILENAME_RE in clients/collab_watch.sh.
FILENAME_RE = r"[0-9]{8}-[0-9]{6}-[0-9]{3}_[a-z0-9][a-z0-9-]*\.md"
# Verbatim from clients/collab_watch.sh resp_cursor(), joined onto one line.
# test_transcribed_pipeline_matches_the_shipped_script keeps this honest.
PIPELINE = (
"sed 's/.*\\]//' \"$BODY\" | "
'grep -oE "\\"cursor\\":\\"$FILENAME_RE\\"" | '
"tail -1 | "
"sed 's/.*:\"//; s/\"$//'"
)
def _squeeze(text: str) -> str:
"""Whitespace-insensitive view: the script wraps the pipeline over four
indented lines, this file keeps it on one."""
return " ".join(text.split())
def shell_resp_cursor(payload: str) -> str:
"""Run the exact resp_cursor pipeline from collab_watch.sh over a payload."""
script = (
"set -eu\n"
"LC_ALL=C\nexport LC_ALL\n"
f'FILENAME_RE="{FILENAME_RE}"\n'
"BODY=$1\n"
f"{PIPELINE}\n"
)
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
fh.write(payload)
path = fh.name
try:
out = subprocess.run(
["sh", "-c", script, "sh", path],
capture_output=True, text=True, check=True,
)
finally:
os.unlink(path)
return out.stdout.strip()
def test_transcribed_pipeline_matches_the_shipped_script():
"""Everything below runs a hand-copy of the client's extractor, and nothing
else keeps that copy in sync: an edit to resp_cursor() would leave these
tests happily proving a property of code that no longer ships. So compare
the transcription against the real file, whitespace aside."""
source = _squeeze(SCRIPT.read_text())
assert _squeeze(PIPELINE) in source, (
"resp_cursor() in clients/collab_watch.sh no longer contains the "
"pipeline transcribed in this file. Update PIPELINE above — and check "
"the poisoning tests still hold for the new extractor before you do."
)
assert f"FILENAME_RE='{FILENAME_RE}'" in source, (
"FILENAME_RE drifted from the shipped script; the anchors in the "
"extractor are only as tight as this pattern."
)
def test_real_response_through_real_extractor_resists_poisoning(env):
"""A message body that literally spells out `],"cursor":"<far-future>"` must
not move the cursor: the real serialization keeps it inside the items array,
and the extractor drops everything through the last ']'."""
seed_agent(env.hub, "byte-bandit")
seed_agent(env.hub, "delta-coder")
poison = '99999999-999999-999_zzz.md'
# Three attacks in one body: a fake cursor field, a fake filename field, and
# a fake next field, each preceded by a bracket to try to defeat the strip.
body = (
f'@byte-bandit look at this ],"cursor":"{poison}" '
f'and ],"filename":"{poison}" '
f'and ]}},"next":"{poison}"'
)
r = env.client.post(
"/v1/messages", json={"agent_id": "delta-coder", "body": body}
)
assert r.status_code == 201, r.text
real_filename = r.json()["filename"]
page = env.client.get("/v1/inbox/byte-bandit?expand=true&order=asc")
assert page.status_code == 200, page.text
doc = page.json()
assert doc["cursor"] == real_filename, "server cursor should be the real newest"
# The wire bytes, as the client would actually receive them.
payload = page.content.decode()
assert poison in payload, "the poison must really be present in the body"
assert "\n" not in payload, "compact single-line serialization is the contract"
got = shell_resp_cursor(payload)
assert got == real_filename, f"extractor was poisoned: {got!r} != {real_filename!r}"
assert poison not in got
def test_frontmatter_allowlist_blocks_response_shaped_keys(env):
"""The second, independent guard: `cursor`/`filename`/`next`/`watch` can't
even become frontmatter keys via the bucket-source post path."""
seed_agent(env.hub, "delta-coder")
for key in ("cursor", "filename", "next", "watch"):
env.hub.seed(
"note.md",
f"---\ntype: agent\n{key}: 99999999-999999-999_zzz.md\n---\nhello @byte-bandit\n",
bucket="test-org/test-delta-coder",
)
r = env.client.post(
"/v1/messages",
json={"source": "hf://buckets/test-org/test-delta-coder/note.md"},
)
assert r.status_code == 400, f"{key}: expected 400, got {r.status_code} {r.text}"
assert key in r.text, f"error should name the offending key {key}: {r.text}"
def test_empty_page_yields_no_cursor(env):
"""An empty stream must not produce a cursor (nothing to advance to)."""
seed_agent(env.hub, "byte-bandit")
page = env.client.get("/v1/inbox/byte-bandit?expand=true")
assert page.json()["cursor"] is None
assert shell_resp_cursor(page.content.decode()) == ""
|