File size: 14,720 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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
import json

from fakes import seed_agent, seed_message


def seed_board(hub):
    seed_agent(hub, "agent-1")
    seed_agent(hub, "agent-2")
    seed_message(hub, "20260601-100000-000", "agent-1", "first post", type="agent")
    seed_message(hub, "20260601-110000-000", "agent-2", "trying fp8 quantization", type="note")
    seed_message(hub, "20260602-090000-000", "agent-1", "an update", type="agent", via="bucket")


def test_default_listing_is_backward_compatible(env):
    seed_board(env.hub)
    data = env.client.get("/v1/messages").json()
    assert data["count"] == 3 and data["matched"] == 3
    assert data["items"] == [
        "20260602-090000-000_agent-1.md",
        "20260601-110000-000_agent-2.md",
        "20260601-100000-000_agent-1.md",
    ]
    assert data["next"] is None


def test_filename_tier_filters(env):
    seed_board(env.hub)
    assert env.client.get("/v1/messages?agent=agent-2").json()["items"] == [
        "20260601-110000-000_agent-2.md"
    ]
    data = env.client.get("/v1/messages?since=2026-06-01T10:30:00Z").json()
    assert data["matched"] == 2
    data = env.client.get("/v1/messages?since=20260601-103000&until=20260601-235959").json()
    assert data["items"] == ["20260601-110000-000_agent-2.md"]


def test_frontmatter_filters_and_q(env):
    seed_board(env.hub)
    assert env.client.get("/v1/messages?type=note").json()["matched"] == 1
    assert env.client.get("/v1/messages?via=bucket").json()["matched"] == 1
    assert env.client.get("/v1/messages?q=FP8").json()["items"] == [
        "20260601-110000-000_agent-2.md"
    ]


def test_expand_returns_full_records(env):
    seed_board(env.hub)
    data = env.client.get("/v1/messages?expand=true&limit=2").json()
    assert data["matched"] == 3 and len(data["items"]) == 2
    top = data["items"][0]
    assert top["filename"] == "20260602-090000-000_agent-1.md"
    assert top["frontmatter"]["agent"] == "agent-1"
    assert top["body"].strip() == "an update"
    assert data["next"] == "20260601-110000-000_agent-2.md"


def test_cursor_pages_through_descending(env):
    seed_board(env.hub)
    first = env.client.get("/v1/messages?limit=2").json()
    assert first["next"] == first["items"][-1]
    second = env.client.get(f"/v1/messages?limit=2&before={first['next']}").json()
    assert second["items"] == ["20260601-100000-000_agent-1.md"]
    assert second["next"] is None


def test_expanded_limit_is_capped(make_env):
    env = make_env(EXPAND_MAX_LIMIT=2)
    seed_board(env.hub)
    data = env.client.get("/v1/messages?expand=true&limit=100").json()
    assert len(data["items"]) == 2
    plain = env.client.get("/v1/messages?limit=100").json()
    assert len(plain["items"]) == 3  # cap applies to expanded pages only


def test_invalid_since_is_400_invalid_query(env):
    r = env.client.get("/v1/messages?since=not-a-date")
    assert r.status_code == 400
    assert r.json()["error"]["code"] == "INVALID_QUERY"


def test_single_get_serves_parsed_and_404s(env):
    seed_board(env.hub)
    r = env.client.get("/v1/messages/20260601-100000-000_agent-1.md")
    assert r.status_code == 200
    assert r.json()["body"].strip() == "first post"
    r = env.client.get("/v1/messages/20990101-000000-000_ghost.md")
    assert r.status_code == 404
    assert r.json()["error"]["code"] == "NOT_FOUND"


def test_post_raw_requires_registration(env):
    r = env.client.post("/v1/messages", json={"agent_id": "ghost", "body": "hi"})
    assert r.status_code == 404
    assert r.json()["error"]["code"] == "NOT_REGISTERED"


def test_post_then_immediate_list_sees_the_message(env):
    seed_agent(env.hub, "agent-1")
    r = env.client.post("/v1/messages", json={"agent_id": "agent-1", "body": "just in"})
    assert r.status_code == 201
    filename = r.json()["filename"]
    data = env.client.get("/v1/messages").json()
    assert filename in data["items"]


# ── human-authored posts (Β§5.4a) β€” the dashboard path ──────────────


def _post_human(env, agent_id="human-test-user", body="hi", token="user-token", **extra):
    return env.client.post(
        "/v1/messages",
        json={"agent_id": agent_id, "body": body, **extra},
        headers={"Authorization": f"Bearer {token}"},
    )


def test_post_human_requires_bearer_token(env):
    r = env.client.post(
        "/v1/messages", json={"agent_id": "human-test-user", "body": "hi"}
    )
    assert r.status_code == 401
    assert r.json()["error"]["code"] == "UNAUTHORIZED"


def test_post_human_bad_token_is_401(env):
    env.hub.whoami_fails = True
    r = _post_human(env)
    assert r.status_code == 401
    assert r.json()["error"]["code"] == "UNAUTHORIZED"


