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()) == ""