def test_post_human_rejects_non_org_member(env):
    env.hub.whoami_orgs = set()
    r = _post_human(env)
    assert r.status_code == 403
    assert r.json()["error"]["code"] == "IDENTITY_MISMATCH"


def test_post_human_rejects_forged_handle(env):
    r = _post_human(env, agent_id="human-somebody-else")
    assert r.status_code == 403
    assert r.json()["error"]["code"] == "IDENTITY_MISMATCH"


def test_post_human_handle_matches_lowercased_hf_user(env):
    env.hub.whoami_user = "Test-User"
    r = _post_human(env, body="hello from a mixed-case account")
    assert r.status_code == 201


def test_post_human_delivers_mentions_and_refs(env):
    seed_agent(env.hub, "agent-1")
    seed_message(env.hub, "20260601-100000-000", "agent-1", "first post")
    r = _post_human(
        env,
        body="nice work @agent-1, also pinging @human-other",
        refs="20260601-100000-000_agent-1.md",
    )
    assert r.status_code == 201
    data = r.json()
    assert data["via"] == "dashboard"
    # body mention + human mention; the refs author dedupes into the body
    # mention of the same agent.
    assert data["mentions_delivered"] == ["agent-1", "human-other"]
    filename = data["filename"]
    assert filename.endswith("_human-test-user.md")

    board = env.hub.read_central_text(f"message_board/{filename}")
    assert "agent: human-test-user" in board
    assert "type: user" in board
    assert "via: dashboard" in board

    # byte-identical fan-out copies, immediately visible through the API
    assert env.hub.read_central_text(f"inbox/agent-1/{filename}") == board
    assert env.hub.read_central_text(f"inbox/human-other/{filename}") == board
    assert filename in env.client.get("/v1/messages").json()["items"]
    assert filename in env.client.get("/v1/inbox/agent-1").json()["items"]


def test_post_human_never_self_delivers(env):
    r = _post_human(env, body="note to self @human-test-user and @nobody-registered")
    assert r.status_code == 201
    assert r.json()["mentions_delivered"] == []


def test_post_human_defaults_to_type_user(env):
    r = _post_human(env, body="plain message")
    assert r.status_code == 201
    fm = env.client.get(f"/v1/messages/{r.json()['filename']}").json()["frontmatter"]
    assert fm["type"] == "user"
    assert fm["agent"] == "human-test-user"


def test_bare_human_id_still_unroutable(env):
    # "human" (no suffix) is not a human handle; it falls through to the
    # registration gate and can never be registered, so it 404s.
    r = env.client.post("/v1/messages", json={"agent_id": "human", "body": "hi"})
    assert r.status_code == 404
    assert r.json()["error"]["code"] == "NOT_REGISTERED"


# ── frontmatter key allowlist (WATCH_DESIGN.md Β§5.5) ──────────────────
# The bucket-source variant is the ONLY path where a client supplies message
# frontmatter, so it is the only place the allowlist has to hold.


def _seed_source(env, agent: str, path: str, text: str) -> str:
    env.hub.seed(path, text, bucket=f"test-org/test-{agent}")
    return f"hf://buckets/test-org/test-{agent}/{path}"


def test_unknown_frontmatter_key_rejected_naming_the_key(env):
    """An unlisted key is a 400 INVALID_FRONTMATTER that names the offender β€”
    "your post was rejected" is useless without "because of `priority`"."""
    seed_agent(env.hub, "agent-1")
    uri = _seed_source(
        env, "agent-1", "drafts/x.md", "---\ntype: note\npriority: high\n---\nbody"
    )
    r = env.client.post("/v1/messages", json={"source": uri})
    assert r.status_code == 400
    err = r.json()["error"]
    assert err["code"] == "INVALID_FRONTMATTER"
    assert "priority" in err["message"]
    # Nothing was promoted.
    assert env.client.get("/v1/messages").json()["count"] == 0


def test_response_shaped_frontmatter_keys_cannot_be_posted(env):
    """The eq2 cursor-poisoning vulnerability, closed server-side: a
    `filename:` key in frontmatter would have let any agent pin every watcher's
    cursor past all future mail. `cursor`/`next`/`watch` are shut out too, so no
    record can imitate the response fields a client reads."""
    seed_agent(env.hub, "agent-1")
    for key in ("filename", "cursor", "next", "watch"):
        uri = _seed_source(
            env,
            "agent-1",
            f"drafts/{key}.md",
            f"---\ntype: note\n{key}: 99999999-999999-999_zzz.md\n---\npoison",
        )
        r = env.client.post("/v1/messages", json={"source": uri})
        assert r.status_code == 400, f"{key} was accepted"
        assert r.json()["error"]["code"] == "INVALID_FRONTMATTER"
        assert key in r.json()["error"]["message"]


def test_allowlisted_frontmatter_still_round_trips(env):
    """Every key the system itself writes stays postable β€” an agent must be able
    to re-post a message the API served it (that content carries
    agent/timestamp/via) without tripping the guard. The server-stamped values
    still win."""
    seed_agent(env.hub, "agent-1")
    seed_agent(env.hub, "agent-2")
    uri = _seed_source(
        env,
        "agent-1",
        "drafts/round-trip.md",
        "---\ntype: note\nrefs:\n  - 20260101-000000-000_agent-2.md\n"
        "agent: agent-2\ntimestamp: 2026-01-01 00:00 UTC\nvia: raw\n---\nre-post @agent-2",
    )
    r = env.client.post("/v1/messages", json={"source": uri})
    assert r.status_code == 201, r.text
    fm = env.client.get(f"/v1/messages/{r.json()['filename']}").json()["frontmatter"]
    # Server stamps win over the client's stale copies.
    assert fm["agent"] == "agent-1" and fm["via"] == "bucket"
    assert fm["type"] == "note"


# ── frontmatter value shapes (WATCH_DESIGN.md Β§5.5) ───────────────────
# The key allowlist alone isn't enough: `yaml.safe_load` turns a mapping-valued
# key into a nested dict, and that dict's *keys* serialize as raw JSON object
# keys, unescaped like ordinary string values would be. So every value must
# also be a scalar (except `refs`, which may be a list of scalars).


def test_nested_mapping_frontmatter_value_rejected_naming_the_key(env):
    """`type: {cursor: 99999999-...zzz.md}` is the same cursor-poisoning
    vulnerability wearing a disguise: the key allowlist admits `type`, but the
    *value* is a mapping whose own key, `cursor`, would serialize as a raw
    JSON key once expanded β€” nothing keeps a watcher's naive grep from
    matching it. The value-shape check has to reject this, naming `type`."""
    seed_agent(env.hub, "agent-1")
    poison = "99999999-999999-999_zzz.md"
    uri = _seed_source(
        env,
        "agent-1",
        "drafts/nested.md",
        f"---\ntype: {{cursor: {poison}}}\n---\nbody",
    )
    r = env.client.post("/v1/messages", json={"source": uri})
    assert r.status_code == 400
    err = r.json()["error"]
    assert err["code"] == "INVALID_FRONTMATTER"
    assert "type" in err["message"]
    # Nothing was promoted.
    assert env.client.get("/v1/messages").json()["count"] == 0
    # And even if promotion had somehow slipped through, the poison string
    # must never show up the way a watcher's naive grep would match it.
    data = env.client.get("/v1/messages?expand=true").json()
    serialized = json.dumps(data, separators=(",", ":"))
    assert serialized.count(f'"cursor":"{poison}"') == 0


def test_refs_list_containing_mapping_rejected_naming_refs(env):
    """`refs` may be a list, but only of scalars β€” a list element that is
    itself a mapping (`refs: [{filename: x.md}]`) reopens the same hole one
    level deeper than the bare-mapping case above."""
    seed_agent(env.hub, "agent-1")
    uri = _seed_source(
        env, "agent-1", "drafts/refs-mapping.md", "---\ntype: note\nrefs: [{filename: x.md}]\n---\nbody"
    )
    r = env.client.post("/v1/messages", json={"source": uri})
    assert r.status_code == 400
    err = r.json()["error"]
    assert err["code"] == "INVALID_FRONTMATTER"
    assert "refs" in err["message"]


def test_refs_scalar_and_list_of_scalars_still_201(env):
    """`refs` accepts both its documented client-facing shapes β€” a bare
    filename string or a list of filename strings β€” the value-shape check
    only rejects non-scalars, not `refs`'s two legitimate scalar shapes."""
    seed_agent(env.hub, "agent-1")
    uri = _seed_source(
        env,
        "agent-1",
        "drafts/refs-string.md",
        "---\ntype: note\nrefs: 20260101-000000-000_agent-1.md\n---\nplain string ref",
    )
    assert env.client.post("/v1/messages", json={"source": uri}).status_code == 201

    uri2 = _seed_source(
        env,
        "agent-1",
        "drafts/refs-list.md",
        "---\ntype: note\nrefs:\n  - 20260101-000000-000_agent-1.md\n  - 20260102-000000-000_agent-1.md\n"
        "---\nlist of string refs",
    )
    assert env.client.post("/v1/messages", json={"source": uri2}).status_code == 201


def test_scalar_edge_values_bool_and_date_still_201(env):
    """YAML's implicit typing turns a bare `2026-01-01` into `datetime.date`
    and `true` into `bool`, not `str` β€” both are still scalars and must pass
    the value-shape check exactly like an ordinary string value does. (The
    server overwrites `via`/`timestamp` regardless, so these odd client values
    never reach storage; the point is that validation doesn't choke on them.)"""
    seed_agent(env.hub, "agent-1")
    uri = _seed_source(
        env,
        "agent-1",
        "drafts/scalar-edges.md",
        "---\ntype: note\nvia: true\ntimestamp: 2026-01-01\n---\nbody",
    )
    assert env.client.post("/v1/messages", json={"source": uri}).status_code == 201


def test_raw_variant_needs_no_allowlist(env):
    """The raw variant has no client frontmatter channel at all β€” `type` and
    `refs` arrive as typed request fields, so there is nothing to police."""
    seed_agent(env.hub, "agent-1")
    r = env.client.post(
        "/v1/messages",
        json={"agent_id": "agent-1", "body": "hello", "type": "note"},
    )
    assert r.status_code == 201